From 62f890b5aa1606ebe968cf43c5f2c56f504d2cbd Mon Sep 17 00:00:00 2001 From: joshualitt Date: Mon, 1 Dec 2025 10:54:28 -0800 Subject: [PATCH] bug(core): Avoid stateful tool use in `executor`. (#14305) --- packages/core/src/agents/executor.test.ts | 16 +++++++--------- packages/core/src/agents/executor.ts | 23 +++++++---------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/core/src/agents/executor.test.ts b/packages/core/src/agents/executor.test.ts index 94f0662e40..254435feaa 100644 --- a/packages/core/src/agents/executor.test.ts +++ b/packages/core/src/agents/executor.test.ts @@ -65,13 +65,11 @@ const { mockExecuteToolCall, mockSetSystemInstruction, mockCompress, - mockSetTools, } = vi.hoisted(() => ({ mockSendMessageStream: vi.fn(), mockExecuteToolCall: vi.fn(), mockSetSystemInstruction: vi.fn(), mockCompress: vi.fn(), - mockSetTools: vi.fn(), })); let mockChatHistory: Content[] = []; @@ -94,7 +92,6 @@ vi.mock('../core/geminiChat.js', async (importOriginal) => { getHistory: vi.fn((_curated?: boolean) => [...mockChatHistory]), setHistory: mockSetHistory, setSystemInstruction: mockSetSystemInstruction, - setTools: mockSetTools, })), }; }); @@ -238,7 +235,6 @@ describe('AgentExecutor', () => { mockSetHistory.mockClear(); mockSendMessageStream.mockReset(); mockSetSystemInstruction.mockReset(); - mockSetTools.mockReset(); mockExecuteToolCall.mockReset(); mockedLogAgentStart.mockReset(); mockedLogAgentFinish.mockReset(); @@ -258,7 +254,6 @@ describe('AgentExecutor', () => { ({ sendMessageStream: mockSendMessageStream, setSystemInstruction: mockSetSystemInstruction, - setTools: mockSetTools, getHistory: vi.fn((_curated?: boolean) => [...mockChatHistory]), getLastPromptTokenCount: vi.fn(() => 100), setHistory: mockSetHistory, @@ -490,8 +485,10 @@ describe('AgentExecutor', () => { const { modelConfigKey } = getMockMessageParams(0); expect(modelConfigKey.model).toBe(getModelConfigAlias(definition)); - const call = mockSetTools.mock.calls[0]; - const sentTools = (call[0] as Tool[])[0].functionDeclarations; + const chatConstructorArgs = MockedGeminiChat.mock.calls[0]; + // tools are the 3rd argument (index 2), passed as [{ functionDeclarations: [...] }] + const passedToolsArg = chatConstructorArgs[2] as Tool[]; + const sentTools = passedToolsArg[0].functionDeclarations; expect(sentTools).toBeDefined(); expect(sentTools).toEqual( @@ -615,8 +612,9 @@ describe('AgentExecutor', () => { const { modelConfigKey } = getMockMessageParams(0); expect(modelConfigKey.model).toBe(getModelConfigAlias(definition)); - const call = mockSetTools.mock.calls[0]; - const sentTools = (call[0] as Tool[])[0].functionDeclarations; + const chatConstructorArgs = MockedGeminiChat.mock.calls[0]; + const passedToolsArg = chatConstructorArgs[2] as Tool[]; + const sentTools = passedToolsArg[0].functionDeclarations; expect(sentTools).toBeDefined(); const completeToolDef = sentTools!.find( diff --git a/packages/core/src/agents/executor.ts b/packages/core/src/agents/executor.ts index 8667aa33bb..2a3b812a6f 100644 --- a/packages/core/src/agents/executor.ts +++ b/packages/core/src/agents/executor.ts @@ -182,7 +182,6 @@ export class AgentExecutor { private async executeTurn( chat: GeminiChat, currentMessage: Content, - tools: FunctionDeclaration[], turnCounter: number, combinedSignal: AbortSignal, timeoutSignal: AbortSignal, // Pass the timeout controller's signal @@ -192,7 +191,7 @@ export class AgentExecutor { await this.tryCompressChat(chat, promptId); const { functionCalls } = await promptIdContext.run(promptId, async () => - this.callModel(chat, currentMessage, tools, combinedSignal, promptId), + this.callModel(chat, currentMessage, combinedSignal, promptId), ); if (combinedSignal.aborted) { @@ -272,7 +271,6 @@ export class AgentExecutor { */ private async executeFinalWarningTurn( chat: GeminiChat, - tools: FunctionDeclaration[], turnCounter: number, reason: | AgentTerminateMode.TIMEOUT @@ -309,7 +307,6 @@ export class AgentExecutor { const turnResult = await this.executeTurn( chat, recoveryMessage, - tools, turnCounter, // This will be the "last" turn number combinedSignal, graceTimeoutController.signal, // Pass grace signal to identify a *grace* timeout @@ -387,8 +384,8 @@ export class AgentExecutor { let chat: GeminiChat | undefined; let tools: FunctionDeclaration[] | undefined; try { - chat = await this.createChatObject(inputs); tools = this.prepareToolsList(); + chat = await this.createChatObject(inputs, tools); const query = this.definition.promptConfig.query ? templateString(this.definition.promptConfig.query, inputs) : 'Get Started!'; @@ -414,7 +411,6 @@ export class AgentExecutor { const turnResult = await this.executeTurn( chat, currentMessage, - tools, turnCounter++, combinedSignal, timeoutController.signal, @@ -443,7 +439,6 @@ export class AgentExecutor { ) { const recoveryResult = await this.executeFinalWarningTurn( chat, - tools, turnCounter, // Use current turnCounter for the recovery attempt terminateReason, signal, // Pass the external signal @@ -509,7 +504,6 @@ export class AgentExecutor { if (chat && tools) { const recoveryResult = await this.executeFinalWarningTurn( chat, - tools, turnCounter, // Use current turnCounter AgentTerminateMode.TIMEOUT, signal, @@ -591,15 +585,9 @@ export class AgentExecutor { private async callModel( chat: GeminiChat, message: Content, - tools: FunctionDeclaration[], signal: AbortSignal, promptId: string, ): Promise<{ functionCalls: FunctionCall[]; textResponse: string }> { - if (tools.length > 0) { - // TODO(12622): Move tools back to config. - chat.setTools([{ functionDeclarations: tools }]); - } - const responseStream = await chat.sendMessageStream( { model: getModelConfigAlias(this.definition), @@ -650,7 +638,10 @@ export class AgentExecutor { } /** Initializes a `GeminiChat` instance for the agent run. */ - private async createChatObject(inputs: AgentInputs): Promise { + private async createChatObject( + inputs: AgentInputs, + tools: FunctionDeclaration[], + ): Promise { const { promptConfig } = this.definition; if (!promptConfig.systemPrompt && !promptConfig.initialMessages) { @@ -673,7 +664,7 @@ export class AgentExecutor { return new GeminiChat( this.runtimeContext, systemInstruction, - [], // set in `callModel`, + [{ functionDeclarations: tools }], startHistory, ); } catch (error) {