diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index 36df456f9ba..0705d878e4d 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -6233,9 +6233,18 @@ describe('CoreToolScheduler plan mode with ask_user_question', () => { expect(completedCalls[0].response.resultDisplay).toBe( 'Plan mode blocked a non-read-only tool call.', ); - expect( - JSON.stringify(completedCalls[0].response.responseParts), - ).toContain('exit_plan_mode tool'); + // Response must use error key (not output) so LLM recognizes it as a failure + const responseParts = completedCalls[0].response.responseParts; + const responseJson = JSON.stringify(responseParts); + expect(responseJson).toContain('"error"'); + expect(responseJson).toContain('Tool blocked by plan mode'); + expect(responseJson).toContain('write_file'); + // Plan-required teammates get exit_plan_mode hint + expect(responseJson).toContain('exit_plan_mode'); + expect(completedCalls[0].response.error).toBeInstanceOf(Error); + expect(completedCalls[0].response.errorType).toBe( + ToolErrorType.EXECUTION_DENIED, + ); } }); @@ -6302,11 +6311,18 @@ describe('CoreToolScheduler plan mode with ask_user_question', () => { .calls[0][0] as ToolCall[]; expect(completedCalls[0].status).toBe('error'); if (completedCalls[0].status === 'error') { - const responseText = JSON.stringify( - completedCalls[0].response.responseParts, + // SDK, subagent, and teammate paths all get the same error format + // but different guidance: SDK/subagents get "Present your plan directly" + const responseParts = completedCalls[0].response.responseParts; + const responseJson = JSON.stringify(responseParts); + expect(responseJson).toContain('"error"'); + expect(responseJson).toContain('Tool blocked by plan mode'); + expect(responseJson).toContain('Present your plan directly'); + expect(responseJson).not.toContain('exit_plan_mode'); + expect(completedCalls[0].response.error).toBeInstanceOf(Error); + expect(completedCalls[0].response.errorType).toBe( + ToolErrorType.EXECUTION_DENIED, ); - expect(responseText).toContain('Present your plan directly'); - expect(responseText).not.toContain('exit_plan_mode tool'); } }, ); diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index b4718dc46cc..cfbea8d5fff 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -112,7 +112,6 @@ import { } from '../tools/modifiable-tool.js'; import * as Diff from 'diff'; import levenshtein from 'fast-levenshtein'; -import { getPlanModeSystemReminder } from './prompts.js'; import { ShellToolInvocation } from '../tools/shell.js'; import { IdeClient } from '../ide/ide-client.js'; import { @@ -2458,22 +2457,27 @@ export class CoreToolScheduler { isEnterPlanModeTool, ) ) { + // SDK and ordinary subagent-like callers should return plans + // directly; they do not have exit_plan_mode available. Plan-required + // teammates have a dedicated exit_plan_mode approval path. + const isPlanRequiredTeammate = + !shouldUsePlanOnlyReminderInSubagentContext() && + !this.config.getSdkMode(); + const planModeError = new Error( + `Tool blocked by plan mode: "${reqInfo.name}" is not a read-only tool. ` + + `Only read-only tools (read_file, grep_search, glob, list_directory, ` + + `web_fetch, etc.) are allowed in plan mode.` + + (isPlanRequiredTeammate + ? ` Call exit_plan_mode to exit plan mode and execute this tool.` + : ` Present your plan directly to the caller instead of executing this tool.`), + ); this.setStatusInternal(reqInfo.callId, 'error', { - callId: reqInfo.callId, - responseParts: convertToFunctionResponse( - reqInfo.name, - reqInfo.callId, - // SDK callers and ordinary subagent-like callers should - // return plans directly. Plan-required teammates have a - // dedicated exit_plan_mode approval path instead. - getPlanModeSystemReminder( - shouldUsePlanOnlyReminderInSubagentContext() || - this.config.getSdkMode(), - ), + ...createErrorResponse( + reqInfo, + planModeError, + ToolErrorType.EXECUTION_DENIED, ), resultDisplay: 'Plan mode blocked a non-read-only tool call.', - error: undefined, - errorType: undefined, }); setToolSpanFailure( toolSpan,