/** * @license * Copyright 2025 Qwen * SPDX-License-Identifier: Apache-2.0 */ import type { ToolPlanConfirmationDetails, ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind, ToolConfirmationOutcome, } from './tools.js'; import type { FunctionDeclaration } from '@google/genai'; import type { Config } from '../config/config.js'; import { ApprovalMode } from '../config/config.js'; import { ToolDisplayNames, ToolNames } from './tool-names.js'; export interface ExitPlanModeParams { plan: string; } const exitPlanModeToolDescription = `Use this tool when you are in plan mode and have finished presenting your plan and are ready to code. This will prompt the user to exit plan mode. IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool. Eg. 1. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task. 2. Initial task: "Help me implement yank mode for vim" - Use the exit plan mode tool after you have finished planning the implementation steps of the task. `; const exitPlanModeToolSchemaData: FunctionDeclaration = { name: 'exit_plan_mode', description: exitPlanModeToolDescription, parametersJsonSchema: { type: 'object', properties: { plan: { type: 'string', description: 'The plan you came up with, that you want to run by the user for approval. Supports markdown. The plan should be pretty concise.', }, }, required: ['plan'], additionalProperties: false, $schema: 'http://json-schema.org/draft-07/schema#', }, }; class ExitPlanModeToolInvocation extends BaseToolInvocation< ExitPlanModeParams, ToolResult > { private wasApproved = false; constructor( private readonly config: Config, params: ExitPlanModeParams, ) { super(params); } getDescription(): string { return 'Present implementation plan for user approval'; } override async shouldConfirmExecute( _abortSignal: AbortSignal, ): Promise { const details: ToolPlanConfirmationDetails = { type: 'plan', title: 'Would you like to proceed?', plan: this.params.plan, onConfirm: async (outcome: ToolConfirmationOutcome) => { switch (outcome) { case ToolConfirmationOutcome.ProceedAlways: this.wasApproved = true; this.setApprovalModeSafely(ApprovalMode.AUTO_EDIT); break; case ToolConfirmationOutcome.ProceedOnce: this.wasApproved = true; this.setApprovalModeSafely(ApprovalMode.DEFAULT); break; case ToolConfirmationOutcome.Cancel: this.wasApproved = false; this.setApprovalModeSafely(ApprovalMode.PLAN); break; default: // Treat any other outcome as manual approval to preserve conservative behaviour. this.wasApproved = true; this.setApprovalModeSafely(ApprovalMode.DEFAULT); break; } }, }; return details; } private setApprovalModeSafely(mode: ApprovalMode): void { try { this.config.setApprovalMode(mode); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error( `[ExitPlanModeTool] Failed to set approval mode to "${mode}": ${errorMessage}`, ); } } async execute(_signal: AbortSignal): Promise { const { plan } = this.params; try { if (!this.wasApproved) { const rejectionMessage = 'Plan execution was not approved. Remaining in plan mode.'; return { llmContent: rejectionMessage, returnDisplay: rejectionMessage, }; } const llmMessage = `User has approved your plan. You can now start coding. Start with updating your todo list if applicable.`; const displayMessage = 'User approved the plan.'; return { llmContent: llmMessage, returnDisplay: { type: 'plan_summary', message: displayMessage, plan, }, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error( `[ExitPlanModeTool] Error executing exit_plan_mode: ${errorMessage}`, ); const errorLlmContent = `Failed to present plan: ${errorMessage}`; return { llmContent: errorLlmContent, returnDisplay: `Error presenting plan: ${errorMessage}`, }; } } } export class ExitPlanModeTool extends BaseDeclarativeTool< ExitPlanModeParams, ToolResult > { static readonly Name: string = ToolNames.EXIT_PLAN_MODE; constructor(private readonly config: Config) { super( ExitPlanModeTool.Name, ToolDisplayNames.EXIT_PLAN_MODE, exitPlanModeToolDescription, Kind.Think, exitPlanModeToolSchemaData.parametersJsonSchema as Record< string, unknown >, ); } override validateToolParams(params: ExitPlanModeParams): string | null { // Validate plan parameter if ( !params.plan || typeof params.plan !== 'string' || params.plan.trim() === '' ) { return 'Parameter "plan" must be a non-empty string.'; } return null; } protected createInvocation(params: ExitPlanModeParams) { return new ExitPlanModeToolInvocation(this.config, params); } }