feat(headless): gate diagnostic output behind --debug flag

Diagnostic monitoring data (auth_method, user_tier, api_requests,
api_errors, retry_count, RETRY events, LOOP_DETECTED events, and
stderr warnings) is now only emitted when --debug / -d is passed.

Without the flag, headless output is identical to before — no new
fields, no new events, no stderr noise. This keeps default output
clean for piped workflows while making diagnostics available on demand.
This commit is contained in:
Dmitry Lyalin
2026-02-25 09:52:14 -08:00
parent a4b3229513
commit 941a479855
4 changed files with 50 additions and 33 deletions
@@ -3,8 +3,8 @@
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'loop detected' 1`] = `
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
{"type":"loop_detected","timestamp":"<TIMESTAMP>"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"api_requests":0,"api_errors":0}}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
"
`;
@@ -12,7 +12,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"api_requests":0,"api_errors":0}}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
"
`;
@@ -23,7 +23,7 @@ exports[`runNonInteractive > should emit appropriate events for streaming JSON o
{"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}}
{"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"api_requests":0,"api_errors":0}}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
"
`;
+29 -17
View File
@@ -96,6 +96,7 @@ export async function runNonInteractive({
const startTime = Date.now();
let retryCount = 0;
const debugMode = config.getDebugMode();
const streamFormatter =
config.getOutputFormat() === OutputFormat.STREAM_JSON
? new StreamJsonFormatter()
@@ -185,6 +186,7 @@ export async function runNonInteractive({
const handleRetryAttempt = (payload: RetryAttemptPayload) => {
retryCount++;
if (!debugMode) return;
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.RETRY,
@@ -252,8 +254,10 @@ export async function runNonInteractive({
}
// Emit init event for streaming JSON
const authMethod = config.getContentGeneratorConfig()?.authType;
const userTier = config.getUserTierName();
const authMethod = debugMode
? config.getContentGeneratorConfig()?.authType
: undefined;
const userTier = debugMode ? config.getUserTierName() : undefined;
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.INIT,
@@ -373,18 +377,27 @@ export async function runNonInteractive({
}
toolCallRequests.push(event.value);
} else if (event.type === GeminiEventType.LoopDetected) {
const loopType = event.value?.loopType;
if (streamFormatter) {
if (debugMode) {
const loopType = event.value?.loopType;
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.LOOP_DETECTED,
timestamp: new Date().toISOString(),
...(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 (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.LOOP_DETECTED,
type: JsonStreamEventType.ERROR,
timestamp: new Date().toISOString(),
...(loopType && { loop_type: loopType }),
severity: 'warning',
message: 'Loop detected, stopping execution',
});
} 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) {
@@ -413,7 +426,7 @@ export async function runNonInteractive({
stats: streamFormatter.convertToStreamStats(
metrics,
durationMs,
retryCount,
{ retryCount, includeDiagnostics: debugMode },
),
});
}
@@ -513,7 +526,7 @@ export async function runNonInteractive({
stats: streamFormatter.convertToStreamStats(
metrics,
durationMs,
retryCount,
{ retryCount, includeDiagnostics: debugMode },
),
});
} else if (config.getOutputFormat() === OutputFormat.JSON) {
@@ -545,11 +558,10 @@ export async function runNonInteractive({
type: JsonStreamEventType.RESULT,
timestamp: new Date().toISOString(),
status: 'success',
stats: streamFormatter.convertToStreamStats(
metrics,
durationMs,
stats: streamFormatter.convertToStreamStats(metrics, durationMs, {
retryCount,
),
includeDiagnostics: debugMode,
}),
});
} else if (config.getOutputFormat() === OutputFormat.JSON) {
const formatter = new JsonFormatter();
@@ -39,25 +39,21 @@ export class StreamJsonFormatter {
convertToStreamStats(
metrics: SessionMetrics,
durationMs: number,
retryCount?: number,
options?: { retryCount?: number; includeDiagnostics?: boolean },
): StreamStats {
let totalTokens = 0;
let inputTokens = 0;
let outputTokens = 0;
let cached = 0;
let input = 0;
let apiRequests = 0;
let apiErrors = 0;
// Aggregate token counts and API stats across all models
// Aggregate token counts across all models
for (const modelMetrics of Object.values(metrics.models)) {
totalTokens += modelMetrics.tokens.total;
inputTokens += modelMetrics.tokens.prompt;
outputTokens += modelMetrics.tokens.candidates;
cached += modelMetrics.tokens.cached;
input += modelMetrics.tokens.input;
apiRequests += modelMetrics.api.totalRequests;
apiErrors += modelMetrics.api.totalErrors;
}
const stats: StreamStats = {
@@ -68,12 +64,21 @@ export class StreamJsonFormatter {
input,
duration_ms: durationMs,
tool_calls: metrics.tools.totalCalls,
api_requests: apiRequests,
api_errors: apiErrors,
};
if (retryCount && retryCount > 0) {
stats.retry_count = retryCount;
if (options?.includeDiagnostics) {
let apiRequests = 0;
let apiErrors = 0;
for (const modelMetrics of Object.values(metrics.models)) {
apiRequests += modelMetrics.api.totalRequests;
apiErrors += modelMetrics.api.totalErrors;
}
stats.api_requests = apiRequests;
stats.api_errors = apiErrors;
if (options.retryCount && options.retryCount > 0) {
stats.retry_count = options.retryCount;
}
}
return stats;
+2 -2
View File
@@ -96,8 +96,8 @@ for model in "${MODELS[@]}"; do
STDERRFILE=$(mktemp)
exit_code=0
echo -e " ${DIM}Running with -o stream-json ...${RESET}"
node "$CLI" -p "$PROMPT" -y -m "$model" -o stream-json \
echo -e " ${DIM}Running with -o stream-json -d ...${RESET}"
node "$CLI" -p "$PROMPT" -y -m "$model" -o stream-json -d \
>"$TMPFILE" 2>"$STDERRFILE" || exit_code=$?
if [[ $exit_code -ne 0 ]]; then