From 01906a9205867d8f43af830252f092591caee2bd Mon Sep 17 00:00:00 2001 From: Sandy Tao Date: Mon, 9 Feb 2026 09:09:17 -0800 Subject: [PATCH] fix: shorten tool call IDs and fix duplicate tool name in truncated output filenames (#18600) --- packages/core/src/core/turn.test.ts | 2 +- packages/core/src/core/turn.ts | 6 ++--- .../core/src/scheduler/tool-executor.test.ts | 1 + packages/core/src/utils/fileUtils.test.ts | 24 +++++++++++++++++-- packages/core/src/utils/fileUtils.ts | 4 +++- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/core/src/core/turn.test.ts b/packages/core/src/core/turn.test.ts index 438ccdb55a..0fc96b444f 100644 --- a/packages/core/src/core/turn.test.ts +++ b/packages/core/src/core/turn.test.ts @@ -168,7 +168,7 @@ describe('Turn', () => { }), ); expect(event2.value.callId).toEqual( - expect.stringMatching(/^tool2-\d{13}-\w{10,}$/), + expect.stringMatching(/^tool2_\d{13}_\d+$/), ); expect(turn.pendingToolCalls[1]).toEqual(event2.value); expect(turn.getDebugResponses().length).toBe(1); diff --git a/packages/core/src/core/turn.ts b/packages/core/src/core/turn.ts index aa46c5d080..fc1619c05d 100644 --- a/packages/core/src/core/turn.ts +++ b/packages/core/src/core/turn.ts @@ -233,6 +233,8 @@ export type ServerGeminiStreamEvent = // A turn manages the agentic loop turn within the server context. export class Turn { + private callCounter = 0; + readonly pendingToolCalls: ToolCallRequestInfo[] = []; private debugResponses: GenerateContentResponse[] = []; private pendingCitations = new Set(); @@ -398,11 +400,9 @@ export class Turn { fnCall: FunctionCall, traceId?: string, ): ServerGeminiStreamEvent | null { - const callId = - fnCall.id ?? - `${fnCall.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`; const name = fnCall.name || 'undefined_tool_name'; const args = fnCall.args || {}; + const callId = fnCall.id ?? `${name}_${Date.now()}_${this.callCounter++}`; const toolCallRequest: ToolCallRequestInfo = { callId, diff --git a/packages/core/src/scheduler/tool-executor.test.ts b/packages/core/src/scheduler/tool-executor.test.ts index d5e8ac0a26..c6fac5734f 100644 --- a/packages/core/src/scheduler/tool-executor.test.ts +++ b/packages/core/src/scheduler/tool-executor.test.ts @@ -180,6 +180,7 @@ describe('ToolExecutor', () => { it('should truncate large shell output', async () => { // 1. Setup Config for Truncation vi.spyOn(config, 'getTruncateToolOutputThreshold').mockReturnValue(10); + vi.spyOn(config.storage, 'getProjectTempDir').mockReturnValue('/tmp'); const mockTool = new MockTool({ name: SHELL_TOOL_NAME }); const invocation = mockTool.build({}); diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index 79ac66d24c..ef24dfca03 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -1110,7 +1110,7 @@ describe('fileUtils', () => { it('should save content to a file with safe name', async () => { const content = 'some content'; const toolName = 'shell'; - const id = '123'; + const id = 'shell_123'; const result = await saveTruncatedToolOutput( content, @@ -1154,6 +1154,26 @@ describe('fileUtils', () => { expect(result.outputFile).toBe(expectedOutputFile); }); + it('should not duplicate tool name when id already starts with it', async () => { + const content = 'content'; + const toolName = 'run_shell_command'; + const id = 'run_shell_command_1707400000000_0'; + + const result = await saveTruncatedToolOutput( + content, + toolName, + id, + tempRootDir, + ); + + const expectedOutputFile = path.join( + tempRootDir, + 'tool-outputs', + 'run_shell_command_1707400000000_0.txt', + ); + expect(result.outputFile).toBe(expectedOutputFile); + }); + it('should sanitize id in filename', async () => { const content = 'content'; const toolName = 'shell'; @@ -1178,7 +1198,7 @@ describe('fileUtils', () => { it('should sanitize sessionId in filename/path', async () => { const content = 'content'; const toolName = 'shell'; - const id = '1'; + const id = 'shell_1'; const sessionId = '../../etc/passwd'; const result = await saveTruncatedToolOutput( diff --git a/packages/core/src/utils/fileUtils.ts b/packages/core/src/utils/fileUtils.ts index d9c01ae36a..32f32129c0 100644 --- a/packages/core/src/utils/fileUtils.ts +++ b/packages/core/src/utils/fileUtils.ts @@ -617,7 +617,9 @@ export async function saveTruncatedToolOutput( ): Promise<{ outputFile: string }> { const safeToolName = sanitizeFilenamePart(toolName).toLowerCase(); const safeId = sanitizeFilenamePart(id.toString()).toLowerCase(); - const fileName = `${safeToolName}_${safeId}.txt`; + const fileName = safeId.startsWith(safeToolName) + ? `${safeId}.txt` + : `${safeToolName}_${safeId}.txt`; let toolOutputDir = path.join(projectTempDir, TOOL_OUTPUTS_DIR); if (sessionId) {