Skip to content
22 changes: 12 additions & 10 deletions docs/users/features/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Every Qwen Code session starts with a fresh context window. Two mechanisms carry knowledge across sessions so you don't have to re-explain yourself every time:

- **QWEN.md** — instructions *you* write once and Qwen reads every session
- **QWEN.md** — instructions _you_ write once and Qwen reads every session
- **Auto-memory** — notes Qwen writes itself based on what it learns from you

---
Expand All @@ -24,9 +24,9 @@ Don't include things Qwen can figure out by reading your code. QWEN.md works bes

### Where to create QWEN.md

| File | Who it applies to |
|---|---|
| `~/.qwen/QWEN.md` | You, across all your projects |
| File | Who it applies to |
| ----------------------------- | --------------------------------------------- |
| `~/.qwen/QWEN.md` | You, across all your projects |
| `QWEN.md` in the project root | Your whole team (commit it to source control) |

You can have both. Qwen loads all QWEN.md files it finds when you start a session — your personal one plus any in the project.
Expand All @@ -45,6 +45,7 @@ You can point QWEN.md at other files so Qwen reads them too:
See @README.md for project overview.

# Conventions

- Git workflow: @docs/git-workflow.md
```

Expand All @@ -62,12 +63,12 @@ This is different from QWEN.md: you don't write it, Qwen does.

Qwen looks for four kinds of things worth remembering:

| What | Examples |
|---|---|
| **About you** | Your role, background, how you like to work |
| **Your feedback** | Corrections you made, approaches you confirmed |
| **Project context** | Ongoing work, decisions, goals not obvious from the code |
| **External references** | Dashboards, ticket trackers, docs links you mentioned |
| What | Examples |
| ----------------------- | -------------------------------------------------------- |
| **About you** | Your role, background, how you like to work |
| **Your feedback** | Corrections you made, approaches you confirmed |
| **Project context** | Ongoing work, decisions, goals not obvious from the code |
| **External references** | Dashboards, ticket trackers, docs links you mentioned |

Qwen doesn't save everything — only things that would actually be useful next time.

Expand Down Expand Up @@ -150,6 +151,7 @@ Runs the memory cleanup now instead of waiting for the automatic schedule:
Open `/memory` to see which files are loaded. If your file isn't listed, Qwen can't see it — make sure it's in the project root or `~/.qwen/`.

Instructions work better when they're specific:

- ✓ `Use 2-space indentation for TypeScript files`
- ✗ `Format code nicely`

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/ui/components/MemoryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ export function MemoryDialog({ onClose }: MemoryDialogProps) {
{index + 1}. {item.label}
</Text>
{item.description ? (
<Text color={theme.text.secondary}>{` ${item.description}`}</Text>
<Text
color={theme.text.secondary}
>{` ${item.description}`}</Text>
) : null}
</Box>
);
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/ui/hooks/useReactToolScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,9 @@ export function mapToDisplay(
return {
type: 'tool_group',
tools: toolDisplays,
memoryWriteCount: toolDisplays.filter((t) => t.isMemoryOp === 'write').length || undefined,
memoryReadCount: toolDisplays.filter((t) => t.isMemoryOp === 'read').length || undefined,
memoryWriteCount:
toolDisplays.filter((t) => t.isMemoryOp === 'write').length || undefined,
memoryReadCount:
toolDisplays.filter((t) => t.isMemoryOp === 'read').length || undefined,
};
}
5 changes: 1 addition & 4 deletions packages/cli/src/utils/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,7 @@ describe('parseSlashCommand', () => {
});

it('should parse a subcommand with arguments', () => {
const result = parseSlashCommand(
'/config set theme dark',
mockCommands,
);
const result = parseSlashCommand('/config set theme dark', mockCommands);
expect(result.commandToExecute?.name).toBe('set');
expect(result.args).toBe('theme dark');
expect(result.canonicalPath).toEqual(['config', 'set']);
Expand Down
82 changes: 35 additions & 47 deletions packages/core/src/agents/runtime/agent-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,9 @@ export interface CreateChatOptions {
/**
* Optional conversation history from a parent session. When provided,
* this history is prepended to the chat so the agent has prior
* conversational context (e.g., from the main session that spawned it).
* conversational context (e.g., from AgentInteractive.start()).
*/
extraHistory?: Content[];
/**
* When provided, replaces the auto-built generationConfig
* (systemInstruction, temperature, etc.) with this exact config.
* Used by fork subagents to share the parent conversation's cache
* prefix for DashScope prompt caching.
*/
generationConfigOverride?: GenerateContentConfig & {
systemInstruction?: string | Content;
};
/**
* When true, skip injecting the env bootstrap messages from
* `getInitialChatHistory()`. Set by fork subagents because their
* `extraHistory` is the full parent history that already contains
* those env messages — re-injecting would duplicate them.
*
* Other callers (e.g. arena interactive agents) pass an
* env-stripped history and DO need fresh env init for their own
* working directory, so they must leave this unset.
*/
skipEnvHistory?: boolean;
}

/**
Expand Down Expand Up @@ -243,21 +223,31 @@ export class AgentCore {
context: ContextState,
options?: CreateChatOptions,
): Promise<GeminiChat | undefined> {
if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) {
if (
!this.promptConfig.systemPrompt &&
!this.promptConfig.renderedSystemPrompt &&
!this.promptConfig.initialMessages
) {
throw new Error(
'PromptConfig must have either `systemPrompt` or `initialMessages` defined.',
'PromptConfig must have `systemPrompt`, `renderedSystemPrompt`, or `initialMessages` defined.',
);
}
if (this.promptConfig.systemPrompt && this.promptConfig.initialMessages) {
if (
this.promptConfig.systemPrompt &&
this.promptConfig.renderedSystemPrompt
) {
throw new Error(
'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.',
'PromptConfig cannot have both `systemPrompt` and `renderedSystemPrompt` defined.',
);
}

// Skip env bootstrap when the caller (fork) explicitly says its
// extraHistory already contains those messages. Other callers that
// provide an env-stripped history (e.g. arena) still get fresh env init.
const envHistory = options?.skipEnvHistory
// When initialMessages is set, the caller owns the full prior history
// (including any env bootstrap it wants). Fork relies on this to inherit
// the parent conversation verbatim without duplicating env messages.
const hasInitialMessages =
!!this.promptConfig.initialMessages &&
this.promptConfig.initialMessages.length > 0;
const envHistory = hasInitialMessages
? []
: await getInitialChatHistory(this.runtimeContext);

Expand All @@ -267,24 +257,19 @@ export class AgentCore {
...(this.promptConfig.initialMessages ?? []),
];

// If an override is provided (fork path), use it directly for cache
// sharing. Otherwise, build the config from this agent's promptConfig.
// Note: buildChatSystemPrompt is called OUTSIDE the try/catch so template
// errors propagate to the caller (not swallowed by reportError).
let generationConfig: GenerateContentConfig & {
// Build generationConfig. For fork subagents, `renderedSystemPrompt`
// carries the parent's exact rendered systemInstruction so the fork
// shares a byte-identical cache prefix. Otherwise, template
// `systemPrompt` via buildChatSystemPrompt (which may throw — kept
// outside the try/catch so template errors surface to the caller).
const generationConfig: GenerateContentConfig & {
systemInstruction?: string | Content;
};

if (options?.generationConfigOverride) {
generationConfig = options.generationConfigOverride;
} else {
const systemInstruction = this.promptConfig.systemPrompt
? this.buildChatSystemPrompt(context, options)
: undefined;
generationConfig = {
temperature: this.modelConfig.temp,
topP: this.modelConfig.top_p,
};
} = {};
if (this.promptConfig.renderedSystemPrompt !== undefined) {
generationConfig.systemInstruction =
this.promptConfig.renderedSystemPrompt;
} else if (this.promptConfig.systemPrompt) {
const systemInstruction = this.buildChatSystemPrompt(context, options);
if (systemInstruction) {
generationConfig.systemInstruction = systemInstruction;
}
Expand Down Expand Up @@ -330,7 +315,10 @@ export class AgentCore {
(t): t is FunctionDeclaration => typeof t !== 'string',
);

if (hasWildcard || asStrings.length === 0) {
if (
hasWildcard ||
(asStrings.length === 0 && onlyInlineDecls.length === 0)
) {
toolsList.push(
...toolRegistry
.getFunctionDeclarations()
Expand Down
68 changes: 49 additions & 19 deletions packages/core/src/agents/runtime/agent-headless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,6 @@ describe('subagent.ts', () => {

const defaultModelConfig: ModelConfig = {
model: 'qwen3-coder-plus',
temp: 0.5, // Specific temp to test override
top_p: 1,
};

const defaultRunConfig: RunConfig = {
Expand Down Expand Up @@ -439,8 +437,6 @@ describe('subagent.ts', () => {
// Check Generation Config
const generationConfig = getGenerationConfigFromMock();

// Check temperature override
expect(generationConfig.temperature).toBe(defaultModelConfig.temp);
expect(generationConfig.systemInstruction).toContain(
'Hello Agent, your task is Testing.',
);
Expand Down Expand Up @@ -556,15 +552,20 @@ describe('subagent.ts', () => {
expect(sysPrompt).not.toContain('---');
});

it('should use initialMessages instead of systemPrompt if provided', async () => {
it('should replace env history with initialMessages when both initialMessages and systemPrompt are set', async () => {
const { config } = await createMockConfig();
vi.mocked(GeminiChat).mockClear();

const initialMessages: Content[] = [
{ role: 'user', parts: [{ text: 'Hi' }] },
{ role: 'user', parts: [{ text: 'prior user turn' }] },
{ role: 'model', parts: [{ text: 'prior model turn' }] },
];
const promptConfig: PromptConfig = { initialMessages };
const promptConfig: PromptConfig = {
systemPrompt: 'System ${name}.',
initialMessages,
};
const context = new ContextState();
context.set('name', 'Agent');

// Model stops immediately
mockSendMessageStream.mockImplementation(createMockStream(['stop']));
Expand All @@ -583,15 +584,44 @@ describe('subagent.ts', () => {
const generationConfig = getGenerationConfigFromMock();
const history = callArgs[2];

expect(generationConfig.systemInstruction).toBeUndefined();
expect(history).toEqual([
{ role: 'user', parts: [{ text: 'Env Context' }] },
{
role: 'model',
parts: [{ text: 'Got it. Thanks for the context!' }],
},
...initialMessages,
]);
// systemPrompt is templated normally.
expect(generationConfig.systemInstruction).toContain('System Agent.');
expect(generationConfig.systemInstruction).toContain(
'Important Rules:',
);
// Env bootstrap is skipped; history is exactly initialMessages.
expect(history).toEqual(initialMessages);
});

it('should use renderedSystemPrompt verbatim and bypass templating', async () => {
const { config } = await createMockConfig();
vi.mocked(GeminiChat).mockClear();

const rendered = 'Verbatim parent system prompt ${name}';
const promptConfig: PromptConfig = {
renderedSystemPrompt: rendered,
initialMessages: [
{ role: 'user', parts: [{ text: 'hi' }] },
{ role: 'model', parts: [{ text: 'ok' }] },
],
};
const context = new ContextState();

mockSendMessageStream.mockImplementation(createMockStream(['stop']));

const scope = await AgentHeadless.create(
'test-agent',
config,
promptConfig,
defaultModelConfig,
defaultRunConfig,
);

await scope.execute(context);

const generationConfig = getGenerationConfigFromMock();
// No ${name} substitution and no non-interactive rules appended.
expect(generationConfig.systemInstruction).toBe(rendered);
});

it('should throw an error if template variables are missing', async () => {
Expand All @@ -618,11 +648,11 @@ describe('subagent.ts', () => {
expect(scope.getTerminateMode()).toBe(AgentTerminateMode.ERROR);
});

it('should validate that systemPrompt and initialMessages are mutually exclusive', async () => {
it('should validate that systemPrompt and renderedSystemPrompt are mutually exclusive', async () => {
const { config } = await createMockConfig();
const promptConfig: PromptConfig = {
systemPrompt: 'System',
initialMessages: [{ role: 'user', parts: [{ text: 'Hi' }] }],
renderedSystemPrompt: 'Rendered',
};
const context = new ContextState();

Expand All @@ -635,7 +665,7 @@ describe('subagent.ts', () => {
);

await expect(agent.execute(context)).rejects.toThrow(
'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.',
'PromptConfig cannot have both `systemPrompt` and `renderedSystemPrompt` defined.',
);
expect(agent.getTerminateMode()).toBe(AgentTerminateMode.ERROR);
});
Expand Down
17 changes: 2 additions & 15 deletions packages/core/src/agents/runtime/agent-headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,21 +192,8 @@ export class AgentHeadless {
async execute(
context: ContextState,
externalSignal?: AbortSignal,
options?: {
extraHistory?: Array<import('@google/genai').Content>;
/** Override generationConfig for cache sharing (fork subagent). */
generationConfigOverride?: import('@google/genai').GenerateContentConfig;
/** Override tool declarations for cache sharing (fork subagent). */
toolsOverride?: Array<import('@google/genai').FunctionDeclaration>;
/** Skip env bootstrap injection (fork already inherits parent env). */
skipEnvHistory?: boolean;
},
): Promise<void> {
const chat = await this.core.createChat(context, {
extraHistory: options?.extraHistory,
generationConfigOverride: options?.generationConfigOverride,
skipEnvHistory: options?.skipEnvHistory,
});
const chat = await this.core.createChat(context);

if (!chat) {
this.terminateMode = AgentTerminateMode.ERROR;
Expand All @@ -225,7 +212,7 @@ export class AgentHeadless {
abortController.abort();
}

const toolsList = options?.toolsOverride ?? this.core.prepareTools();
const toolsList = this.core.prepareTools();

const initialTaskText = String(
(context.get('task_prompt') as string) ?? 'Get Started!',
Expand Down
Loading
Loading