mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
openspec/lightweight-tasks/task1-2-2.md
feat: add support for stream-json format and includePartialMessages flag in CLI arguments
This commit is contained in:
@@ -392,6 +392,49 @@ describe('parseArguments', () => {
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
|
||||
it('should throw an error when include-partial-messages is used without stream-json output', async () => {
|
||||
process.argv = ['node', 'script.js', '--include-partial-messages'];
|
||||
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'--include-partial-messages requires --output-format stream-json',
|
||||
),
|
||||
);
|
||||
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
|
||||
it('should parse stream-json formats and include-partial-messages flag', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'--input-format',
|
||||
'stream-json',
|
||||
'--include-partial-messages',
|
||||
];
|
||||
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
expect(argv.outputFormat).toBe('stream-json');
|
||||
expect(argv.inputFormat).toBe('stream-json');
|
||||
expect(argv.includePartialMessages).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow --approval-mode without --yolo', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'auto-edit'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
@@ -473,6 +516,34 @@ describe('loadCliConfig', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should propagate stream-json formats to config', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'--input-format',
|
||||
'stream-json',
|
||||
'--include-partial-messages',
|
||||
];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
[],
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getOutputFormat()).toBe('stream-json');
|
||||
expect(config.getInputFormat()).toBe('stream-json');
|
||||
expect(config.getIncludePartialMessages()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type {
|
||||
FileFilteringOptions,
|
||||
MCPServerConfig,
|
||||
OutputFormat,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
WriteFileTool,
|
||||
resolveTelemetrySettings,
|
||||
FatalConfigError,
|
||||
OutputFormat,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { Settings } from './settings.js';
|
||||
import yargs, { type Argv } from 'yargs';
|
||||
@@ -119,7 +119,24 @@ export interface CliArgs {
|
||||
screenReader: boolean | undefined;
|
||||
vlmSwitchMode: string | undefined;
|
||||
useSmartEdit: boolean | undefined;
|
||||
inputFormat?: string | undefined;
|
||||
outputFormat: string | undefined;
|
||||
includePartialMessages?: boolean;
|
||||
}
|
||||
|
||||
function normalizeOutputFormat(
|
||||
format: string | OutputFormat | undefined,
|
||||
): OutputFormat | 'stream-json' | undefined {
|
||||
if (!format) {
|
||||
return undefined;
|
||||
}
|
||||
if (format === 'stream-json') {
|
||||
return 'stream-json';
|
||||
}
|
||||
if (format === 'json' || format === OutputFormat.JSON) {
|
||||
return OutputFormat.JSON;
|
||||
}
|
||||
return OutputFormat.TEXT;
|
||||
}
|
||||
|
||||
export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
@@ -337,11 +354,23 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
'Default behavior when images are detected in input. Values: once (one-time switch), session (switch for entire session), persist (continue with current model). Overrides settings files.',
|
||||
default: process.env['VLM_SWITCH_MODE'],
|
||||
})
|
||||
.option('input-format', {
|
||||
type: 'string',
|
||||
choices: ['text', 'stream-json'],
|
||||
description: 'The format consumed from standard input.',
|
||||
default: 'text',
|
||||
})
|
||||
.option('output-format', {
|
||||
alias: 'o',
|
||||
type: 'string',
|
||||
description: 'The format of the CLI output.',
|
||||
choices: ['text', 'json'],
|
||||
choices: ['text', 'json', 'stream-json'],
|
||||
})
|
||||
.option('include-partial-messages', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Include partial assistant messages when using stream-json output.',
|
||||
default: false,
|
||||
})
|
||||
.deprecateOption(
|
||||
'show-memory-usage',
|
||||
@@ -386,6 +415,12 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
if (argv['yolo'] && argv['approvalMode']) {
|
||||
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
|
||||
}
|
||||
if (
|
||||
argv['includePartialMessages'] &&
|
||||
argv['outputFormat'] !== 'stream-json'
|
||||
) {
|
||||
return '--include-partial-messages requires --output-format stream-json';
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
)
|
||||
@@ -566,6 +601,21 @@ export async function loadCliConfig(
|
||||
|
||||
let mcpServers = mergeMcpServers(settings, activeExtensions);
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
const inputFormat =
|
||||
(argv.inputFormat as 'text' | 'stream-json' | undefined) ?? 'text';
|
||||
const argvOutputFormat = normalizeOutputFormat(
|
||||
argv.outputFormat as string | OutputFormat | undefined,
|
||||
);
|
||||
const settingsOutputFormat = normalizeOutputFormat(settings.output?.format);
|
||||
const outputFormat =
|
||||
argvOutputFormat ?? settingsOutputFormat ?? OutputFormat.TEXT;
|
||||
const outputSettingsFormat: OutputFormat =
|
||||
outputFormat === 'stream-json'
|
||||
? settingsOutputFormat && settingsOutputFormat !== 'stream-json'
|
||||
? settingsOutputFormat
|
||||
: OutputFormat.TEXT
|
||||
: (outputFormat as OutputFormat);
|
||||
const includePartialMessages = Boolean(argv.includePartialMessages);
|
||||
|
||||
// Determine approval mode with backward compatibility
|
||||
let approvalMode: ApprovalMode;
|
||||
@@ -610,8 +660,10 @@ export async function loadCliConfig(
|
||||
// Interactive mode: explicit -i flag or (TTY + no args + no -p flag)
|
||||
const hasQuery = !!argv.query;
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
(process.stdin.isTTY && !hasQuery && !argv.prompt);
|
||||
inputFormat === 'stream-json'
|
||||
? false
|
||||
: !!argv.promptInteractive ||
|
||||
(process.stdin.isTTY && !hasQuery && !argv.prompt);
|
||||
// In non-interactive mode, exclude tools that require a prompt.
|
||||
const extraExcludes: string[] = [];
|
||||
if (!interactive && !argv.experimentalAcp) {
|
||||
@@ -733,6 +785,9 @@ export async function loadCliConfig(
|
||||
blockedMcpServers,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
authType: settings.security?.auth?.selectedType,
|
||||
inputFormat,
|
||||
outputFormat,
|
||||
includePartialMessages,
|
||||
generationConfig: {
|
||||
...(settings.model?.generationConfig || {}),
|
||||
model: resolvedModel,
|
||||
@@ -772,7 +827,7 @@ export async function loadCliConfig(
|
||||
eventEmitter: appEvents,
|
||||
useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit,
|
||||
output: {
|
||||
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
||||
format: outputSettingsFormat,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,7 +333,9 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
screenReader: undefined,
|
||||
vlmSwitchMode: undefined,
|
||||
useSmartEdit: undefined,
|
||||
inputFormat: undefined,
|
||||
outputFormat: undefined,
|
||||
includePartialMessages: undefined,
|
||||
});
|
||||
|
||||
await main();
|
||||
|
||||
@@ -214,6 +214,9 @@ export interface ConfigParameters {
|
||||
sandbox?: SandboxConfig;
|
||||
targetDir: string;
|
||||
debugMode: boolean;
|
||||
inputFormat?: 'text' | 'stream-json';
|
||||
outputFormat?: OutputFormat | 'text' | 'json' | 'stream-json';
|
||||
includePartialMessages?: boolean;
|
||||
question?: string;
|
||||
fullContext?: boolean;
|
||||
coreTools?: string[];
|
||||
@@ -282,6 +285,25 @@ export interface ConfigParameters {
|
||||
output?: OutputSettings;
|
||||
}
|
||||
|
||||
function normalizeConfigOutputFormat(
|
||||
format: OutputFormat | 'text' | 'json' | 'stream-json' | undefined,
|
||||
): OutputFormat | 'stream-json' | undefined {
|
||||
if (!format) {
|
||||
return undefined;
|
||||
}
|
||||
switch (format) {
|
||||
case 'stream-json':
|
||||
return 'stream-json';
|
||||
case 'json':
|
||||
case OutputFormat.JSON:
|
||||
return OutputFormat.JSON;
|
||||
case 'text':
|
||||
case OutputFormat.TEXT:
|
||||
default:
|
||||
return OutputFormat.TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
export class Config {
|
||||
private toolRegistry!: ToolRegistry;
|
||||
private promptRegistry!: PromptRegistry;
|
||||
@@ -296,6 +318,9 @@ export class Config {
|
||||
private readonly targetDir: string;
|
||||
private workspaceContext: WorkspaceContext;
|
||||
private readonly debugMode: boolean;
|
||||
private readonly inputFormat: 'text' | 'stream-json';
|
||||
private readonly outputFormat: OutputFormat | 'stream-json';
|
||||
private readonly includePartialMessages: boolean;
|
||||
private readonly question: string | undefined;
|
||||
private readonly fullContext: boolean;
|
||||
private readonly coreTools: string[] | undefined;
|
||||
@@ -370,7 +395,6 @@ export class Config {
|
||||
private readonly enableToolOutputTruncation: boolean;
|
||||
private readonly eventEmitter?: EventEmitter;
|
||||
private readonly useSmartEdit: boolean;
|
||||
private readonly outputSettings: OutputSettings;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this.sessionId = params.sessionId;
|
||||
@@ -383,6 +407,12 @@ export class Config {
|
||||
params.includeDirectories ?? [],
|
||||
);
|
||||
this.debugMode = params.debugMode;
|
||||
this.inputFormat = params.inputFormat ?? 'text';
|
||||
const normalizedOutputFormat = normalizeConfigOutputFormat(
|
||||
params.outputFormat ?? params.output?.format,
|
||||
);
|
||||
this.outputFormat = normalizedOutputFormat ?? OutputFormat.TEXT;
|
||||
this.includePartialMessages = params.includePartialMessages ?? false;
|
||||
this.question = params.question;
|
||||
this.fullContext = params.fullContext ?? false;
|
||||
this.coreTools = params.coreTools;
|
||||
@@ -480,10 +510,6 @@ export class Config {
|
||||
this.vlmSwitchMode = params.vlmSwitchMode;
|
||||
this.fileExclusions = new FileExclusions(this);
|
||||
this.eventEmitter = params.eventEmitter;
|
||||
this.outputSettings = {
|
||||
format: params.output?.format ?? OutputFormat.TEXT,
|
||||
};
|
||||
|
||||
if (params.contextFileName) {
|
||||
setGeminiMdFilename(params.contextFileName);
|
||||
}
|
||||
@@ -768,6 +794,14 @@ export class Config {
|
||||
return this.showMemoryUsage;
|
||||
}
|
||||
|
||||
getInputFormat(): 'text' | 'stream-json' {
|
||||
return this.inputFormat;
|
||||
}
|
||||
|
||||
getIncludePartialMessages(): boolean {
|
||||
return this.includePartialMessages;
|
||||
}
|
||||
|
||||
getAccessibility(): AccessibilitySettings {
|
||||
return this.accessibility;
|
||||
}
|
||||
@@ -1048,10 +1082,8 @@ export class Config {
|
||||
return this.useSmartEdit;
|
||||
}
|
||||
|
||||
getOutputFormat(): OutputFormat {
|
||||
return this.outputSettings?.format
|
||||
? this.outputSettings.format
|
||||
: OutputFormat.TEXT;
|
||||
getOutputFormat(): OutputFormat | 'stream-json' {
|
||||
return this.outputFormat;
|
||||
}
|
||||
|
||||
async getGitService(): Promise<GitService> {
|
||||
|
||||
@@ -9,7 +9,18 @@ import type {
|
||||
ToolCallResponseInfo,
|
||||
Config,
|
||||
} from '../index.js';
|
||||
import { CoreToolScheduler } from './coreToolScheduler.js';
|
||||
import {
|
||||
CoreToolScheduler,
|
||||
type AllToolCallsCompleteHandler,
|
||||
type OutputUpdateHandler,
|
||||
type ToolCallsUpdateHandler,
|
||||
} from './coreToolScheduler.js';
|
||||
|
||||
export interface ExecuteToolCallOptions {
|
||||
outputUpdateHandler?: OutputUpdateHandler;
|
||||
onAllToolCallsComplete?: AllToolCallsCompleteHandler;
|
||||
onToolCallsUpdate?: ToolCallsUpdateHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a single tool call non-interactively by leveraging the CoreToolScheduler.
|
||||
@@ -18,15 +29,21 @@ export async function executeToolCall(
|
||||
config: Config,
|
||||
toolCallRequest: ToolCallRequestInfo,
|
||||
abortSignal: AbortSignal,
|
||||
options: ExecuteToolCallOptions = {},
|
||||
): Promise<ToolCallResponseInfo> {
|
||||
return new Promise<ToolCallResponseInfo>((resolve, reject) => {
|
||||
new CoreToolScheduler({
|
||||
config,
|
||||
getPreferredEditor: () => undefined,
|
||||
onEditorClose: () => {},
|
||||
outputUpdateHandler: options.outputUpdateHandler,
|
||||
onAllToolCallsComplete: async (completedToolCalls) => {
|
||||
if (options.onAllToolCallsComplete) {
|
||||
await options.onAllToolCallsComplete(completedToolCalls);
|
||||
}
|
||||
resolve(completedToolCalls[0].response);
|
||||
},
|
||||
onToolCallsUpdate: options.onToolCallsUpdate,
|
||||
getPreferredEditor: () => undefined,
|
||||
onEditorClose: () => {},
|
||||
})
|
||||
.schedule(toolCallRequest, abortSignal)
|
||||
.catch(reject);
|
||||
|
||||
@@ -46,7 +46,7 @@ export class StartSessionEvent implements BaseTelemetryEvent {
|
||||
mcp_servers_count: number;
|
||||
mcp_tools_count?: number;
|
||||
mcp_tools?: string;
|
||||
output_format: OutputFormat;
|
||||
output_format: OutputFormat | 'stream-json';
|
||||
|
||||
constructor(config: Config, toolRegistry?: ToolRegistry) {
|
||||
const generatorConfig = config.getContentGeneratorConfig();
|
||||
|
||||
Reference in New Issue
Block a user