From 23c014e29cbb3ac28e6fb02ef14d0538377f38ca Mon Sep 17 00:00:00 2001 From: Sandy Tao Date: Wed, 30 Jul 2025 21:47:04 -0700 Subject: [PATCH] Replace FlashDecidedToContinueEvent with NextSpeakerCheckEvent (#5257) --- packages/core/src/core/client.ts | 16 ++++++++++------ packages/core/src/core/turn.ts | 3 +++ .../telemetry/clearcut-logger/clearcut-logger.ts | 16 ++++++++++++---- .../clearcut-logger/event-metadata-key.ts | 10 ++++++++++ packages/core/src/telemetry/constants.ts | 3 +-- packages/core/src/telemetry/loggers.ts | 14 +++++++------- packages/core/src/telemetry/types.ts | 14 +++++++++----- 7 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 49a30294..5b26e32c 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -43,8 +43,8 @@ import { ProxyAgent, setGlobalDispatcher } from 'undici'; import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; import { LoopDetectionService } from '../services/loopDetectionService.js'; import { ideContext } from '../ide/ideContext.js'; -import { logFlashDecidedToContinue } from '../telemetry/loggers.js'; -import { FlashDecidedToContinueEvent } from '../telemetry/types.js'; +import { logNextSpeakerCheck } from '../telemetry/loggers.js'; +import { NextSpeakerCheckEvent } from '../telemetry/types.js'; function isThinkingSupported(model: string) { if (model.startsWith('gemini-2.5')) return true; @@ -415,11 +415,15 @@ export class GeminiClient { this, signal, ); + logNextSpeakerCheck( + this.config, + new NextSpeakerCheckEvent( + prompt_id, + turn.finishReason?.toString() || '', + nextSpeakerCheck?.next_speaker || '', + ), + ); if (nextSpeakerCheck?.next_speaker === 'model') { - logFlashDecidedToContinue( - this.config, - new FlashDecidedToContinueEvent(prompt_id), - ); const nextRequest = [{ text: 'Please continue.' }]; // This recursive call's events will be yielded out, but the final // turn object will be from the top-level call. diff --git a/packages/core/src/core/turn.ts b/packages/core/src/core/turn.ts index bea29b66..b54b3f82 100644 --- a/packages/core/src/core/turn.ts +++ b/packages/core/src/core/turn.ts @@ -163,6 +163,7 @@ export type ServerGeminiStreamEvent = export class Turn { readonly pendingToolCalls: ToolCallRequestInfo[]; private debugResponses: GenerateContentResponse[]; + finishReason: FinishReason | undefined; constructor( private readonly chat: GeminiChat, @@ -170,6 +171,7 @@ export class Turn { ) { this.pendingToolCalls = []; this.debugResponses = []; + this.finishReason = undefined; } // The run method yields simpler events suitable for server logic async *run( @@ -235,6 +237,7 @@ export class Turn { const finishReason = resp.candidates?.[0]?.finishReason; if (finishReason) { + this.finishReason = finishReason; yield { type: GeminiEventType.Finished, value: finishReason as FinishReason, diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index d221ef5e..81a9ca4b 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -18,7 +18,7 @@ import { ApiErrorEvent, FlashFallbackEvent, LoopDetectedEvent, - FlashDecidedToContinueEvent, + NextSpeakerCheckEvent, SlashCommandEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; @@ -40,7 +40,7 @@ const api_error_event_name = 'api_error'; const end_session_event_name = 'end_session'; const flash_fallback_event_name = 'flash_fallback'; const loop_detected_event_name = 'loop_detected'; -const flash_decided_to_continue_event_name = 'flash_decided_to_continue'; +const next_speaker_check_event_name = 'next_speaker_check'; const slash_command_event_name = 'slash_command'; export interface LogResponse { @@ -512,12 +512,20 @@ export class ClearcutLogger { this.flushIfNeeded(); } - logFlashDecidedToContinueEvent(event: FlashDecidedToContinueEvent): void { + logNextSpeakerCheck(event: NextSpeakerCheckEvent): void { const data = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID, value: JSON.stringify(event.prompt_id), }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_RESPONSE_FINISH_REASON, + value: JSON.stringify(event.finish_reason), + }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT, + value: JSON.stringify(event.result), + }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID, value: this.config?.getSessionId() ?? '', @@ -525,7 +533,7 @@ export class ClearcutLogger { ]; this.enqueueLogEvent( - this.createLogEvent(flash_decided_to_continue_event_name, data), + this.createLogEvent(next_speaker_check_event_name, data), ); this.flushIfNeeded(); } diff --git a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts index 9a182f67..01dd42af 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -173,6 +173,16 @@ export enum EventMetadataKey { // Logs the subcommand of the slash command. GEMINI_CLI_SLASH_COMMAND_SUBCOMMAND = 42, + + // ========================================================================== + // Next Speaker Check Event Keys + // =========================================================================== + + // Logs the finish reason of the previous streamGenerateContent response + GEMINI_CLI_RESPONSE_FINISH_REASON = 43, + + // Logs the result of the next speaker check + GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT = 44, } export function getEventMetadataKey( diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index 42572228..7dd5c8d1 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -13,8 +13,7 @@ export const EVENT_API_ERROR = 'gemini_cli.api_error'; export const EVENT_API_RESPONSE = 'gemini_cli.api_response'; export const EVENT_CLI_CONFIG = 'gemini_cli.config'; export const EVENT_FLASH_FALLBACK = 'gemini_cli.flash_fallback'; -export const EVENT_FLASH_DECIDED_TO_CONTINUE = - 'gemini_cli.flash_decided_to_continue'; +export const EVENT_NEXT_SPEAKER_CHECK = 'gemini_cli.next_speaker_check'; export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command'; export const METRIC_TOOL_CALL_COUNT = 'gemini_cli.tool.call.count'; diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 3ee806bb..2aa0d86a 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -15,7 +15,7 @@ import { EVENT_TOOL_CALL, EVENT_USER_PROMPT, EVENT_FLASH_FALLBACK, - EVENT_FLASH_DECIDED_TO_CONTINUE, + EVENT_NEXT_SPEAKER_CHECK, SERVICE_NAME, EVENT_SLASH_COMMAND, } from './constants.js'; @@ -27,7 +27,7 @@ import { ToolCallEvent, UserPromptEvent, FlashFallbackEvent, - FlashDecidedToContinueEvent, + NextSpeakerCheckEvent, LoopDetectedEvent, SlashCommandEvent, } from './types.js'; @@ -314,22 +314,22 @@ export function logLoopDetected( logger.emit(logRecord); } -export function logFlashDecidedToContinue( +export function logNextSpeakerCheck( config: Config, - event: FlashDecidedToContinueEvent, + event: NextSpeakerCheckEvent, ): void { - ClearcutLogger.getInstance(config)?.logFlashDecidedToContinueEvent(event); + ClearcutLogger.getInstance(config)?.logNextSpeakerCheck(event); if (!isTelemetrySdkInitialized()) return; const attributes: LogAttributes = { ...getCommonAttributes(config), ...event, - 'event.name': EVENT_FLASH_DECIDED_TO_CONTINUE, + 'event.name': EVENT_NEXT_SPEAKER_CHECK, }; const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Flash decided to continue.`, + body: `Next speaker check.`, attributes, }; logger.emit(logRecord); diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index d29b97d2..6fe797bf 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -266,15 +266,19 @@ export class LoopDetectedEvent { } } -export class FlashDecidedToContinueEvent { - 'event.name': 'flash_decided_to_continue'; +export class NextSpeakerCheckEvent { + 'event.name': 'next_speaker_check'; 'event.timestamp': string; // ISO 8601 prompt_id: string; + finish_reason: string; + result: string; - constructor(prompt_id: string) { - this['event.name'] = 'flash_decided_to_continue'; + constructor(prompt_id: string, finish_reason: string, result: string) { + this['event.name'] = 'next_speaker_check'; this['event.timestamp'] = new Date().toISOString(); this.prompt_id = prompt_id; + this.finish_reason = finish_reason; + this.result = result; } } @@ -302,5 +306,5 @@ export type TelemetryEvent = | ApiResponseEvent | FlashFallbackEvent | LoopDetectedEvent - | FlashDecidedToContinueEvent + | NextSpeakerCheckEvent | SlashCommandEvent;