diff --git a/packages/core/src/tools/exitPlanMode.test.ts b/packages/core/src/tools/exitPlanMode.test.ts index 39f19babe97..a6f5c9cd8ac 100644 --- a/packages/core/src/tools/exitPlanMode.test.ts +++ b/packages/core/src/tools/exitPlanMode.test.ts @@ -57,6 +57,13 @@ describe('ExitPlanModeTool', () => { expect(tool.kind).toBe('think'); }); + // Regression for #5210: must stay declared so the model can call it + // directly in plan mode. + it('is always declared even though categorised as deferred (#5210)', () => { + expect(tool.shouldDefer).toBe(true); + expect(tool.alwaysLoad).toBe(true); + }); + it('should have correct schema', () => { expect(tool.schema).toEqual({ name: 'exit_plan_mode', diff --git a/packages/core/src/tools/exitPlanMode.ts b/packages/core/src/tools/exitPlanMode.ts index 689b09b426f..a566cedc092 100644 --- a/packages/core/src/tools/exitPlanMode.ts +++ b/packages/core/src/tools/exitPlanMode.ts @@ -425,9 +425,10 @@ export class ExitPlanModeTool extends BaseDeclarativeTool< >, true, // isOutputMarkdown false, // canUpdateOutput - true, // shouldDefer — only used when leaving plan mode - false, // alwaysLoad - 'plan mode exit approve', + true, // shouldDefer + // alwaysLoad: plan mode tells the model to call exit_plan_mode directly, + // so its schema must always be declared, not deferred (issue #5210). + true, // alwaysLoad ); } diff --git a/packages/core/src/tools/tool-registry.test.ts b/packages/core/src/tools/tool-registry.test.ts index 9119708046d..815a746825a 100644 --- a/packages/core/src/tools/tool-registry.test.ts +++ b/packages/core/src/tools/tool-registry.test.ts @@ -11,6 +11,7 @@ import type { ConfigParameters } from '../config/config.js'; import { Config, ApprovalMode } from '../config/config.js'; import { ToolRegistry, DiscoveredTool } from './tool-registry.js'; import { DiscoveredMCPTool } from './mcp-tool.js'; +import { ExitPlanModeTool } from './exitPlanMode.js'; import type { FunctionDeclaration, CallableTool } from '@google/genai'; import { mcpToTool } from '@google/genai'; import { spawn } from 'node:child_process'; @@ -374,6 +375,20 @@ describe('ToolRegistry', () => { expect(names).toEqual(['always-visible']); }); + // Regression for #5210: the real exit_plan_mode is deferred-category but + // must stay declared, otherwise the model cannot call it in plan mode. + it('keeps the real exit_plan_mode tool declared (#5210)', () => { + toolRegistry.registerTool(new ExitPlanModeTool(config)); + + const declared = toolRegistry + .getFunctionDeclarations() + .map((d) => d.name); + const deferred = toolRegistry.getDeferredToolSummary().map((t) => t.name); + + expect(declared).toContain('exit_plan_mode'); + expect(deferred).not.toContain('exit_plan_mode'); + }); + it('includes revealed deferred tools in getFunctionDeclarations', () => { toolRegistry.registerTool( new MockTool({ name: 'hidden', shouldDefer: true }),