Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
58 changes: 58 additions & 0 deletions packages/cli/src/utils/languageUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
97 changes: 97 additions & 0 deletions packages/core/src/subagents/subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/subagents/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}