From 5dec0d9b157a1bd9ac3df370a1e3bf2c92e19a1e Mon Sep 17 00:00:00 2001 From: qqqys Date: Thu, 20 Aug 2026 15:55:46 +0800 Subject: [PATCH 1/7] feat(core): add the output-style layer to the system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces output styles: a named prompt section that changes how the agent reports its work, selected per session. This is the core half — the registry, the four built-in styles, and the prompt wiring. Nothing selects a style yet; the picker, the settings key, and the per-turn reminder follow separately. The style lands at the end of the stable `base` layer: after the mandates it refines, and still ahead of every context/volatile layer, so the prompt prefix stays cacheable for the whole session. Two precedence rules, both following what the file already does for QWEN_SYSTEM_IDENTITY_MD: - A QWEN_SYSTEM_MD override wins. That file is a full, user-owned prompt; layering our section onto it would defeat the override. - The QWEN_WRITE_SYSTEM_MD dump stays a pure base prompt. A style sits on top of a base, so baking it into the dump would apply it twice once that file is fed back through QWEN_SYSTEM_MD. `keepCodingInstructions: false` lets a style replace the base outright, for styles that are not about software engineering at all. All four built-ins keep it true — Proactive in particular changes how much you plan and ask, not what you are allowed to do, and says so in its prompt so it does not read as a second permission knob next to ApprovalMode. Subagents and arena runs deliberately do not inherit the main session's style: it would multiply Explanatory's insight blocks across every child, and skew an arena comparison that is supposed to isolate the model. --- .../src/ui/commands/contextCommand.test.ts | 7 + .../cli/src/ui/commands/contextCommand.ts | 1 + packages/core/src/config/config.ts | 17 ++ packages/core/src/core/client.test.ts | 28 +++ packages/core/src/core/client.ts | 1 + packages/core/src/core/output-styles.test.ts | 117 +++++++++++++ packages/core/src/core/output-styles.ts | 162 ++++++++++++++++++ packages/core/src/core/prompts.test.ts | 76 ++++++++ packages/core/src/core/prompts.ts | 18 +- packages/core/src/index.ts | 1 + 10 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/core/output-styles.test.ts create mode 100644 packages/core/src/core/output-styles.ts diff --git a/packages/cli/src/ui/commands/contextCommand.test.ts b/packages/cli/src/ui/commands/contextCommand.test.ts index 1b6472a3a94..cf97ee11c54 100644 --- a/packages/cli/src/ui/commands/contextCommand.test.ts +++ b/packages/cli/src/ui/commands/contextCommand.test.ts @@ -50,6 +50,7 @@ function makeMockConfig(contextWindowSize = 32_000): Config { }), getVisibleTools: vi.fn().mockReturnValue(new Set()), getUserMemory: vi.fn().mockReturnValue(''), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ listSkills: vi.fn().mockResolvedValue([]), @@ -83,6 +84,7 @@ describe('collectContextData (contextCommand)', () => { }), getVisibleTools: vi.fn().mockReturnValue(new Set()), getUserMemory: vi.fn().mockReturnValue(''), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ listSkills: vi.fn().mockResolvedValue([]), @@ -207,6 +209,7 @@ describe('collectContextData (contextCommand)', () => { }), getVisibleTools: vi.fn().mockReturnValue(new Set()), getUserMemory: vi.fn().mockReturnValue(''), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ listSkills: vi.fn().mockResolvedValue([]), @@ -254,6 +257,7 @@ describe('collectContextData (contextCommand)', () => { }), getVisibleTools: vi.fn().mockReturnValue(new Set(['web_fetch'])), getUserMemory: vi.fn().mockReturnValue(''), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ listSkills: vi.fn().mockResolvedValue([]), @@ -280,6 +284,7 @@ describe('collectContextData (contextCommand)', () => { const config = { ...makeMockConfig(), getUserMemory: vi.fn().mockReturnValue(''), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi .fn() .mockReturnValue('# auto memory\nMEMORY_INDEX_MARKER'), @@ -306,6 +311,7 @@ describe('collectContextData (contextCommand)', () => { const config = { ...makeMockConfig(), getUserMemory: vi.fn().mockReturnValue(memory), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getWorkingDir: vi.fn().mockReturnValue(workingDir), } as unknown as Config; @@ -331,6 +337,7 @@ describe('collectContextData (contextCommand)', () => { const config = { ...makeMockConfig(), getUserMemory: vi.fn().mockReturnValue(memory), + getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getWorkingDir: vi.fn().mockReturnValue(workingDir), } as unknown as Config; diff --git a/packages/cli/src/ui/commands/contextCommand.ts b/packages/cli/src/ui/commands/contextCommand.ts index 6dbeb258f6e..ceb788c194f 100644 --- a/packages/cli/src/ui/commands/contextCommand.ts +++ b/packages/cli/src/ui/commands/contextCommand.ts @@ -144,6 +144,7 @@ export async function collectContextData( modelName, undefined, resolveInteractionMode(config), + config.getOutputStyle(), ); const systemPromptTokens = estimateTokens(systemPromptText); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 365d9de269a..63aea868e0d 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -37,6 +37,7 @@ import type { TeamContext } from '../agents/team/types.js'; import { BaseLlmClient } from '../core/baseLlmClient.js'; import { GeminiClient } from '../core/client.js'; import { resolveInteractionMode } from '../core/prompts.js'; +import type { OutputStyleDefinition } from '../core/output-styles.js'; import { AuthType, createContentGenerator, @@ -975,6 +976,7 @@ export interface ConfigParameters { question?: string; systemPrompt?: string; appendSystemPrompt?: string; + outputStyle?: OutputStyleDefinition; coreTools?: string[]; allowedTools?: string[]; excludeTools?: string[]; @@ -1858,6 +1860,7 @@ export class Config { private readonly systemPrompt: string | undefined; private readonly appendSystemPrompt: string | undefined; private liveAppendSystemPrompt: string | undefined; + private outputStyle: OutputStyleDefinition | undefined; private readonly coreTools: string[] | undefined; private readonly allowedTools: string[] | undefined; private readonly excludeTools: string[] | undefined; @@ -2197,6 +2200,7 @@ export class Config { this.question = params.question; this.systemPrompt = params.systemPrompt; this.appendSystemPrompt = params.appendSystemPrompt; + this.outputStyle = params.outputStyle; this.coreTools = params.coreTools; this.allowedTools = params.allowedTools; this.excludeTools = params.excludeTools; @@ -5483,6 +5487,19 @@ export class Config { this.liveAppendSystemPrompt = prompt; } + getOutputStyle(): OutputStyleDefinition | undefined { + return this.outputStyle; + } + + /** + * Swaps the active output style. Callers that change it mid-session must + * follow up with `GeminiClient.refreshSystemInstruction()`, since the style + * lives in the stable layer of an already-bound system instruction. + */ + setOutputStyle(style: OutputStyleDefinition | undefined): void { + this.outputStyle = style; + } + /** @deprecated Use getPermissionsAllow() instead. */ getCoreTools(): string[] | undefined { if (this.getBareMode()) { diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 1652f9855d8..05b04eabcff 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -79,6 +79,7 @@ import { getCustomSystemPrompt, getPlanModeSystemReminder, } from './prompts.js'; +import { getBuiltInOutputStyle } from './output-styles.js'; import { DEFAULT_QWEN_FLASH_MODEL } from '../config/models.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { promptIdContext } from '../utils/promptIdContext.js'; @@ -602,6 +603,7 @@ describe('Gemini Client (client.ts)', () => { getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSystemPrompt: vi.fn().mockReturnValue(undefined), getAppendSystemPrompt: vi.fn().mockReturnValue(undefined), + getOutputStyle: vi.fn().mockReturnValue(undefined), getStaticSystemPrefix: vi.fn().mockReturnValue(undefined), setStaticSystemPrefix: vi.fn(), getFullContext: vi.fn().mockReturnValue(false), @@ -13260,6 +13262,7 @@ Other open files: 'test-model', undefined, 'headless', + undefined, ); expect(mockContentGenerator.generateContent).toHaveBeenCalledWith( expect.objectContaining({ @@ -13271,6 +13274,30 @@ Other open files: ); }); + it('passes the active output style to the core system prompt', async () => { + const contents = [{ role: 'user', parts: [{ text: 'hello' }] }]; + const abortSignal = new AbortController().signal; + const concise = getBuiltInOutputStyle('Concise'); + + vi.mocked(getCoreSystemPrompt).mockClear(); + vi.spyOn(client['config'], 'getOutputStyle').mockReturnValue(concise); + + await client.generateContent( + contents, + {}, + abortSignal, + DEFAULT_QWEN_FLASH_MODEL, + ); + + expect(getCoreSystemPrompt).toHaveBeenCalledWith( + undefined, + 'test-model', + undefined, + 'headless', + concise, + ); + }); + it.each([ ['interactive', true, false], ['acp', false, true], @@ -13299,6 +13326,7 @@ Other open files: 'test-model', undefined, mode, + undefined, ); }, ); diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 0077c044b64..79f8aff603a 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -1169,6 +1169,7 @@ export class GeminiClient { this.config.getModel(), undefined, resolveInteractionMode(this.config), + this.config.getOutputStyle(), ); const stableLayers = { base, diff --git a/packages/core/src/core/output-styles.test.ts b/packages/core/src/core/output-styles.test.ts new file mode 100644 index 00000000000..5a460a969ae --- /dev/null +++ b/packages/core/src/core/output-styles.test.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { + BUILT_IN_OUTPUT_STYLES, + applyOutputStyle, + getBuiltInOutputStyle, + renderOutputStyleSection, + type OutputStyleDefinition, +} from './output-styles.js'; + +const LAYERED: OutputStyleDefinition = { + name: 'Layered', + source: 'built-in', + description: 'test style that keeps the coding instructions', + keepCodingInstructions: true, + prompt: 'Style body.', +}; + +const REPLACING: OutputStyleDefinition = { + ...LAYERED, + name: 'Replacing', + keepCodingInstructions: false, +}; + +describe('built-in output styles', () => { + it('ships the four documented styles', () => { + expect(BUILT_IN_OUTPUT_STYLES.map((style) => style.name)).toEqual([ + 'Concise', + 'Proactive', + 'Explanatory', + 'Learning', + ]); + }); + + it('gives every style a description and a non-empty prompt', () => { + for (const style of BUILT_IN_OUTPUT_STYLES) { + expect(style.description.trim()).not.toBe(''); + expect(style.prompt.trim()).not.toBe(''); + expect(style.source).toBe('built-in'); + } + }); + + it('keeps the coding instructions for every built-in style', () => { + // A built-in style refines how coding work is reported, so none of them + // may drop the mandates and safety sections of the base prompt. + for (const style of BUILT_IN_OUTPUT_STYLES) { + expect(style.keepCodingInstructions).toBe(true); + } + }); + + it('attaches a turn reminder only to the behaviour-constraining styles', () => { + const withReminder = BUILT_IN_OUTPUT_STYLES.filter( + (style) => style.turnReminder, + ).map((style) => style.name); + expect(withReminder).toEqual(['Concise', 'Proactive']); + }); + + it('has no duplicate names', () => { + const names = BUILT_IN_OUTPUT_STYLES.map((style) => + style.name.toLowerCase(), + ); + expect(new Set(names).size).toBe(names.length); + }); +}); + +describe('getBuiltInOutputStyle', () => { + it('resolves a style by exact name', () => { + expect(getBuiltInOutputStyle('Concise')?.name).toBe('Concise'); + }); + + it('resolves case-insensitively and ignores surrounding whitespace', () => { + expect(getBuiltInOutputStyle(' eXpLaNaToRy ')?.name).toBe('Explanatory'); + }); + + it('returns undefined for an unknown name', () => { + expect(getBuiltInOutputStyle('nope')).toBeUndefined(); + expect(getBuiltInOutputStyle('')).toBeUndefined(); + }); +}); + +describe('renderOutputStyleSection', () => { + it('renders the style under an active-style heading', () => { + expect(renderOutputStyleSection(LAYERED)).toBe( + '# Layered Style Active\n\nStyle body.', + ); + }); + + it('trims the prompt so the heading gap stays a single blank line', () => { + expect( + renderOutputStyleSection({ ...LAYERED, prompt: '\n\nStyle body.\n\n' }), + ).toBe('# Layered Style Active\n\nStyle body.'); + }); +}); + +describe('applyOutputStyle', () => { + it('returns the base prompt unchanged when no style is active', () => { + expect(applyOutputStyle('BASE', undefined)).toBe('BASE'); + expect(applyOutputStyle('BASE', null)).toBe('BASE'); + }); + + it('appends a style that keeps the coding instructions', () => { + expect(applyOutputStyle('BASE', LAYERED)).toBe( + 'BASE\n\n# Layered Style Active\n\nStyle body.', + ); + }); + + it('replaces the base prompt for a style that drops them', () => { + expect(applyOutputStyle('BASE', REPLACING)).toBe( + '# Replacing Style Active\n\nStyle body.', + ); + }); +}); diff --git a/packages/core/src/core/output-styles.ts b/packages/core/src/core/output-styles.ts new file mode 100644 index 00000000000..d09514d1102 --- /dev/null +++ b/packages/core/src/core/output-styles.ts @@ -0,0 +1,162 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Where an output style came from. Only `built-in` is populated today; the + * remaining sources exist so that user/project markdown files and extension + * bundles can be registered later without changing the consumer contract. + */ +export type OutputStyleSource = 'built-in' | 'user' | 'project' | 'extension'; + +export interface OutputStyleDefinition { + /** Display name, and the identifier used to select the style. */ + name: string; + source: OutputStyleSource; + /** One line shown in the style picker. */ + description: string; + /** + * `true` layers `prompt` onto the default coding prompt, so the mandates, + * workflows and tool guidance all stay in force. `false` replaces the base + * prompt outright, for styles that are not about software engineering at + * all; such a style is responsible for its own identity and safety wording. + */ + keepCodingInstructions: boolean; + /** The style section itself, rendered under a `# Style Active` heading. */ + prompt: string; + /** + * Optional one-line restatement injected as a `` on user + * turns. Styles that constrain *behaviour* drift over a long session and + * need it; styles that only add output (an explanation, a practice prompt) + * do not, and paying for a reminder every turn would be waste. + */ + turnReminder?: string; +} + +const CONCISE: OutputStyleDefinition = { + name: 'Concise', + source: 'built-in', + description: + 'Answers first, with no preamble, narration, or closing recap — the work stays as thorough', + keepCodingInstructions: true, + turnReminder: + 'Be concise: answer first, cut the narration, keep only what the user needs.', + prompt: `The user has chosen brevity over narration. + +- **Answer first.** Open with the result or the answer. No preamble ("Let me...", "I'll now...") and no closing summary of what you just said. +- **Cut narration, keep substance.** Do not replay the request, the plan, or a step-by-step account of what you did. Report outcomes, the decisions you made, and anything the user has to act on. +- **Short by default.** Answer a simple question in one to three sentences of prose. Reach for headings, tables, and lists only when the content genuinely has that shape, never as decoration. +- **Say it plainly.** Drop hedging boilerplate. Raise a caveat only when it changes what the user should do next. +- **Full detail on request.** When the user asks for an explanation, a walkthrough, or more depth, give it completely. Brevity is a default, never a reason to withhold what was asked for. +- **Correctness outranks brevity.** Error messages, failing test output, security findings, and confirmations for risky actions keep their full content. + +Where this conflicts with communication or formatting guidance elsewhere in these instructions, this section wins.`, +}; + +const PROACTIVE: OutputStyleDefinition = { + name: 'Proactive', + source: 'built-in', + description: + 'Starts work immediately and prefers a stated assumption over a question', + keepCodingInstructions: true, + turnReminder: + 'Work autonomously: start now and assume rather than ask on low-risk decisions, but keep confirming risky actions.', + prompt: `The user has chosen continuous, autonomous execution. + +- **Start now.** Begin implementing rather than proposing. On low-risk work, make a reasonable assumption and proceed. +- **Ask less.** Prefer a stated assumption over a question for routine decisions, and put that assumption in your response so the user can correct it. +- **Act before planning.** Do not enter plan mode unless the user asks for it. When the next step is unclear but the work is low-risk, start and adapt as you learn. +- **Expect course corrections.** Treat mid-flight suggestions and redirections as normal input rather than as a failure. + +This style changes how much you plan and ask; it does not change what you are allowed to do. The 'Executing actions with care' rules and the active permission policy still apply in full: destructive, hard-to-reverse, and outward-facing actions still need confirmation, and moving fast is never a reason to widen the scope of what was requested.`, +}; + +const EXPLANATORY: OutputStyleDefinition = { + name: 'Explanatory', + source: 'built-in', + description: + 'Explains implementation choices and codebase patterns alongside the work', + keepCodingInstructions: true, + prompt: `Alongside the engineering work, teach the user about this codebase. + +Before and after writing code, add a short educational note about the choices involved, formatted as: + +\`✳ Insight ─────────────────────────────\` +[2-3 key points] +\`───────────────────────────────────────\` + +- Prefer insights specific to this codebase or to the code you just wrote over general programming lessons. +- Insights belong in the conversation, never as comments in the code. +- These explanations may exceed the usual length guidance, but keep them relevant to the task at hand.`, +}; + +const LEARNING: OutputStyleDefinition = { + name: 'Learning', + source: 'built-in', + description: + 'Hands the user small, meaningful pieces of code to write, then waits', + keepCodingInstructions: true, + prompt: `Alongside the engineering work, help the user learn this codebase by writing part of it themselves. + +When you are about to produce 20 or more lines that involve a design decision (error handling, data structure choice), business logic with several valid approaches, or a key algorithm or interface, hand that piece to the user instead: + +1. Write the surrounding code yourself and leave exactly one \`TODO(human)\` marker where their piece goes. Add the marker with your editing tools *before* making the request. +2. Post the request in this shape: + +\`◆ Learn by Doing\` +**Context:** what is already built, and why this decision matters +**Your Task:** the specific function or section, named by file and by the \`TODO(human)\` marker — no line numbers +**Guidance:** the trade-offs and constraints to weigh + +3. Stop. Output nothing after the request and take no further action until the user has written their piece. + +- Ask for 2-10 lines at a time, framed as a real design decision rather than busy work. +- Keep routine implementation for yourself. +- If a todo list is tracking the task, include an item for the handoff so the pause is visible in the plan.`, +}; + +export const BUILT_IN_OUTPUT_STYLES: readonly OutputStyleDefinition[] = [ + CONCISE, + PROACTIVE, + EXPLANATORY, + LEARNING, +]; + +/** + * Resolves a built-in style by name, case-insensitively so that a hand-edited + * settings file or a `--output-style concise` argument works. + */ +export function getBuiltInOutputStyle( + name: string, +): OutputStyleDefinition | undefined { + const wanted = name.trim().toLowerCase(); + return BUILT_IN_OUTPUT_STYLES.find( + (style) => style.name.toLowerCase() === wanted, + ); +} + +/** Renders the style section as it appears in the system prompt. */ +export function renderOutputStyleSection(style: OutputStyleDefinition): string { + return `# ${style.name} Style Active\n\n${style.prompt.trim()}`; +} + +/** + * Layers an output style onto a base system prompt. + * + * Styles that keep the coding instructions are appended to the base, which + * puts them last in the stable layer — after the mandates they are meant to + * refine, and still ahead of every context/volatile layer, so the prompt + * prefix stays cacheable for the whole session. + */ +export function applyOutputStyle( + basePrompt: string, + style?: OutputStyleDefinition | null, +): string { + if (!style) { + return basePrompt; + } + const section = renderOutputStyleSection(style); + return style.keepCodingInstructions ? `${basePrompt}\n\n${section}` : section; +} diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index b91a8f46737..3b857081800 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -15,6 +15,10 @@ import { getCompressionPrompt, resolveInteractionMode, } from './prompts.js'; +import { + BUILT_IN_OUTPUT_STYLES, + getBuiltInOutputStyle, +} from './output-styles.js'; import { InputFormat } from '../output/types.js'; import { isGitRepository } from '../utils/gitUtils.js'; import fs from 'node:fs'; @@ -575,6 +579,78 @@ describe('Core System Prompt (prompts.ts)', () => { ); }); }); + + describe('outputStyle parameter', () => { + const concise = getBuiltInOutputStyle('Concise')!; + + it('leaves the prompt untouched when no style is active', () => { + const prompt = getCoreSystemPrompt(); + for (const style of BUILT_IN_OUTPUT_STYLES) { + expect(prompt).not.toContain(`# ${style.name} Style Active`); + } + }); + + it('appends the style section to the end of the base prompt', () => { + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + expect(prompt).toContain('# Concise Style Active'); + // The style refines the mandates, so it has to land after them... + expect(prompt.indexOf('# Concise Style Active')).toBeGreaterThan( + prompt.indexOf('# Core Mandates'), + ); + // ...and the base prompt must still be intact. + expect(prompt).toContain('# Core Mandates'); + }); + + it('keeps the style ahead of the context and volatile layers', () => { + const prompt = getCoreSystemPrompt( + 'MEMORY_MARKER', + undefined, + 'APPEND_MARKER', + 'interactive', + concise, + ); + const styleIndex = prompt.indexOf('# Concise Style Active'); + expect(styleIndex).toBeGreaterThan(-1); + expect(styleIndex).toBeLessThan(prompt.indexOf('MEMORY_MARKER')); + expect(styleIndex).toBeLessThan(prompt.indexOf('APPEND_MARKER')); + }); + + it('is ignored when QWEN_SYSTEM_MD replaces the base prompt', () => { + vi.stubEnv('QWEN_SYSTEM_MD', '/custom/path/system.md'); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + expect(prompt).toContain('custom system prompt'); + expect(prompt).not.toContain('# Concise Style Active'); + }); + + it('does not bake the style into the QWEN_WRITE_SYSTEM_MD dump', () => { + // The dump is meant to be reusable as a QWEN_SYSTEM_MD base; baking the + // style in would apply it twice when that file is fed back. + vi.stubEnv('QWEN_WRITE_SYSTEM_MD', 'true'); + getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + const [, written] = vi.mocked(fs.writeFileSync).mock.calls[0]; + expect(written).not.toContain('# Concise Style Active'); + }); + }); }); describe('Model-specific tool call formats', () => { diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 72d1aa58f0a..10a84102cda 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -14,6 +14,8 @@ import { QWEN_DIR } from '../config/storage.js'; import type { GenerateContentConfig } from '@google/genai'; import { InputFormat } from '../output/types.js'; import { createDebugLogger } from '../utils/debugLogger.js'; +import { applyOutputStyle } from './output-styles.js'; +import type { OutputStyleDefinition } from './output-styles.js'; const debugLogger = createDebugLogger('PROMPTS'); @@ -248,12 +250,15 @@ export function getCustomSystemPrompt( * @param appendInstruction - Back-compat convenience slot for the append prompt. * @deprecated Prefer the `appendPrompt` slot of `assembleSystemPrompt`. * @param interactionMode - Interactive vs. headless prompt variant. + * @param outputStyle - Active output style, layered onto the base prompt. + * Ignored when `QWEN_SYSTEM_MD` replaces the base prompt (see below). */ export function getCoreSystemPrompt( userMemory?: string, model?: string, appendInstruction?: string, interactionMode: SystemPromptInteractionMode = 'interactive', + outputStyle?: OutputStyleDefinition | null, ): string { // if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file // default path is .qwen/system.md (project-level), can be overridden via QWEN_SYSTEM_MD @@ -285,8 +290,9 @@ export function getCoreSystemPrompt( // awareness (e.g. not instructing the model to ask questions in headless // runs). `appendInstruction` below still applies in both branches. // - // `QWEN_SYSTEM_IDENTITY_MD` only applies on the default-prompt branch and is - // ignored whenever `QWEN_SYSTEM_MD` is in effect (including empty-file clear). + // `QWEN_SYSTEM_IDENTITY_MD` and the active output style only apply on the + // default-prompt branch and are ignored whenever `QWEN_SYSTEM_MD` is in + // effect (including empty-file clear). let basePrompt: string; if (systemMdEnabled) { basePrompt = fs.readFileSync(systemMdPath, 'utf8'); @@ -464,8 +470,14 @@ Interaction mode reminder: ${interaction.questions} fs.writeFileSync(writePath, basePrompt); } + // Layer the output style on after the QWEN_WRITE_SYSTEM_MD dump above, so + // that dump stays a pure base prompt: a style is meant to sit on top of a + // base, and baking it in would apply it twice once the dumped file is fed + // back through QWEN_SYSTEM_MD. return assembleSystemPrompt({ - base: basePrompt, + base: systemMdEnabled + ? basePrompt + : applyOutputStyle(basePrompt, outputStyle), contextFiles: userMemory, appendPrompt: appendInstruction, }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ee525f89e22..1fbe8216d93 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -89,6 +89,7 @@ export * from './core/logger.js'; export * from './core/message-display-dispatcher.js'; export * from './core/nonInteractiveToolExecutor.js'; export * from './core/prompts.js'; +export * from './core/output-styles.js'; export * from './core/session-recovery.js'; export * from './core/tokenLimits.js'; export * from './core/tool-call-preparation.js'; From 2f0402cb5bdcdd547f4a1a0718f02bd5181d1db4 Mon Sep 17 00:00:00 2001 From: qqqys Date: Thu, 20 Aug 2026 16:20:52 +0800 Subject: [PATCH 2/7] fix(core): narrow keepCodingInstructions to the section it names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `keepCodingInstructions: false` replaced the whole base prompt, which took the safety rules down with the workflow guidance: a non-coding style lost `# Executing actions with care`, the mandates, the tool guidance and the tone section along with the part it meant to drop. It now omits exactly one section — the software-engineering workflow guidance, split out as `getSoftwareEngineeringTasksSection()`. Every other section stays under every style. A style adjusts how work is reported; it never switches off the rules for taking risky actions. Two smaller corrections in the same area: The identity sentence now points at the style when one is active ("responding according to your Output Style below") instead of claiming the agent specializes in software engineering under a style that says otherwise. A `QWEN_SYSTEM_IDENTITY_MD` override is still inserted verbatim — that wording is distributor-owned and not ours to rewrite. The style section is headed `# Output Style: ` rather than `# Style Active`. The heading is the contract a custom style file will rely on once user and project styles load: the file body becomes the prompt verbatim and the heading is what names it. The per-turn reminder is now every style's, not just the two that spell one out. `turnReminder` overrides the generic wording rather than deciding whether a reminder exists at all — an Explanatory session drifts back to terse answers as readily as a Concise one does. `getOutputStyleTurnReminder()` renders the line the injection site will use. Because the style now feeds the base prompt rather than only being appended to it, the QWEN_WRITE_SYSTEM_MD dump builds its own unstyled copy, preserving the invariant that the dump is a reusable base. The prompts.test.ts snapshots are unchanged, which is the evidence that a session with no style selected still gets a byte-identical prompt. --- packages/core/src/core/output-styles.test.ts | 49 +++-- packages/core/src/core/output-styles.ts | 58 ++++-- packages/core/src/core/prompts.test.ts | 65 +++++- packages/core/src/core/prompts.ts | 198 ++++++++++++------- 4 files changed, 258 insertions(+), 112 deletions(-) diff --git a/packages/core/src/core/output-styles.test.ts b/packages/core/src/core/output-styles.test.ts index 5a460a969ae..871ba7ae8a5 100644 --- a/packages/core/src/core/output-styles.test.ts +++ b/packages/core/src/core/output-styles.test.ts @@ -7,8 +7,10 @@ import { describe, it, expect } from 'vitest'; import { BUILT_IN_OUTPUT_STYLES, + DEFAULT_OUTPUT_STYLE_TURN_REMINDER, applyOutputStyle, getBuiltInOutputStyle, + getOutputStyleTurnReminder, renderOutputStyleSection, type OutputStyleDefinition, } from './output-styles.js'; @@ -53,11 +55,30 @@ describe('built-in output styles', () => { } }); - it('attaches a turn reminder only to the behaviour-constraining styles', () => { - const withReminder = BUILT_IN_OUTPUT_STYLES.filter( - (style) => style.turnReminder, - ).map((style) => style.name); - expect(withReminder).toEqual(['Concise', 'Proactive']); + it('gives every style a turn reminder, generic or style-specific', () => { + for (const style of BUILT_IN_OUTPUT_STYLES) { + const reminder = getOutputStyleTurnReminder(style); + expect(reminder.startsWith(`${style.name} output style is active.`)).toBe( + true, + ); + expect(reminder.trim()).not.toBe(`${style.name} output style is active.`); + } + }); + + it('uses the style-specific reminder when one is defined', () => { + const concise = getBuiltInOutputStyle('Concise')!; + expect(concise.turnReminder).toBeDefined(); + expect(getOutputStyleTurnReminder(concise)).toBe( + `Concise output style is active. ${concise.turnReminder}`, + ); + }); + + it('falls back to the generic reminder for a style without one', () => { + const explanatory = getBuiltInOutputStyle('Explanatory')!; + expect(explanatory.turnReminder).toBeUndefined(); + expect(getOutputStyleTurnReminder(explanatory)).toBe( + `Explanatory output style is active. ${DEFAULT_OUTPUT_STYLE_TURN_REMINDER}`, + ); }); it('has no duplicate names', () => { @@ -84,16 +105,16 @@ describe('getBuiltInOutputStyle', () => { }); describe('renderOutputStyleSection', () => { - it('renders the style under an active-style heading', () => { + it('names the style with an Output Style heading', () => { expect(renderOutputStyleSection(LAYERED)).toBe( - '# Layered Style Active\n\nStyle body.', + '# Output Style: Layered\nStyle body.', ); }); - it('trims the prompt so the heading gap stays a single blank line', () => { + it('trims the prompt so the heading gap stays a single newline', () => { expect( renderOutputStyleSection({ ...LAYERED, prompt: '\n\nStyle body.\n\n' }), - ).toBe('# Layered Style Active\n\nStyle body.'); + ).toBe('# Output Style: Layered\nStyle body.'); }); }); @@ -103,15 +124,17 @@ describe('applyOutputStyle', () => { expect(applyOutputStyle('BASE', null)).toBe('BASE'); }); - it('appends a style that keeps the coding instructions', () => { + it('appends the style section to the base prompt', () => { expect(applyOutputStyle('BASE', LAYERED)).toBe( - 'BASE\n\n# Layered Style Active\n\nStyle body.', + 'BASE\n\n# Output Style: Layered\nStyle body.', ); }); - it('replaces the base prompt for a style that drops them', () => { + it('still appends when the style drops the coding instructions', () => { + // keepCodingInstructions selects which sections the base was built from; + // it never suppresses the style section itself. expect(applyOutputStyle('BASE', REPLACING)).toBe( - '# Replacing Style Active\n\nStyle body.', + 'BASE\n\n# Output Style: Replacing\nStyle body.', ); }); }); diff --git a/packages/core/src/core/output-styles.ts b/packages/core/src/core/output-styles.ts index d09514d1102..dd2ff818021 100644 --- a/packages/core/src/core/output-styles.ts +++ b/packages/core/src/core/output-styles.ts @@ -18,23 +18,26 @@ export interface OutputStyleDefinition { /** One line shown in the style picker. */ description: string; /** - * `true` layers `prompt` onto the default coding prompt, so the mandates, - * workflows and tool guidance all stay in force. `false` replaces the base - * prompt outright, for styles that are not about software engineering at - * all; such a style is responsible for its own identity and safety wording. + * `false` omits exactly one section of the base prompt — the + * software-engineering workflow guidance — for a style whose work is not + * coding. Identity, mandates, safety rules, tool guidance and tone are kept + * either way; a style never switches those off. */ keepCodingInstructions: boolean; /** The style section itself, rendered under a `# Style Active` heading. */ prompt: string; /** - * Optional one-line restatement injected as a `` on user - * turns. Styles that constrain *behaviour* drift over a long session and - * need it; styles that only add output (an explanation, a practice prompt) - * do not, and paying for a reminder every turn would be waste. + * Overrides the generic wording of the per-turn reminder. Every active style + * is reminded each turn; this only replaces + * `DEFAULT_OUTPUT_STYLE_TURN_REMINDER` with something style-specific. */ turnReminder?: string; } +/** Generic reminder wording for a style that does not supply its own. */ +export const DEFAULT_OUTPUT_STYLE_TURN_REMINDER = + 'Remember to follow the specific guidelines for this style.'; + const CONCISE: OutputStyleDefinition = { name: 'Concise', source: 'built-in', @@ -137,18 +140,40 @@ export function getBuiltInOutputStyle( ); } -/** Renders the style section as it appears in the system prompt. */ +/** + * Renders the style section as it appears in the system prompt. + * + * The `# Output Style: ` wrapper is the contract a custom style file + * relies on: the file's body becomes `prompt` verbatim and this heading is + * what names it in the prompt. + */ export function renderOutputStyleSection(style: OutputStyleDefinition): string { - return `# ${style.name} Style Active\n\n${style.prompt.trim()}`; + return `# Output Style: ${style.name}\n${style.prompt.trim()}`; +} + +/** + * The per-turn reminder line for an active style. + * + * Every non-default style gets one; `turnReminder` only replaces the generic + * wording. Styles drift over a long session — a style that only adds output + * drifts just as readily as one that constrains behaviour. + */ +export function getOutputStyleTurnReminder( + style: OutputStyleDefinition, +): string { + return `${style.name} output style is active. ${ + style.turnReminder ?? DEFAULT_OUTPUT_STYLE_TURN_REMINDER + }`; } /** - * Layers an output style onto a base system prompt. + * Appends an output style's section to a base system prompt. * - * Styles that keep the coding instructions are appended to the base, which - * puts them last in the stable layer — after the mandates they are meant to - * refine, and still ahead of every context/volatile layer, so the prompt - * prefix stays cacheable for the whole session. + * The section lands last in the stable layer — after the mandates it refines, + * and ahead of every context/volatile layer, so a session's prompt prefix + * stays stable for its whole life. `keepCodingInstructions` is not consulted + * here: it selects which sections the base prompt was built from, not whether + * the style is appended. */ export function applyOutputStyle( basePrompt: string, @@ -157,6 +182,5 @@ export function applyOutputStyle( if (!style) { return basePrompt; } - const section = renderOutputStyleSection(style); - return style.keepCodingInstructions ? `${basePrompt}\n\n${section}` : section; + return `${basePrompt}\n\n${renderOutputStyleSection(style)}`; } diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 3b857081800..9dac990e53f 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -586,7 +586,7 @@ describe('Core System Prompt (prompts.ts)', () => { it('leaves the prompt untouched when no style is active', () => { const prompt = getCoreSystemPrompt(); for (const style of BUILT_IN_OUTPUT_STYLES) { - expect(prompt).not.toContain(`# ${style.name} Style Active`); + expect(prompt).not.toContain(`# Output Style: ${style.name}`); } }); @@ -598,9 +598,9 @@ describe('Core System Prompt (prompts.ts)', () => { 'interactive', concise, ); - expect(prompt).toContain('# Concise Style Active'); + expect(prompt).toContain('# Output Style: Concise'); // The style refines the mandates, so it has to land after them... - expect(prompt.indexOf('# Concise Style Active')).toBeGreaterThan( + expect(prompt.indexOf('# Output Style: Concise')).toBeGreaterThan( prompt.indexOf('# Core Mandates'), ); // ...and the base prompt must still be intact. @@ -615,7 +615,7 @@ describe('Core System Prompt (prompts.ts)', () => { 'interactive', concise, ); - const styleIndex = prompt.indexOf('# Concise Style Active'); + const styleIndex = prompt.indexOf('# Output Style: Concise'); expect(styleIndex).toBeGreaterThan(-1); expect(styleIndex).toBeLessThan(prompt.indexOf('MEMORY_MARKER')); expect(styleIndex).toBeLessThan(prompt.indexOf('APPEND_MARKER')); @@ -633,7 +633,58 @@ describe('Core System Prompt (prompts.ts)', () => { concise, ); expect(prompt).toContain('custom system prompt'); - expect(prompt).not.toContain('# Concise Style Active'); + expect(prompt).not.toContain('# Output Style: Concise'); + }); + + it('points the identity sentence at the style when one is active', () => { + const plain = getCoreSystemPrompt(); + expect(plain).toContain('specializing in software engineering tasks'); + + const styled = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + expect(styled).toContain('responding according to your "Output Style"'); + expect(styled).not.toContain( + 'specializing in software engineering tasks', + ); + }); + + it('drops only the software-engineering section for keepCodingInstructions: false', () => { + const nonCoding = { + ...concise, + name: 'NonCoding', + keepCodingInstructions: false, + }; + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + nonCoding, + ); + expect(prompt).not.toContain('## Software Engineering Tasks'); + // Everything else the base prompt carries must survive — dropping the + // safety rules along with the workflow guidance would be a regression. + expect(prompt).toContain('# Core Mandates'); + expect(prompt).toContain('# Executing actions with care'); + expect(prompt).toContain('## Using Your Tools'); + expect(prompt).toContain('## Tone and Style (CLI Interaction)'); + expect(prompt).toContain('# Output Style: NonCoding'); + }); + + it('keeps the software-engineering section under a normal style', () => { + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + expect(prompt).toContain('## Software Engineering Tasks'); }); it('does not bake the style into the QWEN_WRITE_SYSTEM_MD dump', () => { @@ -648,7 +699,9 @@ describe('Core System Prompt (prompts.ts)', () => { concise, ); const [, written] = vi.mocked(fs.writeFileSync).mock.calls[0]; - expect(written).not.toContain('# Concise Style Active'); + expect(written).not.toContain('# Output Style: Concise'); + // ...and the dumped identity sentence is the unstyled one. + expect(written).toContain('specializing in software engineering tasks'); }); }); }); diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 10a84102cda..8fa7d8f605b 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -90,8 +90,17 @@ function getInteractiveInteractionModePrompt(): { * Factored out so `QWEN_SYSTEM_IDENTITY_MD` can replace this single unit * without fragile splicing of the large base-prompt template. */ -function getDefaultCoreIdentitySentence(role: string): string { - return `You are Qwen Code, ${role} developed by Alibaba Group, specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.`; +function getDefaultCoreIdentitySentence( + role: string, + hasOutputStyle = false, +): string { + // With a style active the identity points at it, mirroring what the style + // section further down actually governs. Without one the wording is + // unchanged, so the default prompt stays byte-identical. + const focus = hasOutputStyle + ? 'responding according to your "Output Style" below, which describes how you should respond to user queries' + : 'specializing in software engineering tasks'; + return `You are Qwen Code, ${role} developed by Alibaba Group, ${focus}. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.`; } /** @@ -239,68 +248,53 @@ export function getCustomSystemPrompt( } /** - * Builds the stable base system prompt (identity, mandates, tool guidance). + * The workflow guidance for performing software-engineering work. * - * @param userMemory - Back-compat convenience slot for context files. - * @deprecated Prefer composing layers explicitly via `assembleSystemPrompt` - * (e.g. `assembleSystemPrompt({ base: getCoreSystemPrompt(undefined, model), contextFiles })`) - * so a single site owns the layer order. Passing memory here *and* wrapping - * the result in `assembleSystemPrompt({ contextFiles })` double-includes it. - * @param model - Model id, used to select model-specific prompt variants. - * @param appendInstruction - Back-compat convenience slot for the append prompt. - * @deprecated Prefer the `appendPrompt` slot of `assembleSystemPrompt`. - * @param interactionMode - Interactive vs. headless prompt variant. - * @param outputStyle - Active output style, layered onto the base prompt. - * Ignored when `QWEN_SYSTEM_MD` replaces the base prompt (see below). + * Split out so an output style with `keepCodingInstructions: false` can drop + * exactly this section and nothing else — mandates, safety rules, tool + * guidance and tone stay in force under every style. */ -export function getCoreSystemPrompt( - userMemory?: string, - model?: string, - appendInstruction?: string, - interactionMode: SystemPromptInteractionMode = 'interactive', - outputStyle?: OutputStyleDefinition | null, -): string { - // if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file - // default path is .qwen/system.md (project-level), can be overridden via QWEN_SYSTEM_MD - let systemMdEnabled = false; - let systemMdPath = path.resolve(path.join(QWEN_DIR, 'system.md')); - // Resolve the environment variable to get either a path or a switch value. - const systemMdResolution = resolvePathFromEnv(process.env['QWEN_SYSTEM_MD']); - - // Proceed only if the environment variable is set and is not disabled. - if (systemMdResolution.value && !systemMdResolution.isDisabled) { - systemMdEnabled = true; +function getSoftwareEngineeringTasksSection(): string { + return `## Software Engineering Tasks +When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this iterative approach: +- **Plan:** Use '${ToolNames.TODO_WRITE}' for complex, ambiguous, or multi-step work when visible progress tracking adds value. Keep the plan short and outcome-oriented; skip it for simple tasks unless the user explicitly requests a plan. +- **Implement:** Begin implementing while gathering context as needed. Use available search and editing tools strategically, adhering to project conventions (see 'Core Mandates'). Do not add features, refactor code, or make "improvements" beyond what was asked. Don't add error handling, fallbacks, or validation for scenarios that can't happen—only validate at system boundaries (user input, external APIs). Don't create helpers, utilities, or abstractions for one-time operations. Three similar lines of code is better than a premature abstraction. Prefer editing existing files over creating new ones. +- **Adapt:** Refine your approach as you discover new information or encounter obstacles. If a todo list exists, keep it current as the scope or approach changes. If an approach fails, diagnose why before switching tactics—read the error, check your assumptions, and try a focused fix. Don't retry blindly, but don't abandon a viable approach after a single failure. +- **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands. Before reporting a task complete, verify it actually works. If you can't verify (no test exists, can't run the code), say so explicitly rather than claiming success. +- **Verify (Standards):** When your task involves a code or system change, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. Read-only or explanatory turns do not require verification. +- **Report outcomes faithfully:** If tests fail, say so with the relevant output. If you did not run a verification step, say that rather than implying it succeeded. Never claim "all tests pass" when output shows failures, never suppress failing checks to manufacture a green result, and never characterize incomplete or broken work as done. - // We update systemMdPath to this new custom path. - if (!systemMdResolution.isSwitch) { - systemMdPath = systemMdResolution.value; - } +**Key Principle:** Start with a reasonable approach based on available information, then adapt as you learn. Users prefer seeing progress quickly rather than waiting for perfect understanding. - // require file to exist when override is enabled - if (!fs.existsSync(systemMdPath)) { - throw new Error(`missing system prompt file '${systemMdPath}'`); - } - } +`; +} - const interaction = getInteractionModePrompt(interactionMode); - // A QWEN_SYSTEM_MD override replaces the base prompt verbatim and is - // intentionally not augmented with interaction-mode guidance: the override is - // a full, user-owned prompt, so injecting our mode wording would defeat the - // purpose of the override. Custom prompts are responsible for their own mode - // awareness (e.g. not instructing the model to ask questions in headless - // runs). `appendInstruction` below still applies in both branches. - // - // `QWEN_SYSTEM_IDENTITY_MD` and the active output style only apply on the - // default-prompt branch and are ignored whenever `QWEN_SYSTEM_MD` is in - // effect (including empty-file clear). - let basePrompt: string; - if (systemMdEnabled) { - basePrompt = fs.readFileSync(systemMdPath, 'utf8'); - } else { - const coreIdentity = - resolveCoreIdentityOverride() ?? - getDefaultCoreIdentitySentence(interaction.role); - basePrompt = ` +/** + * Builds the default (non-override) base system prompt. + * + * Extracted so the same text can be produced with and without an output + * style: the QWEN_WRITE_SYSTEM_MD dump always wants the unstyled form. + */ +function buildDefaultBasePrompt( + interaction: { role: string; questions: string }, + model: string | undefined, + outputStyle?: OutputStyleDefinition | null, +): string { + // A style with `keepCodingInstructions: false` drops exactly the + // software-engineering workflow section; every other section, including the + // safety rules in `getActionsSection`, is unaffected. + const softwareEngineeringTasks = + outputStyle?.keepCodingInstructions === false + ? '' + : getSoftwareEngineeringTasksSection(); + + // A QWEN_SYSTEM_IDENTITY_MD override is inserted verbatim, so the styled + // variant applies only to the default identity sentence — the override is + // distributor-owned wording we must not rewrite. + const coreIdentity = + resolveCoreIdentityOverride() ?? + getDefaultCoreIdentitySentence(interaction.role, Boolean(outputStyle)); + return ` ${coreIdentity} # Core Mandates @@ -330,18 +324,7 @@ When you create a todo list: # Primary Workflows -## Software Engineering Tasks -When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this iterative approach: -- **Plan:** Use '${ToolNames.TODO_WRITE}' for complex, ambiguous, or multi-step work when visible progress tracking adds value. Keep the plan short and outcome-oriented; skip it for simple tasks unless the user explicitly requests a plan. -- **Implement:** Begin implementing while gathering context as needed. Use available search and editing tools strategically, adhering to project conventions (see 'Core Mandates'). Do not add features, refactor code, or make "improvements" beyond what was asked. Don't add error handling, fallbacks, or validation for scenarios that can't happen—only validate at system boundaries (user input, external APIs). Don't create helpers, utilities, or abstractions for one-time operations. Three similar lines of code is better than a premature abstraction. Prefer editing existing files over creating new ones. -- **Adapt:** Refine your approach as you discover new information or encounter obstacles. If a todo list exists, keep it current as the scope or approach changes. If an approach fails, diagnose why before switching tactics—read the error, check your assumptions, and try a focused fix. Don't retry blindly, but don't abandon a viable approach after a single failure. -- **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands. Before reporting a task complete, verify it actually works. If you can't verify (no test exists, can't run the code), say so explicitly rather than claiming success. -- **Verify (Standards):** When your task involves a code or system change, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. Read-only or explanatory turns do not require verification. -- **Report outcomes faithfully:** If tests fail, say so with the relevant output. If you did not run a verification step, say that rather than implying it succeeded. Never claim "all tests pass" when output shows failures, never suppress failing checks to manufacture a green result, and never characterize incomplete or broken work as done. - -**Key Principle:** Start with a reasonable approach based on available information, then adapt as you learn. Users prefer seeing progress quickly rather than waiting for perfect understanding. - -- Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. +${softwareEngineeringTasks}- Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. - When you see a tag in a tool result, the full output was saved to disk because it was too large. Use the read_file tool to access the complete content if the preview is insufficient. ## New Applications @@ -452,8 +435,67 @@ Your core function is efficient and safe assistance. Balance conciseness with th Interaction mode reminder: ${interaction.questions} `.trim(); +} + +/** + * Builds the stable base system prompt (identity, mandates, tool guidance). + * + * @param userMemory - Back-compat convenience slot for context files. + * @deprecated Prefer composing layers explicitly via `assembleSystemPrompt` + * (e.g. `assembleSystemPrompt({ base: getCoreSystemPrompt(undefined, model), contextFiles })`) + * so a single site owns the layer order. Passing memory here *and* wrapping + * the result in `assembleSystemPrompt({ contextFiles })` double-includes it. + * @param model - Model id, used to select model-specific prompt variants. + * @param appendInstruction - Back-compat convenience slot for the append prompt. + * @deprecated Prefer the `appendPrompt` slot of `assembleSystemPrompt`. + * @param interactionMode - Interactive vs. headless prompt variant. + * @param outputStyle - Active output style, layered onto the base prompt. + * Ignored when `QWEN_SYSTEM_MD` replaces the base prompt (see below). + */ +export function getCoreSystemPrompt( + userMemory?: string, + model?: string, + appendInstruction?: string, + interactionMode: SystemPromptInteractionMode = 'interactive', + outputStyle?: OutputStyleDefinition | null, +): string { + // if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file + // default path is .qwen/system.md (project-level), can be overridden via QWEN_SYSTEM_MD + let systemMdEnabled = false; + let systemMdPath = path.resolve(path.join(QWEN_DIR, 'system.md')); + // Resolve the environment variable to get either a path or a switch value. + const systemMdResolution = resolvePathFromEnv(process.env['QWEN_SYSTEM_MD']); + + // Proceed only if the environment variable is set and is not disabled. + if (systemMdResolution.value && !systemMdResolution.isDisabled) { + systemMdEnabled = true; + + // We update systemMdPath to this new custom path. + if (!systemMdResolution.isSwitch) { + systemMdPath = systemMdResolution.value; + } + + // require file to exist when override is enabled + if (!fs.existsSync(systemMdPath)) { + throw new Error(`missing system prompt file '${systemMdPath}'`); + } } + const interaction = getInteractionModePrompt(interactionMode); + // A QWEN_SYSTEM_MD override replaces the base prompt verbatim and is + // intentionally not augmented with interaction-mode guidance: the override is + // a full, user-owned prompt, so injecting our mode wording would defeat the + // purpose of the override. Custom prompts are responsible for their own mode + // awareness (e.g. not instructing the model to ask questions in headless + // runs). `appendInstruction` below still applies in both branches. + // + // `QWEN_SYSTEM_IDENTITY_MD` and the active output style only apply on the + // default-prompt branch and are ignored whenever `QWEN_SYSTEM_MD` is in + // effect (including empty-file clear). + const basePrompt = systemMdEnabled + ? fs.readFileSync(systemMdPath, 'utf8') + : buildDefaultBasePrompt(interaction, model, outputStyle); + // if QWEN_WRITE_SYSTEM_MD is set (and not 0|false), write base system prompt to file const writeSystemMdResolution = resolvePathFromEnv( process.env['QWEN_WRITE_SYSTEM_MD'], @@ -467,13 +509,17 @@ Interaction mode reminder: ${interaction.questions} : writeSystemMdResolution.value; fs.mkdirSync(path.dirname(writePath), { recursive: true }); - fs.writeFileSync(writePath, basePrompt); + // Dump the unstyled base. That file is meant to be reusable as a + // QWEN_SYSTEM_MD base, and a style is layered on top of a base — writing + // the styled form would apply the style twice once the dump is fed back. + fs.writeFileSync( + writePath, + systemMdEnabled + ? basePrompt + : buildDefaultBasePrompt(interaction, model, undefined), + ); } - // Layer the output style on after the QWEN_WRITE_SYSTEM_MD dump above, so - // that dump stays a pure base prompt: a style is meant to sit on top of a - // base, and baking it in would apply it twice once the dumped file is fed - // back through QWEN_SYSTEM_MD. return assembleSystemPrompt({ base: systemMdEnabled ? basePrompt From 4c6a1b74313220c9ff9549344f93a79f193f1987 Mon Sep 17 00:00:00 2001 From: qqqys Date: Thu, 20 Aug 2026 18:28:21 +0800 Subject: [PATCH 3/7] test(core): pin the output-style layer where review round 1 found it unpinned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coverage gaps from the r1 review, both confirmed by mutation probes: R1-1 (prompts.test.ts): the `QWEN_SYSTEM_IDENTITY_MD` override combined with an active output style was pinned by nothing — the identity-override tests pass no style and the `outputStyle parameter` tests never stub the override. Skipping the style append under an override kept all 105 tests green while silently dropping the user's style for every deployment shipping an identity override. The new case asserts the override text wins verbatim, the styled identity sentence is skipped, and `# Output Style: Concise` still lands. R1-2 (contextCommand.test.ts): the `config.getOutputStyle()` forwarding into `getCoreSystemPrompt` was never exercised with a style active — every config mock returned `undefined`. Deleting the argument kept the suite at 16/16 while `/context` undercounted system-prompt tokens by the style section. The new case bills a `Concise` style and asserts the estimate grows by that section. Mutation verification: skipping the style append under an identity override fails only the new prompts case (1 failed | 105 passed); deleting the `config.getOutputStyle()` argument fails only the new contextCommand case (1 failed | 16 passed). --- .../src/ui/commands/contextCommand.test.ts | 34 +++++++++++++++++++ packages/core/src/core/prompts.test.ts | 31 +++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/cli/src/ui/commands/contextCommand.test.ts b/packages/cli/src/ui/commands/contextCommand.test.ts index cf97ee11c54..2a51544f7a9 100644 --- a/packages/cli/src/ui/commands/contextCommand.test.ts +++ b/packages/cli/src/ui/commands/contextCommand.test.ts @@ -8,6 +8,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import * as os from 'node:os'; import * as path from 'node:path'; import type { Config } from '@qwen-code/qwen-code-core'; +import { + getBuiltInOutputStyle, + getCoreSystemPrompt, + resolveInteractionMode, +} from '@qwen-code/qwen-code-core'; import { t } from '../../i18n/index.js'; import { collectContextData, @@ -453,6 +458,35 @@ describe('/context shows three-tier thresholds', () => { expect(text).not.toMatch(/Compaction thresholds/); }); + it('bills the active output style into the system-prompt estimate', async () => { + const concise = getBuiltInOutputStyle('Concise')!; + const plainConfig = makeMockConfig(200_000); + const styledConfig = { + ...makeMockConfig(200_000), + getOutputStyle: vi.fn().mockReturnValue(concise), + } as unknown as Config; + + // No API token count, so breakdown.systemPrompt is the raw estimate + // rather than a scaled share — the style section shows up undiluted. + const plain = await collectContextData(plainConfig, false); + const styled = await collectContextData(styledConfig, false); + + const tokenDelta = + styled.breakdown.systemPrompt - plain.breakdown.systemPrompt; + expect(tokenDelta).toBeGreaterThan(0); + + // ...and the delta has to be the style layer itself, not incidental + // drift: estimateTokens bills ASCII at ~4 chars/token. + const mode = resolveInteractionMode(styledConfig); + const charDelta = + getCoreSystemPrompt(undefined, 'test-model', undefined, mode, concise) + .length - + getCoreSystemPrompt(undefined, 'test-model', undefined, mode, undefined) + .length; + expect(tokenDelta).toBeGreaterThan(charDelta / 4 - 5); + expect(tokenDelta).toBeLessThan(charDelta / 4 + 5); + }); + it('propagates custom autoCompactThreshold through to /context thresholds', async () => { // config.getAutoCompactThreshold() returns 0.5 → computeThresholds(32000, 0.5) // = { warn: 0, auto: 16,000, hard: 19,000, effectiveWindow: 12,000 } diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 9dac990e53f..37e4571d5b1 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -687,6 +687,37 @@ describe('Core System Prompt (prompts.ts)', () => { expect(prompt).toContain('## Software Engineering Tasks'); }); + it('keeps the style section under a QWEN_SYSTEM_IDENTITY_MD override', () => { + // The override owns the identity sentence verbatim, so the styled + // wording is skipped there — but the style itself still has to land. + const identityPath = path.resolve('/custom/identity.md'); + const customIdentity = + 'You are Acme Code, an interactive CLI agent for Acme Corp.'; + vi.stubEnv('QWEN_SYSTEM_IDENTITY_MD', identityPath); + vi.mocked(fs.existsSync).mockImplementation( + (p) => path.resolve(String(p)) === identityPath, + ); + vi.mocked(fs.readFileSync).mockImplementation((p) => { + if (path.resolve(String(p)) === identityPath) { + return customIdentity; + } + throw new Error(`unexpected read: ${String(p)}`); + }); + + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + expect(prompt.startsWith(customIdentity)).toBe(true); + expect(prompt).not.toContain( + 'responding according to your "Output Style"', + ); + expect(prompt).toContain('# Output Style: Concise'); + }); + it('does not bake the style into the QWEN_WRITE_SYSTEM_MD dump', () => { // The dump is meant to be reusable as a QWEN_SYSTEM_MD base; baking the // style in would apply it twice when that file is fed back. From cfdbdc5e5124a7893a8bf123e791f675741441e3 Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Sun, 23 Aug 2026 05:26:30 +0800 Subject: [PATCH 4/7] fix(core): align output style prompt consumers --- .../src/ui/commands/contextCommand.test.ts | 16 +++++++++ .../cli/src/ui/commands/contextCommand.ts | 11 ++---- .../src/agents/arena/ArenaManager.test.ts | 9 ++++- .../core/src/agents/arena/ArenaManager.ts | 1 + packages/core/src/core/client.ts | 36 +++++++++++++------ packages/core/src/core/prompts.test.ts | 16 +++++++++ packages/core/src/core/prompts.ts | 9 +++-- 7 files changed, 76 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/ui/commands/contextCommand.test.ts b/packages/cli/src/ui/commands/contextCommand.test.ts index 2a51544f7a9..8e3e0b972a1 100644 --- a/packages/cli/src/ui/commands/contextCommand.test.ts +++ b/packages/cli/src/ui/commands/contextCommand.test.ts @@ -55,6 +55,7 @@ function makeMockConfig(contextWindowSize = 32_000): Config { }), getVisibleTools: vi.fn().mockReturnValue(new Set()), getUserMemory: vi.fn().mockReturnValue(''), + getSystemPrompt: vi.fn().mockReturnValue(undefined), getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ @@ -89,6 +90,7 @@ describe('collectContextData (contextCommand)', () => { }), getVisibleTools: vi.fn().mockReturnValue(new Set()), getUserMemory: vi.fn().mockReturnValue(''), + getSystemPrompt: vi.fn().mockReturnValue(undefined), getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ @@ -214,6 +216,7 @@ describe('collectContextData (contextCommand)', () => { }), getVisibleTools: vi.fn().mockReturnValue(new Set()), getUserMemory: vi.fn().mockReturnValue(''), + getSystemPrompt: vi.fn().mockReturnValue(undefined), getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ @@ -262,6 +265,7 @@ describe('collectContextData (contextCommand)', () => { }), getVisibleTools: vi.fn().mockReturnValue(new Set(['web_fetch'])), getUserMemory: vi.fn().mockReturnValue(''), + getSystemPrompt: vi.fn().mockReturnValue(undefined), getOutputStyle: vi.fn().mockReturnValue(undefined), getAutoMemoryPrompt: vi.fn().mockReturnValue(''), getSkillManager: vi.fn().mockReturnValue({ @@ -487,6 +491,18 @@ describe('/context shows three-tier thresholds', () => { expect(tokenDelta).toBeLessThan(charDelta / 4 + 5); }); + it('estimates the custom system prompt used by the live client', async () => { + const config = { + ...makeMockConfig(200_000), + getSystemPrompt: vi.fn().mockReturnValue('CUSTOM'), + getOutputStyle: vi.fn().mockReturnValue(getBuiltInOutputStyle('Concise')), + } as unknown as Config; + + const data = await collectContextData(config, false); + + expect(data.breakdown.systemPrompt).toBe(2); + }); + it('propagates custom autoCompactThreshold through to /context thresholds', async () => { // config.getAutoCompactThreshold() returns 0.5 → computeThresholds(32000, 0.5) // = { warn: 0, auto: 16,000, hard: 19,000, effectiveWindow: 12,000 } diff --git a/packages/cli/src/ui/commands/contextCommand.ts b/packages/cli/src/ui/commands/contextCommand.ts index ceb788c194f..d4dc99e20bb 100644 --- a/packages/cli/src/ui/commands/contextCommand.ts +++ b/packages/cli/src/ui/commands/contextCommand.ts @@ -21,8 +21,7 @@ import { import { DiscoveredMCPTool, uiTelemetryService, - getCoreSystemPrompt, - resolveInteractionMode, + getMainSessionBaseSystemPrompt, DEFAULT_TOKEN_LIMIT, ToolNames, buildSkillLlmContent, @@ -139,13 +138,7 @@ export async function collectContextData( // refines the messages-vs-cache split, not the headline total or tier. const apiCachedTokens = uiTelemetryService.getLastCachedContentTokenCount(); - const systemPromptText = getCoreSystemPrompt( - undefined, - modelName, - undefined, - resolveInteractionMode(config), - config.getOutputStyle(), - ); + const systemPromptText = getMainSessionBaseSystemPrompt(config); const systemPromptTokens = estimateTokens(systemPromptText); const toolRegistry = config.getToolRegistry(); diff --git a/packages/core/src/agents/arena/ArenaManager.test.ts b/packages/core/src/agents/arena/ArenaManager.test.ts index 84a318b66d7..9b8818f7bca 100644 --- a/packages/core/src/agents/arena/ArenaManager.test.ts +++ b/packages/core/src/agents/arena/ArenaManager.test.ts @@ -13,6 +13,7 @@ import { ArenaEventType } from './arena-events.js'; import { ArenaSessionStatus, ARENA_MAX_AGENTS } from './types.js'; import { AgentStatus } from '../runtime/agent-types.js'; import { ApprovalMode } from '../../config/config.js'; +import { getBuiltInOutputStyle } from '../../core/output-styles.js'; const hoistedMockSetupWorktrees = vi.hoisted(() => vi.fn()); const hoistedMockCleanupSession = vi.hoisted(() => vi.fn()); @@ -72,6 +73,7 @@ const createMockConfig = ( getModel: () => 'test-model', getSessionId: () => 'test-session', getUserMemory: () => '', + getOutputStyle: (): ReturnType => undefined, getAutoMemoryPrompt: () => '', getToolRegistry: () => ({ getFunctionDeclarations: () => [], @@ -405,7 +407,7 @@ describe('ArenaManager', () => { } }); - it('builds the in-process worker system prompt in headless interaction mode', async () => { + it('builds the in-process worker prompt with headless mode and the active style', async () => { // Arena workers run non-interactively, so ArenaManager passes 'headless' // as the interaction mode (4th arg) to getCoreSystemPrompt. A regression // that drops that argument would fall back to the interactive prompt, @@ -413,6 +415,10 @@ describe('ArenaManager', () => { // Assert on the produced prompt: the headless variant carries a // single-turn marker that is absent from every other interaction mode. mockBackend.type = 'in-process'; + mockConfig = { + ...createMockConfig(tempDir, { worktreeBaseDir: tempDir }), + getOutputStyle: () => getBuiltInOutputStyle('Concise'), + }; const manager = new ArenaManager(mockConfig as never); await manager.start(createValidStartOptions()); @@ -429,6 +435,7 @@ describe('ArenaManager', () => { expect(systemPrompt).toContain( 'This is a non-interactive, single-turn run', ); + expect(systemPrompt).toContain('# Output Style: Concise'); } }); diff --git a/packages/core/src/agents/arena/ArenaManager.ts b/packages/core/src/agents/arena/ArenaManager.ts index 9acf09b3e2e..22aeae40e0b 100644 --- a/packages/core/src/agents/arena/ArenaManager.ts +++ b/packages/core/src/agents/arena/ArenaManager.ts @@ -1089,6 +1089,7 @@ export class ArenaManager { model.modelId, undefined, 'headless', + this.config.getOutputStyle(), ), contextFiles: this.config.getUserMemory(), }), diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 79f8aff603a..db7c9440704 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -346,6 +346,31 @@ const SKILL_WRITE_TOOL_NAMES: ReadonlySet = new Set([ ToolNames.EDIT, ]); +type MainSessionPromptConfig = Pick< + Config, + | 'getSystemPrompt' + | 'getModel' + | 'getOutputStyle' + | 'getExperimentalZedIntegration' + | 'getInputFormat' + | 'isInteractive' +>; + +export function getMainSessionBaseSystemPrompt( + config: MainSessionPromptConfig, +): string { + const overrideSystemPrompt = config.getSystemPrompt(); + return overrideSystemPrompt + ? getCustomSystemPrompt(overrideSystemPrompt) + : getCoreSystemPrompt( + undefined, + config.getModel(), + undefined, + resolveInteractionMode(config), + config.getOutputStyle(), + ); +} + export class GeminiClient { private chat?: GeminiChat; private initializedSessionId: string | undefined; @@ -1161,16 +1186,7 @@ export class GeminiClient { } private getMainSessionSystemInstruction(): string { - const overrideSystemPrompt = this.config.getSystemPrompt(); - const base = overrideSystemPrompt - ? getCustomSystemPrompt(overrideSystemPrompt) - : getCoreSystemPrompt( - undefined, - this.config.getModel(), - undefined, - resolveInteractionMode(this.config), - this.config.getOutputStyle(), - ); + const base = getMainSessionBaseSystemPrompt(this.config); const stableLayers = { base, contextFiles: this.config.getUserMemory(), diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 37e4571d5b1..7d2149d5859 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -582,6 +582,7 @@ describe('Core System Prompt (prompts.ts)', () => { describe('outputStyle parameter', () => { const concise = getBuiltInOutputStyle('Concise')!; + const learning = getBuiltInOutputStyle('Learning')!; it('leaves the prompt untouched when no style is active', () => { const prompt = getCoreSystemPrompt(); @@ -687,6 +688,21 @@ describe('Core System Prompt (prompts.ts)', () => { expect(prompt).toContain('## Software Engineering Tasks'); }); + it('omits Learning from headless prompts that cannot receive a reply', () => { + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'headless', + learning, + ); + + expect(prompt).toContain('This is a non-interactive, single-turn run'); + expect(prompt).not.toContain('# Output Style: Learning'); + expect(prompt).not.toContain('TODO(human)'); + expect(prompt).not.toContain('until the user has written their piece'); + }); + it('keeps the style section under a QWEN_SYSTEM_IDENTITY_MD override', () => { // The override owns the identity sentence verbatim, so the styled // wording is skipped there — but the style itself still has to land. diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 8fa7d8f605b..fad76168c5c 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -459,6 +459,11 @@ export function getCoreSystemPrompt( interactionMode: SystemPromptInteractionMode = 'interactive', outputStyle?: OutputStyleDefinition | null, ): string { + // Learning requires a reply to its handoff, which a headless run cannot receive. + const effectiveOutputStyle = + interactionMode === 'headless' && outputStyle?.name === 'Learning' + ? undefined + : outputStyle; // if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file // default path is .qwen/system.md (project-level), can be overridden via QWEN_SYSTEM_MD let systemMdEnabled = false; @@ -494,7 +499,7 @@ export function getCoreSystemPrompt( // effect (including empty-file clear). const basePrompt = systemMdEnabled ? fs.readFileSync(systemMdPath, 'utf8') - : buildDefaultBasePrompt(interaction, model, outputStyle); + : buildDefaultBasePrompt(interaction, model, effectiveOutputStyle); // if QWEN_WRITE_SYSTEM_MD is set (and not 0|false), write base system prompt to file const writeSystemMdResolution = resolvePathFromEnv( @@ -523,7 +528,7 @@ export function getCoreSystemPrompt( return assembleSystemPrompt({ base: systemMdEnabled ? basePrompt - : applyOutputStyle(basePrompt, outputStyle), + : applyOutputStyle(basePrompt, effectiveOutputStyle), contextFiles: userMemory, appendPrompt: appendInstruction, }); From 3750333adaac6cc4f6ab023576c6424eba56ea7f Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:23:08 +0800 Subject: [PATCH 5/7] test(core): pin Learning output style modes --- packages/core/src/core/prompts.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 7d2149d5859..6ed8441b721 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -698,11 +698,28 @@ describe('Core System Prompt (prompts.ts)', () => { ); expect(prompt).toContain('This is a non-interactive, single-turn run'); + expect(prompt).toContain('specializing in software engineering tasks'); + expect(prompt).not.toContain( + 'responding according to your "Output Style"', + ); expect(prompt).not.toContain('# Output Style: Learning'); expect(prompt).not.toContain('TODO(human)'); expect(prompt).not.toContain('until the user has written their piece'); }); + it('keeps Learning in interactive prompts', () => { + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + learning, + ); + + expect(prompt).toContain('# Output Style: Learning'); + expect(prompt).toContain('TODO(human)'); + }); + it('keeps the style section under a QWEN_SYSTEM_IDENTITY_MD override', () => { // The override owns the identity sentence verbatim, so the styled // wording is skipped there — but the style itself still has to land. From 90e4784e4ac38744b80ab9595ff896e040efebf2 Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Sun, 23 Aug 2026 10:11:55 +0800 Subject: [PATCH 6/7] test(core): pin Learning style in ACP prompts --- packages/core/src/core/prompts.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 6ed8441b721..a64f9e48a1f 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -720,6 +720,18 @@ describe('Core System Prompt (prompts.ts)', () => { expect(prompt).toContain('TODO(human)'); }); + it('keeps Learning in acp prompts', () => { + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'acp', + learning, + ); + + expect(prompt).toContain('# Output Style: Learning'); + }); + it('keeps the style section under a QWEN_SYSTEM_IDENTITY_MD override', () => { // The override owns the identity sentence verbatim, so the styled // wording is skipped there — but the style itself still has to land. From ee29373d61f6a55f69b0878d0210a8d650b3712d Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Sun, 23 Aug 2026 18:36:02 +0000 Subject: [PATCH 7/7] fix(core): align output-style doc comment and Concise description --- packages/core/src/core/output-styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/output-styles.ts b/packages/core/src/core/output-styles.ts index dd2ff818021..e955b579e53 100644 --- a/packages/core/src/core/output-styles.ts +++ b/packages/core/src/core/output-styles.ts @@ -24,7 +24,7 @@ export interface OutputStyleDefinition { * either way; a style never switches those off. */ keepCodingInstructions: boolean; - /** The style section itself, rendered under a `# Style Active` heading. */ + /** The style section itself, rendered under the `# Output Style: ` heading. */ prompt: string; /** * Overrides the generic wording of the per-turn reminder. Every active style @@ -42,7 +42,7 @@ const CONCISE: OutputStyleDefinition = { name: 'Concise', source: 'built-in', description: - 'Answers first, with no preamble, narration, or closing recap — the work stays as thorough', + 'Answers first, with no preamble, narration, or closing recap — the work stays as thorough as ever', keepCodingInstructions: true, turnReminder: 'Be concise: answer first, cut the narration, keep only what the user needs.',