mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
feat: add system prompt customization options in SDK and CLI
This commit is contained in:
parent
110fcd7b7b
commit
ee33a3c35e
17 changed files with 529 additions and 14 deletions
|
|
@ -241,6 +241,30 @@ describe('parseArguments', () => {
|
||||||
expect(argv.prompt).toBeUndefined();
|
expect(argv.prompt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse --system-prompt', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--system-prompt',
|
||||||
|
'You are a test system prompt.',
|
||||||
|
];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
expect(argv.systemPrompt).toBe('You are a test system prompt.');
|
||||||
|
expect(argv.appendSystemPrompt).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse --append-system-prompt', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--append-system-prompt',
|
||||||
|
'Be extra concise.',
|
||||||
|
];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
expect(argv.appendSystemPrompt).toBe('Be extra concise.');
|
||||||
|
expect(argv.systemPrompt).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow -r flag as alias for --resume', async () => {
|
it('should allow -r flag as alias for --resume', async () => {
|
||||||
process.argv = [
|
process.argv = [
|
||||||
'node',
|
'node',
|
||||||
|
|
@ -432,6 +456,21 @@ describe('parseArguments', () => {
|
||||||
mockExit.mockRestore();
|
mockExit.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow --system-prompt and --append-system-prompt together', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--system-prompt',
|
||||||
|
'Override prompt',
|
||||||
|
'--append-system-prompt',
|
||||||
|
'Append prompt',
|
||||||
|
];
|
||||||
|
|
||||||
|
const argv = await parseArguments();
|
||||||
|
expect(argv.systemPrompt).toBe('Override prompt');
|
||||||
|
expect(argv.appendSystemPrompt).toBe('Append prompt');
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an error when include-partial-messages is used without stream-json output', async () => {
|
it('should throw an error when include-partial-messages is used without stream-json output', async () => {
|
||||||
process.argv = ['node', 'script.js', '--include-partial-messages'];
|
process.argv = ['node', 'script.js', '--include-partial-messages'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ export interface CliArgs {
|
||||||
debug: boolean | undefined;
|
debug: boolean | undefined;
|
||||||
prompt: string | undefined;
|
prompt: string | undefined;
|
||||||
promptInteractive: string | undefined;
|
promptInteractive: string | undefined;
|
||||||
|
systemPrompt: string | undefined;
|
||||||
|
appendSystemPrompt: string | undefined;
|
||||||
yolo: boolean | undefined;
|
yolo: boolean | undefined;
|
||||||
approvalMode: string | undefined;
|
approvalMode: string | undefined;
|
||||||
telemetry: boolean | undefined;
|
telemetry: boolean | undefined;
|
||||||
|
|
@ -290,6 +292,16 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||||
description:
|
description:
|
||||||
'Execute the provided prompt and continue in interactive mode',
|
'Execute the provided prompt and continue in interactive mode',
|
||||||
})
|
})
|
||||||
|
.option('system-prompt', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Override the main session system prompt for this run. Can be combined with --append-system-prompt.',
|
||||||
|
})
|
||||||
|
.option('append-system-prompt', {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Append instructions to the main session system prompt for this run. Can be combined with --system-prompt.',
|
||||||
|
})
|
||||||
.option('sandbox', {
|
.option('sandbox', {
|
||||||
alias: 's',
|
alias: 's',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
@ -962,6 +974,8 @@ export async function loadCliConfig(
|
||||||
importFormat: settings.context?.importFormat || 'tree',
|
importFormat: settings.context?.importFormat || 'tree',
|
||||||
debugMode,
|
debugMode,
|
||||||
question,
|
question,
|
||||||
|
systemPrompt: argv.systemPrompt,
|
||||||
|
appendSystemPrompt: argv.appendSystemPrompt,
|
||||||
coreTools: argv.coreTools || settings.tools?.core || undefined,
|
coreTools: argv.coreTools || settings.tools?.core || undefined,
|
||||||
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
|
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
|
||||||
excludeTools,
|
excludeTools,
|
||||||
|
|
|
||||||
|
|
@ -467,6 +467,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||||
debug: undefined,
|
debug: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
promptInteractive: undefined,
|
promptInteractive: undefined,
|
||||||
|
systemPrompt: undefined,
|
||||||
|
appendSystemPrompt: undefined,
|
||||||
query: undefined,
|
query: undefined,
|
||||||
yolo: undefined,
|
yolo: undefined,
|
||||||
approvalMode: undefined,
|
approvalMode: undefined,
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,26 @@ describe('Server Config (config.ts)', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should store a system prompt override', () => {
|
||||||
|
const config = new Config({
|
||||||
|
...baseParams,
|
||||||
|
systemPrompt: 'You are a custom system prompt.',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.getSystemPrompt()).toBe('You are a custom system prompt.');
|
||||||
|
expect(config.getAppendSystemPrompt()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store an appended system prompt', () => {
|
||||||
|
const config = new Config({
|
||||||
|
...baseParams,
|
||||||
|
appendSystemPrompt: 'Be extra concise.',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.getAppendSystemPrompt()).toBe('Be extra concise.');
|
||||||
|
expect(config.getSystemPrompt()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
describe('initialize', () => {
|
describe('initialize', () => {
|
||||||
it('should throw an error if checkpointing is enabled and GitService fails', async () => {
|
it('should throw an error if checkpointing is enabled and GitService fails', async () => {
|
||||||
const gitError = new Error('Git is not installed');
|
const gitError = new Error('Git is not installed');
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,8 @@ export interface ConfigParameters {
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
includePartialMessages?: boolean;
|
includePartialMessages?: boolean;
|
||||||
question?: string;
|
question?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
appendSystemPrompt?: string;
|
||||||
coreTools?: string[];
|
coreTools?: string[];
|
||||||
allowedTools?: string[];
|
allowedTools?: string[];
|
||||||
excludeTools?: string[];
|
excludeTools?: string[];
|
||||||
|
|
@ -451,6 +453,8 @@ export class Config {
|
||||||
private readonly outputFormat: OutputFormat;
|
private readonly outputFormat: OutputFormat;
|
||||||
private readonly includePartialMessages: boolean;
|
private readonly includePartialMessages: boolean;
|
||||||
private readonly question: string | undefined;
|
private readonly question: string | undefined;
|
||||||
|
private readonly systemPrompt: string | undefined;
|
||||||
|
private readonly appendSystemPrompt: string | undefined;
|
||||||
private readonly coreTools: string[] | undefined;
|
private readonly coreTools: string[] | undefined;
|
||||||
private readonly allowedTools: string[] | undefined;
|
private readonly allowedTools: string[] | undefined;
|
||||||
private readonly excludeTools: string[] | undefined;
|
private readonly excludeTools: string[] | undefined;
|
||||||
|
|
@ -561,6 +565,8 @@ export class Config {
|
||||||
this.outputFormat = normalizedOutputFormat ?? OutputFormat.TEXT;
|
this.outputFormat = normalizedOutputFormat ?? OutputFormat.TEXT;
|
||||||
this.includePartialMessages = params.includePartialMessages ?? false;
|
this.includePartialMessages = params.includePartialMessages ?? false;
|
||||||
this.question = params.question;
|
this.question = params.question;
|
||||||
|
this.systemPrompt = params.systemPrompt;
|
||||||
|
this.appendSystemPrompt = params.appendSystemPrompt;
|
||||||
this.coreTools = params.coreTools;
|
this.coreTools = params.coreTools;
|
||||||
this.allowedTools = params.allowedTools;
|
this.allowedTools = params.allowedTools;
|
||||||
this.excludeTools = params.excludeTools;
|
this.excludeTools = params.excludeTools;
|
||||||
|
|
@ -1208,6 +1214,14 @@ export class Config {
|
||||||
return this.question;
|
return this.question;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSystemPrompt(): string | undefined {
|
||||||
|
return this.systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppendSystemPrompt(): string | undefined {
|
||||||
|
return this.appendSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
getCoreTools(): string[] | undefined {
|
getCoreTools(): string[] | undefined {
|
||||||
return this.coreTools;
|
return this.coreTools;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
Turn,
|
Turn,
|
||||||
type ChatCompressionInfo,
|
type ChatCompressionInfo,
|
||||||
} from './turn.js';
|
} from './turn.js';
|
||||||
import { getCoreSystemPrompt } from './prompts.js';
|
import { getCoreSystemPrompt, getCustomSystemPrompt } from './prompts.js';
|
||||||
import { DEFAULT_QWEN_FLASH_MODEL } from '../config/models.js';
|
import { DEFAULT_QWEN_FLASH_MODEL } from '../config/models.js';
|
||||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
import { setSimulate429 } from '../utils/testUtils.js';
|
import { setSimulate429 } from '../utils/testUtils.js';
|
||||||
|
|
@ -314,6 +314,8 @@ describe('Gemini Client (client.ts)', () => {
|
||||||
getVertexAI: vi.fn().mockReturnValue(false),
|
getVertexAI: vi.fn().mockReturnValue(false),
|
||||||
getUserAgent: vi.fn().mockReturnValue('test-agent'),
|
getUserAgent: vi.fn().mockReturnValue('test-agent'),
|
||||||
getUserMemory: vi.fn().mockReturnValue(''),
|
getUserMemory: vi.fn().mockReturnValue(''),
|
||||||
|
getSystemPrompt: vi.fn().mockReturnValue(undefined),
|
||||||
|
getAppendSystemPrompt: vi.fn().mockReturnValue(undefined),
|
||||||
getFullContext: vi.fn().mockReturnValue(false),
|
getFullContext: vi.fn().mockReturnValue(false),
|
||||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
getProxy: vi.fn().mockReturnValue(undefined),
|
getProxy: vi.fn().mockReturnValue(undefined),
|
||||||
|
|
@ -2362,6 +2364,104 @@ Other open files:
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use config system prompt override when provided', async () => {
|
||||||
|
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||||
|
const abortSignal = new AbortController().signal;
|
||||||
|
|
||||||
|
vi.spyOn(client['config'], 'getSystemPrompt').mockReturnValue(
|
||||||
|
'Override prompt',
|
||||||
|
);
|
||||||
|
vi.spyOn(client['config'], 'getUserMemory').mockReturnValue(
|
||||||
|
'Saved memory',
|
||||||
|
);
|
||||||
|
vi.mocked(getCustomSystemPrompt).mockReturnValueOnce(
|
||||||
|
'Override prompt with memory',
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.generateContent(
|
||||||
|
contents,
|
||||||
|
{},
|
||||||
|
abortSignal,
|
||||||
|
DEFAULT_QWEN_FLASH_MODEL,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getCustomSystemPrompt).toHaveBeenCalledWith(
|
||||||
|
'Override prompt',
|
||||||
|
'Saved memory',
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
config: expect.objectContaining({
|
||||||
|
systemInstruction: 'Override prompt with memory',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'test-session-id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append config appendSystemPrompt to the core system prompt', async () => {
|
||||||
|
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||||
|
const abortSignal = new AbortController().signal;
|
||||||
|
|
||||||
|
vi.mocked(getCoreSystemPrompt).mockClear();
|
||||||
|
vi.spyOn(client['config'], 'getAppendSystemPrompt').mockReturnValue(
|
||||||
|
'Be extra concise.',
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.generateContent(
|
||||||
|
contents,
|
||||||
|
{},
|
||||||
|
abortSignal,
|
||||||
|
DEFAULT_QWEN_FLASH_MODEL,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getCoreSystemPrompt).toHaveBeenCalledWith(
|
||||||
|
'',
|
||||||
|
'test-model',
|
||||||
|
'Be extra concise.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append config appendSystemPrompt after a config system prompt override', async () => {
|
||||||
|
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||||
|
const abortSignal = new AbortController().signal;
|
||||||
|
|
||||||
|
vi.spyOn(client['config'], 'getSystemPrompt').mockReturnValue(
|
||||||
|
'Override prompt',
|
||||||
|
);
|
||||||
|
vi.spyOn(client['config'], 'getAppendSystemPrompt').mockReturnValue(
|
||||||
|
'Focus on findings only.',
|
||||||
|
);
|
||||||
|
vi.spyOn(client['config'], 'getUserMemory').mockReturnValue(
|
||||||
|
'Saved memory',
|
||||||
|
);
|
||||||
|
vi.mocked(getCustomSystemPrompt).mockReturnValueOnce(
|
||||||
|
'Override prompt with memory and append',
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.generateContent(
|
||||||
|
contents,
|
||||||
|
{},
|
||||||
|
abortSignal,
|
||||||
|
DEFAULT_QWEN_FLASH_MODEL,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getCustomSystemPrompt).toHaveBeenCalledWith(
|
||||||
|
'Override prompt',
|
||||||
|
'Saved memory',
|
||||||
|
'Focus on findings only.',
|
||||||
|
);
|
||||||
|
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
config: expect.objectContaining({
|
||||||
|
systemInstruction: 'Override prompt with memory and append',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'test-session-id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Note: there is currently no "fallback mode" model routing; the model used
|
// Note: there is currently no "fallback mode" model routing; the model used
|
||||||
// is always the one explicitly requested by the caller.
|
// is always the one explicitly requested by the caller.
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,26 @@ export class GeminiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getMainSessionSystemInstruction(): string {
|
||||||
|
const userMemory = this.config.getUserMemory();
|
||||||
|
const overrideSystemPrompt = this.config.getSystemPrompt();
|
||||||
|
const appendSystemPrompt = this.config.getAppendSystemPrompt();
|
||||||
|
|
||||||
|
if (overrideSystemPrompt) {
|
||||||
|
return getCustomSystemPrompt(
|
||||||
|
overrideSystemPrompt,
|
||||||
|
userMemory,
|
||||||
|
appendSystemPrompt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCoreSystemPrompt(
|
||||||
|
userMemory,
|
||||||
|
this.config.getModel(),
|
||||||
|
appendSystemPrompt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async startChat(extraHistory?: Content[]): Promise<GeminiChat> {
|
async startChat(extraHistory?: Content[]): Promise<GeminiChat> {
|
||||||
this.forceFullIdeContext = true;
|
this.forceFullIdeContext = true;
|
||||||
this.hasFailedCompressionAttempt = false;
|
this.hasFailedCompressionAttempt = false;
|
||||||
|
|
@ -194,9 +214,7 @@ export class GeminiClient {
|
||||||
const history = await getInitialChatHistory(this.config, extraHistory);
|
const history = await getInitialChatHistory(this.config, extraHistory);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userMemory = this.config.getUserMemory();
|
const systemInstruction = this.getMainSessionSystemInstruction();
|
||||||
const model = this.config.getModel();
|
|
||||||
const systemInstruction = getCoreSystemPrompt(userMemory, model);
|
|
||||||
|
|
||||||
return new GeminiChat(
|
return new GeminiChat(
|
||||||
this.config,
|
this.config,
|
||||||
|
|
@ -690,7 +708,7 @@ export class GeminiClient {
|
||||||
const userMemory = this.config.getUserMemory();
|
const userMemory = this.config.getUserMemory();
|
||||||
const finalSystemInstruction = generationConfig.systemInstruction
|
const finalSystemInstruction = generationConfig.systemInstruction
|
||||||
? getCustomSystemPrompt(generationConfig.systemInstruction, userMemory)
|
? getCustomSystemPrompt(generationConfig.systemInstruction, userMemory)
|
||||||
: getCoreSystemPrompt(userMemory, this.config.getModel());
|
: this.getMainSessionSystemInstruction();
|
||||||
|
|
||||||
const requestConfig: GenerateContentConfig = {
|
const requestConfig: GenerateContentConfig = {
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,35 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||||
expect(prompt).toMatchSnapshot(); // Snapshot the combined prompt
|
expect(prompt).toMatchSnapshot(); // Snapshot the combined prompt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should append extra system prompt instructions after user memory when provided', () => {
|
||||||
|
vi.stubEnv('SANDBOX', undefined);
|
||||||
|
const memory = 'Remember the project conventions.';
|
||||||
|
const appendInstruction = 'Always answer in exactly one sentence.';
|
||||||
|
const prompt = getCoreSystemPrompt(memory, undefined, appendInstruction);
|
||||||
|
|
||||||
|
expect(prompt).toContain(`\n\n---\n\n${memory}`);
|
||||||
|
expect(prompt).toContain(`\n\n---\n\n${appendInstruction}`);
|
||||||
|
expect(prompt.indexOf(memory)).toBeLessThan(
|
||||||
|
prompt.indexOf(appendInstruction),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append extra instructions after a custom system prompt and user memory', () => {
|
||||||
|
const customInstruction = 'You are a release manager.';
|
||||||
|
const userMemory = 'The repo uses pnpm.';
|
||||||
|
const appendInstruction = 'Only report blocking issues.';
|
||||||
|
|
||||||
|
const result = getCustomSystemPrompt(
|
||||||
|
customInstruction,
|
||||||
|
userMemory,
|
||||||
|
appendInstruction,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
[customInstruction, userMemory, appendInstruction].join('\n\n---\n\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should include sandbox-specific instructions when SANDBOX env var is set', () => {
|
it('should include sandbox-specific instructions when SANDBOX env var is set', () => {
|
||||||
vi.stubEnv('SANDBOX', 'true'); // Generic sandbox value
|
vi.stubEnv('SANDBOX', 'true'); // Generic sandbox value
|
||||||
const prompt = getCoreSystemPrompt();
|
const prompt = getCoreSystemPrompt();
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,13 @@ export function resolvePathFromEnv(envVar?: string): {
|
||||||
*
|
*
|
||||||
* @param customInstruction - Custom system instruction (ContentUnion from @google/genai)
|
* @param customInstruction - Custom system instruction (ContentUnion from @google/genai)
|
||||||
* @param userMemory - User memory to append
|
* @param userMemory - User memory to append
|
||||||
* @returns Processed custom system instruction with user memory appended
|
* @param appendInstruction - Extra instructions to append after user memory
|
||||||
|
* @returns Processed custom system instruction with user memory and extra append instructions applied
|
||||||
*/
|
*/
|
||||||
export function getCustomSystemPrompt(
|
export function getCustomSystemPrompt(
|
||||||
customInstruction: GenerateContentConfig['systemInstruction'],
|
customInstruction: GenerateContentConfig['systemInstruction'],
|
||||||
userMemory?: string,
|
userMemory?: string,
|
||||||
|
appendInstruction?: string,
|
||||||
): string {
|
): string {
|
||||||
// Extract text from custom instruction
|
// Extract text from custom instruction
|
||||||
let instructionText = '';
|
let instructionText = '';
|
||||||
|
|
@ -100,17 +102,20 @@ export function getCustomSystemPrompt(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append user memory using the same pattern as getCoreSystemPrompt
|
// Append user memory using the same pattern as getCoreSystemPrompt
|
||||||
const memorySuffix =
|
const memorySuffix = buildSystemPromptSuffix(userMemory);
|
||||||
userMemory && userMemory.trim().length > 0
|
|
||||||
? `\n\n---\n\n${userMemory.trim()}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `${instructionText}${memorySuffix}`;
|
return `${instructionText}${memorySuffix}${buildSystemPromptSuffix(appendInstruction)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSystemPromptSuffix(text?: string): string {
|
||||||
|
const trimmed = text?.trim();
|
||||||
|
return trimmed ? `\n\n---\n\n${trimmed}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCoreSystemPrompt(
|
export function getCoreSystemPrompt(
|
||||||
userMemory?: string,
|
userMemory?: string,
|
||||||
model?: string,
|
model?: string,
|
||||||
|
appendInstruction?: string,
|
||||||
): string {
|
): string {
|
||||||
// if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file
|
// if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file
|
||||||
// default path is .qwen/system.md but can be modified via custom path in QWEN_SYSTEM_MD
|
// default path is .qwen/system.md but can be modified via custom path in QWEN_SYSTEM_MD
|
||||||
|
|
@ -338,10 +343,11 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
|
||||||
|
|
||||||
const memorySuffix =
|
const memorySuffix =
|
||||||
userMemory && userMemory.trim().length > 0
|
userMemory && userMemory.trim().length > 0
|
||||||
? `\n\n---\n\n${userMemory.trim()}`
|
? buildSystemPromptSuffix(userMemory)
|
||||||
: '';
|
: '';
|
||||||
|
const appendSuffix = buildSystemPromptSuffix(appendInstruction);
|
||||||
|
|
||||||
return `${basePrompt}${memorySuffix}`;
|
return `${basePrompt}${memorySuffix}${appendSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ Creates a new query session with the Qwen Code.
|
||||||
| `permissionMode` | `'default' \| 'plan' \| 'auto-edit' \| 'yolo'` | `'default'` | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details. |
|
| `permissionMode` | `'default' \| 'plan' \| 'auto-edit' \| 'yolo'` | `'default'` | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details. |
|
||||||
| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). |
|
| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). |
|
||||||
| `env` | `Record<string, string>` | - | Environment variables to pass to the Qwen Code process. Merged with the current process environment. |
|
| `env` | `Record<string, string>` | - | Environment variables to pass to the Qwen Code process. Merged with the current process environment. |
|
||||||
|
| `systemPrompt` | `string \| QuerySystemPromptPreset` | - | System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions. |
|
||||||
| `mcpServers` | `Record<string, McpServerConfig>` | - | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`. |
|
| `mcpServers` | `Record<string, McpServerConfig>` | - | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`. |
|
||||||
| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. |
|
| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. |
|
||||||
| `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. |
|
| `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. |
|
||||||
|
|
@ -247,6 +248,36 @@ const result = query({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Override the System Prompt
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { query } from '@qwen-code/sdk';
|
||||||
|
|
||||||
|
const result = query({
|
||||||
|
prompt: 'Say hello in one sentence.',
|
||||||
|
options: {
|
||||||
|
systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Append to the Built-in System Prompt
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { query } from '@qwen-code/sdk';
|
||||||
|
|
||||||
|
const result = query({
|
||||||
|
prompt: 'Review the current directory.',
|
||||||
|
options: {
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'preset',
|
||||||
|
preset: 'qwen_code',
|
||||||
|
append: 'Be terse and focus on concrete findings.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### With SDK-Embedded MCP Servers
|
### With SDK-Embedded MCP Servers
|
||||||
|
|
||||||
The SDK provides `tool` and `createSdkMcpServer` to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.
|
The SDK provides `tool` and `createSdkMcpServer` to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ export type {
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
CanUseTool,
|
CanUseTool,
|
||||||
PermissionResult,
|
PermissionResult,
|
||||||
|
QuerySystemPrompt,
|
||||||
|
QuerySystemPromptPreset,
|
||||||
CLIMcpServerConfig,
|
CLIMcpServerConfig,
|
||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
McpOAuthConfig,
|
McpOAuthConfig,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ import { serializeJsonLine } from '../utils/jsonLines.js';
|
||||||
import { ProcessTransport } from '../transport/ProcessTransport.js';
|
import { ProcessTransport } from '../transport/ProcessTransport.js';
|
||||||
import { prepareSpawnInfo, type SpawnInfo } from '../utils/cliPath.js';
|
import { prepareSpawnInfo, type SpawnInfo } from '../utils/cliPath.js';
|
||||||
import { Query } from './Query.js';
|
import { Query } from './Query.js';
|
||||||
import type { QueryOptions } from '../types/types.js';
|
import type {
|
||||||
|
QueryOptions,
|
||||||
|
QuerySystemPrompt,
|
||||||
|
TransportOptions,
|
||||||
|
} from '../types/types.js';
|
||||||
import { QueryOptionsSchema } from '../types/queryOptionsSchema.js';
|
import { QueryOptionsSchema } from '../types/queryOptionsSchema.js';
|
||||||
import { SdkLogger } from '../utils/logger.js';
|
import { SdkLogger } from '../utils/logger.js';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
@ -44,6 +48,7 @@ export function query({
|
||||||
|
|
||||||
// Generate or use provided session ID for SDK-CLI alignment
|
// Generate or use provided session ID for SDK-CLI alignment
|
||||||
const sessionId = options.resume ?? options.sessionId ?? randomUUID();
|
const sessionId = options.resume ?? options.sessionId ?? randomUUID();
|
||||||
|
const resolvedSystemPrompt = resolveSystemPromptOption(options.systemPrompt);
|
||||||
|
|
||||||
const transport = new ProcessTransport({
|
const transport = new ProcessTransport({
|
||||||
pathToQwenExecutable,
|
pathToQwenExecutable,
|
||||||
|
|
@ -52,6 +57,7 @@ export function query({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
permissionMode: options.permissionMode,
|
permissionMode: options.permissionMode,
|
||||||
env: options.env,
|
env: options.env,
|
||||||
|
...resolvedSystemPrompt,
|
||||||
abortController,
|
abortController,
|
||||||
debug: options.debug,
|
debug: options.debug,
|
||||||
stderr: options.stderr,
|
stderr: options.stderr,
|
||||||
|
|
@ -112,6 +118,20 @@ export function query({
|
||||||
return queryInstance;
|
return queryInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSystemPromptOption(
|
||||||
|
systemPrompt: QuerySystemPrompt | undefined,
|
||||||
|
): Pick<TransportOptions, 'systemPrompt' | 'appendSystemPrompt'> {
|
||||||
|
if (!systemPrompt) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof systemPrompt === 'string') {
|
||||||
|
return { systemPrompt };
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPrompt.append ? { appendSystemPrompt: systemPrompt.append } : {};
|
||||||
|
}
|
||||||
|
|
||||||
function validateOptions(options: QueryOptions): SpawnInfo | undefined {
|
function validateOptions(options: QueryOptions): SpawnInfo | undefined {
|
||||||
const validationResult = QueryOptionsSchema.safeParse(options);
|
const validationResult = QueryOptionsSchema.safeParse(options);
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,14 @@ export class ProcessTransport implements Transport {
|
||||||
args.push('--model', this.options.model);
|
args.push('--model', this.options.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.systemPrompt) {
|
||||||
|
args.push('--system-prompt', this.options.systemPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.appendSystemPrompt) {
|
||||||
|
args.push('--append-system-prompt', this.options.appendSystemPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.permissionMode) {
|
if (this.options.permissionMode) {
|
||||||
args.push('--approval-mode', this.options.permissionMode);
|
args.push('--approval-mode', this.options.permissionMode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,12 +123,29 @@ export const TimeoutConfigSchema = z.object({
|
||||||
streamClose: z.number().positive().optional(),
|
streamClose: z.number().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const QuerySystemPromptPresetSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.literal('preset'),
|
||||||
|
preset: z.literal('qwen_code'),
|
||||||
|
append: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'systemPrompt.append must be a non-empty string')
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const QueryOptionsSchema = z
|
export const QueryOptionsSchema = z
|
||||||
.object({
|
.object({
|
||||||
cwd: z.string().optional(),
|
cwd: z.string().optional(),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
pathToQwenExecutable: z.string().optional(),
|
pathToQwenExecutable: z.string().optional(),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
|
systemPrompt: z
|
||||||
|
.union([
|
||||||
|
z.string().min(1, 'systemPrompt must be a non-empty string'),
|
||||||
|
QuerySystemPromptPresetSchema,
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
permissionMode: z.enum(['default', 'plan', 'auto-edit', 'yolo']).optional(),
|
permissionMode: z.enum(['default', 'plan', 'auto-edit', 'yolo']).optional(),
|
||||||
canUseTool: z
|
canUseTool: z
|
||||||
.custom<CanUseTool>((val) => typeof val === 'function', {
|
.custom<CanUseTool>((val) => typeof val === 'function', {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export type TransportOptions = {
|
||||||
model?: string;
|
model?: string;
|
||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
systemPrompt?: string;
|
||||||
|
appendSystemPrompt?: string;
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
stderr?: (message: string) => void;
|
stderr?: (message: string) => void;
|
||||||
|
|
@ -46,6 +48,14 @@ export type TransportOptions = {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface QuerySystemPromptPreset {
|
||||||
|
type: 'preset';
|
||||||
|
preset: 'qwen_code';
|
||||||
|
append?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuerySystemPrompt = string | QuerySystemPromptPreset;
|
||||||
|
|
||||||
type ToolInput = Record<string, unknown>;
|
type ToolInput = Record<string, unknown>;
|
||||||
|
|
||||||
export type CanUseTool = (
|
export type CanUseTool = (
|
||||||
|
|
@ -226,6 +236,16 @@ export interface QueryOptions {
|
||||||
*/
|
*/
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System prompt configuration for the Qwen CLI session.
|
||||||
|
*
|
||||||
|
* - `string`: fully overrides the main session system prompt
|
||||||
|
* - `{ type: 'preset', preset: 'qwen_code', append?: string }`:
|
||||||
|
* uses Qwen Code's built-in prompt as the base and optionally appends extra
|
||||||
|
* instructions for the main session
|
||||||
|
*/
|
||||||
|
systemPrompt?: QuerySystemPrompt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permission mode controlling how the SDK handles tool execution approval.
|
* Permission mode controlling how the SDK handles tool execution approval.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,84 @@ describe('ProcessTransport', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass systemPrompt through --system-prompt', () => {
|
||||||
|
mockPrepareSpawnInfo.mockReturnValue({
|
||||||
|
command: 'qwen',
|
||||||
|
args: [],
|
||||||
|
type: 'native',
|
||||||
|
originalInput: 'qwen',
|
||||||
|
});
|
||||||
|
mockSpawn.mockReturnValue(mockChildProcess);
|
||||||
|
|
||||||
|
const options: TransportOptions = {
|
||||||
|
pathToQwenExecutable: 'qwen',
|
||||||
|
systemPrompt: 'You are a test system prompt.',
|
||||||
|
};
|
||||||
|
|
||||||
|
new ProcessTransport(options);
|
||||||
|
|
||||||
|
expect(mockSpawn).toHaveBeenCalledWith(
|
||||||
|
'qwen',
|
||||||
|
expect.arrayContaining([
|
||||||
|
'--system-prompt',
|
||||||
|
'You are a test system prompt.',
|
||||||
|
]),
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass appendSystemPrompt through --append-system-prompt', () => {
|
||||||
|
mockPrepareSpawnInfo.mockReturnValue({
|
||||||
|
command: 'qwen',
|
||||||
|
args: [],
|
||||||
|
type: 'native',
|
||||||
|
originalInput: 'qwen',
|
||||||
|
});
|
||||||
|
mockSpawn.mockReturnValue(mockChildProcess);
|
||||||
|
|
||||||
|
const options: TransportOptions = {
|
||||||
|
pathToQwenExecutable: 'qwen',
|
||||||
|
appendSystemPrompt: 'Be extra concise.',
|
||||||
|
};
|
||||||
|
|
||||||
|
new ProcessTransport(options);
|
||||||
|
|
||||||
|
expect(mockSpawn).toHaveBeenCalledWith(
|
||||||
|
'qwen',
|
||||||
|
expect.arrayContaining(['--append-system-prompt', 'Be extra concise.']),
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass both systemPrompt and appendSystemPrompt when provided', () => {
|
||||||
|
mockPrepareSpawnInfo.mockReturnValue({
|
||||||
|
command: 'qwen',
|
||||||
|
args: [],
|
||||||
|
type: 'native',
|
||||||
|
originalInput: 'qwen',
|
||||||
|
});
|
||||||
|
mockSpawn.mockReturnValue(mockChildProcess);
|
||||||
|
|
||||||
|
const options: TransportOptions = {
|
||||||
|
pathToQwenExecutable: 'qwen',
|
||||||
|
systemPrompt: 'Override prompt',
|
||||||
|
appendSystemPrompt: 'Append prompt',
|
||||||
|
};
|
||||||
|
|
||||||
|
new ProcessTransport(options);
|
||||||
|
|
||||||
|
expect(mockSpawn).toHaveBeenCalledWith(
|
||||||
|
'qwen',
|
||||||
|
expect.arrayContaining([
|
||||||
|
'--system-prompt',
|
||||||
|
'Override prompt',
|
||||||
|
'--append-system-prompt',
|
||||||
|
'Append prompt',
|
||||||
|
]),
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should include --resume argument when provided', () => {
|
it('should include --resume argument when provided', () => {
|
||||||
mockPrepareSpawnInfo.mockReturnValue({
|
mockPrepareSpawnInfo.mockReturnValue({
|
||||||
command: 'qwen',
|
command: 'qwen',
|
||||||
|
|
|
||||||
97
packages/sdk-typescript/test/unit/createQuery.test.ts
Normal file
97
packages/sdk-typescript/test/unit/createQuery.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Unit tests for query() option mapping
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
import type { QueryOptions } from '../../src/query/createQuery.js';
|
||||||
|
|
||||||
|
const mockProcessTransport = vi.fn();
|
||||||
|
const mockQuery = vi.fn();
|
||||||
|
const mockPrepareSpawnInfo = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../../src/transport/ProcessTransport.js', () => ({
|
||||||
|
ProcessTransport: mockProcessTransport,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../src/query/Query.js', () => ({
|
||||||
|
Query: mockQuery,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../src/utils/cliPath.js', () => ({
|
||||||
|
prepareSpawnInfo: mockPrepareSpawnInfo,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('query()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockPrepareSpawnInfo.mockReturnValue(undefined);
|
||||||
|
mockProcessTransport.mockImplementation(() => ({
|
||||||
|
write: vi.fn(),
|
||||||
|
readMessages: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
waitForExit: vi.fn(),
|
||||||
|
endInput: vi.fn(),
|
||||||
|
exitError: null,
|
||||||
|
}));
|
||||||
|
mockQuery.mockImplementation(() => ({
|
||||||
|
initialized: Promise.resolve(),
|
||||||
|
getSessionId: () => 'test-session-id',
|
||||||
|
streamInput: vi.fn(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps string systemPrompt to TransportOptions.systemPrompt', async () => {
|
||||||
|
const { query } = await import('../../src/query/createQuery.js');
|
||||||
|
|
||||||
|
query({
|
||||||
|
prompt: 'hello',
|
||||||
|
options: {
|
||||||
|
systemPrompt: 'You are a strict reviewer.',
|
||||||
|
} satisfies QueryOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockProcessTransport).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
systemPrompt: 'You are a strict reviewer.',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps preset systemPrompt append to TransportOptions.appendSystemPrompt', async () => {
|
||||||
|
const { query } = await import('../../src/query/createQuery.js');
|
||||||
|
|
||||||
|
query({
|
||||||
|
prompt: 'hello',
|
||||||
|
options: {
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'preset',
|
||||||
|
preset: 'qwen_code',
|
||||||
|
append: 'Be terse.',
|
||||||
|
},
|
||||||
|
} satisfies QueryOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const transportOptions = mockProcessTransport.mock.calls[0]?.[0];
|
||||||
|
|
||||||
|
expect(transportOptions.appendSystemPrompt).toBe('Be terse.');
|
||||||
|
expect(transportOptions.systemPrompt).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-qwen preset names at runtime validation', async () => {
|
||||||
|
const { query } = await import('../../src/query/createQuery.js');
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
query({
|
||||||
|
prompt: 'hello',
|
||||||
|
options: {
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'preset',
|
||||||
|
preset: 'claude_code',
|
||||||
|
append: 'Be terse.',
|
||||||
|
} as never,
|
||||||
|
} satisfies QueryOptions,
|
||||||
|
}),
|
||||||
|
).toThrow(/systemPrompt/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue