diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index c31ffa216c..f5a48676a3 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -701,14 +701,21 @@ export async function loadCliConfig( } // Automatically load output-language.md if it exists - let outputLanguageFilePath: string | undefined = path.join( + const projectStorage = new Storage(cwd); + const projectOutputLanguagePath = path.join( + projectStorage.getQwenDir(), + 'output-language.md', + ); + const globalOutputLanguagePath = path.join( Storage.getGlobalQwenDir(), 'output-language.md', ); - if (fs.existsSync(outputLanguageFilePath)) { - // output-language.md found - will be added to context files - } else { - outputLanguageFilePath = undefined; + + let outputLanguageFilePath: string | undefined; + if (fs.existsSync(projectOutputLanguagePath)) { + outputLanguageFilePath = projectOutputLanguagePath; + } else if (fs.existsSync(globalOutputLanguagePath)) { + outputLanguageFilePath = globalOutputLanguagePath; } const fileService = new FileDiscoveryService(cwd); diff --git a/packages/cli/src/utils/languageUtils.test.ts b/packages/cli/src/utils/languageUtils.test.ts index a0f0ca717e..7081f0c943 100644 --- a/packages/cli/src/utils/languageUtils.test.ts +++ b/packages/cli/src/utils/languageUtils.test.ts @@ -380,4 +380,62 @@ describe('languageUtils', () => { expect(fs.writeFileSync).not.toHaveBeenCalled(); }); }); + + describe('output-language.md path resolution priority', () => { + it('should prefer project-level path over global path', () => { + const projectPath = '/project/.qwen/output-language.md'; + const globalPath = '/mock/home/.qwen/output-language.md'; + + vi.mocked(fs.existsSync).mockImplementation((p) => { + if (p.toString() === projectPath) return true; + if (p.toString() === globalPath) return true; + return false; + }); + + let resolvedPath: string | undefined; + if (fs.existsSync(projectPath)) { + resolvedPath = projectPath; + } else if (fs.existsSync(globalPath)) { + resolvedPath = globalPath; + } + + expect(resolvedPath).toBe(projectPath); + }); + + it('should fall back to global path when project-level does not exist', () => { + const projectPath = '/project/.qwen/output-language.md'; + const globalPath = '/mock/home/.qwen/output-language.md'; + + vi.mocked(fs.existsSync).mockImplementation((p) => { + if (p.toString() === projectPath) return false; + if (p.toString() === globalPath) return true; + return false; + }); + + let resolvedPath: string | undefined; + if (fs.existsSync(projectPath)) { + resolvedPath = projectPath; + } else if (fs.existsSync(globalPath)) { + resolvedPath = globalPath; + } + + expect(resolvedPath).toBe(globalPath); + }); + + it('should return undefined when neither path exists', () => { + const projectPath = '/project/.qwen/output-language.md'; + const globalPath = '/mock/home/.qwen/output-language.md'; + + vi.mocked(fs.existsSync).mockReturnValue(false); + + let resolvedPath: string | undefined; + if (fs.existsSync(projectPath)) { + resolvedPath = projectPath; + } else if (fs.existsSync(globalPath)) { + resolvedPath = globalPath; + } + + expect(resolvedPath).toBeUndefined(); + }); + }); }); diff --git a/packages/core/src/subagents/subagent.test.ts b/packages/core/src/subagents/subagent.test.ts index ce6e64ae49..0286d11c85 100644 --- a/packages/core/src/subagents/subagent.test.ts +++ b/packages/core/src/subagents/subagent.test.ts @@ -458,6 +458,103 @@ describe('subagent.ts', () => { ]); }); + it('should append userMemory to the system prompt when available', async () => { + const { config } = await createMockConfig(); + const userMemoryContent = + '# Output language preference: English\nRespond in English.'; + vi.spyOn(config, 'getUserMemory').mockReturnValue(userMemoryContent); + + vi.mocked(GeminiChat).mockClear(); + + const promptConfig: PromptConfig = { + systemPrompt: 'You are a test agent.', + }; + const context = new ContextState(); + + mockSendMessageStream.mockImplementation(createMockStream(['stop'])); + + const scope = await SubAgentScope.create( + 'test-agent', + config, + promptConfig, + defaultModelConfig, + defaultRunConfig, + ); + + await scope.runNonInteractive(context); + + const generationConfig = getGenerationConfigFromMock(); + expect(generationConfig.systemInstruction).toContain( + 'You are a test agent.', + ); + expect(generationConfig.systemInstruction).toContain( + 'Important Rules:', + ); + expect(generationConfig.systemInstruction).toContain( + '# Output language preference: English', + ); + expect(generationConfig.systemInstruction).toContain( + 'Respond in English.', + ); + }); + + it('should not append userMemory separator when userMemory is empty', async () => { + const { config } = await createMockConfig(); + vi.spyOn(config, 'getUserMemory').mockReturnValue(''); + + vi.mocked(GeminiChat).mockClear(); + + const promptConfig: PromptConfig = { + systemPrompt: 'You are a test agent.', + }; + const context = new ContextState(); + + mockSendMessageStream.mockImplementation(createMockStream(['stop'])); + + const scope = await SubAgentScope.create( + 'test-agent', + config, + promptConfig, + defaultModelConfig, + defaultRunConfig, + ); + + await scope.runNonInteractive(context); + + const generationConfig = getGenerationConfigFromMock(); + const sysPrompt = generationConfig.systemInstruction as string; + expect(sysPrompt).toContain('You are a test agent.'); + expect(sysPrompt).not.toContain('---'); + }); + + it('should not append userMemory separator when userMemory is whitespace-only', async () => { + const { config } = await createMockConfig(); + vi.spyOn(config, 'getUserMemory').mockReturnValue(' \n\n '); + + vi.mocked(GeminiChat).mockClear(); + + const promptConfig: PromptConfig = { + systemPrompt: 'You are a test agent.', + }; + const context = new ContextState(); + + mockSendMessageStream.mockImplementation(createMockStream(['stop'])); + + const scope = await SubAgentScope.create( + 'test-agent', + config, + promptConfig, + defaultModelConfig, + defaultRunConfig, + ); + + await scope.runNonInteractive(context); + + const generationConfig = getGenerationConfigFromMock(); + const sysPrompt = generationConfig.systemInstruction as string; + expect(sysPrompt).not.toContain('---'); + }); + it('should use initialMessages instead of systemPrompt if provided', async () => { const { config } = await createMockConfig(); vi.mocked(GeminiChat).mockClear(); diff --git a/packages/core/src/subagents/subagent.ts b/packages/core/src/subagents/subagent.ts index c9328e5ad0..613bc80441 100644 --- a/packages/core/src/subagents/subagent.ts +++ b/packages/core/src/subagents/subagent.ts @@ -999,6 +999,12 @@ Important Rules: - Use tools only when necessary to obtain facts or make changes. - When the task is complete, return the final result as a normal model response (not a tool call) and stop.`; + // Append user memory (QWEN.md + output-language.md) to ensure subagent respects project conventions + const userMemory = this.runtimeContext.getUserMemory(); + if (userMemory && userMemory.trim().length > 0) { + finalPrompt += `\n\n---\n\n${userMemory.trim()}`; + } + return finalPrompt; } }