diff --git a/public/config.json b/public/config.json index 3940f0b..de62599 100644 --- a/public/config.json +++ b/public/config.json @@ -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, diff --git a/src/agent/core/AgentCore.test.ts b/src/agent/core/AgentCore.test.ts index cf0a27f..2cc9f7c 100644 --- a/src/agent/core/AgentCore.test.ts +++ b/src/agent/core/AgentCore.test.ts @@ -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([ + ['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 = []; + public tools: OpenAIToolDefinition[][] = []; constructor(private readonly scripts: StreamChunk[][]) {} - async *generateStream(messages: Message[]): AsyncIterableIterator { + async *generateStream( + messages: Message[], + systemPrompt?: string, + tools?: OpenAIToolDefinition[], + ): AsyncIterableIterator { 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 { 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(); + }); }); diff --git a/src/agent/core/AgentCore.ts b/src/agent/core/AgentCore.ts index 26e1ba9..ec38c15 100644 --- a/src/agent/core/AgentCore.ts +++ b/src/agent/core/AgentCore.ts @@ -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 { 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()), diff --git a/src/agent/llm/LocalBrowserLLMProvider.ts b/src/agent/llm/LocalBrowserLLMProvider.ts index b0b9a37..511d9f7 100644 --- a/src/agent/llm/LocalBrowserLLMProvider.ts +++ b/src/agent/llm/LocalBrowserLLMProvider.ts @@ -56,10 +56,6 @@ async function importMediaPipe(): Promise { export class LocalBrowserLLMProvider implements LLMProvider { private inference: GemmaInference | null = null; - getPreferredSystemPromptPath(): string | undefined { - return 'prompts/system_compact.md'; - } - getContextWindow(): number { return this.getConfiguredContextLength(); } diff --git a/src/agent/tools/BaseTool.ts b/src/agent/tools/BaseTool.ts index a3469cd..18e09c7 100644 --- a/src/agent/tools/BaseTool.ts +++ b/src/agent/tools/BaseTool.ts @@ -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. diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index 6399f92..141d136 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -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 = ({ 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); diff --git a/src/components/settings/sections/GeneralSettings.test.tsx b/src/components/settings/sections/GeneralSettings.test.tsx index c744b97..6e2f8d0 100644 --- a/src/components/settings/sections/GeneralSettings.test.tsx +++ b/src/components/settings/sections/GeneralSettings.test.tsx @@ -14,6 +14,7 @@ const { localSeparatorModelCacheMock } = vi.hoisted(() => ({ const configState = new Map([ ['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(); diff --git a/src/components/settings/sections/GeneralSettings.tsx b/src/components/settings/sections/GeneralSettings.tsx index 9d937af..33354c5 100644 --- a/src/components/settings/sections/GeneralSettings.tsx +++ b/src/components/settings/sections/GeneralSettings.tsx @@ -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, string> = { en_us: 'English', @@ -29,6 +36,7 @@ const LANGUAGE_OPTION_LABELS: Record, string> = const GeneralSettings: React.FC = () => { const { t, setLanguageSetting } = useI18n(); const [language, setLanguage] = useState('auto'); + const [agentMode, setAgentMode] = useState(DEFAULT_AGENT_MODE); const [llmProvider, setLlmProvider] = useState(LOCAL_LLM_PROVIDER_KEY); const [openaiKey, setOpenaiKey] = useState(''); const [openaiModel, setOpenaiModel] = useState(''); @@ -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 = () => { +
+

{t('settings.general.musicAssistant.section')}

+ +
+ + +
+ {isAgentModeOverriddenByLocalProvider + ? t('settings.general.musicAssistant.agentMode.localOverride') + : t('settings.general.musicAssistant.agentMode.help')} +
+ {effectiveAgentMode !== agentMode && ( +
+ {t('settings.general.musicAssistant.agentMode.effectiveValue', { + mode: t('settings.general.musicAssistant.agentMode.efficient'), + })} +
+ )} +
+
+

{LOCAL_LLM_DISPLAY_NAME} Local Runtime

diff --git a/src/constants/coreConstants.ts b/src/constants/coreConstants.ts index 4f885fe..ec73d83 100644 --- a/src/constants/coreConstants.ts +++ b/src/constants/coreConstants.ts @@ -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 = { diff --git a/src/core/config-upgrader/KGConfigUpgrader.ts b/src/core/config-upgrader/KGConfigUpgrader.ts index 96acc28..06a9c2d 100644 --- a/src/core/config-upgrader/KGConfigUpgrader.ts +++ b/src/core/config-upgrader/KGConfigUpgrader.ts @@ -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}`); } diff --git a/src/core/config-upgrader/upgradeConfigToV5.test.ts b/src/core/config-upgrader/upgradeConfigToV5.test.ts new file mode 100644 index 0000000..abcd86b --- /dev/null +++ b/src/core/config-upgrader/upgradeConfigToV5.test.ts @@ -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 = { + 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 = { + general: { + agent_mode: 'efficient', + }, + }; + getRawMock.mockResolvedValue(config); + + await upgradeConfigToV5(); + + expect(saveRawMock).not.toHaveBeenCalled(); + expect((config.general as Record).agent_mode).toBe('efficient'); + }); +}); diff --git a/src/core/config-upgrader/upgradeConfigToV5.ts b/src/core/config-upgrader/upgradeConfigToV5.ts new file mode 100644 index 0000000..07746be --- /dev/null +++ b/src/core/config-upgrader/upgradeConfigToV5.ts @@ -0,0 +1,30 @@ +import { KGConfigStorage } from '../io/KGConfigStorage'; + +const CONFIG_KEY = 'userConfig'; + +export async function upgradeConfigToV5(): Promise { + const storage = KGConfigStorage.getInstance(); + const rawConfig = await storage.getRaw(CONFIG_KEY); + if (!rawConfig || typeof rawConfig !== 'object') { + return; + } + + const config = rawConfig as Record; + 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; + if ('agent_mode' in generalRecord) { + return; + } + + generalRecord.agent_mode = 'regular'; + await storage.saveRaw(CONFIG_KEY, config); +} diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index 1e7c0db..86571b7 100644 --- a/src/core/config/ConfigManager.ts +++ b/src/core/config/ConfigManager.ts @@ -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, diff --git a/src/i18n/messages/en_us.ts b/src/i18n/messages/en_us.ts index 0c7e852..a58f6b7 100644 --- a/src/i18n/messages/en_us.ts +++ b/src/i18n/messages/en_us.ts @@ -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.', diff --git a/src/i18n/messages/fr_fr.ts b/src/i18n/messages/fr_fr.ts index f2bd72d..5769f7c 100644 --- a/src/i18n/messages/fr_fr.ts +++ b/src/i18n/messages/fr_fr.ts @@ -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.', diff --git a/src/i18n/messages/zh_cn.ts b/src/i18n/messages/zh_cn.ts index 641c765..23b654f 100644 --- a/src/i18n/messages/zh_cn.ts +++ b/src/i18n/messages/zh_cn.ts @@ -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': '已下载到浏览器缓存。', diff --git a/src/i18n/messages/zh_hk.ts b/src/i18n/messages/zh_hk.ts index c49b6c6..cb548c0 100644 --- a/src/i18n/messages/zh_hk.ts +++ b/src/i18n/messages/zh_hk.ts @@ -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': '已下載到瀏覽器緩存。', diff --git a/src/util/agentMode.ts b/src/util/agentMode.ts new file mode 100644 index 0000000..6829579 --- /dev/null +++ b/src/util/agentMode.ts @@ -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'; +}