mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
feat(headless): surface diagnostic monitoring data in non-interactive output
When running Gemini CLI in headless mode (-p), critical diagnostic data like auth method, API retry attempts, loop detection, and request stats was invisible despite being tracked internally. This change surfaces that data across all three output formats (stream-json, json, text). Changes: - Add RETRY and LOOP_DETECTED event types to stream-json output - Include auth_method and user_tier in init events and JSON output - Add api_requests, api_errors, and retry_count to result stats - Track and expose detected loop type (tool call, chanting, LLM-detected) - Emit [RETRY] and [WARNING] messages to stderr in text mode - Listen to CoreEvent.RetryAttempt in non-interactive CLI - Add test script (scripts/test_gemini.sh) for manual verification
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
ToolCallRequestInfo,
|
||||
ResumedSessionData,
|
||||
UserFeedbackPayload,
|
||||
RetryAttemptPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { isSlashCommand } from './ui/utils/commandUtils.js';
|
||||
import type { LoadedSettings } from './config/settings.js';
|
||||
@@ -94,6 +95,7 @@ export async function runNonInteractive({
|
||||
};
|
||||
|
||||
const startTime = Date.now();
|
||||
let retryCount = 0;
|
||||
const streamFormatter =
|
||||
config.getOutputFormat() === OutputFormat.STREAM_JSON
|
||||
? new StreamJsonFormatter()
|
||||
@@ -181,6 +183,26 @@ export async function runNonInteractive({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetryAttempt = (payload: RetryAttemptPayload) => {
|
||||
retryCount++;
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RETRY,
|
||||
timestamp: new Date().toISOString(),
|
||||
attempt: payload.attempt,
|
||||
max_attempts: payload.maxAttempts,
|
||||
delay_ms: payload.delayMs,
|
||||
error: payload.error,
|
||||
model: payload.model,
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
const errorSuffix = payload.error ? `: ${payload.error}` : '';
|
||||
process.stderr.write(
|
||||
`[RETRY] Attempt ${payload.attempt}/${payload.maxAttempts} for model ${payload.model} (delay: ${payload.delayMs}ms)${errorSuffix}\n`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let errorToHandle: unknown | undefined;
|
||||
try {
|
||||
consolePatcher.patch();
|
||||
@@ -199,6 +221,8 @@ export async function runNonInteractive({
|
||||
setupStdinCancellation();
|
||||
|
||||
coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback);
|
||||
coreEvents.on(CoreEvent.RetryAttempt, handleRetryAttempt);
|
||||
|
||||
coreEvents.drainBacklogs();
|
||||
|
||||
// Handle EPIPE errors when the output is piped to a command that closes early.
|
||||
@@ -228,12 +252,16 @@ export async function runNonInteractive({
|
||||
}
|
||||
|
||||
// Emit init event for streaming JSON
|
||||
const authMethod = config.getContentGeneratorConfig()?.authType;
|
||||
const userTier = config.getUserTierName();
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.INIT,
|
||||
timestamp: new Date().toISOString(),
|
||||
session_id: config.getSessionId(),
|
||||
model: config.getModel(),
|
||||
...(authMethod && { auth_method: authMethod }),
|
||||
...(userTier && { user_tier: userTier }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -345,13 +373,18 @@ export async function runNonInteractive({
|
||||
}
|
||||
toolCallRequests.push(event.value);
|
||||
} else if (event.type === GeminiEventType.LoopDetected) {
|
||||
const loopType = event.value?.loopType;
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.ERROR,
|
||||
type: JsonStreamEventType.LOOP_DETECTED,
|
||||
timestamp: new Date().toISOString(),
|
||||
severity: 'warning',
|
||||
message: 'Loop detected, stopping execution',
|
||||
...(loopType && { loop_type: loopType }),
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
const loopTypeStr = loopType ? ` (${loopType})` : '';
|
||||
process.stderr.write(
|
||||
`[WARNING] Loop detected${loopTypeStr}, stopping execution\n`,
|
||||
);
|
||||
}
|
||||
} else if (event.type === GeminiEventType.MaxSessionTurns) {
|
||||
if (streamFormatter) {
|
||||
@@ -380,6 +413,7 @@ export async function runNonInteractive({
|
||||
stats: streamFormatter.convertToStreamStats(
|
||||
metrics,
|
||||
durationMs,
|
||||
retryCount,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -479,13 +513,21 @@ export async function runNonInteractive({
|
||||
stats: streamFormatter.convertToStreamStats(
|
||||
metrics,
|
||||
durationMs,
|
||||
retryCount,
|
||||
),
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const stats = uiTelemetryService.getMetrics();
|
||||
textOutput.write(
|
||||
formatter.format(config.getSessionId(), responseText, stats),
|
||||
formatter.format(
|
||||
config.getSessionId(),
|
||||
responseText,
|
||||
stats,
|
||||
undefined,
|
||||
authMethod,
|
||||
userTier,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
textOutput.ensureTrailingNewline(); // Ensure a final newline
|
||||
@@ -503,13 +545,24 @@ export async function runNonInteractive({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'success',
|
||||
stats: streamFormatter.convertToStreamStats(metrics, durationMs),
|
||||
stats: streamFormatter.convertToStreamStats(
|
||||
metrics,
|
||||
durationMs,
|
||||
retryCount,
|
||||
),
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const stats = uiTelemetryService.getMetrics();
|
||||
textOutput.write(
|
||||
formatter.format(config.getSessionId(), responseText, stats),
|
||||
formatter.format(
|
||||
config.getSessionId(),
|
||||
responseText,
|
||||
stats,
|
||||
undefined,
|
||||
authMethod,
|
||||
userTier,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
textOutput.ensureTrailingNewline(); // Ensure a final newline
|
||||
@@ -524,6 +577,7 @@ export async function runNonInteractive({
|
||||
cleanupStdinCancellation();
|
||||
|
||||
consolePatcher.cleanup();
|
||||
coreEvents.off(CoreEvent.RetryAttempt, handleRetryAttempt);
|
||||
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user