11 KiB
Plan: Implement enter_plan and exit_plan Tools
Summary
The plan mode workflow in prompt.ts references exit_plan tool that doesn't exist. We need to implement two tools:
exit_plan- Called when the AI finishes planning; uses the Question module to ask the user if they want to switch to build mode (yes/no). Only available in plan mode. If user says yes, creates a synthetic user message with the "build" agent to trigger the mode switch in the loop.enter_plan- Called to enter plan mode. Only available in build mode. If user says yes, creates a synthetic user message with the "plan" agent.
Key Insight: How Mode Switching Works
Looking at prompt.ts:455-478, the session loop determines the current agent from the last user message's agent field (line 510: const agent = await Agent.get(lastUser.agent)).
To switch modes, we need to:
- Ask the user for confirmation
- If confirmed, create a synthetic user message with the new agent specified
- The loop will pick up this new user message and use the new agent
Files to Modify
| File | Action |
|---|---|
packages/opencode/src/tool/plan.ts |
CREATE - New file with both tools |
packages/opencode/src/tool/exitplan.txt |
CREATE - Description for exit_plan tool |
packages/opencode/src/tool/enterplan.txt |
CREATE - Description for enter_plan tool |
packages/opencode/src/tool/registry.ts |
MODIFY - Register the new tools |
packages/opencode/src/agent/agent.ts |
MODIFY - Add permission rules to restrict tool availability |
Implementation Details
1. Create packages/opencode/src/tool/plan.ts
import z from "zod"
import { Tool } from "./tool"
import { Question } from "../question"
import { Session } from "../session"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Provider } from "../provider/provider"
import EXIT_DESCRIPTION from "./exitplan.txt"
import ENTER_DESCRIPTION from "./enterplan.txt"
export const ExitPlanTool = Tool.define("exit_plan", {
description: EXIT_DESCRIPTION,
parameters: z.object({}),
async execute(_params, ctx) {
const answers = await Question.ask({
sessionID: ctx.sessionID,
questions: [
{
question: "Planning is complete. Would you like to switch to build mode and start implementing?",
header: "Build Mode",
options: [
{ label: "Yes", description: "Switch to build mode and start implementing the plan" },
{ label: "No", description: "Stay in plan mode to continue refining the plan" },
],
},
],
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
})
const answer = answers[0]?.[0]
const shouldSwitch = answer === "Yes"
// If user wants to switch, create a synthetic user message with the new agent
if (shouldSwitch) {
// Get model from the last user message in the session
const model = await getLastModel(ctx.sessionID)
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: ctx.sessionID,
role: "user",
time: {
created: Date.now(),
},
agent: "build", // Switch to build agent
model,
}
await Session.updateMessage(userMsg)
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: ctx.sessionID,
type: "text",
text: "User has approved the plan. Switch to build mode and begin implementing the plan.",
synthetic: true,
} satisfies MessageV2.TextPart)
}
return {
title: shouldSwitch ? "Switching to build mode" : "Staying in plan mode",
output: shouldSwitch
? "User confirmed to switch to build mode. A new message has been created to switch you to build mode. Begin implementing the plan."
: "User chose to stay in plan mode. Continue refining the plan or address any concerns.",
metadata: {
switchToBuild: shouldSwitch,
answer,
},
}
},
})
export const EnterPlanTool = Tool.define("enter_plan", {
description: ENTER_DESCRIPTION,
parameters: z.object({}),
async execute(_params, ctx) {
const answers = await Question.ask({
sessionID: ctx.sessionID,
questions: [
{
question:
"Would you like to switch to plan mode? In plan mode, the AI will only research and create a plan without making changes.",
header: "Plan Mode",
options: [
{ label: "Yes", description: "Switch to plan mode for research and planning" },
{ label: "No", description: "Stay in build mode to continue making changes" },
],
},
],
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
})
const answer = answers[0]?.[0]
const shouldSwitch = answer === "Yes"
// If user wants to switch, create a synthetic user message with the new agent
if (shouldSwitch) {
const model = await getLastModel(ctx.sessionID)
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: ctx.sessionID,
role: "user",
time: {
created: Date.now(),
},
agent: "plan", // Switch to plan agent
model,
}
await Session.updateMessage(userMsg)
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: ctx.sessionID,
type: "text",
text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
synthetic: true,
} satisfies MessageV2.TextPart)
}
return {
title: shouldSwitch ? "Switching to plan mode" : "Staying in build mode",
output: shouldSwitch
? "User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. Begin planning."
: "User chose to stay in build mode. Continue with the current task.",
metadata: {
switchToPlan: shouldSwitch,
answer,
},
}
},
})
// Helper to get the model from the last user message
async function getLastModel(sessionID: string) {
for await (const item of MessageV2.stream(sessionID)) {
if (item.info.role === "user" && item.info.model) return item.info.model
}
return Provider.defaultModel()
}
2. Create packages/opencode/src/tool/exitplan.txt
Use this tool when you have completed the planning phase and are ready to exit plan mode.
This tool will ask the user if they want to switch to build mode to start implementing the plan.
Call this tool:
- After you have written a complete plan to the plan file
- After you have clarified any questions with the user
- When you are confident the plan is ready for implementation
Do NOT call this tool:
- Before you have created or finalized the plan
- If you still have unanswered questions about the implementation
- If the user has indicated they want to continue planning
3. Create packages/opencode/src/tool/enterplan.txt
Use this tool to suggest entering plan mode when the user's request would benefit from planning before implementation.
This tool will ask the user if they want to switch to plan mode.
Call this tool when:
- The user's request is complex and would benefit from planning first
- You want to research and design before making changes
- The task involves multiple files or significant architectural decisions
Do NOT call this tool:
- For simple, straightforward tasks
- When the user explicitly wants immediate implementation
- When already in plan mode
4. Modify packages/opencode/src/tool/registry.ts
Add import and register tools:
// Add import at top (around line 27)
import { ExitPlanTool, EnterPlanTool } from "./plan"
// Add to the all() function return array (around line 110-112)
return [
// ... existing tools
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
ExitPlanTool,
EnterPlanTool,
...custom,
]
5. Modify packages/opencode/src/agent/agent.ts
Add permission rules to control which agent can use which tool:
In the defaults ruleset (around line 47-63):
const defaults = PermissionNext.fromConfig({
"*": "allow",
doom_loop: "ask",
// Add these new defaults - both denied by default
exit_plan: "deny",
enter_plan: "deny",
external_directory: {
// ... existing
},
// ... rest of existing defaults
})
In the build agent (around line 67-79):
build: {
name: "build",
options: {},
permission: PermissionNext.merge(
defaults,
PermissionNext.fromConfig({
question: "allow",
enter_plan: "allow", // Allow build agent to suggest plan mode
}),
user,
),
mode: "primary",
native: true,
},
In the plan agent (around line 80-96):
plan: {
name: "plan",
options: {},
permission: PermissionNext.merge(
defaults,
PermissionNext.fromConfig({
question: "allow",
exit_plan: "allow", // Allow plan agent to exit plan mode
edit: {
"*": "deny",
".opencode/plans/*.md": "allow",
},
}),
user,
),
mode: "primary",
native: true,
},
Design Decisions
-
Synthetic user message for mode switching: When the user confirms a mode switch, a synthetic user message is created with the new agent specified. The loop picks this up on the next iteration and switches to the new agent. This follows the existing pattern in
prompt.ts:455-478. -
Permission-based tool availability: Uses the existing permission system to control which tools are available to which agents.
exit_planis only available in plan mode,enter_planonly in build mode. -
Question-based confirmation: Both tools use the Question module for consistent UX.
-
Model preservation: The synthetic user message preserves the model from the previous user message.
Verification
- Run
bun devinpackages/opencode - Start a session in build mode
- Verify
exit_planis NOT available (denied by permission) - Verify
enter_planIS available
- Verify
- Call
enter_planin build mode- Verify the question prompt appears
- Select "Yes" and verify:
- A synthetic user message is created with
agent: "plan" - The next assistant response is from the plan agent
- The plan mode system reminder appears
- A synthetic user message is created with
- In plan mode, call
exit_plan- Verify the question prompt appears
- Select "Yes" and verify:
- A synthetic user message is created with
agent: "build" - The next assistant response is from the build agent
- A synthetic user message is created with
- Test "No" responses - verify no mode switch occurs