mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 19:44:30 -07:00
Add support for output-format stream-jsonflag for headless mode (#10883)
This commit is contained in:
@@ -3432,6 +3432,22 @@ describe('Output format', () => {
|
||||
expect(config.getOutputFormat()).toBe(OutputFormat.JSON);
|
||||
});
|
||||
|
||||
it('should accept stream-json as a valid output format', async () => {
|
||||
process.argv = ['node', 'script.js', '--output-format', 'stream-json'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{},
|
||||
[],
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getOutputFormat()).toBe(OutputFormat.STREAM_JSON);
|
||||
});
|
||||
|
||||
it('should error on invalid --output-format argument', async () => {
|
||||
process.argv = ['node', 'script.js', '--output-format', 'yaml'];
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
|
||||
@@ -286,7 +286,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
alias: 'o',
|
||||
type: 'string',
|
||||
description: 'The format of the CLI output.',
|
||||
choices: ['text', 'json'],
|
||||
choices: ['text', 'json', 'stream-json'],
|
||||
})
|
||||
.deprecateOption(
|
||||
'show-memory-usage',
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
promptIdContext,
|
||||
OutputFormat,
|
||||
JsonFormatter,
|
||||
StreamJsonFormatter,
|
||||
JsonStreamEventType,
|
||||
uiTelemetryService,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
@@ -47,6 +49,12 @@ export async function runNonInteractive(
|
||||
debugMode: config.getDebugMode(),
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const streamFormatter =
|
||||
config.getOutputFormat() === OutputFormat.STREAM_JSON
|
||||
? new StreamJsonFormatter()
|
||||
: null;
|
||||
|
||||
try {
|
||||
consolePatcher.patch();
|
||||
// Handle EPIPE errors when the output is piped to a command that closes early.
|
||||
@@ -59,6 +67,16 @@ export async function runNonInteractive(
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
|
||||
// Emit init event for streaming JSON
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.INIT,
|
||||
timestamp: new Date().toISOString(),
|
||||
session_id: config.getSessionId(),
|
||||
model: config.getModel(),
|
||||
});
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
let query: Part[] | undefined;
|
||||
@@ -98,6 +116,16 @@ export async function runNonInteractive(
|
||||
query = processedQuery as Part[];
|
||||
}
|
||||
|
||||
// Emit user message event for streaming JSON
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.MESSAGE,
|
||||
timestamp: new Date().toISOString(),
|
||||
role: 'user',
|
||||
content: input,
|
||||
});
|
||||
}
|
||||
|
||||
let currentMessages: Content[] = [{ role: 'user', parts: query }];
|
||||
|
||||
let turnCount = 0;
|
||||
@@ -124,13 +152,48 @@ export async function runNonInteractive(
|
||||
}
|
||||
|
||||
if (event.type === GeminiEventType.Content) {
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.MESSAGE,
|
||||
timestamp: new Date().toISOString(),
|
||||
role: 'assistant',
|
||||
content: event.value,
|
||||
delta: true,
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
responseText += event.value;
|
||||
} else {
|
||||
process.stdout.write(event.value);
|
||||
}
|
||||
} else if (event.type === GeminiEventType.ToolCallRequest) {
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.TOOL_USE,
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: event.value.name,
|
||||
tool_id: event.value.callId,
|
||||
parameters: event.value.args,
|
||||
});
|
||||
}
|
||||
toolCallRequests.push(event.value);
|
||||
} else if (event.type === GeminiEventType.LoopDetected) {
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.ERROR,
|
||||
timestamp: new Date().toISOString(),
|
||||
severity: 'warning',
|
||||
message: 'Loop detected, stopping execution',
|
||||
});
|
||||
}
|
||||
} else if (event.type === GeminiEventType.MaxSessionTurns) {
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.ERROR,
|
||||
timestamp: new Date().toISOString(),
|
||||
severity: 'error',
|
||||
message: 'Maximum session turns exceeded',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +211,25 @@ export async function runNonInteractive(
|
||||
|
||||
completedToolCalls.push(completedToolCall);
|
||||
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.TOOL_RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_id: requestInfo.callId,
|
||||
status: toolResponse.error ? 'error' : 'success',
|
||||
output:
|
||||
typeof toolResponse.resultDisplay === 'string'
|
||||
? toolResponse.resultDisplay
|
||||
: undefined,
|
||||
error: toolResponse.error
|
||||
? {
|
||||
type: toolResponse.errorType || 'TOOL_EXECUTION_ERROR',
|
||||
message: toolResponse.error.message,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (toolResponse.error) {
|
||||
handleToolError(
|
||||
requestInfo.name,
|
||||
@@ -180,7 +262,17 @@ export async function runNonInteractive(
|
||||
|
||||
currentMessages = [{ role: 'user', parts: toolResponseParts }];
|
||||
} else {
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
// Emit final result event for streaming JSON
|
||||
if (streamFormatter) {
|
||||
const metrics = uiTelemetryService.getMetrics();
|
||||
const durationMs = Date.now() - startTime;
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'success',
|
||||
stats: streamFormatter.convertToStreamStats(metrics, durationMs),
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const stats = uiTelemetryService.getMetrics();
|
||||
process.stdout.write(formatter.format(responseText, stats));
|
||||
|
||||
@@ -8,6 +8,9 @@ import type { Config } from '@google/gemini-cli-core';
|
||||
import {
|
||||
OutputFormat,
|
||||
JsonFormatter,
|
||||
StreamJsonFormatter,
|
||||
JsonStreamEventType,
|
||||
uiTelemetryService,
|
||||
parseAndFormatApiError,
|
||||
FatalTurnLimitedError,
|
||||
FatalCancellationError,
|
||||
@@ -58,6 +61,7 @@ function getNumericExitCode(errorCode: string | number): number {
|
||||
/**
|
||||
* Handles errors consistently for both JSON and text output formats.
|
||||
* In JSON mode, outputs formatted JSON error and exits.
|
||||
* In streaming JSON mode, emits a result event with error status.
|
||||
* In text mode, outputs error message and re-throws.
|
||||
*/
|
||||
export function handleError(
|
||||
@@ -70,7 +74,24 @@ export function handleError(
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
);
|
||||
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
if (config.getOutputFormat() === OutputFormat.STREAM_JSON) {
|
||||
const streamFormatter = new StreamJsonFormatter();
|
||||
const errorCode = customErrorCode ?? extractErrorCode(error);
|
||||
const metrics = uiTelemetryService.getMetrics();
|
||||
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'error',
|
||||
error: {
|
||||
type: error instanceof Error ? error.constructor.name : 'Error',
|
||||
message: errorMessage,
|
||||
},
|
||||
stats: streamFormatter.convertToStreamStats(metrics, 0),
|
||||
});
|
||||
|
||||
process.exit(getNumericExitCode(errorCode));
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const errorCode = customErrorCode ?? extractErrorCode(error);
|
||||
|
||||
@@ -110,7 +131,20 @@ export function handleToolError(
|
||||
|
||||
if (isFatal) {
|
||||
const toolExecutionError = new FatalToolExecutionError(errorMessage);
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
if (config.getOutputFormat() === OutputFormat.STREAM_JSON) {
|
||||
const streamFormatter = new StreamJsonFormatter();
|
||||
const metrics = uiTelemetryService.getMetrics();
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'error',
|
||||
error: {
|
||||
type: errorType ?? 'FatalToolExecutionError',
|
||||
message: toolExecutionError.message,
|
||||
},
|
||||
stats: streamFormatter.convertToStreamStats(metrics, 0),
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const formattedError = formatter.formatError(
|
||||
toolExecutionError,
|
||||
@@ -133,7 +167,21 @@ export function handleToolError(
|
||||
export function handleCancellationError(config: Config): never {
|
||||
const cancellationError = new FatalCancellationError('Operation cancelled.');
|
||||
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
if (config.getOutputFormat() === OutputFormat.STREAM_JSON) {
|
||||
const streamFormatter = new StreamJsonFormatter();
|
||||
const metrics = uiTelemetryService.getMetrics();
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'error',
|
||||
error: {
|
||||
type: 'FatalCancellationError',
|
||||
message: cancellationError.message,
|
||||
},
|
||||
stats: streamFormatter.convertToStreamStats(metrics, 0),
|
||||
});
|
||||
process.exit(cancellationError.exitCode);
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const formattedError = formatter.formatError(
|
||||
cancellationError,
|
||||
@@ -156,7 +204,21 @@ export function handleMaxTurnsExceededError(config: Config): never {
|
||||
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
|
||||
);
|
||||
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
if (config.getOutputFormat() === OutputFormat.STREAM_JSON) {
|
||||
const streamFormatter = new StreamJsonFormatter();
|
||||
const metrics = uiTelemetryService.getMetrics();
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'error',
|
||||
error: {
|
||||
type: 'FatalTurnLimitedError',
|
||||
message: maxTurnsError.message,
|
||||
},
|
||||
stats: streamFormatter.convertToStreamStats(metrics, 0),
|
||||
});
|
||||
process.exit(maxTurnsError.exitCode);
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const formattedError = formatter.formatError(
|
||||
maxTurnsError,
|
||||
|
||||
Reference in New Issue
Block a user