mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge branch 'feature/stream-json-migration' into mingholy/feat/cli-sdk
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -57,4 +57,3 @@ gha-creds-*.json
|
|||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
patch_output.log
|
patch_output.log
|
||||||
QWEN.md
|
|
||||||
|
|||||||
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -73,7 +73,16 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch CLI Non-Interactive",
|
"name": "Launch CLI Non-Interactive",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "start", "--", "-p", "${input:prompt}", "-y"],
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"start",
|
||||||
|
"--",
|
||||||
|
"-p",
|
||||||
|
"${input:prompt}",
|
||||||
|
"-y",
|
||||||
|
"--output-format",
|
||||||
|
"stream-json"
|
||||||
|
],
|
||||||
"skipFiles": ["<node_internals>/**"],
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export default {
|
|||||||
commands: 'Commands',
|
commands: 'Commands',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
'configuration-v1': 'Configuration (v1)',
|
'configuration-v1': 'Configuration (v1)',
|
||||||
'structured-output': 'Structured Output',
|
|
||||||
themes: 'Themes',
|
themes: 'Themes',
|
||||||
tutorials: 'Tutorials',
|
tutorials: 'Tutorials',
|
||||||
'keyboard-shortcuts': 'Keyboard Shortcuts',
|
'keyboard-shortcuts': 'Keyboard Shortcuts',
|
||||||
|
|||||||
@@ -74,35 +74,236 @@ describe('JSON output', () => {
|
|||||||
expect(thrown).toBeDefined();
|
expect(thrown).toBeDefined();
|
||||||
const message = (thrown as Error).message;
|
const message = (thrown as Error).message;
|
||||||
|
|
||||||
// The error JSON is written to stderr, so it should be in the error message
|
// The error JSON is written to stdout as a CLIResultMessageError
|
||||||
// Use a regex to find the first complete JSON object in the string
|
// Extract stdout from the error message
|
||||||
const jsonMatch = message.match(/{[\s\S]*}/);
|
const stdoutMatch = message.match(/Stdout:\n([\s\S]*?)(?:\n\nStderr:|$)/);
|
||||||
|
|
||||||
// Fail if no JSON-like text was found
|
|
||||||
expect(
|
expect(
|
||||||
jsonMatch,
|
stdoutMatch,
|
||||||
'Expected to find a JSON object in the error output',
|
'Expected to find stdout in the error message',
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
let payload;
|
const stdout = stdoutMatch![1];
|
||||||
|
let parsed: unknown[];
|
||||||
try {
|
try {
|
||||||
// Parse the matched JSON string
|
// Parse the JSON array from stdout
|
||||||
payload = JSON.parse(jsonMatch![0]);
|
parsed = JSON.parse(stdout);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error('Failed to parse the following JSON:', jsonMatch![0]);
|
console.error('Failed to parse the following JSON:', stdout);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Test failed: Could not parse JSON from error message. Details: ${parseError}`,
|
`Test failed: Could not parse JSON from stdout. Details: ${parseError}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The JsonFormatter.formatError() outputs: { error: { type, message, code } }
|
// The output should be an array of messages
|
||||||
expect(payload).toHaveProperty('error');
|
expect(Array.isArray(parsed)).toBe(true);
|
||||||
expect(payload.error).toBeDefined();
|
expect(parsed.length).toBeGreaterThan(0);
|
||||||
expect(payload.error.type).toBe('Error');
|
|
||||||
expect(payload.error.code).toBe(1);
|
// Find the result message with error
|
||||||
expect(payload.error.message).toContain(
|
const resultMessage = parsed.find(
|
||||||
|
(msg: unknown) =>
|
||||||
|
typeof msg === 'object' &&
|
||||||
|
msg !== null &&
|
||||||
|
'type' in msg &&
|
||||||
|
msg.type === 'result' &&
|
||||||
|
'is_error' in msg &&
|
||||||
|
msg.is_error === true,
|
||||||
|
) as {
|
||||||
|
type: string;
|
||||||
|
is_error: boolean;
|
||||||
|
subtype: string;
|
||||||
|
error?: { message: string; type?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(resultMessage).toBeDefined();
|
||||||
|
expect(resultMessage.is_error).toBe(true);
|
||||||
|
expect(resultMessage).toHaveProperty('subtype');
|
||||||
|
expect(resultMessage.subtype).toBe('error_during_execution');
|
||||||
|
expect(resultMessage).toHaveProperty('error');
|
||||||
|
expect(resultMessage.error).toBeDefined();
|
||||||
|
expect(resultMessage.error?.message).toContain(
|
||||||
'configured auth type is qwen-oauth',
|
'configured auth type is qwen-oauth',
|
||||||
);
|
);
|
||||||
expect(payload.error.message).toContain('current auth type is openai');
|
expect(resultMessage.error?.message).toContain(
|
||||||
|
'current auth type is openai',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return line-delimited JSON messages for stream-json output format', async () => {
|
||||||
|
const result = await rig.run(
|
||||||
|
'What is the capital of France?',
|
||||||
|
'--output-format',
|
||||||
|
'stream-json',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stream-json output is line-delimited JSON (one JSON object per line)
|
||||||
|
const lines = result
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim());
|
||||||
|
expect(lines.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Parse each line as a JSON object
|
||||||
|
const messages: unknown[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
messages.push(parsed);
|
||||||
|
} catch (parseError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse JSON line: ${line}. Error: ${parseError}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have at least system, assistant, and result messages
|
||||||
|
expect(messages.length).toBeGreaterThanOrEqual(3);
|
||||||
|
|
||||||
|
// Find system message
|
||||||
|
const systemMessage = messages.find(
|
||||||
|
(msg: unknown) =>
|
||||||
|
typeof msg === 'object' &&
|
||||||
|
msg !== null &&
|
||||||
|
'type' in msg &&
|
||||||
|
msg.type === 'system',
|
||||||
|
);
|
||||||
|
expect(systemMessage).toBeDefined();
|
||||||
|
expect(systemMessage).toHaveProperty('subtype');
|
||||||
|
expect(systemMessage).toHaveProperty('session_id');
|
||||||
|
|
||||||
|
// Find assistant message
|
||||||
|
const assistantMessage = messages.find(
|
||||||
|
(msg: unknown) =>
|
||||||
|
typeof msg === 'object' &&
|
||||||
|
msg !== null &&
|
||||||
|
'type' in msg &&
|
||||||
|
msg.type === 'assistant',
|
||||||
|
);
|
||||||
|
expect(assistantMessage).toBeDefined();
|
||||||
|
expect(assistantMessage).toHaveProperty('message');
|
||||||
|
expect(assistantMessage).toHaveProperty('session_id');
|
||||||
|
|
||||||
|
// Find result message (should be the last message)
|
||||||
|
const resultMessage = messages[messages.length - 1] as {
|
||||||
|
type: string;
|
||||||
|
is_error: boolean;
|
||||||
|
result: string;
|
||||||
|
};
|
||||||
|
expect(resultMessage).toBeDefined();
|
||||||
|
expect(
|
||||||
|
typeof resultMessage === 'object' &&
|
||||||
|
resultMessage !== null &&
|
||||||
|
'type' in resultMessage &&
|
||||||
|
resultMessage.type === 'result',
|
||||||
|
).toBe(true);
|
||||||
|
expect(resultMessage).toHaveProperty('is_error');
|
||||||
|
expect(resultMessage.is_error).toBe(false);
|
||||||
|
expect(resultMessage).toHaveProperty('result');
|
||||||
|
expect(typeof resultMessage.result).toBe('string');
|
||||||
|
expect(resultMessage.result.toLowerCase()).toContain('paris');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include stream events when using stream-json with include-partial-messages', async () => {
|
||||||
|
const result = await rig.run(
|
||||||
|
'What is the capital of France?',
|
||||||
|
'--output-format',
|
||||||
|
'stream-json',
|
||||||
|
'--include-partial-messages',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stream-json output is line-delimited JSON (one JSON object per line)
|
||||||
|
const lines = result
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim());
|
||||||
|
expect(lines.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Parse each line as a JSON object
|
||||||
|
const messages: unknown[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
messages.push(parsed);
|
||||||
|
} catch (parseError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse JSON line: ${line}. Error: ${parseError}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have more messages than without include-partial-messages
|
||||||
|
// because we're including stream events
|
||||||
|
expect(messages.length).toBeGreaterThan(3);
|
||||||
|
|
||||||
|
// Find stream_event messages
|
||||||
|
const streamEvents = messages.filter(
|
||||||
|
(msg: unknown) =>
|
||||||
|
typeof msg === 'object' &&
|
||||||
|
msg !== null &&
|
||||||
|
'type' in msg &&
|
||||||
|
msg.type === 'stream_event',
|
||||||
|
);
|
||||||
|
expect(streamEvents.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify stream event structure
|
||||||
|
const firstStreamEvent = streamEvents[0];
|
||||||
|
expect(firstStreamEvent).toHaveProperty('event');
|
||||||
|
expect(firstStreamEvent).toHaveProperty('session_id');
|
||||||
|
expect(firstStreamEvent).toHaveProperty('uuid');
|
||||||
|
|
||||||
|
// Check for expected stream event types
|
||||||
|
const eventTypes = streamEvents.map((event: unknown) =>
|
||||||
|
typeof event === 'object' &&
|
||||||
|
event !== null &&
|
||||||
|
'event' in event &&
|
||||||
|
typeof event.event === 'object' &&
|
||||||
|
event.event !== null &&
|
||||||
|
'type' in event.event
|
||||||
|
? event.event.type
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should have message_start event
|
||||||
|
expect(eventTypes).toContain('message_start');
|
||||||
|
|
||||||
|
// Should have content_block_start event
|
||||||
|
expect(eventTypes).toContain('content_block_start');
|
||||||
|
|
||||||
|
// Should have content_block_delta events
|
||||||
|
expect(eventTypes).toContain('content_block_delta');
|
||||||
|
|
||||||
|
// Should have content_block_stop event
|
||||||
|
expect(eventTypes).toContain('content_block_stop');
|
||||||
|
|
||||||
|
// Should have message_stop event
|
||||||
|
expect(eventTypes).toContain('message_stop');
|
||||||
|
|
||||||
|
// Verify that we still have the complete assistant message
|
||||||
|
const assistantMessage = messages.find(
|
||||||
|
(msg: unknown) =>
|
||||||
|
typeof msg === 'object' &&
|
||||||
|
msg !== null &&
|
||||||
|
'type' in msg &&
|
||||||
|
msg.type === 'assistant',
|
||||||
|
);
|
||||||
|
expect(assistantMessage).toBeDefined();
|
||||||
|
expect(assistantMessage).toHaveProperty('message');
|
||||||
|
|
||||||
|
// Verify that we still have the result message
|
||||||
|
const resultMessage = messages[messages.length - 1] as {
|
||||||
|
type: string;
|
||||||
|
is_error: boolean;
|
||||||
|
result: string;
|
||||||
|
};
|
||||||
|
expect(resultMessage).toBeDefined();
|
||||||
|
expect(
|
||||||
|
typeof resultMessage === 'object' &&
|
||||||
|
resultMessage !== null &&
|
||||||
|
'type' in resultMessage &&
|
||||||
|
resultMessage.type === 'result',
|
||||||
|
).toBe(true);
|
||||||
|
expect(resultMessage).toHaveProperty('is_error');
|
||||||
|
expect(resultMessage.is_error).toBe(false);
|
||||||
|
expect(resultMessage).toHaveProperty('result');
|
||||||
|
expect(resultMessage.result.toLowerCase()).toContain('paris');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -340,7 +340,8 @@ export class TestRig {
|
|||||||
// as it would corrupt the JSON
|
// as it would corrupt the JSON
|
||||||
const isJsonOutput =
|
const isJsonOutput =
|
||||||
commandArgs.includes('--output-format') &&
|
commandArgs.includes('--output-format') &&
|
||||||
commandArgs.includes('json');
|
(commandArgs.includes('json') ||
|
||||||
|
commandArgs.includes('stream-json'));
|
||||||
|
|
||||||
// If we have stderr output and it's not a JSON test, include that also
|
// If we have stderr output and it's not a JSON test, include that also
|
||||||
if (stderr && !isJsonOutput) {
|
if (stderr && !isJsonOutput) {
|
||||||
@@ -349,7 +350,23 @@ export class TestRig {
|
|||||||
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`Process exited with code ${code}:\n${stderr}`));
|
// Check if this is a JSON output test - for JSON errors, the error is in stdout
|
||||||
|
const isJsonOutputOnError =
|
||||||
|
commandArgs.includes('--output-format') &&
|
||||||
|
(commandArgs.includes('json') ||
|
||||||
|
commandArgs.includes('stream-json'));
|
||||||
|
|
||||||
|
// For JSON output tests, include stdout in the error message
|
||||||
|
// as the error JSON is written to stdout
|
||||||
|
if (isJsonOutputOnError && stdout) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Process exited with code ${code}:\nStdout:\n${stdout}\n\nStderr:\n${stderr}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Process exited with code ${code}:\n${stderr}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -484,6 +484,27 @@ export class LoadedSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a minimal LoadedSettings instance with empty settings.
|
||||||
|
* Used in stream-json mode where settings are ignored.
|
||||||
|
*/
|
||||||
|
export function createMinimalSettings(): LoadedSettings {
|
||||||
|
const emptySettingsFile: SettingsFile = {
|
||||||
|
path: '',
|
||||||
|
settings: {},
|
||||||
|
originalSettings: {},
|
||||||
|
rawJson: '{}',
|
||||||
|
};
|
||||||
|
return new LoadedSettings(
|
||||||
|
emptySettingsFile,
|
||||||
|
emptySettingsFile,
|
||||||
|
emptySettingsFile,
|
||||||
|
emptySettingsFile,
|
||||||
|
false,
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function findEnvFile(startDir: string): string | null {
|
function findEnvFile(startDir: string): string | null {
|
||||||
let currentDir = path.resolve(startDir);
|
let currentDir = path.resolve(startDir);
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -357,12 +357,9 @@ describe('gemini.tsx main function', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(runStreamJsonSpy).toHaveBeenCalledTimes(1);
|
expect(runStreamJsonSpy).toHaveBeenCalledTimes(1);
|
||||||
const [configArg, settingsArg, promptArg] = runStreamJsonSpy.mock.calls[0];
|
const [configArg, inputArg] = runStreamJsonSpy.mock.calls[0];
|
||||||
expect(configArg).toBe(validatedConfig);
|
expect(configArg).toBe(validatedConfig);
|
||||||
expect(settingsArg).toMatchObject({
|
expect(inputArg).toBe('hello stream');
|
||||||
merged: expect.objectContaining({ security: expect.any(Object) }),
|
|
||||||
});
|
|
||||||
expect(promptArg).toBe('hello stream');
|
|
||||||
|
|
||||||
expect(validateAuthSpy).toHaveBeenCalledWith(
|
expect(validateAuthSpy).toHaveBeenCalledWith(
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -220,12 +220,6 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDebugMode = cliConfig.isDebugMode(argv);
|
const isDebugMode = cliConfig.isDebugMode(argv);
|
||||||
const consolePatcher = new ConsolePatcher({
|
|
||||||
stderr: true,
|
|
||||||
debugMode: isDebugMode,
|
|
||||||
});
|
|
||||||
consolePatcher.patch();
|
|
||||||
registerCleanup(consolePatcher.cleanup);
|
|
||||||
|
|
||||||
dns.setDefaultResultOrder(
|
dns.setDefaultResultOrder(
|
||||||
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
|
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
|
||||||
@@ -350,6 +344,15 @@ export async function main() {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup unified ConsolePatcher based on interactive mode
|
||||||
|
const isInteractive = config.isInteractive();
|
||||||
|
const consolePatcher = new ConsolePatcher({
|
||||||
|
stderr: isInteractive,
|
||||||
|
debugMode: isDebugMode,
|
||||||
|
});
|
||||||
|
consolePatcher.patch();
|
||||||
|
registerCleanup(consolePatcher.cleanup);
|
||||||
|
|
||||||
const wasRaw = process.stdin.isRaw;
|
const wasRaw = process.stdin.isRaw;
|
||||||
let kittyProtocolDetectionComplete: Promise<boolean> | undefined;
|
let kittyProtocolDetectionComplete: Promise<boolean> | undefined;
|
||||||
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
|
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
|
||||||
@@ -443,9 +446,7 @@ export async function main() {
|
|||||||
|
|
||||||
await runNonInteractiveStreamJson(
|
await runNonInteractiveStreamJson(
|
||||||
nonInteractiveConfig,
|
nonInteractiveConfig,
|
||||||
settings,
|
|
||||||
trimmedInput.length > 0 ? trimmedInput : '',
|
trimmedInput.length > 0 ? trimmedInput : '',
|
||||||
prompt_id,
|
|
||||||
);
|
);
|
||||||
await runExitCleanup();
|
await runExitCleanup();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import type { Config } from '@qwen-code/qwen-code-core';
|
import type { Config } from '@qwen-code/qwen-code-core';
|
||||||
import type { LoadedSettings } from '../config/settings.js';
|
|
||||||
import { runNonInteractiveStreamJson } from './session.js';
|
import { runNonInteractiveStreamJson } from './session.js';
|
||||||
import type {
|
import type {
|
||||||
CLIUserMessage,
|
CLIUserMessage,
|
||||||
@@ -74,14 +73,6 @@ function createConfig(overrides: ConfigOverrides = {}): Config {
|
|||||||
return { ...base, ...overrides } as unknown as Config;
|
return { ...base, ...overrides } as unknown as Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSettings(): LoadedSettings {
|
|
||||||
return {
|
|
||||||
merged: {
|
|
||||||
security: { auth: {} },
|
|
||||||
},
|
|
||||||
} as unknown as LoadedSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createUserMessage(content: string): CLIUserMessage {
|
function createUserMessage(content: string): CLIUserMessage {
|
||||||
return {
|
return {
|
||||||
type: 'user',
|
type: 'user',
|
||||||
@@ -145,7 +136,6 @@ function createControlCancel(requestId: string): ControlCancelRequest {
|
|||||||
|
|
||||||
describe('runNonInteractiveStreamJson', () => {
|
describe('runNonInteractiveStreamJson', () => {
|
||||||
let config: Config;
|
let config: Config;
|
||||||
let settings: LoadedSettings;
|
|
||||||
let mockInputReader: {
|
let mockInputReader: {
|
||||||
read: () => AsyncGenerator<
|
read: () => AsyncGenerator<
|
||||||
| CLIUserMessage
|
| CLIUserMessage
|
||||||
@@ -170,7 +160,6 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
config = createConfig();
|
config = createConfig();
|
||||||
settings = createSettings();
|
|
||||||
runNonInteractiveMock.mockReset();
|
runNonInteractiveMock.mockReset();
|
||||||
|
|
||||||
// Setup mocks
|
// Setup mocks
|
||||||
@@ -232,7 +221,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield initRequest;
|
yield initRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
|
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
|
||||||
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest);
|
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest);
|
||||||
@@ -246,7 +235,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage;
|
yield userMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledTimes(1);
|
expect(runNonInteractiveMock).toHaveBeenCalledTimes(1);
|
||||||
const runCall = runNonInteractiveMock.mock.calls[0];
|
const runCall = runNonInteractiveMock.mock.calls[0];
|
||||||
@@ -272,7 +261,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage2;
|
yield userMessage2;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledTimes(2);
|
expect(runNonInteractiveMock).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
@@ -293,7 +282,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage2;
|
yield userMessage2;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
// Both messages should be processed
|
// Both messages should be processed
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledTimes(2);
|
expect(runNonInteractiveMock).toHaveBeenCalledTimes(2);
|
||||||
@@ -308,7 +297,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield controlRequest;
|
yield controlRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockDispatcher.dispatch).toHaveBeenCalledTimes(2);
|
expect(mockDispatcher.dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(mockDispatcher.dispatch).toHaveBeenNthCalledWith(1, initRequest);
|
expect(mockDispatcher.dispatch).toHaveBeenNthCalledWith(1, initRequest);
|
||||||
@@ -324,7 +313,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield controlResponse;
|
yield controlResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockDispatcher.handleControlResponse).toHaveBeenCalledWith(
|
expect(mockDispatcher.handleControlResponse).toHaveBeenCalledWith(
|
||||||
controlResponse,
|
controlResponse,
|
||||||
@@ -340,7 +329,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield cancelRequest;
|
yield cancelRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockDispatcher.handleCancel).toHaveBeenCalledWith('req-2');
|
expect(mockDispatcher.handleCancel).toHaveBeenCalledWith('req-2');
|
||||||
});
|
});
|
||||||
@@ -360,7 +349,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield controlRequest;
|
yield controlRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(controlRequest);
|
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(controlRequest);
|
||||||
});
|
});
|
||||||
@@ -380,7 +369,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield controlResponse;
|
yield controlResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockDispatcher.handleControlResponse).toHaveBeenCalledWith(
|
expect(mockDispatcher.handleControlResponse).toHaveBeenCalledWith(
|
||||||
controlResponse,
|
controlResponse,
|
||||||
@@ -394,12 +383,12 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage;
|
yield userMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledTimes(1);
|
expect(runNonInteractiveMock).toHaveBeenCalledTimes(1);
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledWith(
|
expect(runNonInteractiveMock).toHaveBeenCalledWith(
|
||||||
config,
|
config,
|
||||||
settings,
|
expect.objectContaining({ merged: expect.any(Object) }),
|
||||||
'Test message',
|
'Test message',
|
||||||
expect.stringContaining('test-session'),
|
expect.stringContaining('test-session'),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -427,12 +416,12 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage;
|
yield userMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledTimes(1);
|
expect(runNonInteractiveMock).toHaveBeenCalledTimes(1);
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledWith(
|
expect(runNonInteractiveMock).toHaveBeenCalledWith(
|
||||||
config,
|
config,
|
||||||
settings,
|
expect.objectContaining({ merged: expect.any(Object) }),
|
||||||
'First part\nSecond part',
|
'First part\nSecond part',
|
||||||
expect.stringContaining('test-session'),
|
expect.stringContaining('test-session'),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -457,7 +446,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage;
|
yield userMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(runNonInteractiveMock).not.toHaveBeenCalled();
|
expect(runNonInteractiveMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -472,7 +461,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage;
|
yield userMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
// Error should be caught and handled gracefully
|
// Error should be caught and handled gracefully
|
||||||
});
|
});
|
||||||
@@ -484,9 +473,9 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
throw streamError;
|
throw streamError;
|
||||||
} as typeof mockInputReader.read;
|
} as typeof mockInputReader.read;
|
||||||
|
|
||||||
await expect(
|
await expect(runNonInteractiveStreamJson(config, '')).rejects.toThrow(
|
||||||
runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id'),
|
'Stream error',
|
||||||
).rejects.toThrow('Stream error');
|
);
|
||||||
|
|
||||||
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
|
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -517,7 +506,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage;
|
yield userMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
// Verify initialization happened
|
// Verify initialization happened
|
||||||
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest);
|
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest);
|
||||||
@@ -536,7 +525,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield userMessage2;
|
yield userMessage2;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(runNonInteractiveMock).toHaveBeenCalledTimes(2);
|
expect(runNonInteractiveMock).toHaveBeenCalledTimes(2);
|
||||||
const promptId1 = runNonInteractiveMock.mock.calls[0][3] as string;
|
const promptId1 = runNonInteractiveMock.mock.calls[0][3] as string;
|
||||||
@@ -553,7 +542,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield controlRequest;
|
yield controlRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
// Should not transition to idle since it's not an initialize request
|
// Should not transition to idle since it's not an initialize request
|
||||||
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
|
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
|
||||||
@@ -564,7 +553,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
// Empty stream - should complete immediately
|
// Empty stream - should complete immediately
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
|
expect(mockConsolePatcher.patch).toHaveBeenCalledTimes(1);
|
||||||
expect(mockConsolePatcher.cleanup).toHaveBeenCalledTimes(1);
|
expect(mockConsolePatcher.cleanup).toHaveBeenCalledTimes(1);
|
||||||
@@ -575,7 +564,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
// Empty stream
|
// Empty stream
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls dispatcher shutdown on completion', async () => {
|
it('calls dispatcher shutdown on completion', async () => {
|
||||||
@@ -585,7 +574,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
yield initRequest;
|
yield initRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockDispatcher.shutdown).toHaveBeenCalledTimes(1);
|
expect(mockDispatcher.shutdown).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -595,7 +584,7 @@ describe('runNonInteractiveStreamJson', () => {
|
|||||||
// Empty stream
|
// Empty stream
|
||||||
};
|
};
|
||||||
|
|
||||||
await runNonInteractiveStreamJson(config, settings, '', 'test-prompt-id');
|
await runNonInteractiveStreamJson(config, '');
|
||||||
|
|
||||||
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
|
expect(mockConsolePatcher.cleanup).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Config } from '@qwen-code/qwen-code-core';
|
import type { Config } from '@qwen-code/qwen-code-core';
|
||||||
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
|
|
||||||
import { StreamJsonInputReader } from './io/StreamJsonInputReader.js';
|
import { StreamJsonInputReader } from './io/StreamJsonInputReader.js';
|
||||||
import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js';
|
import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js';
|
||||||
import { ControlContext } from './control/ControlContext.js';
|
import { ControlContext } from './control/ControlContext.js';
|
||||||
@@ -39,8 +38,9 @@ import {
|
|||||||
isControlResponse,
|
isControlResponse,
|
||||||
isControlCancel,
|
isControlCancel,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type { LoadedSettings } from '../config/settings.js';
|
import { createMinimalSettings } from '../config/settings.js';
|
||||||
import { runNonInteractive } from '../nonInteractiveCli.js';
|
import { runNonInteractive } from '../nonInteractiveCli.js';
|
||||||
|
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
|
||||||
|
|
||||||
const SESSION_STATE = {
|
const SESSION_STATE = {
|
||||||
INITIALIZING: 'initializing',
|
INITIALIZING: 'initializing',
|
||||||
@@ -87,7 +87,6 @@ class SessionManager {
|
|||||||
private userMessageQueue: CLIUserMessage[] = [];
|
private userMessageQueue: CLIUserMessage[] = [];
|
||||||
private abortController: AbortController;
|
private abortController: AbortController;
|
||||||
private config: Config;
|
private config: Config;
|
||||||
private settings: LoadedSettings;
|
|
||||||
private sessionId: string;
|
private sessionId: string;
|
||||||
private promptIdCounter: number = 0;
|
private promptIdCounter: number = 0;
|
||||||
private inputReader: StreamJsonInputReader;
|
private inputReader: StreamJsonInputReader;
|
||||||
@@ -96,28 +95,17 @@ class SessionManager {
|
|||||||
private dispatcher: ControlDispatcher | null = null;
|
private dispatcher: ControlDispatcher | null = null;
|
||||||
private controlService: ControlService | null = null;
|
private controlService: ControlService | null = null;
|
||||||
private controlSystemEnabled: boolean | null = null;
|
private controlSystemEnabled: boolean | null = null;
|
||||||
private consolePatcher: ConsolePatcher;
|
|
||||||
private debugMode: boolean;
|
private debugMode: boolean;
|
||||||
private shutdownHandler: (() => void) | null = null;
|
private shutdownHandler: (() => void) | null = null;
|
||||||
private initialPrompt: CLIUserMessage | null = null;
|
private initialPrompt: CLIUserMessage | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(config: Config, initialPrompt?: CLIUserMessage) {
|
||||||
config: Config,
|
|
||||||
settings: LoadedSettings,
|
|
||||||
initialPrompt?: CLIUserMessage,
|
|
||||||
) {
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.settings = settings;
|
|
||||||
this.sessionId = config.getSessionId();
|
this.sessionId = config.getSessionId();
|
||||||
this.debugMode = config.getDebugMode();
|
this.debugMode = config.getDebugMode();
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
this.initialPrompt = initialPrompt ?? null;
|
this.initialPrompt = initialPrompt ?? null;
|
||||||
|
|
||||||
this.consolePatcher = new ConsolePatcher({
|
|
||||||
stderr: true,
|
|
||||||
debugMode: this.debugMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.inputReader = new StreamJsonInputReader();
|
this.inputReader = new StreamJsonInputReader();
|
||||||
this.outputAdapter = new StreamJsonOutputAdapter(
|
this.outputAdapter = new StreamJsonOutputAdapter(
|
||||||
config,
|
config,
|
||||||
@@ -232,8 +220,6 @@ class SessionManager {
|
|||||||
*/
|
*/
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.consolePatcher.patch();
|
|
||||||
|
|
||||||
if (this.debugMode) {
|
if (this.debugMode) {
|
||||||
console.error('[SessionManager] Starting session', this.sessionId);
|
console.error('[SessionManager] Starting session', this.sessionId);
|
||||||
}
|
}
|
||||||
@@ -264,7 +250,6 @@ class SessionManager {
|
|||||||
await this.shutdown();
|
await this.shutdown();
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.consolePatcher.cleanup();
|
|
||||||
// Ensure signal handlers are always cleaned up even if shutdown wasn't called
|
// Ensure signal handlers are always cleaned up even if shutdown wasn't called
|
||||||
this.cleanupSignalHandlers();
|
this.cleanupSignalHandlers();
|
||||||
}
|
}
|
||||||
@@ -578,11 +563,17 @@ class SessionManager {
|
|||||||
const promptId = this.getNextPromptId();
|
const promptId = this.getNextPromptId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runNonInteractive(this.config, this.settings, input, promptId, {
|
await runNonInteractive(
|
||||||
abortController: this.abortController,
|
this.config,
|
||||||
adapter: this.outputAdapter,
|
createMinimalSettings(),
|
||||||
controlService: this.controlService ?? undefined,
|
input,
|
||||||
});
|
promptId,
|
||||||
|
{
|
||||||
|
abortController: this.abortController,
|
||||||
|
adapter: this.outputAdapter,
|
||||||
|
controlService: this.controlService ?? undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error already handled by runNonInteractive via adapter.emitResult
|
// Error already handled by runNonInteractive via adapter.emitResult
|
||||||
if (this.debugMode) {
|
if (this.debugMode) {
|
||||||
@@ -695,31 +686,36 @@ function extractUserMessageText(message: CLIUserMessage): string | null {
|
|||||||
* Entry point for stream-json mode
|
* Entry point for stream-json mode
|
||||||
*
|
*
|
||||||
* @param config - Configuration object
|
* @param config - Configuration object
|
||||||
* @param settings - Loaded settings
|
|
||||||
* @param input - Optional initial prompt input to process before reading from stream
|
* @param input - Optional initial prompt input to process before reading from stream
|
||||||
* @param promptId - Prompt ID (not used in stream-json mode but kept for API compatibility)
|
|
||||||
*/
|
*/
|
||||||
export async function runNonInteractiveStreamJson(
|
export async function runNonInteractiveStreamJson(
|
||||||
config: Config,
|
config: Config,
|
||||||
settings: LoadedSettings,
|
|
||||||
input: string,
|
input: string,
|
||||||
_promptId: string,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Create initial user message from prompt input if provided
|
const consolePatcher = new ConsolePatcher({
|
||||||
let initialPrompt: CLIUserMessage | undefined = undefined;
|
debugMode: config.getDebugMode(),
|
||||||
if (input && input.trim().length > 0) {
|
});
|
||||||
const sessionId = config.getSessionId();
|
consolePatcher.patch();
|
||||||
initialPrompt = {
|
|
||||||
type: 'user',
|
|
||||||
session_id: sessionId,
|
|
||||||
message: {
|
|
||||||
role: 'user',
|
|
||||||
content: input.trim(),
|
|
||||||
},
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const manager = new SessionManager(config, settings, initialPrompt);
|
try {
|
||||||
await manager.run();
|
// Create initial user message from prompt input if provided
|
||||||
|
let initialPrompt: CLIUserMessage | undefined = undefined;
|
||||||
|
if (input && input.trim().length > 0) {
|
||||||
|
const sessionId = config.getSessionId();
|
||||||
|
initialPrompt = {
|
||||||
|
type: 'user',
|
||||||
|
session_id: sessionId,
|
||||||
|
message: {
|
||||||
|
role: 'user',
|
||||||
|
content: input.trim(),
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new SessionManager(config, initialPrompt);
|
||||||
|
await manager.run();
|
||||||
|
} finally {
|
||||||
|
consolePatcher.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { StreamJsonOutputAdapter } from './nonInteractive/io/StreamJsonOutputAda
|
|||||||
import type { ControlService } from './nonInteractive/control/ControlService.js';
|
import type { ControlService } from './nonInteractive/control/ControlService.js';
|
||||||
|
|
||||||
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
|
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
|
||||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
|
||||||
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
|
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
|
||||||
import {
|
import {
|
||||||
handleError,
|
handleError,
|
||||||
@@ -67,11 +66,6 @@ export async function runNonInteractive(
|
|||||||
options: RunNonInteractiveOptions = {},
|
options: RunNonInteractiveOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return promptIdContext.run(prompt_id, async () => {
|
return promptIdContext.run(prompt_id, async () => {
|
||||||
const consolePatcher = new ConsolePatcher({
|
|
||||||
stderr: true,
|
|
||||||
debugMode: config.getDebugMode(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create output adapter based on format
|
// Create output adapter based on format
|
||||||
let adapter: JsonOutputAdapterInterface | undefined;
|
let adapter: JsonOutputAdapterInterface | undefined;
|
||||||
const outputFormat = config.getOutputFormat();
|
const outputFormat = config.getOutputFormat();
|
||||||
@@ -102,12 +96,22 @@ export async function runNonInteractive(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const geminiClient = config.getGeminiClient();
|
||||||
|
const abortController = options.abortController ?? new AbortController();
|
||||||
|
|
||||||
|
// Setup signal handlers for graceful shutdown
|
||||||
|
const shutdownHandler = () => {
|
||||||
|
if (config.getDebugMode()) {
|
||||||
|
console.error('[runNonInteractive] Shutdown signal received');
|
||||||
|
}
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
consolePatcher.patch();
|
|
||||||
process.stdout.on('error', stdoutErrorHandler);
|
process.stdout.on('error', stdoutErrorHandler);
|
||||||
|
|
||||||
const geminiClient = config.getGeminiClient();
|
process.on('SIGINT', shutdownHandler);
|
||||||
const abortController = options.abortController ?? new AbortController();
|
process.on('SIGTERM', shutdownHandler);
|
||||||
|
|
||||||
let initialPartList: PartListUnion | null = extractPartsFromUserMessage(
|
let initialPartList: PartListUnion | null = extractPartsFromUserMessage(
|
||||||
options.userMessage,
|
options.userMessage,
|
||||||
@@ -362,7 +366,9 @@ export async function runNonInteractive(
|
|||||||
handleError(error, config);
|
handleError(error, config);
|
||||||
} finally {
|
} finally {
|
||||||
process.stdout.removeListener('error', stdoutErrorHandler);
|
process.stdout.removeListener('error', stdoutErrorHandler);
|
||||||
consolePatcher.cleanup();
|
// Cleanup signal handlers
|
||||||
|
process.removeListener('SIGINT', shutdownHandler);
|
||||||
|
process.removeListener('SIGTERM', shutdownHandler);
|
||||||
if (isTelemetrySdkInitialized()) {
|
if (isTelemetrySdkInitialized()) {
|
||||||
await shutdownTelemetry(config);
|
await shutdownTelemetry(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { AuthType, OutputFormat } from '@qwen-code/qwen-code-core';
|
|||||||
import type { Config } from '@qwen-code/qwen-code-core';
|
import type { Config } from '@qwen-code/qwen-code-core';
|
||||||
import * as auth from './config/auth.js';
|
import * as auth from './config/auth.js';
|
||||||
import { type LoadedSettings } from './config/settings.js';
|
import { type LoadedSettings } from './config/settings.js';
|
||||||
|
import * as JsonOutputAdapterModule from './nonInteractive/io/JsonOutputAdapter.js';
|
||||||
|
import * as StreamJsonOutputAdapterModule from './nonInteractive/io/StreamJsonOutputAdapter.js';
|
||||||
|
import * as cleanupModule from './utils/cleanup.js';
|
||||||
|
|
||||||
describe('validateNonInterActiveAuth', () => {
|
describe('validateNonInterActiveAuth', () => {
|
||||||
let originalEnvGeminiApiKey: string | undefined;
|
let originalEnvGeminiApiKey: string | undefined;
|
||||||
@@ -17,8 +20,8 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
let originalEnvGcp: string | undefined;
|
let originalEnvGcp: string | undefined;
|
||||||
let originalEnvOpenAiApiKey: string | undefined;
|
let originalEnvOpenAiApiKey: string | undefined;
|
||||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||||
let processExitSpy: ReturnType<typeof vi.spyOn>;
|
let processExitSpy: ReturnType<typeof vi.spyOn<[code?: number], never>>;
|
||||||
let refreshAuthMock: vi.Mock;
|
let refreshAuthMock: ReturnType<typeof vi.fn>;
|
||||||
let mockSettings: LoadedSettings;
|
let mockSettings: LoadedSettings;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -33,7 +36,7 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
||||||
throw new Error(`process.exit(${code}) called`);
|
throw new Error(`process.exit(${code}) called`);
|
||||||
});
|
}) as ReturnType<typeof vi.spyOn<[code?: number], never>>;
|
||||||
refreshAuthMock = vi.fn().mockResolvedValue('refreshed');
|
refreshAuthMock = vi.fn().mockResolvedValue('refreshed');
|
||||||
mockSettings = {
|
mockSettings = {
|
||||||
system: { path: '', settings: {} },
|
system: { path: '', settings: {} },
|
||||||
@@ -235,7 +238,24 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('JSON output mode', () => {
|
describe('JSON output mode', () => {
|
||||||
it('prints JSON error when no auth is configured and exits with code 1', async () => {
|
let emitResultMock: ReturnType<typeof vi.fn>;
|
||||||
|
let runExitCleanupMock: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
emitResultMock = vi.fn();
|
||||||
|
runExitCleanupMock = vi.fn().mockResolvedValue(undefined);
|
||||||
|
vi.spyOn(JsonOutputAdapterModule, 'JsonOutputAdapter').mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
emitResult: emitResultMock,
|
||||||
|
}) as unknown as JsonOutputAdapterModule.JsonOutputAdapter,
|
||||||
|
);
|
||||||
|
vi.spyOn(cleanupModule, 'runExitCleanup').mockImplementation(
|
||||||
|
runExitCleanupMock,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits error result and exits when no auth is configured', async () => {
|
||||||
const nonInteractiveConfig = {
|
const nonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
||||||
@@ -244,7 +264,6 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
.mockReturnValue({ authType: undefined }),
|
.mockReturnValue({ authType: undefined }),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
let thrown: Error | undefined;
|
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -252,21 +271,27 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
nonInteractiveConfig,
|
nonInteractiveConfig,
|
||||||
mockSettings,
|
mockSettings,
|
||||||
);
|
);
|
||||||
|
expect.fail('Should have exited');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
thrown = e as Error;
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(thrown?.message).toBe('process.exit(1) called');
|
expect(emitResultMock).toHaveBeenCalledWith({
|
||||||
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
isError: true,
|
||||||
const payload = JSON.parse(errorArg);
|
errorMessage: expect.stringContaining(
|
||||||
expect(payload.error.type).toBe('Error');
|
'Please set an Auth method in your',
|
||||||
expect(payload.error.code).toBe(1);
|
),
|
||||||
expect(payload.error.message).toContain(
|
durationMs: 0,
|
||||||
'Please set an Auth method in your',
|
apiDurationMs: 0,
|
||||||
);
|
numTurns: 0,
|
||||||
|
usage: undefined,
|
||||||
|
});
|
||||||
|
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prints JSON error when enforced auth mismatches current auth and exits with code 1', async () => {
|
it('emits error result and exits when enforced auth mismatches current auth', async () => {
|
||||||
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
|
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
|
||||||
process.env['OPENAI_API_KEY'] = 'fake-key';
|
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||||
|
|
||||||
@@ -278,7 +303,6 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
.mockReturnValue({ authType: undefined }),
|
.mockReturnValue({ authType: undefined }),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
let thrown: Error | undefined;
|
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -286,23 +310,27 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
nonInteractiveConfig,
|
nonInteractiveConfig,
|
||||||
mockSettings,
|
mockSettings,
|
||||||
);
|
);
|
||||||
|
expect.fail('Should have exited');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
thrown = e as Error;
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(thrown?.message).toBe('process.exit(1) called');
|
expect(emitResultMock).toHaveBeenCalledWith({
|
||||||
{
|
isError: true,
|
||||||
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
errorMessage: expect.stringContaining(
|
||||||
const payload = JSON.parse(errorArg);
|
|
||||||
expect(payload.error.type).toBe('Error');
|
|
||||||
expect(payload.error.code).toBe(1);
|
|
||||||
expect(payload.error.message).toContain(
|
|
||||||
'The configured auth type is qwen-oauth, but the current auth type is openai.',
|
'The configured auth type is qwen-oauth, but the current auth type is openai.',
|
||||||
);
|
),
|
||||||
}
|
durationMs: 0,
|
||||||
|
apiDurationMs: 0,
|
||||||
|
numTurns: 0,
|
||||||
|
usage: undefined,
|
||||||
|
});
|
||||||
|
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
|
it('emits error result and exits when validateAuthMethod fails', async () => {
|
||||||
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||||
process.env['OPENAI_API_KEY'] = 'fake-key';
|
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||||
|
|
||||||
@@ -314,7 +342,6 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
.mockReturnValue({ authType: undefined }),
|
.mockReturnValue({ authType: undefined }),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
let thrown: Error | undefined;
|
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
AuthType.USE_OPENAI,
|
AuthType.USE_OPENAI,
|
||||||
@@ -322,18 +349,159 @@ describe('validateNonInterActiveAuth', () => {
|
|||||||
nonInteractiveConfig,
|
nonInteractiveConfig,
|
||||||
mockSettings,
|
mockSettings,
|
||||||
);
|
);
|
||||||
|
expect.fail('Should have exited');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
thrown = e as Error;
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(thrown?.message).toBe('process.exit(1) called');
|
expect(emitResultMock).toHaveBeenCalledWith({
|
||||||
{
|
isError: true,
|
||||||
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
errorMessage: 'Auth error!',
|
||||||
const payload = JSON.parse(errorArg);
|
durationMs: 0,
|
||||||
expect(payload.error.type).toBe('Error');
|
apiDurationMs: 0,
|
||||||
expect(payload.error.code).toBe(1);
|
numTurns: 0,
|
||||||
expect(payload.error.message).toBe('Auth error!');
|
usage: undefined,
|
||||||
|
});
|
||||||
|
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('STREAM_JSON output mode', () => {
|
||||||
|
let emitResultMock: ReturnType<typeof vi.fn>;
|
||||||
|
let runExitCleanupMock: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
emitResultMock = vi.fn();
|
||||||
|
runExitCleanupMock = vi.fn().mockResolvedValue(undefined);
|
||||||
|
vi.spyOn(
|
||||||
|
StreamJsonOutputAdapterModule,
|
||||||
|
'StreamJsonOutputAdapter',
|
||||||
|
).mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
emitResult: emitResultMock,
|
||||||
|
}) as unknown as StreamJsonOutputAdapterModule.StreamJsonOutputAdapter,
|
||||||
|
);
|
||||||
|
vi.spyOn(cleanupModule, 'runExitCleanup').mockImplementation(
|
||||||
|
runExitCleanupMock,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits error result and exits when no auth is configured', async () => {
|
||||||
|
const nonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
|
||||||
|
getIncludePartialMessages: vi.fn().mockReturnValue(false),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
expect.fail('Should have exited');
|
||||||
|
} catch (e) {
|
||||||
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(emitResultMock).toHaveBeenCalledWith({
|
||||||
|
isError: true,
|
||||||
|
errorMessage: expect.stringContaining(
|
||||||
|
'Please set an Auth method in your',
|
||||||
|
),
|
||||||
|
durationMs: 0,
|
||||||
|
apiDurationMs: 0,
|
||||||
|
numTurns: 0,
|
||||||
|
usage: undefined,
|
||||||
|
});
|
||||||
|
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits error result and exits when enforced auth mismatches current auth', async () => {
|
||||||
|
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
|
||||||
|
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||||
|
|
||||||
|
const nonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
|
||||||
|
getIncludePartialMessages: vi.fn().mockReturnValue(false),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
expect.fail('Should have exited');
|
||||||
|
} catch (e) {
|
||||||
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(emitResultMock).toHaveBeenCalledWith({
|
||||||
|
isError: true,
|
||||||
|
errorMessage: expect.stringContaining(
|
||||||
|
'The configured auth type is qwen-oauth, but the current auth type is openai.',
|
||||||
|
),
|
||||||
|
durationMs: 0,
|
||||||
|
apiDurationMs: 0,
|
||||||
|
numTurns: 0,
|
||||||
|
usage: undefined,
|
||||||
|
});
|
||||||
|
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits error result and exits when validateAuthMethod fails', async () => {
|
||||||
|
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||||
|
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||||
|
|
||||||
|
const nonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
|
||||||
|
getIncludePartialMessages: vi.fn().mockReturnValue(false),
|
||||||
|
getContentGeneratorConfig: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ authType: undefined }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
AuthType.USE_OPENAI,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
expect.fail('Should have exited');
|
||||||
|
} catch (e) {
|
||||||
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(emitResultMock).toHaveBeenCalledWith({
|
||||||
|
isError: true,
|
||||||
|
errorMessage: 'Auth error!',
|
||||||
|
durationMs: 0,
|
||||||
|
apiDurationMs: 0,
|
||||||
|
numTurns: 0,
|
||||||
|
usage: undefined,
|
||||||
|
});
|
||||||
|
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { AuthType, OutputFormat } from '@qwen-code/qwen-code-core';
|
|||||||
import { USER_SETTINGS_PATH } from './config/settings.js';
|
import { USER_SETTINGS_PATH } from './config/settings.js';
|
||||||
import { validateAuthMethod } from './config/auth.js';
|
import { validateAuthMethod } from './config/auth.js';
|
||||||
import { type LoadedSettings } from './config/settings.js';
|
import { type LoadedSettings } from './config/settings.js';
|
||||||
import { handleError } from './utils/errors.js';
|
import { JsonOutputAdapter } from './nonInteractive/io/JsonOutputAdapter.js';
|
||||||
|
import { StreamJsonOutputAdapter } from './nonInteractive/io/StreamJsonOutputAdapter.js';
|
||||||
|
import { runExitCleanup } from './utils/cleanup.js';
|
||||||
|
|
||||||
function getAuthTypeFromEnv(): AuthType | undefined {
|
function getAuthTypeFromEnv(): AuthType | undefined {
|
||||||
if (process.env['OPENAI_API_KEY']) {
|
if (process.env['OPENAI_API_KEY']) {
|
||||||
@@ -27,7 +29,7 @@ export async function validateNonInteractiveAuth(
|
|||||||
useExternalAuth: boolean | undefined,
|
useExternalAuth: boolean | undefined,
|
||||||
nonInteractiveConfig: Config,
|
nonInteractiveConfig: Config,
|
||||||
settings: LoadedSettings,
|
settings: LoadedSettings,
|
||||||
) {
|
): Promise<Config> {
|
||||||
try {
|
try {
|
||||||
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
||||||
if (enforcedType) {
|
if (enforcedType) {
|
||||||
@@ -58,15 +60,38 @@ export async function validateNonInteractiveAuth(
|
|||||||
await nonInteractiveConfig.refreshAuth(authType);
|
await nonInteractiveConfig.refreshAuth(authType);
|
||||||
return nonInteractiveConfig;
|
return nonInteractiveConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (nonInteractiveConfig.getOutputFormat() === OutputFormat.JSON) {
|
const outputFormat = nonInteractiveConfig.getOutputFormat();
|
||||||
handleError(
|
|
||||||
error instanceof Error ? error : new Error(String(error)),
|
// In JSON and STREAM_JSON modes, emit error result and exit
|
||||||
nonInteractiveConfig,
|
if (
|
||||||
1,
|
outputFormat === OutputFormat.JSON ||
|
||||||
);
|
outputFormat === OutputFormat.STREAM_JSON
|
||||||
} else {
|
) {
|
||||||
console.error(error instanceof Error ? error.message : String(error));
|
let adapter;
|
||||||
|
if (outputFormat === OutputFormat.JSON) {
|
||||||
|
adapter = new JsonOutputAdapter(nonInteractiveConfig);
|
||||||
|
} else {
|
||||||
|
adapter = new StreamJsonOutputAdapter(
|
||||||
|
nonInteractiveConfig,
|
||||||
|
nonInteractiveConfig.getIncludePartialMessages(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
adapter.emitResult({
|
||||||
|
isError: true,
|
||||||
|
errorMessage,
|
||||||
|
durationMs: 0,
|
||||||
|
apiDurationMs: 0,
|
||||||
|
numTurns: 0,
|
||||||
|
usage: undefined,
|
||||||
|
});
|
||||||
|
await runExitCleanup();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For other modes (text), use existing error handling
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -620,6 +620,17 @@ export class SubAgentScope {
|
|||||||
success,
|
success,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
responseParts: call.response.responseParts,
|
responseParts: call.response.responseParts,
|
||||||
|
/**
|
||||||
|
* Tools like todoWrite will add some extra contents to the result,
|
||||||
|
* making it unable to deserialize the `responseParts` to a JSON object.
|
||||||
|
* While `resultDisplay` is normally a string, if not we stringify it,
|
||||||
|
* so that we can deserialize it to a JSON object when needed.
|
||||||
|
*/
|
||||||
|
resultDisplay: call.response.resultDisplay
|
||||||
|
? typeof call.response.resultDisplay === 'string'
|
||||||
|
? call.response.resultDisplay
|
||||||
|
: JSON.stringify(call.response.resultDisplay)
|
||||||
|
: undefined,
|
||||||
durationMs: duration,
|
durationMs: duration,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as SubAgentToolResultEvent);
|
} as SubAgentToolResultEvent);
|
||||||
|
|||||||
Reference in New Issue
Block a user