diff --git a/packages/core/src/services/observationMaskingService.test.ts b/packages/core/src/services/observationMaskingService.test.ts index 9581f10e01..cfdaa62808 100644 --- a/packages/core/src/services/observationMaskingService.test.ts +++ b/packages/core/src/services/observationMaskingService.test.ts @@ -32,6 +32,7 @@ describe('ObservationMaskingService', () => { storage: { getHistoryDir: () => '/mock/history', }, + getUsageStatisticsEnabled: () => false, } as unknown as Config; vi.clearAllMocks(); }); diff --git a/packages/core/src/services/observationMaskingService.ts b/packages/core/src/services/observationMaskingService.ts index b7fe1bf6ba..f0d984fca7 100644 --- a/packages/core/src/services/observationMaskingService.ts +++ b/packages/core/src/services/observationMaskingService.ts @@ -10,6 +10,8 @@ import * as fsPromises from 'node:fs/promises'; import { estimateTokenCountSync } from '../utils/tokenCalculation.js'; import { debugLogger } from '../utils/debugLogger.js'; import type { Config } from '../config/config.js'; +import { logObservationMasking } from '../telemetry/loggers.js'; +import { ObservationMaskingEvent } from '../telemetry/types.js'; export const TOOL_PROTECTION_THRESHOLD = 50_000; export const HYSTERESIS_THRESHOLD = 30_000; @@ -176,11 +178,23 @@ export class ObservationMaskingService { `[ObservationMasking] Masked ${prunableParts.length} tool outputs. Saved ~${actualTokensSaved.toLocaleString()} tokens.`, ); - return { + const result = { newHistory, maskedCount: prunableParts.length, tokensSaved: actualTokensSaved, }; + + logObservationMasking( + config, + new ObservationMaskingEvent({ + tokens_before: totalPrunableTokens, + tokens_after: totalPrunableTokens - actualTokensSaved, + masked_count: prunableParts.length, + total_prunable_tokens: totalPrunableTokens, + }), + ); + + return result; } private getObservationContent(part: Part): string | null { diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 9417bbe983..79154a60e7 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -44,6 +44,7 @@ import type { HookCallEvent, ApprovalModeSwitchEvent, ApprovalModeDurationEvent, + ObservationMaskingEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; @@ -104,6 +105,7 @@ export enum EventNames { HOOK_CALL = 'hook_call', APPROVAL_MODE_SWITCH = 'approval_mode_switch', APPROVAL_MODE_DURATION = 'approval_mode_duration', + OBSERVATION_MASKING = 'observation_masking', } export interface LogResponse { @@ -1201,8 +1203,40 @@ export class ClearcutLogger { }, ]; + const logEvent = this.createLogEvent( + EventNames.TOOL_OUTPUT_TRUNCATED, + data, + ); + this.enqueueLogEvent(logEvent); + this.flushIfNeeded(); + } + + logObservationMaskingEvent(event: ObservationMaskingEvent): void { + const data: EventValue[] = [ + { + gemini_cli_key: + EventMetadataKey.GEMINI_CLI_OBSERVATION_MASKING_TOKENS_BEFORE, + value: event.tokens_before.toString(), + }, + { + gemini_cli_key: + EventMetadataKey.GEMINI_CLI_OBSERVATION_MASKING_TOKENS_AFTER, + value: event.tokens_after.toString(), + }, + { + gemini_cli_key: + EventMetadataKey.GEMINI_CLI_OBSERVATION_MASKING_MASKED_COUNT, + value: event.masked_count.toString(), + }, + { + gemini_cli_key: + EventMetadataKey.GEMINI_CLI_OBSERVATION_MASKING_TOTAL_PRUNABLE_TOKENS, + value: event.total_prunable_tokens.toString(), + }, + ]; + this.enqueueLogEvent( - this.createLogEvent(EventNames.TOOL_OUTPUT_TRUNCATED, data), + this.createLogEvent(EventNames.OBSERVATION_MASKING, 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 a3b22ce58e..14a67e744d 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -542,4 +542,20 @@ export enum EventMetadataKey { // Logs the duration spent in an approval mode in milliseconds. GEMINI_CLI_APPROVAL_MODE_DURATION_MS = 143, + + // ========================================================================== + // Observation Masking Event Keys + // ========================================================================== + + // Logs the total tokens in the prunable block before masking. + GEMINI_CLI_OBSERVATION_MASKING_TOKENS_BEFORE = 144, + + // Logs the total tokens in the masked remnants after masking. + GEMINI_CLI_OBSERVATION_MASKING_TOKENS_AFTER = 145, + + // Logs the number of tool outputs masked in this operation. + GEMINI_CLI_OBSERVATION_MASKING_MASKED_COUNT = 146, + + // Logs the total prunable tokens identified at the trigger point. + GEMINI_CLI_OBSERVATION_MASKING_TOTAL_PRUNABLE_TOKENS = 147, } diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index ae25424464..33841d489e 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -53,6 +53,7 @@ import type { HookCallEvent, StartupStatsEvent, LlmLoopCheckEvent, + ObservationMaskingEvent, } from './types.js'; import { recordApiErrorMetrics, @@ -159,6 +160,21 @@ export function logToolOutputTruncated( }); } +export function logObservationMasking( + config: Config, + event: ObservationMaskingEvent, +): void { + ClearcutLogger.getInstance(config)?.logObservationMaskingEvent(event); + bufferTelemetryEvent(() => { + const logger = logs.getLogger(SERVICE_NAME); + const logRecord: LogRecord = { + body: event.toLogBody(), + attributes: event.toOpenTelemetryAttributes(config), + }; + logger.emit(logRecord); + }); +} + export function logFileOperation( config: Config, event: FileOperationEvent, diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index d10c7e9876..8ea3db0b30 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -1350,6 +1350,49 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { } } +export const EVENT_OBSERVATION_MASKING = 'gemini_cli.observation_masking'; + +export class ObservationMaskingEvent implements BaseTelemetryEvent { + 'event.name': 'observation_masking'; + 'event.timestamp': string; + tokens_before: number; + tokens_after: number; + masked_count: number; + total_prunable_tokens: number; + + constructor(details: { + tokens_before: number; + tokens_after: number; + masked_count: number; + total_prunable_tokens: number; + }) { + this['event.name'] = 'observation_masking'; + this['event.timestamp'] = new Date().toISOString(); + this.tokens_before = details.tokens_before; + this.tokens_after = details.tokens_after; + this.masked_count = details.masked_count; + this.total_prunable_tokens = details.total_prunable_tokens; + } + + toOpenTelemetryAttributes(config: Config): LogAttributes { + return { + ...getCommonAttributes(config), + 'event.name': EVENT_OBSERVATION_MASKING, + 'event.timestamp': this['event.timestamp'], + tokens_before: this.tokens_before, + tokens_after: this.tokens_after, + masked_count: this.masked_count, + total_prunable_tokens: this.total_prunable_tokens, + }; + } + + toLogBody(): string { + return `Observation masking (Masked ${this.masked_count} tool outputs. Saved ${ + this.tokens_before - this.tokens_after + } tokens)`; + } +} + export const EVENT_EXTENSION_UNINSTALL = 'gemini_cli.extension_uninstall'; export class ExtensionUninstallEvent implements BaseTelemetryEvent { 'event.name': 'extension_uninstall'; @@ -1576,6 +1619,7 @@ export type TelemetryEvent = | LlmLoopCheckEvent | StartupStatsEvent | WebFetchFallbackAttemptEvent + | ObservationMaskingEvent | EditStrategyEvent | EditCorrectionEvent;