feat: separated efficient agent mode (lite mode) for small language models
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"general": {
|
||||
"language": "auto",
|
||||
"agent_mode": "regular",
|
||||
"llm_provider": "local_browser",
|
||||
"persist_api_keys_non_localhost": false,
|
||||
"auto_compact_threshold_percent": 90,
|
||||
|
||||
@@ -3,6 +3,14 @@ import { AgentCore } from './AgentCore';
|
||||
import type { LLMProvider } from '../llm/LLMProvider';
|
||||
import type { Message, ToolCall } from './AgentState';
|
||||
import type { StreamChunk } from '../llm/StreamingTypes';
|
||||
import type { OpenAIToolDefinition } from '../tools/BaseTool';
|
||||
import { ReadMusicTool } from '../tools/ReadMusicTool';
|
||||
|
||||
const configState = new Map<string, unknown>([
|
||||
['general.agent_mode', 'regular'],
|
||||
['general.llm_provider', 'openai'],
|
||||
['general.auto_compact_threshold_percent', 90],
|
||||
]);
|
||||
|
||||
vi.mock('../../stores/projectStore', () => ({
|
||||
useProjectStore: {
|
||||
@@ -14,17 +22,35 @@ vi.mock('../../stores/projectStore', () => ({
|
||||
|
||||
vi.mock('./SystemPrompts', () => ({
|
||||
SystemPrompts: {
|
||||
getSystemPromptWithContext: vi.fn(async () => 'system prompt'),
|
||||
getSystemPromptWithContext: vi.fn(async (templatePath?: string) => `system prompt:${templatePath ?? 'default'}`),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../core/config/ConfigManager', () => ({
|
||||
ConfigManager: {
|
||||
instance: () => ({
|
||||
getIsInitialized: () => true,
|
||||
initialize: vi.fn(async () => undefined),
|
||||
get: (key: string) => configState.get(key),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
class ScriptedProvider implements LLMProvider {
|
||||
public calls: Message[][] = [];
|
||||
public systemPrompts: Array<string | undefined> = [];
|
||||
public tools: OpenAIToolDefinition[][] = [];
|
||||
|
||||
constructor(private readonly scripts: StreamChunk[][]) {}
|
||||
|
||||
async *generateStream(messages: Message[]): AsyncIterableIterator<StreamChunk> {
|
||||
async *generateStream(
|
||||
messages: Message[],
|
||||
systemPrompt?: string,
|
||||
tools?: OpenAIToolDefinition[],
|
||||
): AsyncIterableIterator<StreamChunk> {
|
||||
this.calls.push(messages.map(message => ({ ...message })));
|
||||
this.systemPrompts.push(systemPrompt);
|
||||
this.tools.push(tools ?? []);
|
||||
const script = this.scripts.shift() ?? [{ type: 'done', content: '', finishReason: 'stop' }];
|
||||
for (const chunk of script) {
|
||||
yield chunk;
|
||||
@@ -53,6 +79,9 @@ async function collectChunks(input: string): Promise<StreamChunk[]> {
|
||||
|
||||
describe('AgentCore todo integration', () => {
|
||||
beforeEach(() => {
|
||||
configState.set('general.agent_mode', 'regular');
|
||||
configState.set('general.llm_provider', 'openai');
|
||||
configState.set('general.auto_compact_threshold_percent', 90);
|
||||
AgentCore.instance().clearConversation();
|
||||
AgentCore.instance().setLLMProvider(new ScriptedProvider([
|
||||
[{ type: 'done', content: '', finishReason: 'stop' }],
|
||||
@@ -211,4 +240,81 @@ describe('AgentCore todo integration', () => {
|
||||
{ id: 'todo-1', text: 'Continue work', status: 'in_progress', updatedAt: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the regular system prompt in regular mode', async () => {
|
||||
configState.set('general.agent_mode', 'regular');
|
||||
const provider = new ScriptedProvider([
|
||||
[{ type: 'done', content: '', finishReason: 'stop' }],
|
||||
]);
|
||||
AgentCore.instance().setLLMProvider(provider);
|
||||
|
||||
await collectChunks('Read the current region.');
|
||||
|
||||
expect(provider.systemPrompts[0]).toBe('system prompt:prompts/system.md');
|
||||
});
|
||||
|
||||
it('uses the compact system prompt in efficient mode', async () => {
|
||||
configState.set('general.agent_mode', 'efficient');
|
||||
const provider = new ScriptedProvider([
|
||||
[{ type: 'done', content: '', finishReason: 'stop' }],
|
||||
]);
|
||||
AgentCore.instance().setLLMProvider(provider);
|
||||
|
||||
await collectChunks('Read the current region.');
|
||||
|
||||
expect(provider.systemPrompts[0]).toBe('system prompt:prompts/system_compact.md');
|
||||
});
|
||||
|
||||
it('forces efficient mode when the local browser provider is selected', async () => {
|
||||
configState.set('general.agent_mode', 'regular');
|
||||
configState.set('general.llm_provider', 'local_browser');
|
||||
const provider = new ScriptedProvider([
|
||||
[{ type: 'done', content: '', finishReason: 'stop' }],
|
||||
]);
|
||||
AgentCore.instance().setLLMProvider(provider);
|
||||
|
||||
await collectChunks('Read the current region.');
|
||||
|
||||
expect(provider.systemPrompts[0]).toBe('system prompt:prompts/system_compact.md');
|
||||
});
|
||||
|
||||
it('filters tool definitions by the active agent mode', async () => {
|
||||
configState.set('general.agent_mode', 'efficient');
|
||||
const provider = new ScriptedProvider([
|
||||
[{ type: 'done', content: '', finishReason: 'stop' }],
|
||||
]);
|
||||
AgentCore.instance().setLLMProvider(provider);
|
||||
|
||||
const spy = vi.spyOn(ReadMusicTool.prototype, 'isAvailableInEfficientMode')
|
||||
.mockReturnValue(false);
|
||||
|
||||
await collectChunks('Read the current region.');
|
||||
|
||||
expect(provider.tools[0].map(tool => tool.function.name)).not.toContain('read_music');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('rejects tool calls for tools unavailable in the active mode', async () => {
|
||||
configState.set('general.agent_mode', 'efficient');
|
||||
const availabilitySpy = vi.spyOn(ReadMusicTool.prototype, 'isAvailableInEfficientMode')
|
||||
.mockReturnValue(false);
|
||||
const provider = new ScriptedProvider([
|
||||
[
|
||||
{ type: 'tool_call', content: '', toolCall: makeToolCall('read_music', {}, 'tool_1') },
|
||||
{ type: 'done', content: '', finishReason: 'tool_calls' },
|
||||
],
|
||||
[
|
||||
{ type: 'text', content: 'Done' },
|
||||
{ type: 'done', content: '', finishReason: 'stop' },
|
||||
],
|
||||
]);
|
||||
AgentCore.instance().setLLMProvider(provider);
|
||||
|
||||
const chunks = await collectChunks('Read the current region.');
|
||||
const toolResultChunk = chunks.find(chunk => chunk.type === 'tool_result' && chunk.toolResult?.name === 'read_music');
|
||||
|
||||
expect(toolResultChunk?.toolResult?.success).toBe(false);
|
||||
expect(toolResultChunk?.toolResult?.result).toBe("Tool 'read_music' is not available in Efficient Mode.");
|
||||
availabilitySpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
+43
-11
@@ -5,11 +5,13 @@ import { AVAILABLE_TOOLS, createToolInstance } from '../tools';
|
||||
import { useProjectStore } from '../../stores/projectStore';
|
||||
import type { StreamChunk, ToolApprovalDecision } from '../llm/StreamingTypes';
|
||||
import type { ToolCall } from './AgentState';
|
||||
import type { OpenAIToolDefinition } from '../tools/BaseTool';
|
||||
import type { BaseTool, OpenAIToolDefinition } from '../tools/BaseTool';
|
||||
import { ConversationCompactor, type CompactProgress } from '../compact/ConversationCompactor';
|
||||
import { ConfigManager } from '../../core/config/ConfigManager';
|
||||
import { buildTodoContext } from './todo';
|
||||
import type { SavedConversationDocument } from '../../types/conversationTypes';
|
||||
import type { AgentMode } from '../../util/agentMode';
|
||||
import { getEffectiveAgentMode, getSystemPromptPathForAgentMode } from '../../util/agentMode';
|
||||
|
||||
export interface CompactConversationOptions {
|
||||
trigger: 'manual' | 'auto';
|
||||
@@ -69,26 +71,54 @@ export class AgentCore {
|
||||
/**
|
||||
* Get OpenAI tool definitions for all available tools
|
||||
*/
|
||||
private getToolDefinitions(): OpenAIToolDefinition[] {
|
||||
private getToolDefinitions(agentMode: AgentMode): OpenAIToolDefinition[] {
|
||||
return Object.values(AVAILABLE_TOOLS).map(ToolClass => {
|
||||
const tool = new ToolClass();
|
||||
return tool.getDefinition();
|
||||
});
|
||||
return this.isToolAvailableInMode(tool, agentMode) ? tool.getDefinition() : null;
|
||||
}).filter((tool): tool is OpenAIToolDefinition => tool !== null);
|
||||
}
|
||||
|
||||
private async getSystemPrompt(templatePath?: string): Promise<string> {
|
||||
return SystemPrompts.getSystemPromptWithContext(templatePath);
|
||||
}
|
||||
|
||||
private getEffectiveAgentMode(): AgentMode {
|
||||
return getEffectiveAgentMode(ConfigManager.instance());
|
||||
}
|
||||
|
||||
private getSystemPromptTemplatePath(agentMode: AgentMode): string {
|
||||
return getSystemPromptPathForAgentMode(agentMode);
|
||||
}
|
||||
|
||||
private isToolAvailableInMode(toolInstance: BaseTool | null, agentMode: AgentMode): boolean {
|
||||
if (!toolInstance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return agentMode === 'efficient'
|
||||
? toolInstance.isAvailableInEfficientMode()
|
||||
: toolInstance.isAvailableInRegularMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single tool call and return the result
|
||||
*/
|
||||
private async executeTool(toolCall: ToolCall): Promise<{ success: boolean; result: string }> {
|
||||
private async executeTool(
|
||||
toolCall: ToolCall,
|
||||
agentMode: AgentMode,
|
||||
): Promise<{ success: boolean; result: string }> {
|
||||
const toolInstance = createToolInstance(toolCall.function.name);
|
||||
if (!toolInstance) {
|
||||
return { success: false, result: `Unknown tool: ${toolCall.function.name}` };
|
||||
}
|
||||
|
||||
if (!this.isToolAvailableInMode(toolInstance, agentMode)) {
|
||||
return {
|
||||
success: false,
|
||||
result: `Tool '${toolCall.function.name}' is not available in ${agentMode === 'efficient' ? 'Efficient Mode' : 'Regular Mode'}.`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const params = JSON.parse(toolCall.function.arguments);
|
||||
const result = await toolInstance.execute(params);
|
||||
@@ -121,10 +151,11 @@ export class AgentCore {
|
||||
this.currentUserMessageId = this.agentState.addMessage('user', userInput);
|
||||
this.currentTurnLikelyMultiStep = this.isLikelyMultiStepTask(userInput);
|
||||
|
||||
const agentMode = this.getEffectiveAgentMode();
|
||||
const systemPrompt = await SystemPrompts.getSystemPromptWithContext(
|
||||
this.llmProvider.getPreferredSystemPromptPath?.(),
|
||||
this.getSystemPromptTemplatePath(agentMode),
|
||||
);
|
||||
const tools = this.getToolDefinitions();
|
||||
const tools = this.getToolDefinitions(agentMode);
|
||||
|
||||
try {
|
||||
// Agentic loop: stream → check for tool calls → execute → repeat
|
||||
@@ -179,7 +210,7 @@ export class AgentCore {
|
||||
|
||||
const result = denied
|
||||
? { success: false, result: 'Execution was denied by the user.' }
|
||||
: await this.executeTool(toolCall);
|
||||
: await this.executeTool(toolCall, agentMode);
|
||||
|
||||
// Add tool result message to conversation history
|
||||
this.agentState.addMessage('tool', JSON.stringify(result), {
|
||||
@@ -295,9 +326,10 @@ export class AgentCore {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tools = this.getToolDefinitions();
|
||||
const agentMode = this.getEffectiveAgentMode();
|
||||
const tools = this.getToolDefinitions(agentMode);
|
||||
const systemPrompt = await this.getSystemPrompt(
|
||||
this.llmProvider.getPreferredSystemPromptPath?.(),
|
||||
this.getSystemPromptTemplatePath(agentMode),
|
||||
);
|
||||
const thresholdPercent = await this.getAutoCompactThresholdPercent();
|
||||
const reservedOutputTokens = this.llmProvider.getReservedOutputTokens?.() ?? 4096;
|
||||
@@ -345,7 +377,7 @@ export class AgentCore {
|
||||
const compactor = new ConversationCompactor({
|
||||
provider: this.llmProvider,
|
||||
systemPrompt: compactionPrompt,
|
||||
tools: this.getToolDefinitions(),
|
||||
tools: this.getToolDefinitions(this.getEffectiveAgentMode()),
|
||||
focus: options.focus,
|
||||
onProgress: options.onProgress,
|
||||
supplementalContext: buildTodoContext(this.agentState.getTodos()),
|
||||
|
||||
@@ -56,10 +56,6 @@ async function importMediaPipe(): Promise<MediaPipeGenAI> {
|
||||
export class LocalBrowserLLMProvider implements LLMProvider {
|
||||
private inference: GemmaInference | null = null;
|
||||
|
||||
getPreferredSystemPromptPath(): string | undefined {
|
||||
return 'prompts/system_compact.md';
|
||||
}
|
||||
|
||||
getContextWindow(): number {
|
||||
return this.getConfiguredContextLength();
|
||||
}
|
||||
|
||||
@@ -74,6 +74,20 @@ export abstract class BaseTool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the tool is available when the assistant runs in Regular Mode.
|
||||
*/
|
||||
isAvailableInRegularMode(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the tool is available when the assistant runs in Efficient Mode.
|
||||
*/
|
||||
isAvailableInEfficientMode(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally build a compact UI summary for a successful tool result.
|
||||
* The raw tool result remains the canonical output stored in agent history.
|
||||
|
||||
@@ -23,6 +23,7 @@ import { KGConversationStorage } from '../core/io/KGConversationStorage';
|
||||
import { SAVED_CONVERSATION_VERSION, type SavedConversationDocument, type SavedConversationMeta } from '../types/conversationTypes';
|
||||
import type { Message } from '../agent/core/AgentState';
|
||||
import { showConfirm } from '../util/dialogUtil';
|
||||
import { getEffectiveAgentMode, getSystemPromptPathForAgentMode } from '../util/agentMode';
|
||||
|
||||
import type { ChatMessage } from '../types/projectTypes';
|
||||
|
||||
@@ -631,9 +632,9 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
// Log system prompt only for first message
|
||||
if (isFirstMessage) {
|
||||
try {
|
||||
const provider = AgentCore.instance().getLLMProvider();
|
||||
const agentMode = getEffectiveAgentMode(ConfigManager.instance());
|
||||
const systemPrompt = await SystemPrompts.getSystemPromptWithContext(
|
||||
provider?.getPreferredSystemPromptPath?.(),
|
||||
getSystemPromptPathForAgentMode(agentMode),
|
||||
);
|
||||
console.log('------------ SYSTEM ------------');
|
||||
console.log(systemPrompt);
|
||||
|
||||
@@ -14,6 +14,7 @@ const { localSeparatorModelCacheMock } = vi.hoisted(() => ({
|
||||
|
||||
const configState = new Map<string, unknown>([
|
||||
['general.language', 'auto'],
|
||||
['general.agent_mode', 'regular'],
|
||||
['general.llm_provider', 'local_browser'],
|
||||
['general.persist_api_keys_non_localhost', false],
|
||||
['general.auto_compact_threshold_percent', 90],
|
||||
@@ -107,6 +108,8 @@ describe('GeneralSettings', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
configState.set('general.language', 'auto');
|
||||
configState.set('general.agent_mode', 'regular');
|
||||
configState.set('general.llm_provider', 'local_browser');
|
||||
configState.set('general.local_browser.context_length', 65536);
|
||||
configState.set('general.auto_compact_threshold_percent', 90);
|
||||
configManagerMock.get.mockClear();
|
||||
@@ -196,6 +199,41 @@ describe('GeneralSettings', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the music assistant section and initializes the agent mode selector', async () => {
|
||||
configState.set('general.llm_provider', 'openai');
|
||||
configState.set('general.agent_mode', 'efficient');
|
||||
|
||||
renderSettings();
|
||||
|
||||
expect(await screen.findByText('K.G.Studio Music Assistant')).toBeTruthy();
|
||||
const select = screen.getByLabelText('Agent Mode');
|
||||
expect((select as HTMLSelectElement).value).toBe('efficient');
|
||||
});
|
||||
|
||||
it('persists agent mode changes for non-local providers', async () => {
|
||||
configState.set('general.llm_provider', 'openai');
|
||||
|
||||
renderSettings();
|
||||
|
||||
const select = await screen.findByLabelText('Agent Mode');
|
||||
fireEvent.change(select, { target: { value: 'efficient' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(configManagerMock.set).toHaveBeenCalledWith('general.agent_mode', 'efficient');
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the agent mode selector for the local browser provider and shows override help', async () => {
|
||||
configState.set('general.llm_provider', 'local_browser');
|
||||
configState.set('general.agent_mode', 'regular');
|
||||
|
||||
renderSettings();
|
||||
|
||||
const select = await screen.findByLabelText('Agent Mode');
|
||||
expect(select).toBeDisabled();
|
||||
expect(screen.getByText('Local LLM (Browser) always runs the assistant in Efficient Mode.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders and persists local runtime download URLs', async () => {
|
||||
renderSettings();
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ import {
|
||||
LOCAL_SEPARATOR_MODEL_CONFIGS,
|
||||
LOCAL_SEPARATOR_MODEL_IDS,
|
||||
} from '../../../util/local-separator/config';
|
||||
import {
|
||||
DEFAULT_AGENT_MODE,
|
||||
getEffectiveAgentMode,
|
||||
isAgentModeForcedByProvider,
|
||||
normalizeAgentMode,
|
||||
type AgentMode,
|
||||
} from '../../../util/agentMode';
|
||||
|
||||
const LANGUAGE_OPTION_LABELS: Record<Exclude<LanguageSetting, 'auto'>, string> = {
|
||||
en_us: 'English',
|
||||
@@ -29,6 +36,7 @@ const LANGUAGE_OPTION_LABELS: Record<Exclude<LanguageSetting, 'auto'>, string> =
|
||||
const GeneralSettings: React.FC = () => {
|
||||
const { t, setLanguageSetting } = useI18n();
|
||||
const [language, setLanguage] = useState<LanguageSetting>('auto');
|
||||
const [agentMode, setAgentMode] = useState<AgentMode>(DEFAULT_AGENT_MODE);
|
||||
const [llmProvider, setLlmProvider] = useState<string>(LOCAL_LLM_PROVIDER_KEY);
|
||||
const [openaiKey, setOpenaiKey] = useState<string>('');
|
||||
const [openaiModel, setOpenaiModel] = useState<string>('');
|
||||
@@ -103,6 +111,7 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
|
||||
setLanguage(((configManager.get('general.language') as LanguageSetting | undefined) ?? 'auto'));
|
||||
setAgentMode(normalizeAgentMode(configManager.get('general.agent_mode')));
|
||||
setLlmProvider((configManager.get('general.llm_provider') as string) || LOCAL_LLM_PROVIDER_KEY);
|
||||
setOpenaiKey((configManager.get('general.openai.api_key') as string) || '');
|
||||
setOpenaiModel((configManager.get('general.openai.model') as string) || '');
|
||||
@@ -169,6 +178,16 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAgentModeChange = async (value: AgentMode) => {
|
||||
setAgentMode(value);
|
||||
try {
|
||||
await configManager.set('general.agent_mode', value);
|
||||
console.log('Agent mode changed to:', value);
|
||||
} catch (error) {
|
||||
console.error('Failed to save agent mode:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (value: LanguageSetting) => {
|
||||
setLanguage(value);
|
||||
try {
|
||||
@@ -354,6 +373,8 @@ const GeneralSettings: React.FC = () => {
|
||||
|
||||
const localRuntimeMessage = localModelState.runtimeSupport.reason;
|
||||
const hasLocalRuntimeHardFailure = !localModelState.runtimeSupport.supported;
|
||||
const isAgentModeOverriddenByLocalProvider = isAgentModeForcedByProvider(llmProvider);
|
||||
const effectiveAgentMode = getEffectiveAgentMode(configManager);
|
||||
|
||||
// NOTE: Gemini and Claude are not supported yet due to CORS issues.
|
||||
return (
|
||||
@@ -444,6 +465,38 @@ const GeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>{t('settings.general.musicAssistant.section')}</h4>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label" htmlFor="general-agent-mode-select">
|
||||
{t('settings.general.musicAssistant.agentMode.label')}
|
||||
</label>
|
||||
<select
|
||||
id="general-agent-mode-select"
|
||||
className="settings-select"
|
||||
value={agentMode}
|
||||
onChange={(e) => void handleAgentModeChange(e.target.value as AgentMode)}
|
||||
disabled={isAgentModeOverriddenByLocalProvider}
|
||||
>
|
||||
<option value="regular">{t('settings.general.musicAssistant.agentMode.regular')}</option>
|
||||
<option value="efficient">{t('settings.general.musicAssistant.agentMode.efficient')}</option>
|
||||
</select>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{isAgentModeOverriddenByLocalProvider
|
||||
? t('settings.general.musicAssistant.agentMode.localOverride')
|
||||
: t('settings.general.musicAssistant.agentMode.help')}
|
||||
</div>
|
||||
{effectiveAgentMode !== agentMode && (
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
{t('settings.general.musicAssistant.agentMode.effectiveValue', {
|
||||
mode: t('settings.general.musicAssistant.agentMode.efficient'),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h4>{LOCAL_LLM_DISPLAY_NAME} Local Runtime</h4>
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export const OPFS_CONSTANTS = {
|
||||
|
||||
export const CONFIG_UPGRADER_CONSTANTS = {
|
||||
VERSION_KEY: '__config_version',
|
||||
CURRENT_VERSION: 4,
|
||||
CURRENT_VERSION: 5,
|
||||
};
|
||||
|
||||
export const URL_CONSTANTS = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { upgradeConfigToV1 } from './upgradeConfigToV1';
|
||||
import { upgradeConfigToV2 } from './upgradeConfigToV2';
|
||||
import { upgradeConfigToV3 } from './upgradeConfigToV3';
|
||||
import { upgradeConfigToV4 } from './upgradeConfigToV4';
|
||||
import { upgradeConfigToV5 } from './upgradeConfigToV5';
|
||||
|
||||
/**
|
||||
* KGConfigUpgrader — Orchestrates app-level migrations (e.g., storage backend changes).
|
||||
@@ -48,6 +49,10 @@ export class KGConfigUpgrader {
|
||||
await upgradeConfigToV4();
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
await upgradeConfigToV5();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`No config upgrader found for version ${nextVersion}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const getRawMock = vi.fn();
|
||||
const saveRawMock = vi.fn();
|
||||
|
||||
vi.mock('../io/KGConfigStorage', () => ({
|
||||
KGConfigStorage: {
|
||||
getInstance: vi.fn(() => ({
|
||||
getRaw: getRawMock,
|
||||
saveRaw: saveRawMock,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
import { upgradeConfigToV5 } from './upgradeConfigToV5';
|
||||
|
||||
describe('upgradeConfigToV5', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('defaults general.agent_mode to regular when the key is missing', async () => {
|
||||
const config: Record<string, unknown> = {
|
||||
general: {
|
||||
llm_provider: 'openai',
|
||||
},
|
||||
};
|
||||
getRawMock.mockResolvedValue(config);
|
||||
|
||||
await upgradeConfigToV5();
|
||||
|
||||
expect(config.general).toEqual({
|
||||
llm_provider: 'openai',
|
||||
agent_mode: 'regular',
|
||||
});
|
||||
expect(saveRawMock).toHaveBeenCalledWith('userConfig', config);
|
||||
});
|
||||
|
||||
it('preserves an explicit general.agent_mode value', async () => {
|
||||
const config: Record<string, unknown> = {
|
||||
general: {
|
||||
agent_mode: 'efficient',
|
||||
},
|
||||
};
|
||||
getRawMock.mockResolvedValue(config);
|
||||
|
||||
await upgradeConfigToV5();
|
||||
|
||||
expect(saveRawMock).not.toHaveBeenCalled();
|
||||
expect((config.general as Record<string, unknown>).agent_mode).toBe('efficient');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { KGConfigStorage } from '../io/KGConfigStorage';
|
||||
|
||||
const CONFIG_KEY = 'userConfig';
|
||||
|
||||
export async function upgradeConfigToV5(): Promise<void> {
|
||||
const storage = KGConfigStorage.getInstance();
|
||||
const rawConfig = await storage.getRaw(CONFIG_KEY);
|
||||
if (!rawConfig || typeof rawConfig !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = rawConfig as Record<string, unknown>;
|
||||
const general = config.general;
|
||||
|
||||
if (!general || typeof general !== 'object') {
|
||||
config.general = {
|
||||
agent_mode: 'regular',
|
||||
};
|
||||
await storage.saveRaw(CONFIG_KEY, config);
|
||||
return;
|
||||
}
|
||||
|
||||
const generalRecord = general as Record<string, unknown>;
|
||||
if ('agent_mode' in generalRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
generalRecord.agent_mode = 'regular';
|
||||
await storage.saveRaw(CONFIG_KEY, config);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChordGuideCustomConfig } from '../ChordGuideTypes';
|
||||
import { KGConfigStorage } from '../io/KGConfigStorage';
|
||||
import type { LanguageSetting } from '../../i18n/types';
|
||||
import type { AgentMode } from '../../util/agentMode';
|
||||
|
||||
/**
|
||||
* Application configuration interface
|
||||
@@ -8,6 +9,7 @@ import type { LanguageSetting } from '../../i18n/types';
|
||||
interface AppConfig {
|
||||
general: {
|
||||
language: LanguageSetting;
|
||||
agent_mode: AgentMode;
|
||||
llm_provider: 'local_browser' | 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible';
|
||||
persist_api_keys_non_localhost: boolean;
|
||||
auto_compact_threshold_percent: 80 | 90 | 95;
|
||||
@@ -208,6 +210,7 @@ export class ConfigManager {
|
||||
this.defaultConfig = {
|
||||
general: {
|
||||
language: 'auto',
|
||||
agent_mode: 'regular',
|
||||
llm_provider: 'local_browser',
|
||||
persist_api_keys_non_localhost: false,
|
||||
auto_compact_threshold_percent: 90,
|
||||
|
||||
@@ -56,6 +56,13 @@ export const enUsMessages: TranslationMessages = {
|
||||
'settings.general.autoCompactThreshold.conservative': 'Conservative (95%)',
|
||||
'settings.general.autoCompactThreshold.standard': 'Standard (90%)',
|
||||
'settings.general.autoCompactThreshold.early': 'Early (80%)',
|
||||
'settings.general.musicAssistant.section': 'K.G.Studio Music Assistant',
|
||||
'settings.general.musicAssistant.agentMode.label': 'Agent Mode',
|
||||
'settings.general.musicAssistant.agentMode.help': 'Regular Mode uses the standard system prompt. Efficient Mode uses a compact system prompt and only tools marked for Efficient Mode.',
|
||||
'settings.general.musicAssistant.agentMode.localOverride': 'Local LLM (Browser) always runs the assistant in Efficient Mode.',
|
||||
'settings.general.musicAssistant.agentMode.effectiveValue': 'Effective mode: {mode}.',
|
||||
'settings.general.musicAssistant.agentMode.regular': 'Regular Mode',
|
||||
'settings.general.musicAssistant.agentMode.efficient': 'Efficient Mode',
|
||||
'settings.general.localRuntime.cachedStatus': 'Cached Model Status',
|
||||
'settings.general.localRuntime.cacheChecking': 'Checking local model cache...',
|
||||
'settings.general.localRuntime.cacheDownloaded': 'Downloaded in browser cache.',
|
||||
|
||||
@@ -53,6 +53,13 @@ export const frFrMessages: TranslationMessages = {
|
||||
'settings.general.autoCompactThreshold.conservative': 'Prudent (95 %)',
|
||||
'settings.general.autoCompactThreshold.standard': 'Standard (90 %)',
|
||||
'settings.general.autoCompactThreshold.early': 'Précoce (80 %)',
|
||||
'settings.general.musicAssistant.section': 'Assistant musical K.G.Studio',
|
||||
'settings.general.musicAssistant.agentMode.label': 'Mode agent',
|
||||
'settings.general.musicAssistant.agentMode.help': 'Le mode Régulier utilise le prompt système standard. Le mode Efficace utilise un prompt système compact et seulement les outils marqués pour le mode Efficace.',
|
||||
'settings.general.musicAssistant.agentMode.localOverride': 'Le LLM local (navigateur) exécute toujours l’assistant en mode Efficace.',
|
||||
'settings.general.musicAssistant.agentMode.effectiveValue': 'Mode effectif : {mode}.',
|
||||
'settings.general.musicAssistant.agentMode.regular': 'Mode régulier',
|
||||
'settings.general.musicAssistant.agentMode.efficient': 'Mode efficace',
|
||||
'settings.general.localRuntime.cachedStatus': 'État du modèle en cache',
|
||||
'settings.general.localRuntime.cacheChecking': 'Vérification du cache du modèle local...',
|
||||
'settings.general.localRuntime.cacheDownloaded': 'Téléchargé dans le cache du navigateur.',
|
||||
|
||||
@@ -54,6 +54,13 @@ export const zhCnMessages: TranslationMessages = {
|
||||
'settings.general.autoCompactThreshold.conservative': '保守(95%)',
|
||||
'settings.general.autoCompactThreshold.standard': '标准(90%)',
|
||||
'settings.general.autoCompactThreshold.early': '提前(80%)',
|
||||
'settings.general.musicAssistant.section': 'K.G.Studio 音乐创作助手',
|
||||
'settings.general.musicAssistant.agentMode.label': 'Agent 模式',
|
||||
'settings.general.musicAssistant.agentMode.help': '常规模式使用标准系统提示词。高效模式使用紧凑系统提示词,并且只允许使用标记为可用于高效模式的工具。',
|
||||
'settings.general.musicAssistant.agentMode.localOverride': '本地 LLM(浏览器)始终以高效模式运行助手。',
|
||||
'settings.general.musicAssistant.agentMode.effectiveValue': '当前生效模式:{mode}。',
|
||||
'settings.general.musicAssistant.agentMode.regular': '常规模式',
|
||||
'settings.general.musicAssistant.agentMode.efficient': '高效模式',
|
||||
'settings.general.localRuntime.cachedStatus': '缓存模型状态',
|
||||
'settings.general.localRuntime.cacheChecking': '正在检查本地模型缓存...',
|
||||
'settings.general.localRuntime.cacheDownloaded': '已下载到浏览器缓存。',
|
||||
|
||||
@@ -54,6 +54,13 @@ export const zhHkMessages: TranslationMessages = {
|
||||
'settings.general.autoCompactThreshold.conservative': '保守(95%)',
|
||||
'settings.general.autoCompactThreshold.standard': '標準(90%)',
|
||||
'settings.general.autoCompactThreshold.early': '提前(80%)',
|
||||
'settings.general.musicAssistant.section': 'K.G.Studio 音樂創作助手',
|
||||
'settings.general.musicAssistant.agentMode.label': 'Agent 模式',
|
||||
'settings.general.musicAssistant.agentMode.help': '常規模式使用標準系統提示詞。高效模式使用精簡系統提示詞,並且只允許使用標記為可用於高效模式的工具。',
|
||||
'settings.general.musicAssistant.agentMode.localOverride': '本地 LLM(瀏覽器)會固定以高效模式執行助手。',
|
||||
'settings.general.musicAssistant.agentMode.effectiveValue': '目前生效模式:{mode}。',
|
||||
'settings.general.musicAssistant.agentMode.regular': '常規模式',
|
||||
'settings.general.musicAssistant.agentMode.efficient': '高效模式',
|
||||
'settings.general.localRuntime.cachedStatus': '緩存模型狀態',
|
||||
'settings.general.localRuntime.cacheChecking': '正在檢查本地模型緩存...',
|
||||
'settings.general.localRuntime.cacheDownloaded': '已下載到瀏覽器緩存。',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ConfigManager } from '../core/config/ConfigManager';
|
||||
import { LOCAL_LLM_PROVIDER_KEY } from './localLLMConfig';
|
||||
|
||||
export type AgentMode = 'regular' | 'efficient';
|
||||
|
||||
export const DEFAULT_AGENT_MODE: AgentMode = 'regular';
|
||||
export const EFFICIENT_AGENT_MODE: AgentMode = 'efficient';
|
||||
|
||||
export function normalizeAgentMode(value: unknown): AgentMode {
|
||||
return value === EFFICIENT_AGENT_MODE ? EFFICIENT_AGENT_MODE : DEFAULT_AGENT_MODE;
|
||||
}
|
||||
|
||||
export function isAgentModeForcedByProvider(providerType: string | null | undefined): boolean {
|
||||
return providerType === LOCAL_LLM_PROVIDER_KEY;
|
||||
}
|
||||
|
||||
export function getConfiguredAgentMode(configManager: ConfigManager = ConfigManager.instance()): AgentMode {
|
||||
return normalizeAgentMode(configManager.get('general.agent_mode'));
|
||||
}
|
||||
|
||||
export function getEffectiveAgentMode(configManager: ConfigManager = ConfigManager.instance()): AgentMode {
|
||||
const providerType = configManager.get('general.llm_provider');
|
||||
if (typeof providerType === 'string' && isAgentModeForcedByProvider(providerType)) {
|
||||
return EFFICIENT_AGENT_MODE;
|
||||
}
|
||||
|
||||
return getConfiguredAgentMode(configManager);
|
||||
}
|
||||
|
||||
export function getSystemPromptPathForAgentMode(mode: AgentMode): string {
|
||||
return mode === EFFICIENT_AGENT_MODE ? 'prompts/system_compact.md' : 'prompts/system.md';
|
||||
}
|
||||
Reference in New Issue
Block a user