mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
feat: add system prompt customization options in SDK and CLI
This commit is contained in:
1 parent
110fcd7b7b
commit
ee33a3c35e
17 files changed
+529
-14
No files matched your search
@@ -241,6 +241,30 @@ describe('parseArguments', () => {
|
||||
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 () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
@@ -432,6 +456,21 @@ describe('parseArguments', () => {
|
||||
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 () => {
|
||||
process.argv = ['node', 'script.js', '--include-partial-messages'];
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface CliArgs {
|
||||
debug: boolean | undefined;
|
||||
prompt: string | undefined;
|
||||
promptInteractive: string | undefined;
|
||||
systemPrompt: string | undefined;
|
||||
appendSystemPrompt: string | undefined;
|
||||
yolo: boolean | undefined;
|
||||
approvalMode: string | undefined;
|
||||
telemetry: boolean | undefined;
|
||||
@@ -290,6 +292,16 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
description:
|
||||
'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', {
|
||||
alias: 's',
|
||||
type: 'boolean',
|
||||
@@ -962,6 +974,8 @@ export async function loadCliConfig(
|
||||
importFormat: settings.context?.importFormat || 'tree',
|
||||
debugMode,
|
||||
question,
|
||||
systemPrompt: argv.systemPrompt,
|
||||
appendSystemPrompt: argv.appendSystemPrompt,
|
||||
coreTools: argv.coreTools || settings.tools?.core || undefined,
|
||||
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
|
||||
excludeTools,
|
||||
|
||||
@@ -467,6 +467,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
debug: undefined,
|
||||
prompt: undefined,
|
||||
promptInteractive: undefined,
|
||||
systemPrompt: undefined,
|
||||
appendSystemPrompt: undefined,
|
||||
query: undefined,
|
||||
yolo: 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', () => {
|
||||
it('should throw an error if checkpointing is enabled and GitService fails', async () => {
|
||||
const gitError = new Error('Git is not installed');
|
||||
|
||||
@@ -298,6 +298,8 @@ export interface ConfigParameters {
|
||||
debugMode: boolean;
|
||||
includePartialMessages?: boolean;
|
||||
question?: string;
|
||||
systemPrompt?: string;
|
||||
appendSystemPrompt?: string;
|
||||
coreTools?: string[];
|
||||
allowedTools?: string[];
|
||||
excludeTools?: string[];
|
||||
@@ -451,6 +453,8 @@ export class Config {
|
||||
private readonly outputFormat: OutputFormat;
|
||||
private readonly includePartialMessages: boolean;
|
||||
private readonly question: string | undefined;
|
||||
private readonly systemPrompt: string | undefined;
|
||||
private readonly appendSystemPrompt: string | undefined;
|
||||
private readonly coreTools: string[] | undefined;
|
||||
private readonly allowedTools: string[] | undefined;
|
||||
private readonly excludeTools: string[] | undefined;
|
||||
@@ -561,6 +565,8 @@ export class Config {
|
||||
this.outputFormat = normalizedOutputFormat ?? OutputFormat.TEXT;
|
||||
this.includePartialMessages = params.includePartialMessages ?? false;
|
||||
this.question = params.question;
|
||||
this.systemPrompt = params.systemPrompt;
|
||||
this.appendSystemPrompt = params.appendSystemPrompt;
|
||||
this.coreTools = params.coreTools;
|
||||
this.allowedTools = params.allowedTools;
|
||||
this.excludeTools = params.excludeTools;
|
||||
@@ -1208,6 +1214,14 @@ export class Config {
|
||||
return this.question;
|
||||
}
|
||||
|
||||
getSystemPrompt(): string | undefined {
|
||||
return this.systemPrompt;
|
||||
}
|
||||
|
||||
getAppendSystemPrompt(): string | undefined {
|
||||
return this.appendSystemPrompt;
|
||||
}
|
||||
|
||||
getCoreTools(): string[] | undefined {
|
||||
return this.coreTools;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
Turn,
|
||||
type ChatCompressionInfo,
|
||||
} from './turn.js';
|
||||
import { getCoreSystemPrompt } from './prompts.js';
|
||||
import { getCoreSystemPrompt, getCustomSystemPrompt } from './prompts.js';
|
||||
import { DEFAULT_QWEN_FLASH_MODEL } from '../config/models.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { setSimulate429 } from '../utils/testUtils.js';
|
||||
@@ -314,6 +314,8 @@ describe('Gemini Client (client.ts)', () => {
|
||||
getVertexAI: vi.fn().mockReturnValue(false),
|
||||
getUserAgent: vi.fn().mockReturnValue('test-agent'),
|
||||
getUserMemory: vi.fn().mockReturnValue(''),
|
||||
getSystemPrompt: vi.fn().mockReturnValue(undefined),
|
||||
getAppendSystemPrompt: vi.fn().mockReturnValue(undefined),
|
||||
getFullContext: vi.fn().mockReturnValue(false),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
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
|
||||
// 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> {
|
||||
this.forceFullIdeContext = true;
|
||||
this.hasFailedCompressionAttempt = false;
|
||||
@@ -194,9 +214,7 @@ export class GeminiClient {
|
||||
const history = await getInitialChatHistory(this.config, extraHistory);
|
||||
|
||||
try {
|
||||
const userMemory = this.config.getUserMemory();
|
||||
const model = this.config.getModel();
|
||||
const systemInstruction = getCoreSystemPrompt(userMemory, model);
|
||||
const systemInstruction = this.getMainSessionSystemInstruction();
|
||||
|
||||
return new GeminiChat(
|
||||
this.config,
|
||||
@@ -690,7 +708,7 @@ export class GeminiClient {
|
||||
const userMemory = this.config.getUserMemory();
|
||||
const finalSystemInstruction = generationConfig.systemInstruction
|
||||
? getCustomSystemPrompt(generationConfig.systemInstruction, userMemory)
|
||||
: getCoreSystemPrompt(userMemory, this.config.getModel());
|
||||
: this.getMainSessionSystemInstruction();
|
||||
|
||||
const requestConfig: GenerateContentConfig = {
|
||||
abortSignal,
|
||||
|
||||
@@ -80,6 +80,35 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
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', () => {
|
||||
vi.stubEnv('SANDBOX', 'true'); // Generic sandbox value
|
||||
const prompt = getCoreSystemPrompt();
|
||||
|
||||
@@ -72,11 +72,13 @@ export function resolvePathFromEnv(envVar?: string): {
|
||||
*
|
||||
* @param customInstruction - Custom system instruction (ContentUnion from @google/genai)
|
||||
* @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(
|
||||
customInstruction: GenerateContentConfig['systemInstruction'],
|
||||
userMemory?: string,
|
||||
appendInstruction?: string,
|
||||
): string {
|
||||
// Extract text from custom instruction
|
||||
let instructionText = '';
|
||||
@@ -100,17 +102,20 @@ export function getCustomSystemPrompt(
|
||||
}
|
||||
|
||||
// Append user memory using the same pattern as getCoreSystemPrompt
|
||||
const memorySuffix =
|
||||
userMemory && userMemory.trim().length > 0
|
||||
? `\n\n---\n\n${userMemory.trim()}`
|
||||
: '';
|
||||
const memorySuffix = buildSystemPromptSuffix(userMemory);
|
||||
|
||||
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(
|
||||
userMemory?: string,
|
||||
model?: string,
|
||||
appendInstruction?: string,
|
||||
): string {
|
||||
// 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
|
||||
@@ -338,10 +343,11 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
|
||||
|
||||
const memorySuffix =
|
||||
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. |
|
||||
| `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. |
|
||||
| `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 }`. |
|
||||
| `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. |
|
||||
@@ -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
|
||||
|
||||
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,
|
||||
CanUseTool,
|
||||
PermissionResult,
|
||||
QuerySystemPrompt,
|
||||
QuerySystemPromptPreset,
|
||||
CLIMcpServerConfig,
|
||||
McpServerConfig,
|
||||
McpOAuthConfig,
|
||||
|
||||
@@ -7,7 +7,11 @@ import { serializeJsonLine } from '../utils/jsonLines.js';
|
||||
import { ProcessTransport } from '../transport/ProcessTransport.js';
|
||||
import { prepareSpawnInfo, type SpawnInfo } from '../utils/cliPath.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 { SdkLogger } from '../utils/logger.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
@@ -44,6 +48,7 @@ export function query({
|
||||
|
||||
// Generate or use provided session ID for SDK-CLI alignment
|
||||
const sessionId = options.resume ?? options.sessionId ?? randomUUID();
|
||||
const resolvedSystemPrompt = resolveSystemPromptOption(options.systemPrompt);
|
||||
|
||||
const transport = new ProcessTransport({
|
||||
pathToQwenExecutable,
|
||||
@@ -52,6 +57,7 @@ export function query({
|
||||
model: options.model,
|
||||
permissionMode: options.permissionMode,
|
||||
env: options.env,
|
||||
...resolvedSystemPrompt,
|
||||
abortController,
|
||||
debug: options.debug,
|
||||
stderr: options.stderr,
|
||||
@@ -112,6 +118,20 @@ export function query({
|
||||
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 {
|
||||
const validationResult = QueryOptionsSchema.safeParse(options);
|
||||
if (!validationResult.success) {
|
||||
|
||||
@@ -232,6 +232,14 @@ export class ProcessTransport implements Transport {
|
||||
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) {
|
||||
args.push('--approval-mode', this.options.permissionMode);
|
||||
}
|
||||
|
||||
@@ -123,12 +123,29 @@ export const TimeoutConfigSchema = z.object({
|
||||
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
|
||||
.object({
|
||||
cwd: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
pathToQwenExecutable: 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(),
|
||||
canUseTool: z
|
||||
.custom<CanUseTool>((val) => typeof val === 'function', {
|
||||
|
||||
@@ -16,6 +16,8 @@ export type TransportOptions = {
|
||||
model?: string;
|
||||
permissionMode?: PermissionMode;
|
||||
env?: Record<string, string>;
|
||||
systemPrompt?: string;
|
||||
appendSystemPrompt?: string;
|
||||
abortController?: AbortController;
|
||||
debug?: boolean;
|
||||
stderr?: (message: string) => void;
|
||||
@@ -46,6 +48,14 @@ export type TransportOptions = {
|
||||
sessionId?: string;
|
||||
};
|
||||
|
||||
export interface QuerySystemPromptPreset {
|
||||
type: 'preset';
|
||||
preset: 'qwen_code';
|
||||
append?: string;
|
||||
}
|
||||
|
||||
export type QuerySystemPrompt = string | QuerySystemPromptPreset;
|
||||
|
||||
type ToolInput = Record<string, unknown>;
|
||||
|
||||
export type CanUseTool = (
|
||||
@@ -226,6 +236,16 @@ export interface QueryOptions {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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', () => {
|
||||
mockPrepareSpawnInfo.mockReturnValue({
|
||||
command: 'qwen',
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user