feat: separated efficient agent mode (lite mode) for small language models

This commit is contained in:
Xiaohan-Tian
2026-06-04 16:45:06 -07:00
parent a3a0f2f4ad
commit 5c5a07b839
18 changed files with 411 additions and 20 deletions
+1
View File
@@ -1,6 +1,7 @@
{ {
"general": { "general": {
"language": "auto", "language": "auto",
"agent_mode": "regular",
"llm_provider": "local_browser", "llm_provider": "local_browser",
"persist_api_keys_non_localhost": false, "persist_api_keys_non_localhost": false,
"auto_compact_threshold_percent": 90, "auto_compact_threshold_percent": 90,
+108 -2
View File
@@ -3,6 +3,14 @@ import { AgentCore } from './AgentCore';
import type { LLMProvider } from '../llm/LLMProvider'; import type { LLMProvider } from '../llm/LLMProvider';
import type { Message, ToolCall } from './AgentState'; import type { Message, ToolCall } from './AgentState';
import type { StreamChunk } from '../llm/StreamingTypes'; 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', () => ({ vi.mock('../../stores/projectStore', () => ({
useProjectStore: { useProjectStore: {
@@ -14,17 +22,35 @@ vi.mock('../../stores/projectStore', () => ({
vi.mock('./SystemPrompts', () => ({ vi.mock('./SystemPrompts', () => ({
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 { class ScriptedProvider implements LLMProvider {
public calls: Message[][] = []; public calls: Message[][] = [];
public systemPrompts: Array<string | undefined> = [];
public tools: OpenAIToolDefinition[][] = [];
constructor(private readonly scripts: StreamChunk[][]) {} 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.calls.push(messages.map(message => ({ ...message })));
this.systemPrompts.push(systemPrompt);
this.tools.push(tools ?? []);
const script = this.scripts.shift() ?? [{ type: 'done', content: '', finishReason: 'stop' }]; const script = this.scripts.shift() ?? [{ type: 'done', content: '', finishReason: 'stop' }];
for (const chunk of script) { for (const chunk of script) {
yield chunk; yield chunk;
@@ -53,6 +79,9 @@ async function collectChunks(input: string): Promise<StreamChunk[]> {
describe('AgentCore todo integration', () => { describe('AgentCore todo integration', () => {
beforeEach(() => { 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().clearConversation();
AgentCore.instance().setLLMProvider(new ScriptedProvider([ AgentCore.instance().setLLMProvider(new ScriptedProvider([
[{ type: 'done', content: '', finishReason: 'stop' }], [{ type: 'done', content: '', finishReason: 'stop' }],
@@ -211,4 +240,81 @@ describe('AgentCore todo integration', () => {
{ id: 'todo-1', text: 'Continue work', status: 'in_progress', updatedAt: 3 }, { 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
View File
@@ -5,11 +5,13 @@ import { AVAILABLE_TOOLS, createToolInstance } from '../tools';
import { useProjectStore } from '../../stores/projectStore'; import { useProjectStore } from '../../stores/projectStore';
import type { StreamChunk, ToolApprovalDecision } from '../llm/StreamingTypes'; import type { StreamChunk, ToolApprovalDecision } from '../llm/StreamingTypes';
import type { ToolCall } from './AgentState'; 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 { ConversationCompactor, type CompactProgress } from '../compact/ConversationCompactor';
import { ConfigManager } from '../../core/config/ConfigManager'; import { ConfigManager } from '../../core/config/ConfigManager';
import { buildTodoContext } from './todo'; import { buildTodoContext } from './todo';
import type { SavedConversationDocument } from '../../types/conversationTypes'; import type { SavedConversationDocument } from '../../types/conversationTypes';
import type { AgentMode } from '../../util/agentMode';
import { getEffectiveAgentMode, getSystemPromptPathForAgentMode } from '../../util/agentMode';
export interface CompactConversationOptions { export interface CompactConversationOptions {
trigger: 'manual' | 'auto'; trigger: 'manual' | 'auto';
@@ -69,26 +71,54 @@ export class AgentCore {
/** /**
* Get OpenAI tool definitions for all available tools * Get OpenAI tool definitions for all available tools
*/ */
private getToolDefinitions(): OpenAIToolDefinition[] { private getToolDefinitions(agentMode: AgentMode): OpenAIToolDefinition[] {
return Object.values(AVAILABLE_TOOLS).map(ToolClass => { return Object.values(AVAILABLE_TOOLS).map(ToolClass => {
const tool = new 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> { private async getSystemPrompt(templatePath?: string): Promise<string> {
return SystemPrompts.getSystemPromptWithContext(templatePath); 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 * 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); const toolInstance = createToolInstance(toolCall.function.name);
if (!toolInstance) { if (!toolInstance) {
return { success: false, result: `Unknown tool: ${toolCall.function.name}` }; 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 { try {
const params = JSON.parse(toolCall.function.arguments); const params = JSON.parse(toolCall.function.arguments);
const result = await toolInstance.execute(params); const result = await toolInstance.execute(params);
@@ -121,10 +151,11 @@ export class AgentCore {
this.currentUserMessageId = this.agentState.addMessage('user', userInput); this.currentUserMessageId = this.agentState.addMessage('user', userInput);
this.currentTurnLikelyMultiStep = this.isLikelyMultiStepTask(userInput); this.currentTurnLikelyMultiStep = this.isLikelyMultiStepTask(userInput);
const agentMode = this.getEffectiveAgentMode();
const systemPrompt = await SystemPrompts.getSystemPromptWithContext( const systemPrompt = await SystemPrompts.getSystemPromptWithContext(
this.llmProvider.getPreferredSystemPromptPath?.(), this.getSystemPromptTemplatePath(agentMode),
); );
const tools = this.getToolDefinitions(); const tools = this.getToolDefinitions(agentMode);
try { try {
// Agentic loop: stream → check for tool calls → execute → repeat // Agentic loop: stream → check for tool calls → execute → repeat
@@ -179,7 +210,7 @@ export class AgentCore {
const result = denied const result = denied
? { success: false, result: 'Execution was denied by the user.' } ? { 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 // Add tool result message to conversation history
this.agentState.addMessage('tool', JSON.stringify(result), { this.agentState.addMessage('tool', JSON.stringify(result), {
@@ -295,9 +326,10 @@ export class AgentCore {
return false; return false;
} }
const tools = this.getToolDefinitions(); const agentMode = this.getEffectiveAgentMode();
const tools = this.getToolDefinitions(agentMode);
const systemPrompt = await this.getSystemPrompt( const systemPrompt = await this.getSystemPrompt(
this.llmProvider.getPreferredSystemPromptPath?.(), this.getSystemPromptTemplatePath(agentMode),
); );
const thresholdPercent = await this.getAutoCompactThresholdPercent(); const thresholdPercent = await this.getAutoCompactThresholdPercent();
const reservedOutputTokens = this.llmProvider.getReservedOutputTokens?.() ?? 4096; const reservedOutputTokens = this.llmProvider.getReservedOutputTokens?.() ?? 4096;
@@ -345,7 +377,7 @@ export class AgentCore {
const compactor = new ConversationCompactor({ const compactor = new ConversationCompactor({
provider: this.llmProvider, provider: this.llmProvider,
systemPrompt: compactionPrompt, systemPrompt: compactionPrompt,
tools: this.getToolDefinitions(), tools: this.getToolDefinitions(this.getEffectiveAgentMode()),
focus: options.focus, focus: options.focus,
onProgress: options.onProgress, onProgress: options.onProgress,
supplementalContext: buildTodoContext(this.agentState.getTodos()), supplementalContext: buildTodoContext(this.agentState.getTodos()),
-4
View File
@@ -56,10 +56,6 @@ async function importMediaPipe(): Promise<MediaPipeGenAI> {
export class LocalBrowserLLMProvider implements LLMProvider { export class LocalBrowserLLMProvider implements LLMProvider {
private inference: GemmaInference | null = null; private inference: GemmaInference | null = null;
getPreferredSystemPromptPath(): string | undefined {
return 'prompts/system_compact.md';
}
getContextWindow(): number { getContextWindow(): number {
return this.getConfiguredContextLength(); return this.getConfiguredContextLength();
} }
+14
View File
@@ -74,6 +74,20 @@ export abstract class BaseTool {
return true; 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. * Optionally build a compact UI summary for a successful tool result.
* The raw tool result remains the canonical output stored in agent history. * The raw tool result remains the canonical output stored in agent history.
+3 -2
View File
@@ -23,6 +23,7 @@ import { KGConversationStorage } from '../core/io/KGConversationStorage';
import { SAVED_CONVERSATION_VERSION, type SavedConversationDocument, type SavedConversationMeta } from '../types/conversationTypes'; import { SAVED_CONVERSATION_VERSION, type SavedConversationDocument, type SavedConversationMeta } from '../types/conversationTypes';
import type { Message } from '../agent/core/AgentState'; import type { Message } from '../agent/core/AgentState';
import { showConfirm } from '../util/dialogUtil'; import { showConfirm } from '../util/dialogUtil';
import { getEffectiveAgentMode, getSystemPromptPathForAgentMode } from '../util/agentMode';
import type { ChatMessage } from '../types/projectTypes'; import type { ChatMessage } from '../types/projectTypes';
@@ -631,9 +632,9 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
// Log system prompt only for first message // Log system prompt only for first message
if (isFirstMessage) { if (isFirstMessage) {
try { try {
const provider = AgentCore.instance().getLLMProvider(); const agentMode = getEffectiveAgentMode(ConfigManager.instance());
const systemPrompt = await SystemPrompts.getSystemPromptWithContext( const systemPrompt = await SystemPrompts.getSystemPromptWithContext(
provider?.getPreferredSystemPromptPath?.(), getSystemPromptPathForAgentMode(agentMode),
); );
console.log('------------ SYSTEM ------------'); console.log('------------ SYSTEM ------------');
console.log(systemPrompt); console.log(systemPrompt);
@@ -14,6 +14,7 @@ const { localSeparatorModelCacheMock } = vi.hoisted(() => ({
const configState = new Map<string, unknown>([ const configState = new Map<string, unknown>([
['general.language', 'auto'], ['general.language', 'auto'],
['general.agent_mode', 'regular'],
['general.llm_provider', 'local_browser'], ['general.llm_provider', 'local_browser'],
['general.persist_api_keys_non_localhost', false], ['general.persist_api_keys_non_localhost', false],
['general.auto_compact_threshold_percent', 90], ['general.auto_compact_threshold_percent', 90],
@@ -107,6 +108,8 @@ describe('GeneralSettings', () => {
beforeEach(() => { beforeEach(() => {
configState.set('general.language', 'auto'); 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.local_browser.context_length', 65536);
configState.set('general.auto_compact_threshold_percent', 90); configState.set('general.auto_compact_threshold_percent', 90);
configManagerMock.get.mockClear(); configManagerMock.get.mockClear();
@@ -196,6 +199,41 @@ describe('GeneralSettings', () => {
).toBeTruthy(); ).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 () => { it('renders and persists local runtime download URLs', async () => {
renderSettings(); renderSettings();
@@ -18,6 +18,13 @@ import {
LOCAL_SEPARATOR_MODEL_CONFIGS, LOCAL_SEPARATOR_MODEL_CONFIGS,
LOCAL_SEPARATOR_MODEL_IDS, LOCAL_SEPARATOR_MODEL_IDS,
} from '../../../util/local-separator/config'; } 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> = { const LANGUAGE_OPTION_LABELS: Record<Exclude<LanguageSetting, 'auto'>, string> = {
en_us: 'English', en_us: 'English',
@@ -29,6 +36,7 @@ const LANGUAGE_OPTION_LABELS: Record<Exclude<LanguageSetting, 'auto'>, string> =
const GeneralSettings: React.FC = () => { const GeneralSettings: React.FC = () => {
const { t, setLanguageSetting } = useI18n(); const { t, setLanguageSetting } = useI18n();
const [language, setLanguage] = useState<LanguageSetting>('auto'); const [language, setLanguage] = useState<LanguageSetting>('auto');
const [agentMode, setAgentMode] = useState<AgentMode>(DEFAULT_AGENT_MODE);
const [llmProvider, setLlmProvider] = useState<string>(LOCAL_LLM_PROVIDER_KEY); const [llmProvider, setLlmProvider] = useState<string>(LOCAL_LLM_PROVIDER_KEY);
const [openaiKey, setOpenaiKey] = useState<string>(''); const [openaiKey, setOpenaiKey] = useState<string>('');
const [openaiModel, setOpenaiModel] = useState<string>(''); const [openaiModel, setOpenaiModel] = useState<string>('');
@@ -103,6 +111,7 @@ const GeneralSettings: React.FC = () => {
} }
setLanguage(((configManager.get('general.language') as LanguageSetting | undefined) ?? 'auto')); 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); setLlmProvider((configManager.get('general.llm_provider') as string) || LOCAL_LLM_PROVIDER_KEY);
setOpenaiKey((configManager.get('general.openai.api_key') as string) || ''); setOpenaiKey((configManager.get('general.openai.api_key') as string) || '');
setOpenaiModel((configManager.get('general.openai.model') 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) => { const handleLanguageChange = async (value: LanguageSetting) => {
setLanguage(value); setLanguage(value);
try { try {
@@ -354,6 +373,8 @@ const GeneralSettings: React.FC = () => {
const localRuntimeMessage = localModelState.runtimeSupport.reason; const localRuntimeMessage = localModelState.runtimeSupport.reason;
const hasLocalRuntimeHardFailure = !localModelState.runtimeSupport.supported; 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. // NOTE: Gemini and Claude are not supported yet due to CORS issues.
return ( return (
@@ -444,6 +465,38 @@ const GeneralSettings: React.FC = () => {
</div> </div>
</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"> <div className="settings-group">
<h4>{LOCAL_LLM_DISPLAY_NAME} Local Runtime</h4> <h4>{LOCAL_LLM_DISPLAY_NAME} Local Runtime</h4>
+1 -1
View File
@@ -106,7 +106,7 @@ export const OPFS_CONSTANTS = {
export const CONFIG_UPGRADER_CONSTANTS = { export const CONFIG_UPGRADER_CONSTANTS = {
VERSION_KEY: '__config_version', VERSION_KEY: '__config_version',
CURRENT_VERSION: 4, CURRENT_VERSION: 5,
}; };
export const URL_CONSTANTS = { export const URL_CONSTANTS = {
@@ -4,6 +4,7 @@ import { upgradeConfigToV1 } from './upgradeConfigToV1';
import { upgradeConfigToV2 } from './upgradeConfigToV2'; import { upgradeConfigToV2 } from './upgradeConfigToV2';
import { upgradeConfigToV3 } from './upgradeConfigToV3'; import { upgradeConfigToV3 } from './upgradeConfigToV3';
import { upgradeConfigToV4 } from './upgradeConfigToV4'; import { upgradeConfigToV4 } from './upgradeConfigToV4';
import { upgradeConfigToV5 } from './upgradeConfigToV5';
/** /**
* KGConfigUpgrader Orchestrates app-level migrations (e.g., storage backend changes). * KGConfigUpgrader Orchestrates app-level migrations (e.g., storage backend changes).
@@ -48,6 +49,10 @@ export class KGConfigUpgrader {
await upgradeConfigToV4(); await upgradeConfigToV4();
break; break;
} }
case 5: {
await upgradeConfigToV5();
break;
}
default: { default: {
throw new Error(`No config upgrader found for version ${nextVersion}`); 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);
}
+3
View File
@@ -1,6 +1,7 @@
import type { ChordGuideCustomConfig } from '../ChordGuideTypes'; import type { ChordGuideCustomConfig } from '../ChordGuideTypes';
import { KGConfigStorage } from '../io/KGConfigStorage'; import { KGConfigStorage } from '../io/KGConfigStorage';
import type { LanguageSetting } from '../../i18n/types'; import type { LanguageSetting } from '../../i18n/types';
import type { AgentMode } from '../../util/agentMode';
/** /**
* Application configuration interface * Application configuration interface
@@ -8,6 +9,7 @@ import type { LanguageSetting } from '../../i18n/types';
interface AppConfig { interface AppConfig {
general: { general: {
language: LanguageSetting; language: LanguageSetting;
agent_mode: AgentMode;
llm_provider: 'local_browser' | 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible'; llm_provider: 'local_browser' | 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible';
persist_api_keys_non_localhost: boolean; persist_api_keys_non_localhost: boolean;
auto_compact_threshold_percent: 80 | 90 | 95; auto_compact_threshold_percent: 80 | 90 | 95;
@@ -208,6 +210,7 @@ export class ConfigManager {
this.defaultConfig = { this.defaultConfig = {
general: { general: {
language: 'auto', language: 'auto',
agent_mode: 'regular',
llm_provider: 'local_browser', llm_provider: 'local_browser',
persist_api_keys_non_localhost: false, persist_api_keys_non_localhost: false,
auto_compact_threshold_percent: 90, auto_compact_threshold_percent: 90,
+7
View File
@@ -56,6 +56,13 @@ export const enUsMessages: TranslationMessages = {
'settings.general.autoCompactThreshold.conservative': 'Conservative (95%)', 'settings.general.autoCompactThreshold.conservative': 'Conservative (95%)',
'settings.general.autoCompactThreshold.standard': 'Standard (90%)', 'settings.general.autoCompactThreshold.standard': 'Standard (90%)',
'settings.general.autoCompactThreshold.early': 'Early (80%)', '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.cachedStatus': 'Cached Model Status',
'settings.general.localRuntime.cacheChecking': 'Checking local model cache...', 'settings.general.localRuntime.cacheChecking': 'Checking local model cache...',
'settings.general.localRuntime.cacheDownloaded': 'Downloaded in browser cache.', 'settings.general.localRuntime.cacheDownloaded': 'Downloaded in browser cache.',
+7
View File
@@ -53,6 +53,13 @@ export const frFrMessages: TranslationMessages = {
'settings.general.autoCompactThreshold.conservative': 'Prudent (95 %)', 'settings.general.autoCompactThreshold.conservative': 'Prudent (95 %)',
'settings.general.autoCompactThreshold.standard': 'Standard (90 %)', 'settings.general.autoCompactThreshold.standard': 'Standard (90 %)',
'settings.general.autoCompactThreshold.early': 'Précoce (80 %)', '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 lassistant 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.cachedStatus': 'État du modèle en cache',
'settings.general.localRuntime.cacheChecking': 'Vérification du cache du modèle local...', 'settings.general.localRuntime.cacheChecking': 'Vérification du cache du modèle local...',
'settings.general.localRuntime.cacheDownloaded': 'Téléchargé dans le cache du navigateur.', 'settings.general.localRuntime.cacheDownloaded': 'Téléchargé dans le cache du navigateur.',
+7
View File
@@ -54,6 +54,13 @@ export const zhCnMessages: TranslationMessages = {
'settings.general.autoCompactThreshold.conservative': '保守(95%', 'settings.general.autoCompactThreshold.conservative': '保守(95%',
'settings.general.autoCompactThreshold.standard': '标准(90%', 'settings.general.autoCompactThreshold.standard': '标准(90%',
'settings.general.autoCompactThreshold.early': '提前(80%', '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.cachedStatus': '缓存模型状态',
'settings.general.localRuntime.cacheChecking': '正在检查本地模型缓存...', 'settings.general.localRuntime.cacheChecking': '正在检查本地模型缓存...',
'settings.general.localRuntime.cacheDownloaded': '已下载到浏览器缓存。', 'settings.general.localRuntime.cacheDownloaded': '已下载到浏览器缓存。',
+7
View File
@@ -54,6 +54,13 @@ export const zhHkMessages: TranslationMessages = {
'settings.general.autoCompactThreshold.conservative': '保守(95%', 'settings.general.autoCompactThreshold.conservative': '保守(95%',
'settings.general.autoCompactThreshold.standard': '標準(90%', 'settings.general.autoCompactThreshold.standard': '標準(90%',
'settings.general.autoCompactThreshold.early': '提前(80%', '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.cachedStatus': '緩存模型狀態',
'settings.general.localRuntime.cacheChecking': '正在檢查本地模型緩存...', 'settings.general.localRuntime.cacheChecking': '正在檢查本地模型緩存...',
'settings.general.localRuntime.cacheDownloaded': '已下載到瀏覽器緩存。', 'settings.general.localRuntime.cacheDownloaded': '已下載到瀏覽器緩存。',
+32
View File
@@ -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';
}