Adding TurnId to Tool call and API responses and error logs. (#3039)

Co-authored-by: Scott Densmore <scottdensmore@mac.com>
This commit is contained in:
uttamkanodia14 2025-07-10 00:19:30 +05:30 committed by GitHub
parent 6c12f9e0d9
commit 063481faa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 289 additions and 72 deletions

View File

@ -196,10 +196,12 @@ export async function main() {
process.exit(1); process.exit(1);
} }
const prompt_id = Math.random().toString(16).slice(2);
logUserPrompt(config, { logUserPrompt(config, {
'event.name': 'user_prompt', 'event.name': 'user_prompt',
'event.timestamp': new Date().toISOString(), 'event.timestamp': new Date().toISOString(),
prompt: input, prompt: input,
prompt_id,
prompt_length: input.length, prompt_length: input.length,
}); });
@ -210,7 +212,7 @@ export async function main() {
settings, settings,
); );
await runNonInteractive(nonInteractiveConfig, input); await runNonInteractive(nonInteractiveConfig, input, prompt_id);
process.exit(0); process.exit(0);
} }

View File

@ -81,15 +81,18 @@ describe('runNonInteractive', () => {
})(); })();
mockChat.sendMessageStream.mockResolvedValue(inputStream); mockChat.sendMessageStream.mockResolvedValue(inputStream);
await runNonInteractive(mockConfig, 'Test input'); await runNonInteractive(mockConfig, 'Test input', 'prompt-id-1');
expect(mockChat.sendMessageStream).toHaveBeenCalledWith({ expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
message: [{ text: 'Test input' }], {
config: { message: [{ text: 'Test input' }],
abortSignal: expect.any(AbortSignal), config: {
tools: [{ functionDeclarations: [] }], abortSignal: expect.any(AbortSignal),
tools: [{ functionDeclarations: [] }],
},
}, },
}); expect.any(String),
);
expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Hello'); expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Hello');
expect(mockProcessStdoutWrite).toHaveBeenCalledWith(' World'); expect(mockProcessStdoutWrite).toHaveBeenCalledWith(' World');
expect(mockProcessStdoutWrite).toHaveBeenCalledWith('\n'); expect(mockProcessStdoutWrite).toHaveBeenCalledWith('\n');
@ -131,7 +134,7 @@ describe('runNonInteractive', () => {
.mockResolvedValueOnce(stream1) .mockResolvedValueOnce(stream1)
.mockResolvedValueOnce(stream2); .mockResolvedValueOnce(stream2);
await runNonInteractive(mockConfig, 'Use a tool'); await runNonInteractive(mockConfig, 'Use a tool', 'prompt-id-2');
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2); expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith( expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
@ -144,6 +147,7 @@ describe('runNonInteractive', () => {
expect.objectContaining({ expect.objectContaining({
message: [toolResponsePart], message: [toolResponsePart],
}), }),
expect.any(String),
); );
expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Final answer'); expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Final answer');
}); });
@ -190,7 +194,7 @@ describe('runNonInteractive', () => {
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
await runNonInteractive(mockConfig, 'Trigger tool error'); await runNonInteractive(mockConfig, 'Trigger tool error', 'prompt-id-3');
expect(mockCoreExecuteToolCall).toHaveBeenCalled(); expect(mockCoreExecuteToolCall).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
@ -200,6 +204,7 @@ describe('runNonInteractive', () => {
expect.objectContaining({ expect.objectContaining({
message: [errorResponsePart], message: [errorResponsePart],
}), }),
expect.any(String),
); );
expect(mockProcessStdoutWrite).toHaveBeenCalledWith( expect(mockProcessStdoutWrite).toHaveBeenCalledWith(
'Could not complete request.', 'Could not complete request.',
@ -213,7 +218,7 @@ describe('runNonInteractive', () => {
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
await runNonInteractive(mockConfig, 'Initial fail'); await runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'[API Error: API connection failed]', '[API Error: API connection failed]',
@ -265,7 +270,11 @@ describe('runNonInteractive', () => {
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
await runNonInteractive(mockConfig, 'Trigger tool not found'); await runNonInteractive(
mockConfig,
'Trigger tool not found',
'prompt-id-5',
);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool nonExistentTool: Tool "nonExistentTool" not found in registry.', 'Error executing tool nonExistentTool: Tool "nonExistentTool" not found in registry.',
@ -278,6 +287,7 @@ describe('runNonInteractive', () => {
expect.objectContaining({ expect.objectContaining({
message: [errorResponsePart], message: [errorResponsePart],
}), }),
expect.any(String),
); );
expect(mockProcessStdoutWrite).toHaveBeenCalledWith( expect(mockProcessStdoutWrite).toHaveBeenCalledWith(

View File

@ -46,6 +46,7 @@ function getResponseText(response: GenerateContentResponse): string | null {
export async function runNonInteractive( export async function runNonInteractive(
config: Config, config: Config,
input: string, input: string,
prompt_id: string,
): Promise<void> { ): Promise<void> {
await config.initialize(); await config.initialize();
// Handle EPIPE errors when the output is piped to a command that closes early. // Handle EPIPE errors when the output is piped to a command that closes early.
@ -67,15 +68,18 @@ export async function runNonInteractive(
while (true) { while (true) {
const functionCalls: FunctionCall[] = []; const functionCalls: FunctionCall[] = [];
const responseStream = await chat.sendMessageStream({ const responseStream = await chat.sendMessageStream(
message: currentMessages[0]?.parts || [], // Ensure parts are always provided {
config: { message: currentMessages[0]?.parts || [], // Ensure parts are always provided
abortSignal: abortController.signal, config: {
tools: [ abortSignal: abortController.signal,
{ functionDeclarations: toolRegistry.getFunctionDeclarations() }, tools: [
], { functionDeclarations: toolRegistry.getFunctionDeclarations() },
],
},
}, },
}); prompt_id,
);
for await (const resp of responseStream) { for await (const resp of responseStream) {
if (abortController.signal.aborted) { if (abortController.signal.aborted) {
@ -101,6 +105,7 @@ export async function runNonInteractive(
name: fc.name as string, name: fc.name as string,
args: (fc.args ?? {}) as Record<string, unknown>, args: (fc.args ?? {}) as Record<string, unknown>,
isClientInitiated: false, isClientInitiated: false,
prompt_id,
}; };
const toolResponse = await executeToolCall( const toolResponse = await executeToolCall(

View File

@ -27,7 +27,11 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics, metrics,
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 5,
}, },
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
}); });
return render(<ModelStatsDisplay />); return render(<ModelStatsDisplay />);

View File

@ -26,7 +26,11 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics, metrics,
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 5,
}, },
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
}); });
return render(<SessionSummaryDisplay duration="1h 23m 45s" />); return render(<SessionSummaryDisplay duration="1h 23m 45s" />);

View File

@ -27,7 +27,11 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics, metrics,
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 5,
}, },
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
}); });
return render(<StatsDisplay duration="1s" />); return render(<StatsDisplay duration="1s" />);
@ -288,7 +292,11 @@ describe('<StatsDisplay />', () => {
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics: zeroMetrics, metrics: zeroMetrics,
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 5,
}, },
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
}); });
const { lastFrame } = render( const { lastFrame } = render(

View File

@ -27,7 +27,11 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics, metrics,
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 5,
}, },
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
}); });
return render(<ToolStatsDisplay />); return render(<ToolStatsDisplay />);

View File

@ -6,6 +6,7 @@
import React, { import React, {
createContext, createContext,
useCallback,
useContext, useContext,
useState, useState,
useMemo, useMemo,
@ -26,6 +27,7 @@ export interface SessionStatsState {
sessionStartTime: Date; sessionStartTime: Date;
metrics: SessionMetrics; metrics: SessionMetrics;
lastPromptTokenCount: number; lastPromptTokenCount: number;
promptCount: number;
} }
export interface ComputedSessionStats { export interface ComputedSessionStats {
@ -46,6 +48,8 @@ export interface ComputedSessionStats {
// and the functions to update it. // and the functions to update it.
interface SessionStatsContextValue { interface SessionStatsContextValue {
stats: SessionStatsState; stats: SessionStatsState;
startNewPrompt: () => void;
getPromptCount: () => number;
} }
// --- Context Definition --- // --- Context Definition ---
@ -63,6 +67,7 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics: uiTelemetryService.getMetrics(), metrics: uiTelemetryService.getMetrics(),
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 0,
}); });
useEffect(() => { useEffect(() => {
@ -92,11 +97,25 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
}; };
}, []); }, []);
const startNewPrompt = useCallback(() => {
setStats((prevState) => ({
...prevState,
promptCount: prevState.promptCount + 1,
}));
}, []);
const getPromptCount = useCallback(
() => stats.promptCount,
[stats.promptCount],
);
const value = useMemo( const value = useMemo(
() => ({ () => ({
stats, stats,
startNewPrompt,
getPromptCount,
}), }),
[stats], [stats, startNewPrompt, getPromptCount],
); );
return ( return (

View File

@ -159,7 +159,7 @@ describe('useSlashCommandProcessor', () => {
stats: { stats: {
sessionStartTime: new Date('2025-01-01T00:00:00.000Z'), sessionStartTime: new Date('2025-01-01T00:00:00.000Z'),
cumulative: { cumulative: {
turnCount: 0, promptCount: 0,
promptTokenCount: 0, promptTokenCount: 0,
candidatesTokenCount: 0, candidatesTokenCount: 0,
totalTokenCount: 0, totalTokenCount: 0,
@ -1311,7 +1311,10 @@ describe('useSlashCommandProcessor', () => {
hook.rerender(); hook.rerender();
}); });
expect(hook.result.current.pendingHistoryItems).toEqual([]); expect(hook.result.current.pendingHistoryItems).toEqual([]);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledWith(true); expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledWith(
'Prompt Id not set',
true,
);
expect(mockAddItem).toHaveBeenNthCalledWith( expect(mockAddItem).toHaveBeenNthCalledWith(
2, 2,
expect.objectContaining({ expect.objectContaining({

View File

@ -880,7 +880,8 @@ export const useSlashCommandProcessor = (
try { try {
const compressed = await config! const compressed = await config!
.getGeminiClient()! .getGeminiClient()!
.tryCompressChat(true); // TODO: Set Prompt id for CompressChat from SlashCommandProcessor.
.tryCompressChat('Prompt Id not set', true);
if (compressed) { if (compressed) {
addMessage({ addMessage({
type: MessageType.COMPRESSION, type: MessageType.COMPRESSION,

View File

@ -109,12 +109,13 @@ vi.mock('./useLogger.js', () => ({
}), }),
})); }));
const mockStartNewTurn = vi.fn(); const mockStartNewPrompt = vi.fn();
const mockAddUsage = vi.fn(); const mockAddUsage = vi.fn();
vi.mock('../contexts/SessionContext.js', () => ({ vi.mock('../contexts/SessionContext.js', () => ({
useSessionStats: vi.fn(() => ({ useSessionStats: vi.fn(() => ({
startNewTurn: mockStartNewTurn, startNewPrompt: mockStartNewPrompt,
addUsage: mockAddUsage, addUsage: mockAddUsage,
getPromptCount: vi.fn(() => 5),
})), })),
})); }));
@ -301,6 +302,9 @@ describe('useGeminiStream', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
addHistory: vi.fn(), addHistory: vi.fn(),
getSessionId() {
return 'test-session-id';
},
setQuotaErrorOccurred: vi.fn(), setQuotaErrorOccurred: vi.fn(),
getQuotaErrorOccurred: vi.fn(() => false), getQuotaErrorOccurred: vi.fn(() => false),
} as unknown as Config; } as unknown as Config;
@ -426,6 +430,7 @@ describe('useGeminiStream', () => {
name: 'tool1', name: 'tool1',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-1',
}, },
status: 'success', status: 'success',
responseSubmittedToGemini: false, responseSubmittedToGemini: false,
@ -444,7 +449,12 @@ describe('useGeminiStream', () => {
endTime: Date.now(), endTime: Date.now(),
} as TrackedCompletedToolCall, } as TrackedCompletedToolCall,
{ {
request: { callId: 'call2', name: 'tool2', args: {} }, request: {
callId: 'call2',
name: 'tool2',
args: {},
prompt_id: 'prompt-id-1',
},
status: 'executing', status: 'executing',
responseSubmittedToGemini: false, responseSubmittedToGemini: false,
tool: { tool: {
@ -481,6 +491,7 @@ describe('useGeminiStream', () => {
name: 'tool1', name: 'tool1',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-2',
}, },
status: 'success', status: 'success',
responseSubmittedToGemini: false, responseSubmittedToGemini: false,
@ -492,6 +503,7 @@ describe('useGeminiStream', () => {
name: 'tool2', name: 'tool2',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-2',
}, },
status: 'error', status: 'error',
responseSubmittedToGemini: false, responseSubmittedToGemini: false,
@ -546,6 +558,7 @@ describe('useGeminiStream', () => {
expect(mockSendMessageStream).toHaveBeenCalledWith( expect(mockSendMessageStream).toHaveBeenCalledWith(
expectedMergedResponse, expectedMergedResponse,
expect.any(AbortSignal), expect.any(AbortSignal),
'prompt-id-2',
); );
}); });
@ -557,6 +570,7 @@ describe('useGeminiStream', () => {
name: 'testTool', name: 'testTool',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-3',
}, },
status: 'cancelled', status: 'cancelled',
response: { callId: '1', responseParts: [{ text: 'cancelled' }] }, response: { callId: '1', responseParts: [{ text: 'cancelled' }] },
@ -618,6 +632,7 @@ describe('useGeminiStream', () => {
name: 'toolA', name: 'toolA',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-7',
}, },
tool: { tool: {
name: 'toolA', name: 'toolA',
@ -641,6 +656,7 @@ describe('useGeminiStream', () => {
name: 'toolB', name: 'toolB',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-8',
}, },
tool: { tool: {
name: 'toolB', name: 'toolB',
@ -731,6 +747,7 @@ describe('useGeminiStream', () => {
name: 'tool1', name: 'tool1',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-4',
}, },
status: 'executing', status: 'executing',
responseSubmittedToGemini: false, responseSubmittedToGemini: false,
@ -824,6 +841,7 @@ describe('useGeminiStream', () => {
expect(mockSendMessageStream).toHaveBeenCalledWith( expect(mockSendMessageStream).toHaveBeenCalledWith(
toolCallResponseParts, toolCallResponseParts,
expect.any(AbortSignal), expect.any(AbortSignal),
'prompt-id-4',
); );
}); });
@ -1036,6 +1054,7 @@ describe('useGeminiStream', () => {
name: 'save_memory', name: 'save_memory',
args: { fact: 'test' }, args: { fact: 'test' },
isClientInitiated: true, isClientInitiated: true,
prompt_id: 'prompt-id-6',
}, },
status: 'success', status: 'success',
responseSubmittedToGemini: false, responseSubmittedToGemini: false,

View File

@ -53,6 +53,7 @@ import {
TrackedCompletedToolCall, TrackedCompletedToolCall,
TrackedCancelledToolCall, TrackedCancelledToolCall,
} from './useReactToolScheduler.js'; } from './useReactToolScheduler.js';
import { useSessionStats } from '../contexts/SessionContext.js';
export function mergePartListUnions(list: PartListUnion[]): PartListUnion { export function mergePartListUnions(list: PartListUnion[]): PartListUnion {
const resultParts: PartListUnion = []; const resultParts: PartListUnion = [];
@ -101,6 +102,7 @@ export const useGeminiStream = (
const [pendingHistoryItemRef, setPendingHistoryItem] = const [pendingHistoryItemRef, setPendingHistoryItem] =
useStateAndRef<HistoryItemWithoutId | null>(null); useStateAndRef<HistoryItemWithoutId | null>(null);
const processedMemoryToolsRef = useRef<Set<string>>(new Set()); const processedMemoryToolsRef = useRef<Set<string>>(new Set());
const { startNewPrompt, getPromptCount } = useSessionStats();
const logger = useLogger(); const logger = useLogger();
const gitService = useMemo(() => { const gitService = useMemo(() => {
if (!config.getProjectRoot()) { if (!config.getProjectRoot()) {
@ -203,6 +205,7 @@ export const useGeminiStream = (
query: PartListUnion, query: PartListUnion,
userMessageTimestamp: number, userMessageTimestamp: number,
abortSignal: AbortSignal, abortSignal: AbortSignal,
prompt_id: string,
): Promise<{ ): Promise<{
queryToSend: PartListUnion | null; queryToSend: PartListUnion | null;
shouldProceed: boolean; shouldProceed: boolean;
@ -220,7 +223,7 @@ export const useGeminiStream = (
const trimmedQuery = query.trim(); const trimmedQuery = query.trim();
logUserPrompt( logUserPrompt(
config, config,
new UserPromptEvent(trimmedQuery.length, trimmedQuery), new UserPromptEvent(trimmedQuery.length, prompt_id, trimmedQuery),
); );
onDebugMessage(`User query: '${trimmedQuery}'`); onDebugMessage(`User query: '${trimmedQuery}'`);
await logger?.logMessage(MessageSenderType.USER, trimmedQuery); await logger?.logMessage(MessageSenderType.USER, trimmedQuery);
@ -236,6 +239,7 @@ export const useGeminiStream = (
name: toolName, name: toolName,
args: toolArgs, args: toolArgs,
isClientInitiated: true, isClientInitiated: true,
prompt_id,
}; };
scheduleToolCalls([toolCallRequest], abortSignal); scheduleToolCalls([toolCallRequest], abortSignal);
} }
@ -485,7 +489,11 @@ export const useGeminiStream = (
); );
const submitQuery = useCallback( const submitQuery = useCallback(
async (query: PartListUnion, options?: { isContinuation: boolean }) => { async (
query: PartListUnion,
options?: { isContinuation: boolean },
prompt_id?: string,
) => {
if ( if (
(streamingState === StreamingState.Responding || (streamingState === StreamingState.Responding ||
streamingState === StreamingState.WaitingForConfirmation) && streamingState === StreamingState.WaitingForConfirmation) &&
@ -506,21 +514,34 @@ export const useGeminiStream = (
const abortSignal = abortControllerRef.current.signal; const abortSignal = abortControllerRef.current.signal;
turnCancelledRef.current = false; turnCancelledRef.current = false;
if (!prompt_id) {
prompt_id = config.getSessionId() + '########' + getPromptCount();
}
const { queryToSend, shouldProceed } = await prepareQueryForGemini( const { queryToSend, shouldProceed } = await prepareQueryForGemini(
query, query,
userMessageTimestamp, userMessageTimestamp,
abortSignal, abortSignal,
prompt_id!,
); );
if (!shouldProceed || queryToSend === null) { if (!shouldProceed || queryToSend === null) {
return; return;
} }
if (!options?.isContinuation) {
startNewPrompt();
}
setIsResponding(true); setIsResponding(true);
setInitError(null); setInitError(null);
try { try {
const stream = geminiClient.sendMessageStream(queryToSend, abortSignal); const stream = geminiClient.sendMessageStream(
queryToSend,
abortSignal,
prompt_id!,
);
const processingStatus = await processGeminiStreamEvents( const processingStatus = await processGeminiStreamEvents(
stream, stream,
userMessageTimestamp, userMessageTimestamp,
@ -570,6 +591,8 @@ export const useGeminiStream = (
geminiClient, geminiClient,
onAuthError, onAuthError,
config, config,
startNewPrompt,
getPromptCount,
], ],
); );
@ -676,6 +699,10 @@ export const useGeminiStream = (
(toolCall) => toolCall.request.callId, (toolCall) => toolCall.request.callId,
); );
const prompt_ids = geminiTools.map(
(toolCall) => toolCall.request.prompt_id,
);
markToolsAsSubmitted(callIdsToMarkAsSubmitted); markToolsAsSubmitted(callIdsToMarkAsSubmitted);
// Don't continue if model was switched due to quota error // Don't continue if model was switched due to quota error
@ -683,9 +710,13 @@ export const useGeminiStream = (
return; return;
} }
submitQuery(mergePartListUnions(responsesToSend), { submitQuery(
isContinuation: true, mergePartListUnions(responsesToSend),
}); {
isContinuation: true,
},
prompt_ids[0],
);
}, },
[ [
isResponding, isResponding,

View File

@ -450,7 +450,7 @@ describe('Gemini Client (client.ts)', () => {
}); });
const initialChat = client.getChat(); const initialChat = client.getChat();
const result = await client.tryCompressChat(); const result = await client.tryCompressChat('prompt-id-2');
const newChat = client.getChat(); const newChat = client.getChat();
expect(tokenLimit).toHaveBeenCalled(); expect(tokenLimit).toHaveBeenCalled();
@ -476,7 +476,7 @@ describe('Gemini Client (client.ts)', () => {
}); });
const initialChat = client.getChat(); const initialChat = client.getChat();
const result = await client.tryCompressChat(); const result = await client.tryCompressChat('prompt-id-3');
const newChat = client.getChat(); const newChat = client.getChat();
expect(tokenLimit).toHaveBeenCalled(); expect(tokenLimit).toHaveBeenCalled();
@ -507,7 +507,7 @@ describe('Gemini Client (client.ts)', () => {
}); });
const initialChat = client.getChat(); const initialChat = client.getChat();
const result = await client.tryCompressChat(true); // force = true const result = await client.tryCompressChat('prompt-id-1', true); // force = true
const newChat = client.getChat(); const newChat = client.getChat();
expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalled();
@ -545,6 +545,7 @@ describe('Gemini Client (client.ts)', () => {
const stream = client.sendMessageStream( const stream = client.sendMessageStream(
[{ text: 'Hi' }], [{ text: 'Hi' }],
new AbortController().signal, new AbortController().signal,
'prompt-id-1',
); );
// Consume the stream manually to get the final return value. // Consume the stream manually to get the final return value.
@ -597,6 +598,7 @@ describe('Gemini Client (client.ts)', () => {
const stream = client.sendMessageStream( const stream = client.sendMessageStream(
[{ text: 'Start conversation' }], [{ text: 'Start conversation' }],
signal, signal,
'prompt-id-2',
); );
// Count how many stream events we get // Count how many stream events we get
@ -697,6 +699,7 @@ describe('Gemini Client (client.ts)', () => {
const stream = client.sendMessageStream( const stream = client.sendMessageStream(
[{ text: 'Start conversation' }], [{ text: 'Start conversation' }],
signal, signal,
'prompt-id-3',
Number.MAX_SAFE_INTEGER, // Bypass the MAX_TURNS protection Number.MAX_SAFE_INTEGER, // Bypass the MAX_TURNS protection
); );
@ -806,7 +809,7 @@ describe('Gemini Client (client.ts)', () => {
client['contentGenerator'] = mockGenerator as ContentGenerator; client['contentGenerator'] = mockGenerator as ContentGenerator;
client['startChat'] = vi.fn().mockResolvedValue(mockChat); client['startChat'] = vi.fn().mockResolvedValue(mockChat);
const result = await client.tryCompressChat(true); const result = await client.tryCompressChat('prompt-id-4', true);
expect(mockCountTokens).toHaveBeenCalledTimes(2); expect(mockCountTokens).toHaveBeenCalledTimes(2);
expect(mockCountTokens).toHaveBeenNthCalledWith(1, { expect(mockCountTokens).toHaveBeenNthCalledWith(1, {

View File

@ -261,23 +261,25 @@ export class GeminiClient {
async *sendMessageStream( async *sendMessageStream(
request: PartListUnion, request: PartListUnion,
signal: AbortSignal, signal: AbortSignal,
prompt_id: string,
turns: number = this.MAX_TURNS, turns: number = this.MAX_TURNS,
originalModel?: string, originalModel?: string,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> { ): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
// Ensure turns never exceeds MAX_TURNS to prevent infinite loops // Ensure turns never exceeds MAX_TURNS to prevent infinite loops
const boundedTurns = Math.min(turns, this.MAX_TURNS); const boundedTurns = Math.min(turns, this.MAX_TURNS);
if (!boundedTurns) { if (!boundedTurns) {
return new Turn(this.getChat()); return new Turn(this.getChat(), prompt_id);
} }
// Track the original model from the first call to detect model switching // Track the original model from the first call to detect model switching
const initialModel = originalModel || this.config.getModel(); const initialModel = originalModel || this.config.getModel();
const compressed = await this.tryCompressChat(); const compressed = await this.tryCompressChat(prompt_id);
if (compressed) { if (compressed) {
yield { type: GeminiEventType.ChatCompressed, value: compressed }; yield { type: GeminiEventType.ChatCompressed, value: compressed };
} }
const turn = new Turn(this.getChat()); const turn = new Turn(this.getChat(), prompt_id);
const resultStream = turn.run(request, signal); const resultStream = turn.run(request, signal);
for await (const event of resultStream) { for await (const event of resultStream) {
yield event; yield event;
@ -303,6 +305,7 @@ export class GeminiClient {
yield* this.sendMessageStream( yield* this.sendMessageStream(
nextRequest, nextRequest,
signal, signal,
prompt_id,
boundedTurns - 1, boundedTurns - 1,
initialModel, initialModel,
); );
@ -492,6 +495,7 @@ export class GeminiClient {
} }
async tryCompressChat( async tryCompressChat(
prompt_id: string,
force: boolean = false, force: boolean = false,
): Promise<ChatCompressionInfo | null> { ): Promise<ChatCompressionInfo | null> {
const curatedHistory = this.getChat().getHistory(true); const curatedHistory = this.getChat().getHistory(true);
@ -538,14 +542,17 @@ export class GeminiClient {
this.getChat().setHistory(historyToCompress); this.getChat().setHistory(historyToCompress);
const { text: summary } = await this.getChat().sendMessage({ const { text: summary } = await this.getChat().sendMessage(
message: { {
text: 'First, reason in your scratchpad. Then, generate the <state_snapshot>.', message: {
text: 'First, reason in your scratchpad. Then, generate the <state_snapshot>.',
},
config: {
systemInstruction: { text: getCompressionPrompt() },
},
}, },
config: { prompt_id,
systemInstruction: { text: getCompressionPrompt() }, );
},
});
this.chat = await this.startChat([ this.chat = await this.startChat([
{ {
role: 'user', role: 'user',

View File

@ -139,6 +139,7 @@ describe('CoreToolScheduler', () => {
name: 'mockTool', name: 'mockTool',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-1',
}; };
abortController.abort(); abortController.abort();
@ -206,6 +207,7 @@ describe('CoreToolScheduler with payload', () => {
name: 'mockModifiableTool', name: 'mockModifiableTool',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-2',
}; };
await scheduler.schedule([request], abortController.signal); await scheduler.schedule([request], abortController.signal);

View File

@ -77,7 +77,7 @@ describe('GeminiChat', () => {
} as unknown as GenerateContentResponse; } as unknown as GenerateContentResponse;
vi.mocked(mockModelsModule.generateContent).mockResolvedValue(response); vi.mocked(mockModelsModule.generateContent).mockResolvedValue(response);
await chat.sendMessage({ message: 'hello' }); await chat.sendMessage({ message: 'hello' }, 'prompt-id-1');
expect(mockModelsModule.generateContent).toHaveBeenCalledWith({ expect(mockModelsModule.generateContent).toHaveBeenCalledWith({
model: 'gemini-pro', model: 'gemini-pro',
@ -109,7 +109,7 @@ describe('GeminiChat', () => {
response, response,
); );
await chat.sendMessageStream({ message: 'hello' }); await chat.sendMessageStream({ message: 'hello' }, 'prompt-id-1');
expect(mockModelsModule.generateContentStream).toHaveBeenCalledWith({ expect(mockModelsModule.generateContentStream).toHaveBeenCalledWith({
model: 'gemini-pro', model: 'gemini-pro',

View File

@ -151,13 +151,18 @@ export class GeminiChat {
private async _logApiRequest( private async _logApiRequest(
contents: Content[], contents: Content[],
model: string, model: string,
prompt_id: string,
): Promise<void> { ): Promise<void> {
const requestText = this._getRequestTextFromContents(contents); const requestText = this._getRequestTextFromContents(contents);
logApiRequest(this.config, new ApiRequestEvent(model, requestText)); logApiRequest(
this.config,
new ApiRequestEvent(model, prompt_id, requestText),
);
} }
private async _logApiResponse( private async _logApiResponse(
durationMs: number, durationMs: number,
prompt_id: string,
usageMetadata?: GenerateContentResponseUsageMetadata, usageMetadata?: GenerateContentResponseUsageMetadata,
responseText?: string, responseText?: string,
): Promise<void> { ): Promise<void> {
@ -166,13 +171,18 @@ export class GeminiChat {
new ApiResponseEvent( new ApiResponseEvent(
this.config.getModel(), this.config.getModel(),
durationMs, durationMs,
prompt_id,
usageMetadata, usageMetadata,
responseText, responseText,
), ),
); );
} }
private _logApiError(durationMs: number, error: unknown): void { private _logApiError(
durationMs: number,
error: unknown,
prompt_id: string,
): void {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
const errorType = error instanceof Error ? error.name : 'unknown'; const errorType = error instanceof Error ? error.name : 'unknown';
@ -182,6 +192,7 @@ export class GeminiChat {
this.config.getModel(), this.config.getModel(),
errorMessage, errorMessage,
durationMs, durationMs,
prompt_id,
errorType, errorType,
), ),
); );
@ -255,12 +266,13 @@ export class GeminiChat {
*/ */
async sendMessage( async sendMessage(
params: SendMessageParameters, params: SendMessageParameters,
prompt_id: string,
): Promise<GenerateContentResponse> { ): Promise<GenerateContentResponse> {
await this.sendPromise; await this.sendPromise;
const userContent = createUserContent(params.message); const userContent = createUserContent(params.message);
const requestContents = this.getHistory(true).concat(userContent); const requestContents = this.getHistory(true).concat(userContent);
this._logApiRequest(requestContents, this.config.getModel()); this._logApiRequest(requestContents, this.config.getModel(), prompt_id);
const startTime = Date.now(); const startTime = Date.now();
let response: GenerateContentResponse; let response: GenerateContentResponse;
@ -301,6 +313,7 @@ export class GeminiChat {
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
await this._logApiResponse( await this._logApiResponse(
durationMs, durationMs,
prompt_id,
response.usageMetadata, response.usageMetadata,
getStructuredResponse(response), getStructuredResponse(response),
); );
@ -332,7 +345,7 @@ export class GeminiChat {
return response; return response;
} catch (error) { } catch (error) {
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
this._logApiError(durationMs, error); this._logApiError(durationMs, error, prompt_id);
this.sendPromise = Promise.resolve(); this.sendPromise = Promise.resolve();
throw error; throw error;
} }
@ -362,11 +375,12 @@ export class GeminiChat {
*/ */
async sendMessageStream( async sendMessageStream(
params: SendMessageParameters, params: SendMessageParameters,
prompt_id: string,
): Promise<AsyncGenerator<GenerateContentResponse>> { ): Promise<AsyncGenerator<GenerateContentResponse>> {
await this.sendPromise; await this.sendPromise;
const userContent = createUserContent(params.message); const userContent = createUserContent(params.message);
const requestContents = this.getHistory(true).concat(userContent); const requestContents = this.getHistory(true).concat(userContent);
this._logApiRequest(requestContents, this.config.getModel()); this._logApiRequest(requestContents, this.config.getModel(), prompt_id);
const startTime = Date.now(); const startTime = Date.now();
@ -420,11 +434,12 @@ export class GeminiChat {
streamResponse, streamResponse,
userContent, userContent,
startTime, startTime,
prompt_id,
); );
return result; return result;
} catch (error) { } catch (error) {
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
this._logApiError(durationMs, error); this._logApiError(durationMs, error, prompt_id);
this.sendPromise = Promise.resolve(); this.sendPromise = Promise.resolve();
throw error; throw error;
} }
@ -496,6 +511,7 @@ export class GeminiChat {
streamResponse: AsyncGenerator<GenerateContentResponse>, streamResponse: AsyncGenerator<GenerateContentResponse>,
inputContent: Content, inputContent: Content,
startTime: number, startTime: number,
prompt_id: string,
) { ) {
const outputContent: Content[] = []; const outputContent: Content[] = [];
const chunks: GenerateContentResponse[] = []; const chunks: GenerateContentResponse[] = [];
@ -519,7 +535,7 @@ export class GeminiChat {
} catch (error) { } catch (error) {
errorOccurred = true; errorOccurred = true;
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
this._logApiError(durationMs, error); this._logApiError(durationMs, error, prompt_id);
throw error; throw error;
} }
@ -534,6 +550,7 @@ export class GeminiChat {
const fullText = getStructuredResponseFromParts(allParts); const fullText = getStructuredResponseFromParts(allParts);
await this._logApiResponse( await this._logApiResponse(
durationMs, durationMs,
prompt_id,
this.getFinalUsageMetadata(chunks), this.getFinalUsageMetadata(chunks),
fullText, fullText,
); );

View File

@ -67,6 +67,7 @@ describe('executeToolCall', () => {
name: 'testTool', name: 'testTool',
args: { param1: 'value1' }, args: { param1: 'value1' },
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-1',
}; };
const toolResult: ToolResult = { const toolResult: ToolResult = {
llmContent: 'Tool executed successfully', llmContent: 'Tool executed successfully',
@ -105,6 +106,7 @@ describe('executeToolCall', () => {
name: 'nonExistentTool', name: 'nonExistentTool',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-2',
}; };
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined); vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
@ -140,6 +142,7 @@ describe('executeToolCall', () => {
name: 'testTool', name: 'testTool',
args: { param1: 'value1' }, args: { param1: 'value1' },
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-3',
}; };
const executionError = new Error('Tool execution failed'); const executionError = new Error('Tool execution failed');
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
@ -172,6 +175,7 @@ describe('executeToolCall', () => {
name: 'testTool', name: 'testTool',
args: { param1: 'value1' }, args: { param1: 'value1' },
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-4',
}; };
const cancellationError = new Error('Operation cancelled'); const cancellationError = new Error('Operation cancelled');
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
@ -215,6 +219,7 @@ describe('executeToolCall', () => {
name: 'testTool', name: 'testTool',
args: {}, args: {},
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-5',
}; };
const imageDataPart: Part = { const imageDataPart: Part = {
inlineData: { mimeType: 'image/png', data: 'base64data' }, inlineData: { mimeType: 'image/png', data: 'base64data' },

View File

@ -40,6 +40,7 @@ export async function executeToolCall(
duration_ms: durationMs, duration_ms: durationMs,
success: false, success: false,
error: error.message, error: error.message,
prompt_id: toolCallRequest.prompt_id,
}); });
// Ensure the response structure matches what the API expects for an error // Ensure the response structure matches what the API expects for an error
return { return {
@ -75,6 +76,7 @@ export async function executeToolCall(
function_args: toolCallRequest.args, function_args: toolCallRequest.args,
duration_ms: durationMs, duration_ms: durationMs,
success: true, success: true,
prompt_id: toolCallRequest.prompt_id,
}); });
const response = convertToFunctionResponse( const response = convertToFunctionResponse(
@ -100,6 +102,7 @@ export async function executeToolCall(
duration_ms: durationMs, duration_ms: durationMs,
success: false, success: false,
error: error.message, error: error.message,
prompt_id: toolCallRequest.prompt_id,
}); });
return { return {
callId: toolCallRequest.callId, callId: toolCallRequest.callId,

View File

@ -55,7 +55,7 @@ describe('Turn', () => {
sendMessageStream: mockSendMessageStream, sendMessageStream: mockSendMessageStream,
getHistory: mockGetHistory, getHistory: mockGetHistory,
}; };
turn = new Turn(mockChatInstance as unknown as GeminiChat); turn = new Turn(mockChatInstance as unknown as GeminiChat, 'prompt-id-1');
mockGetHistory.mockReturnValue([]); mockGetHistory.mockReturnValue([]);
mockSendMessageStream.mockResolvedValue((async function* () {})()); mockSendMessageStream.mockResolvedValue((async function* () {})());
}); });
@ -92,10 +92,13 @@ describe('Turn', () => {
events.push(event); events.push(event);
} }
expect(mockSendMessageStream).toHaveBeenCalledWith({ expect(mockSendMessageStream).toHaveBeenCalledWith(
message: reqParts, {
config: { abortSignal: expect.any(AbortSignal) }, message: reqParts,
}); config: { abortSignal: expect.any(AbortSignal) },
},
'prompt-id-1',
);
expect(events).toEqual([ expect(events).toEqual([
{ type: GeminiEventType.Content, value: 'Hello' }, { type: GeminiEventType.Content, value: 'Hello' },

View File

@ -64,6 +64,7 @@ export interface ToolCallRequestInfo {
name: string; name: string;
args: Record<string, unknown>; args: Record<string, unknown>;
isClientInitiated: boolean; isClientInitiated: boolean;
prompt_id: string;
} }
export interface ToolCallResponseInfo { export interface ToolCallResponseInfo {
@ -143,7 +144,10 @@ export class Turn {
readonly pendingToolCalls: ToolCallRequestInfo[]; readonly pendingToolCalls: ToolCallRequestInfo[];
private debugResponses: GenerateContentResponse[]; private debugResponses: GenerateContentResponse[];
constructor(private readonly chat: GeminiChat) { constructor(
private readonly chat: GeminiChat,
private readonly prompt_id: string,
) {
this.pendingToolCalls = []; this.pendingToolCalls = [];
this.debugResponses = []; this.debugResponses = [];
} }
@ -153,12 +157,15 @@ export class Turn {
signal: AbortSignal, signal: AbortSignal,
): AsyncGenerator<ServerGeminiStreamEvent> { ): AsyncGenerator<ServerGeminiStreamEvent> {
try { try {
const responseStream = await this.chat.sendMessageStream({ const responseStream = await this.chat.sendMessageStream(
message: req, {
config: { message: req,
abortSignal: signal, config: {
abortSignal: signal,
},
}, },
}); this.prompt_id,
);
for await (const resp of responseStream) { for await (const resp of responseStream) {
if (signal?.aborted) { if (signal?.aborted) {
@ -252,6 +259,7 @@ export class Turn {
name, name,
args, args,
isClientInitiated: false, isClientInitiated: false,
prompt_id: this.prompt_id,
}; };
this.pendingToolCalls.push(toolCallRequest); this.pendingToolCalls.push(toolCallRequest);

View File

@ -265,6 +265,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_PROMPT_LENGTH, gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_PROMPT_LENGTH,
value: JSON.stringify(event.prompt_length), value: JSON.stringify(event.prompt_length),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
]; ];
this.enqueueLogEvent(this.createLogEvent(new_prompt_event_name, data)); this.enqueueLogEvent(this.createLogEvent(new_prompt_event_name, data));
@ -279,6 +283,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
value: JSON.stringify(event.function_name), value: JSON.stringify(event.function_name),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DECISION, gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DECISION,
value: JSON.stringify(event.decision), value: JSON.stringify(event.decision),
@ -313,6 +321,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL,
value: JSON.stringify(event.model), value: JSON.stringify(event.model),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
]; ];
this.enqueueLogEvent(this.createLogEvent(api_request_event_name, data)); this.enqueueLogEvent(this.createLogEvent(api_request_event_name, data));
@ -327,6 +339,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_MODEL,
value: JSON.stringify(event.model), value: JSON.stringify(event.model),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_STATUS_CODE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_STATUS_CODE,
value: JSON.stringify(event.status_code), value: JSON.stringify(event.status_code),
@ -378,6 +394,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_MODEL,
value: JSON.stringify(event.model), value: JSON.stringify(event.model),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_TYPE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_TYPE,
value: JSON.stringify(event.error_type), value: JSON.stringify(event.error_type),

View File

@ -137,6 +137,13 @@ export enum EventMetadataKey {
// Logs the end of a session. // Logs the end of a session.
GEMINI_CLI_END_SESSION_ID = 34, GEMINI_CLI_END_SESSION_ID = 34,
// ==========================================================================
// Shared Keys
// ===========================================================================
// Logs the Prompt Id
GEMINI_CLI_PROMPT_ID = 35,
} }
export function getEventMetadataKey( export function getEventMetadataKey(

View File

@ -127,7 +127,7 @@ describe('loggers', () => {
} as unknown as Config; } as unknown as Config;
it('should log a user prompt', () => { it('should log a user prompt', () => {
const event = new UserPromptEvent(11, 'test-prompt'); const event = new UserPromptEvent(11, 'prompt-id-8', 'test-prompt');
logUserPrompt(mockConfig, event); logUserPrompt(mockConfig, event);
@ -201,6 +201,7 @@ describe('loggers', () => {
const event = new ApiResponseEvent( const event = new ApiResponseEvent(
'test-model', 'test-model',
100, 100,
'prompt-id-1',
usageData, usageData,
'test-response', 'test-response',
); );
@ -224,6 +225,7 @@ describe('loggers', () => {
tool_token_count: 2, tool_token_count: 2,
total_token_count: 0, total_token_count: 0,
response_text: 'test-response', response_text: 'test-response',
prompt_id: 'prompt-id-1',
}, },
}); });
@ -260,6 +262,7 @@ describe('loggers', () => {
const event = new ApiResponseEvent( const event = new ApiResponseEvent(
'test-model', 'test-model',
100, 100,
'prompt-id-1',
usageData, usageData,
'test-response', 'test-response',
'test-error', 'test-error',
@ -296,7 +299,11 @@ describe('loggers', () => {
} as Config; } as Config;
it('should log an API request with request_text', () => { it('should log an API request with request_text', () => {
const event = new ApiRequestEvent('test-model', 'This is a test request'); const event = new ApiRequestEvent(
'test-model',
'prompt-id-7',
'This is a test request',
);
logApiRequest(mockConfig, event); logApiRequest(mockConfig, event);
@ -308,12 +315,13 @@ describe('loggers', () => {
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
model: 'test-model', model: 'test-model',
request_text: 'This is a test request', request_text: 'This is a test request',
prompt_id: 'prompt-id-7',
}, },
}); });
}); });
it('should log an API request without request_text', () => { it('should log an API request without request_text', () => {
const event = new ApiRequestEvent('test-model'); const event = new ApiRequestEvent('test-model', 'prompt-id-6');
logApiRequest(mockConfig, event); logApiRequest(mockConfig, event);
@ -324,6 +332,7 @@ describe('loggers', () => {
'event.name': EVENT_API_REQUEST, 'event.name': EVENT_API_REQUEST,
'event.timestamp': '2025-01-01T00:00:00.000Z', 'event.timestamp': '2025-01-01T00:00:00.000Z',
model: 'test-model', model: 'test-model',
prompt_id: 'prompt-id-6',
}, },
}); });
}); });
@ -394,6 +403,7 @@ describe('loggers', () => {
}, },
callId: 'test-call-id', callId: 'test-call-id',
isClientInitiated: true, isClientInitiated: true,
prompt_id: 'prompt-id-1',
}, },
response: { response: {
callId: 'test-call-id', callId: 'test-call-id',
@ -427,6 +437,7 @@ describe('loggers', () => {
duration_ms: 100, duration_ms: 100,
success: true, success: true,
decision: ToolCallDecision.ACCEPT, decision: ToolCallDecision.ACCEPT,
prompt_id: 'prompt-id-1',
}, },
}); });
@ -455,6 +466,7 @@ describe('loggers', () => {
}, },
callId: 'test-call-id', callId: 'test-call-id',
isClientInitiated: true, isClientInitiated: true,
prompt_id: 'prompt-id-2',
}, },
response: { response: {
callId: 'test-call-id', callId: 'test-call-id',
@ -487,6 +499,7 @@ describe('loggers', () => {
duration_ms: 100, duration_ms: 100,
success: false, success: false,
decision: ToolCallDecision.REJECT, decision: ToolCallDecision.REJECT,
prompt_id: 'prompt-id-2',
}, },
}); });
@ -516,6 +529,7 @@ describe('loggers', () => {
}, },
callId: 'test-call-id', callId: 'test-call-id',
isClientInitiated: true, isClientInitiated: true,
prompt_id: 'prompt-id-3',
}, },
response: { response: {
callId: 'test-call-id', callId: 'test-call-id',
@ -549,6 +563,7 @@ describe('loggers', () => {
duration_ms: 100, duration_ms: 100,
success: true, success: true,
decision: ToolCallDecision.MODIFY, decision: ToolCallDecision.MODIFY,
prompt_id: 'prompt-id-3',
}, },
}); });
@ -578,6 +593,7 @@ describe('loggers', () => {
}, },
callId: 'test-call-id', callId: 'test-call-id',
isClientInitiated: true, isClientInitiated: true,
prompt_id: 'prompt-id-4',
}, },
response: { response: {
callId: 'test-call-id', callId: 'test-call-id',
@ -609,6 +625,7 @@ describe('loggers', () => {
), ),
duration_ms: 100, duration_ms: 100,
success: true, success: true,
prompt_id: 'prompt-id-4',
}, },
}); });
@ -638,6 +655,7 @@ describe('loggers', () => {
}, },
callId: 'test-call-id', callId: 'test-call-id',
isClientInitiated: true, isClientInitiated: true,
prompt_id: 'prompt-id-5',
}, },
response: { response: {
callId: 'test-call-id', callId: 'test-call-id',
@ -675,6 +693,7 @@ describe('loggers', () => {
'error.message': 'test-error', 'error.message': 'test-error',
error_type: 'test-error-type', error_type: 'test-error-type',
'error.type': 'test-error-type', 'error.type': 'test-error-type',
prompt_id: 'prompt-id-5',
}, },
}); });

View File

@ -95,12 +95,14 @@ export class UserPromptEvent {
'event.name': 'user_prompt'; 'event.name': 'user_prompt';
'event.timestamp': string; // ISO 8601 'event.timestamp': string; // ISO 8601
prompt_length: number; prompt_length: number;
prompt_id: string;
prompt?: string; prompt?: string;
constructor(prompt_length: number, prompt?: string) { constructor(prompt_length: number, prompt_Id: string, prompt?: string) {
this['event.name'] = 'user_prompt'; this['event.name'] = 'user_prompt';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.prompt_length = prompt_length; this.prompt_length = prompt_length;
this.prompt_id = prompt_Id;
this.prompt = prompt; this.prompt = prompt;
} }
} }
@ -115,6 +117,7 @@ export class ToolCallEvent {
decision?: ToolCallDecision; decision?: ToolCallDecision;
error?: string; error?: string;
error_type?: string; error_type?: string;
prompt_id: string;
constructor(call: CompletedToolCall) { constructor(call: CompletedToolCall) {
this['event.name'] = 'tool_call'; this['event.name'] = 'tool_call';
@ -128,6 +131,7 @@ export class ToolCallEvent {
: undefined; : undefined;
this.error = call.response.error?.message; this.error = call.response.error?.message;
this.error_type = call.response.error?.name; this.error_type = call.response.error?.name;
this.prompt_id = call.request.prompt_id;
} }
} }
@ -135,12 +139,14 @@ export class ApiRequestEvent {
'event.name': 'api_request'; 'event.name': 'api_request';
'event.timestamp': string; // ISO 8601 'event.timestamp': string; // ISO 8601
model: string; model: string;
prompt_id: string;
request_text?: string; request_text?: string;
constructor(model: string, request_text?: string) { constructor(model: string, prompt_id: string, request_text?: string) {
this['event.name'] = 'api_request'; this['event.name'] = 'api_request';
this['event.timestamp'] = new Date().toISOString(); this['event.timestamp'] = new Date().toISOString();
this.model = model; this.model = model;
this.prompt_id = prompt_id;
this.request_text = request_text; this.request_text = request_text;
} }
} }
@ -153,11 +159,13 @@ export class ApiErrorEvent {
error_type?: string; error_type?: string;
status_code?: number | string; status_code?: number | string;
duration_ms: number; duration_ms: number;
prompt_id: string;
constructor( constructor(
model: string, model: string,
error: string, error: string,
duration_ms: number, duration_ms: number,
prompt_id: string,
error_type?: string, error_type?: string,
status_code?: number | string, status_code?: number | string,
) { ) {
@ -168,6 +176,7 @@ export class ApiErrorEvent {
this.error_type = error_type; this.error_type = error_type;
this.status_code = status_code; this.status_code = status_code;
this.duration_ms = duration_ms; this.duration_ms = duration_ms;
this.prompt_id = prompt_id;
} }
} }
@ -185,10 +194,12 @@ export class ApiResponseEvent {
tool_token_count: number; tool_token_count: number;
total_token_count: number; total_token_count: number;
response_text?: string; response_text?: string;
prompt_id: string;
constructor( constructor(
model: string, model: string,
duration_ms: number, duration_ms: number,
prompt_id: string,
usage_data?: GenerateContentResponseUsageMetadata, usage_data?: GenerateContentResponseUsageMetadata,
response_text?: string, response_text?: string,
error?: string, error?: string,
@ -206,6 +217,7 @@ export class ApiResponseEvent {
this.total_token_count = usage_data?.totalTokenCount ?? 0; this.total_token_count = usage_data?.totalTokenCount ?? 0;
this.response_text = response_text; this.response_text = response_text;
this.error = error; this.error = error;
this.prompt_id = prompt_id;
} }
} }

View File

@ -36,6 +36,7 @@ const createFakeCompletedToolCall = (
name, name,
args: { foo: 'bar' }, args: { foo: 'bar' },
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-1',
}; };
if (success) { if (success) {