Files
qwen-code/packages/core/src/tools/exitPlanMode.ts

188 lines
5.7 KiB
TypeScript

/**
* @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<ToolPlanConfirmationDetails> {
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<ToolResult> {
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);
}
}