diff --git a/packages/cli/src/ui/commands/contextCommand.test.ts b/packages/cli/src/ui/commands/contextCommand.test.ts index 1b6472a3a94..8e3e0b972a1 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, @@ -50,6 +55,8 @@ 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({ listSkills: vi.fn().mockResolvedValue([]), @@ -83,6 +90,8 @@ 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({ listSkills: vi.fn().mockResolvedValue([]), @@ -207,6 +216,8 @@ 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({ listSkills: vi.fn().mockResolvedValue([]), @@ -254,6 +265,8 @@ 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({ listSkills: vi.fn().mockResolvedValue([]), @@ -280,6 +293,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 +320,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 +346,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; @@ -446,6 +462,47 @@ 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('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 6dbeb258f6e..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,12 +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), - ); + 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/config/config.ts b/packages/core/src/config/config.ts index f2a87de9a34..dfafe6e4cb4 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, @@ -978,6 +979,7 @@ export interface ConfigParameters { question?: string; systemPrompt?: string; appendSystemPrompt?: string; + outputStyle?: OutputStyleDefinition; coreTools?: string[]; allowedTools?: string[]; excludeTools?: string[]; @@ -1873,6 +1875,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; @@ -2220,6 +2223,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; @@ -5510,6 +5514,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 c35f8d7a967..fdf87712ad8 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), @@ -13389,6 +13391,7 @@ Other open files: 'test-model', undefined, 'headless', + undefined, ); expect(mockContentGenerator.generateContent).toHaveBeenCalledWith( expect.objectContaining({ @@ -13400,6 +13403,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], @@ -13428,6 +13455,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 98b6279e900..1f5175f9270 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -347,6 +347,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; @@ -1165,15 +1190,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), - ); + const base = getMainSessionBaseSystemPrompt(this.config); const stableLayers = { base, contextFiles: this.config.getUserMemory(), 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..871ba7ae8a5 --- /dev/null +++ b/packages/core/src/core/output-styles.test.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +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'; + +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('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', () => { + 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('names the style with an Output Style heading', () => { + expect(renderOutputStyleSection(LAYERED)).toBe( + '# Output Style: Layered\nStyle body.', + ); + }); + + it('trims the prompt so the heading gap stays a single newline', () => { + expect( + renderOutputStyleSection({ ...LAYERED, prompt: '\n\nStyle body.\n\n' }), + ).toBe('# Output Style: Layered\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 the style section to the base prompt', () => { + expect(applyOutputStyle('BASE', LAYERED)).toBe( + 'BASE\n\n# Output Style: Layered\nStyle body.', + ); + }); + + 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( + '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 new file mode 100644 index 00000000000..e955b579e53 --- /dev/null +++ b/packages/core/src/core/output-styles.ts @@ -0,0 +1,186 @@ +/** + * @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; + /** + * `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 the `# Output Style: ` heading. */ + prompt: string; + /** + * 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', + description: + '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.', + 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. + * + * 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 `# 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 + }`; +} + +/** + * Appends an output style's section to a base system prompt. + * + * 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, + style?: OutputStyleDefinition | null, +): string { + if (!style) { + return basePrompt; + } + 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 7203886a8e7..088ec7a55c0 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,207 @@ 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(); + for (const style of BUILT_IN_OUTPUT_STYLES) { + expect(prompt).not.toContain(`# Output Style: ${style.name}`); + } + }); + + it('appends the style section to the end of the base prompt', () => { + const prompt = getCoreSystemPrompt( + undefined, + undefined, + undefined, + 'interactive', + concise, + ); + expect(prompt).toContain('# Output Style: Concise'); + // The style refines the mandates, so it has to land after them... + expect(prompt.indexOf('# Output Style: Concise')).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('# Output Style: Concise'); + 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('# 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('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).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 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. + 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. + 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('# Output Style: Concise'); + // ...and the dumped identity sentence is the unstyled one. + expect(written).toContain('specializing in software engineering tasks'); + }); + }); }); describe('Model-specific tool call formats', () => { diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 1355b6e8631..9627b6e8773 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'); @@ -88,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.`; } /** @@ -237,64 +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. + * 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', -): 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` only applies on the default-prompt branch and is - // 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 @@ -324,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 @@ -446,8 +435,72 @@ 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 { + // 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; + 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, effectiveOutputStyle); + // 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'], @@ -461,11 +514,21 @@ 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), + ); } return assembleSystemPrompt({ - base: basePrompt, + base: systemMdEnabled + ? basePrompt + : applyOutputStyle(basePrompt, effectiveOutputStyle), contextFiles: userMemory, appendPrompt: appendInstruction, }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1ee35e7fbe4..f0017574ea5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -91,6 +91,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/ask-user-question-restore.js'; export * from './core/tokenLimits.js';