diff options
| author | Jerop Kipruto <[email protected]> | 2025-06-12 16:48:10 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-12 20:48:10 +0000 |
| commit | 6723c72fa5468be713c05205c75be532729e8f92 (patch) | |
| tree | 2392b344fb942f1c452e9fca5b5b6d131a827805 /packages/core/src/telemetry | |
| parent | f8863f4d00f23a3e29496535be6cf0bb80ee43e9 (diff) | |
telemetry: include user decisions in tool call logs (#966)
Add the user's decision (accept, reject, modify) to tool call telemetry to better understand user intent. The decision provides crucial context to the `success` metric, as a user can reject a call that would have succeeded or accept one that fails.
Also prettify the arguments json.
Example:

#750
Diffstat (limited to 'packages/core/src/telemetry')
| -rw-r--r-- | packages/core/src/telemetry/loggers.test.ts | 238 | ||||
| -rw-r--r-- | packages/core/src/telemetry/loggers.ts | 36 | ||||
| -rw-r--r-- | packages/core/src/telemetry/metrics.ts | 2 | ||||
| -rw-r--r-- | packages/core/src/telemetry/types.ts | 3 |
4 files changed, 276 insertions, 3 deletions
diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 2153ef48..a09f3eaf 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ToolConfirmationOutcome } from '../index.js'; import { logs } from '@opentelemetry/api-logs'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { Config } from '../config/config.js'; @@ -12,6 +13,8 @@ import { logApiResponse, logCliConfiguration, logUserPrompt, + logToolCall, + ToolCallDecision, } from './loggers.js'; import * as metrics from './metrics.js'; import * as sdk from './sdk.js'; @@ -236,4 +239,239 @@ describe('loggers', () => { }); }); }); + + describe('logToolCall', () => { + const mockConfig = { + getSessionId: () => 'test-session-id', + } as Config; + + const mockMetrics = { + recordToolCallMetrics: vi.fn(), + }; + + beforeEach(() => { + vi.spyOn(metrics, 'recordToolCallMetrics').mockImplementation( + mockMetrics.recordToolCallMetrics, + ); + mockLogger.emit.mockReset(); + }); + + it('should log a tool call with all fields', () => { + const event = { + function_name: 'test-function', + function_args: { + arg1: 'value1', + arg2: 2, + }, + duration_ms: 100, + success: true, + }; + + logToolCall(mockConfig, event, ToolConfirmationOutcome.ProceedOnce); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'Tool call: test-function. Decision: accept. Success: true. Duration: 100ms.', + attributes: { + 'session.id': 'test-session-id', + 'event.name': 'gemini_cli.tool_call', + 'event.timestamp': '2025-01-01T00:00:00.000Z', + function_name: 'test-function', + function_args: JSON.stringify( + { + arg1: 'value1', + arg2: 2, + }, + null, + 2, + ), + duration_ms: 100, + success: true, + decision: ToolCallDecision.ACCEPT, + }, + }); + + expect(mockMetrics.recordToolCallMetrics).toHaveBeenCalledWith( + mockConfig, + 'test-function', + 100, + true, + ToolCallDecision.ACCEPT, + ); + }); + it('should log a tool call with a reject decision', () => { + const event = { + function_name: 'test-function', + function_args: { + arg1: 'value1', + arg2: 2, + }, + duration_ms: 100, + success: false, + }; + + logToolCall(mockConfig, event, ToolConfirmationOutcome.Cancel); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'Tool call: test-function. Decision: reject. Success: false. Duration: 100ms.', + attributes: { + 'session.id': 'test-session-id', + 'event.name': 'gemini_cli.tool_call', + 'event.timestamp': '2025-01-01T00:00:00.000Z', + function_name: 'test-function', + function_args: JSON.stringify( + { + arg1: 'value1', + arg2: 2, + }, + null, + 2, + ), + duration_ms: 100, + success: false, + decision: ToolCallDecision.REJECT, + }, + }); + + expect(mockMetrics.recordToolCallMetrics).toHaveBeenCalledWith( + mockConfig, + 'test-function', + 100, + false, + ToolCallDecision.REJECT, + ); + }); + + it('should log a tool call with a modify decision', () => { + const event = { + function_name: 'test-function', + function_args: { + arg1: 'value1', + arg2: 2, + }, + duration_ms: 100, + success: true, + }; + + logToolCall(mockConfig, event, ToolConfirmationOutcome.ModifyWithEditor); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'Tool call: test-function. Decision: modify. Success: true. Duration: 100ms.', + attributes: { + 'session.id': 'test-session-id', + 'event.name': 'gemini_cli.tool_call', + 'event.timestamp': '2025-01-01T00:00:00.000Z', + function_name: 'test-function', + function_args: JSON.stringify( + { + arg1: 'value1', + arg2: 2, + }, + null, + 2, + ), + duration_ms: 100, + success: true, + decision: ToolCallDecision.MODIFY, + }, + }); + + expect(mockMetrics.recordToolCallMetrics).toHaveBeenCalledWith( + mockConfig, + 'test-function', + 100, + true, + ToolCallDecision.MODIFY, + ); + }); + + it('should log a tool call without a decision', () => { + const event = { + function_name: 'test-function', + function_args: { + arg1: 'value1', + arg2: 2, + }, + duration_ms: 100, + success: true, + }; + + logToolCall(mockConfig, event); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'Tool call: test-function. Success: true. Duration: 100ms.', + attributes: { + 'session.id': 'test-session-id', + 'event.name': 'gemini_cli.tool_call', + 'event.timestamp': '2025-01-01T00:00:00.000Z', + function_name: 'test-function', + function_args: JSON.stringify( + { + arg1: 'value1', + arg2: 2, + }, + null, + 2, + ), + duration_ms: 100, + success: true, + }, + }); + + expect(mockMetrics.recordToolCallMetrics).toHaveBeenCalledWith( + mockConfig, + 'test-function', + 100, + true, + undefined, + ); + }); + + it('should log a failed tool call with an error', () => { + const event = { + function_name: 'test-function', + function_args: { + arg1: 'value1', + arg2: 2, + }, + duration_ms: 100, + success: false, + error: 'test-error', + error_type: 'test-error-type', + }; + + logToolCall(mockConfig, event); + + expect(mockLogger.emit).toHaveBeenCalledWith({ + body: 'Tool call: test-function. Success: false. Duration: 100ms.', + attributes: { + 'session.id': 'test-session-id', + 'event.name': 'gemini_cli.tool_call', + 'event.timestamp': '2025-01-01T00:00:00.000Z', + function_name: 'test-function', + function_args: JSON.stringify( + { + arg1: 'value1', + arg2: 2, + }, + null, + 2, + ), + duration_ms: 100, + success: false, + error: 'test-error', + 'error.message': 'test-error', + error_type: 'test-error-type', + 'error.type': 'test-error-type', + }, + }); + + expect(mockMetrics.recordToolCallMetrics).toHaveBeenCalledWith( + mockConfig, + 'test-function', + 100, + false, + undefined, + ); + }); + }); }); diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 49a7019a..66f584e7 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -30,6 +30,7 @@ import { recordToolCallMetrics, } from './metrics.js'; import { isTelemetrySdkInitialized } from './sdk.js'; +import { ToolConfirmationOutcome } from '../index.js'; const shouldLogUserPrompts = (config: Config): boolean => config.getTelemetryLogUserPromptsEnabled() ?? false; @@ -40,6 +41,29 @@ function getCommonAttributes(config: Config): LogAttributes { }; } +export enum ToolCallDecision { + ACCEPT = 'accept', + REJECT = 'reject', + MODIFY = 'modify', +} + +export function getDecisionFromOutcome( + outcome: ToolConfirmationOutcome, +): ToolCallDecision { + switch (outcome) { + case ToolConfirmationOutcome.ProceedOnce: + case ToolConfirmationOutcome.ProceedAlways: + case ToolConfirmationOutcome.ProceedAlwaysServer: + case ToolConfirmationOutcome.ProceedAlwaysTool: + return ToolCallDecision.ACCEPT; + case ToolConfirmationOutcome.ModifyWithEditor: + return ToolCallDecision.MODIFY; + case ToolConfirmationOutcome.Cancel: + default: + return ToolCallDecision.REJECT; + } +} + export function logCliConfiguration(config: Config): void { if (!isTelemetrySdkInitialized()) return; @@ -103,15 +127,20 @@ export function logUserPrompt( export function logToolCall( config: Config, - event: Omit<ToolCallEvent, 'event.name' | 'event.timestamp'>, + event: Omit<ToolCallEvent, 'event.name' | 'event.timestamp' | 'decision'>, + outcome?: ToolConfirmationOutcome, ): void { if (!isTelemetrySdkInitialized()) return; + + const decision = outcome ? getDecisionFromOutcome(outcome) : undefined; + const attributes: LogAttributes = { ...getCommonAttributes(config), ...event, 'event.name': EVENT_TOOL_CALL, 'event.timestamp': new Date().toISOString(), - function_args: JSON.stringify(event.function_args), + function_args: JSON.stringify(event.function_args, null, 2), + decision, }; if (event.error) { attributes['error.message'] = event.error; @@ -121,7 +150,7 @@ export function logToolCall( } const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `Tool call: ${event.function_name}. Success: ${event.success}. Duration: ${event.duration_ms}ms.`, + body: `Tool call: ${event.function_name}${decision ? `. Decision: ${decision}` : ''}. Success: ${event.success}. Duration: ${event.duration_ms}ms.`, attributes, }; logger.emit(logRecord); @@ -130,6 +159,7 @@ export function logToolCall( event.function_name, event.duration_ms, event.success, + decision, ); } diff --git a/packages/core/src/telemetry/metrics.ts b/packages/core/src/telemetry/metrics.ts index 93aa2189..59979ef3 100644 --- a/packages/core/src/telemetry/metrics.ts +++ b/packages/core/src/telemetry/metrics.ts @@ -89,6 +89,7 @@ export function recordToolCallMetrics( functionName: string, durationMs: number, success: boolean, + decision?: 'accept' | 'reject' | 'modify', ): void { if (!toolCallCounter || !toolCallLatencyHistogram || !isMetricsInitialized) return; @@ -97,6 +98,7 @@ export function recordToolCallMetrics( ...getCommonAttributes(config), function_name: functionName, success, + decision, }; toolCallCounter.add(1, metricAttributes); toolCallLatencyHistogram.record(durationMs, { diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index da352862..926f1384 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ToolCallDecision } from './loggers.js'; + export interface UserPromptEvent { 'event.name': 'user_prompt'; 'event.timestamp': string; // ISO 8601 @@ -18,6 +20,7 @@ export interface ToolCallEvent { function_args: Record<string, unknown>; duration_ms: number; success: boolean; + decision?: ToolCallDecision; error?: string; error_type?: string; } |
