core: add plan mode enter/exit tools and permissions
- Add PlanEnterTool and PlanExitTool to tool registry for CLI clients - Configure plan_enter and plan_exit permissions in agent defaults - Add plan mode switching logic in session prompt - Update build switch message to reference existing plan file - Rename exit_plan to plan_exit for consistency
This commit is contained in:
parent
caa0a28821
commit
758e3465b9
8 changed files with 512 additions and 4 deletions
|
|
@ -53,6 +53,8 @@ export namespace Agent {
|
|||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
question: "deny",
|
||||
plan_enter: "deny",
|
||||
plan_exit: "deny",
|
||||
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
||||
read: {
|
||||
"*": "allow",
|
||||
|
|
@ -71,6 +73,7 @@ export namespace Agent {
|
|||
defaults,
|
||||
PermissionNext.fromConfig({
|
||||
question: "allow",
|
||||
plan_enter: "allow",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
|
|
@ -84,6 +87,7 @@ export namespace Agent {
|
|||
defaults,
|
||||
PermissionNext.fromConfig({
|
||||
question: "allow",
|
||||
plan_exit: "allow",
|
||||
edit: {
|
||||
"*": "deny",
|
||||
".opencode/plans/*.md": "allow",
|
||||
|
|
|
|||
|
|
@ -1190,6 +1190,25 @@ export namespace SessionPrompt {
|
|||
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
|
||||
if (!userMessage) return input.messages
|
||||
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
|
||||
|
||||
// Switching from plan mode to build mode
|
||||
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
|
||||
const plan = Session.plan(input.session)
|
||||
const exists = await Bun.file(plan).exists()
|
||||
if (exists) {
|
||||
const part = await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMessage.info.id,
|
||||
sessionID: userMessage.info.sessionID,
|
||||
type: "text",
|
||||
text: BUILD_SWITCH.replace("{{plan}}", plan),
|
||||
synthetic: true,
|
||||
})
|
||||
userMessage.parts.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
// Entering plan mode
|
||||
if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
|
||||
const plan = Session.plan(input.session)
|
||||
const exists = await Bun.file(plan).exists()
|
||||
|
|
@ -1261,11 +1280,11 @@ Goal: Write your final plan to the plan file (the only file you can edit).
|
|||
- Include the paths of critical files to be modified
|
||||
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
|
||||
|
||||
### Phase 5: Call exit_plan tool
|
||||
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call exit_plan to indicate to the user that you are done planning.
|
||||
This is critical - your turn should only end with either asking the user a question or calling exit_plan. Do not stop unless it's for these 2 reasons.
|
||||
### Phase 5: Call plan_exit tool
|
||||
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
|
||||
This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
|
||||
|
||||
**Important:** Use question tool to clarify requirements/approach, use exit_plan to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what exit_plan does.
|
||||
**Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
|
||||
|
||||
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
|
||||
</system-reminder>`,
|
||||
|
|
|
|||
|
|
@ -2,4 +2,6 @@
|
|||
Your operational mode has changed from plan to build.
|
||||
You are no longer in read-only mode.
|
||||
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
|
||||
|
||||
A plan file exists at {{plan}}. You should read this file and execute on the plan defined within it.
|
||||
</system-reminder>
|
||||
|
|
|
|||
13
packages/opencode/src/tool/plan-enter.txt
Normal file
13
packages/opencode/src/tool/plan-enter.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
13
packages/opencode/src/tool/plan-exit.txt
Normal file
13
packages/opencode/src/tool/plan-exit.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
135
packages/opencode/src/tool/plan.ts
Normal file
135
packages/opencode/src/tool/plan.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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 "./plan-exit.txt"
|
||||
import ENTER_DESCRIPTION from "./plan-enter.txt"
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
export const PlanExitTool = Tool.define("plan_exit", {
|
||||
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 (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: "build",
|
||||
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 PlanEnterTool = Tool.define("plan_enter", {
|
||||
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 (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",
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -25,6 +25,7 @@ import { Flag } from "@/flag/flag"
|
|||
import { Log } from "@/util/log"
|
||||
import { LspTool } from "./lsp"
|
||||
import { Truncate } from "./truncation"
|
||||
import { PlanExitTool, PlanEnterTool } from "./plan"
|
||||
|
||||
export namespace ToolRegistry {
|
||||
const log = Log.create({ service: "tool.registry" })
|
||||
|
|
@ -109,6 +110,7 @@ export namespace ToolRegistry {
|
|||
SkillTool,
|
||||
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
|
||||
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
|
||||
...(Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),
|
||||
...custom,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue