-
Notifications
You must be signed in to change notification settings - Fork 6
ByteMind Plan Mode Implementation
本文档用于指导 ByteMind 在现有 Go 架构下落地真正可用的 Plan Mode。
目标不是再加一层“会说计划”的 prompt,而是把当前已经存在的:
-
Tab模式切换 -
update_plan工具 -
session.Plan持久化 -
mode-plan.md/block-plan.md
升级成一个完整的、可执行的、可展示的规划工作流。
当前仓库已经具备 Plan Mode 的基础骨架:
-
internal/tui/model.go-
Tab可以在build/plan间切换。 -
submitPrompt会把当前mode传给runner.RunPrompt(...)。
-
-
internal/agent/prompt.go- 已按模式注入
mode-build.md和mode-plan.md。 - 当
session.Plan非空时,会注入block-plan.md。
- 已按模式注入
-
internal/tools/update_plan.go- 已存在
update_plan工具。 - 已校验
plan非空,且最多一个in_progress。
- 已存在
-
internal/agent/runner.go- 收到
update_plan后会把sess.Plan写回,并发送EventPlanUpdated。
- 收到
-
internal/session/store.go-
Session已能持久化Plan。
-
当前实现还不是真正的 Plan Mode,核心问题有五个:
-
Tab只改了 UI 状态,没有真正做模式级行为隔离。- 代码中已有提示:
Switched to Plan mode. UI only for now.
- 代码中已有提示:
- Plan 模式下仍暴露全部工具。
-
internal/tools/registry.go当前无模式过滤,write_file、replace_in_file、apply_patch、run_shell都会继续暴露。
-
-
session.Plan结构过于简单。- 目前只有
step和status,不足以支撑 UI、风险、验证、文件范围、执行接管。
- 目前只有
- TUI 没有独立的计划展示区。
-
m.plan能更新,但不会在主界面形成稳定的计划面板。
-
- Build 模式和 Plan 模式之间没有“计划接管”。
- 计划生成后,没有明确的审批、恢复、继续执行、阻塞处理策略。
Plan Mode 应定义为“规划优先工作流模式”,不是单纯的文案模式。
它需要同时包含三层能力:
- 模式层
- 用户通过
Tab进入plan。 - 系统切换 prompt、工具权限、UI 展示和输入预期。
- 用户通过
- 状态层
- 计划以结构化状态保存到 session。
- 计划可以被展示、恢复、继续、修订。
- 执行层
- Build 模式可以接管既有计划继续实施。
- 执行过程持续更新计划状态。
-
Tab是唯一主入口,不增加/plan命令作为主交互。 - Plan 模式必须只读,不能只靠 prompt 自律。
- 计划必须结构化,不能只保留自然语言段落。
- Build 与 Plan 共享同一 session 和同一计划对象。
- 当前阶段不做多 agent,不做后台任务,不做复杂审批流引擎。
- 实现必须完全基于 Go 现有代码组织演进。
建议引入一个新的领域包:
internal/plan
建议职责:
- 计划领域模型
- 状态校验
- 旧 session 计划结构兼容迁移
- Plan Mode 与 Build Mode 的公共判定逻辑
建议改造后的主链路:
TUI(Tab切换)
-> runner(mode=plan/build)
-> prompt(mode block + plan block)
-> tool registry(按 mode 过滤)
-> update_plan / read-only shell / repo inspection
-> session 持久化
-> TUI 计划面板渲染
建议拆成两个概念:
-
AgentModebuildplan
-
PlanPhasenonedraftingreadyapprovedexecutingblockedcompleted
这样能解决一个关键问题:
- 用户当前可能处在
build标签页 - 但当前计划处于
approved或executing
这两个状态不能互相覆盖。
建议新增 internal/plan/types.go:
package plan
import "time"
type AgentMode string
const (
ModeBuild AgentMode = "build"
ModePlan AgentMode = "plan"
)
type Phase string
const (
PhaseNone Phase = "none"
PhaseDrafting Phase = "drafting"
PhaseReady Phase = "ready"
PhaseApproved Phase = "approved"
PhaseExecuting Phase = "executing"
PhaseBlocked Phase = "blocked"
PhaseCompleted Phase = "completed"
)
type StepStatus string
const (
StepPending StepStatus = "pending"
StepInProgress StepStatus = "in_progress"
StepCompleted StepStatus = "completed"
StepBlocked StepStatus = "blocked"
)
type RiskLevel string
const (
RiskLow RiskLevel = "low"
RiskMedium RiskLevel = "medium"
RiskHigh RiskLevel = "high"
)
type Step struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Status StepStatus `json:"status"`
Files []string `json:"files,omitempty"`
Verify []string `json:"verify,omitempty"`
Risk RiskLevel `json:"risk,omitempty"`
}
type State struct {
Goal string `json:"goal,omitempty"`
Summary string `json:"summary,omitempty"`
Phase Phase `json:"phase,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
Steps []Step `json:"steps,omitempty"`
NextAction string `json:"next_action,omitempty"`
BlockReason string `json:"block_reason,omitempty"`
}建议把 internal/session/store.go 里的:
Plan []PlanItem改成:
Mode plan.AgentMode `json:"mode,omitempty"`
Plan plan.State `json:"plan,omitempty"`同时必须做兼容迁移:
- 旧 session 里的
plan是[]PlanItem - 新 session 里的
plan是plan.State
建议用自定义 UnmarshalJSON 兼容旧格式:
- 若
plan是数组,则自动转成:PhaseReadySteps = old items
- 若
plan是对象,则按新结构解析
这样不会破坏已有会话恢复。
PlanPhase 不能只定义枚举,不定义触发器。
建议第一版明确采用下面这套规则:
- 进入
Plan模式且当前无计划时Phase = drafting
- Agent 在 Plan 模式下成功调用
update_planPhase = ready
- 用户从
Plan切回Build,且当前计划非空- 不自动视为
approved - 只保留
Phase = ready
- 不自动视为
- 用户在
Build模式下明确表达“按计划执行”“继续执行当前计划”“开始实现”Phase = approved- 紧接着进入执行回合时切到
Phase = executing
- 执行过程中存在
in_progress步骤Phase = executing
- 执行遇到阻塞,且已记录
blocked步骤Phase = blocked
- 所有步骤都为
completedPhase = completed
这样做的原因是:
-
Tab切换只是工作视图切换,不应隐式代表用户审批 - “是否同意执行”必须由用户明确表达
- Phase 的变化必须和 session 中的真实执行状态一致
建议在 internal/plan/validate.go 中提供一个显式流转校验函数:
func CanTransition(from, to Phase) bool第一版推荐允许的流转关系:
none -> drafting
drafting -> ready
ready -> drafting
ready -> approved
approved -> executing
executing -> blocked
executing -> completed
blocked -> drafting
blocked -> executing
只靠 mode-plan.md 不够,因为当前 registry 仍会把所有工具暴露给模型。
建议新增:
type ToolPolicy struct {
Mode plan.AgentMode
}list_filesread_filesearch_textwrite_filereplace_in_fileapply_patchupdate_planrun_shell
list_filesread_filesearch_textupdate_plan-
run_shell仅限只读命令
write_filereplace_in_fileapply_patch-
run_shell的修改型命令
在 internal/tools/registry.go 增加两个能力:
DefinitionsForMode(mode plan.AgentMode) []llm.ToolDefinitionExecuteForMode(ctx, mode, name, rawArgs, execCtx)
其中:
-
DefinitionsForMode决定模型“看得到什么工具” -
ExecuteForMode决定即便模型越界也“跑不起来”
这是双保险设计。
建议在 internal/tools/run_shell.go 中增加:
func assessShellCommandForMode(mode plan.AgentMode, command string) error规则:
-
build:沿用现有allow / approval / blocked -
plan:- 不采用“宽松只读判断”
- 第一版直接采用“严格白名单 + 语法拦截”
- 任何重定向、管道、脚本执行、解释器执行都直接拒绝
推荐第一版白名单仅允许:
lsdirpwdcattyperggrepfindtreegit statusgit diffgit loggo envgo list
Plan 模式下额外禁止:
|>>><;&&||bashshpwshpowershellpythonpython3node- 任何
.sh/.ps1/.bat/.cmd脚本直接执行
也就是说,Plan 模式下的 run_shell 不是“理论上只读”即可,而是“命令文本必须落在白名单内”。
原因很简单:
- 真实 shell 里很难通过语义分析证明一个命令绝对只读
- 管道、重定向、脚本执行会迅速突破分析边界
- 第一版应优先保证安全边界,而不是追求 shell 灵活性
建议新增:
func isPlanSafeCommand(command string) bool它与现有 isReadOnlyCommand(...) 分离:
-
isReadOnlyCommand(...)继续服务 Build 模式审批判断 -
isPlanSafeCommand(...)专门服务 Plan 模式硬拒绝
后续如果需要更强隔离,再考虑:
- 独立只读沙盒
- 更低权限的执行用户
- Plan 模式下完全关闭 shell,只保留文件工具
当前 internal/agent/prompt.go 的分层是合理的,不建议推翻。
建议只做增强:
- 保留
mode-plan.md - 保留
block-plan.md - 增强
block-plan.md的渲染内容
当前只渲染:
- [status] step
建议改成:
[Current Execution Plan]
Goal: ...
Summary: ...
Phase: ...
- [in_progress] 拆分 runner 的 mode-aware tool policy
files: internal/tools/registry.go, internal/agent/runner.go
verify: go test ./internal/tools ./internal/agent
risk: medium
这样模型在 Build 阶段更容易:
- 继续执行当前步骤
- 不偏离文件范围
- 在完成后更新计划
同时要避免长计划导致 prompt 膨胀。
建议在 block-plan.md 的实际渲染层,不直接注入完整 JSON,而是做压缩视图:
- 已完成步骤
- 只保留标题简述,最多展示最近 3 条
- 当前
in_progress或blocked步骤- 展示完整信息
- 包括
files / verify / risk / block_reason
- 后续待办步骤
- 只展示标题列表
- 总步骤数超过 8 条时
- 自动进入压缩渲染模式
推荐渲染示例:
[Current Execution Plan]
Goal: ...
Phase: executing
Recently completed:
- 拆分 plan domain model
- 接入 mode-aware registry
Current step:
- [in_progress] 给 TUI 增加计划侧栏
files: internal/tui/model.go, internal/tui/styles.go
verify: go test ./internal/tui
Upcoming:
- 接入 blocked -> re-plan 流程
- 补 session 兼容迁移测试
建议在 internal/agent/runner.go 增加一条软约束:
当 mode == plan 且本轮结束时:
- 如果没有
tool_calls - 且
sess.Plan.Steps为空
则本轮不应算成功规划,可返回一段系统化提示:
Plan mode requires a structured plan before finishing. Please restate the plan using update_plan.
第一阶段可以先做“软失败”。 第二阶段再考虑自动重试一次。
Build 模式不需要一个新 executor。
建议继续复用当前 runner 循环,只增加以下约束:
- 若 session 中存在有效计划,则 prompt 必须带上当前计划
- 若有
in_progress步骤,默认围绕该步骤推进 - 若 scope 变化明显,则先
update_plan再继续
这样能在不重写执行框架的前提下,把 Plan 和 Build 接起来。
Build 与 Plan 共享同一份计划状态,必须考虑计划和真实仓库状态脱步的问题。
典型脱步场景:
- 用户在 IDE 外部手动改了代码
- 用户切到别的终端执行了测试或脚本
- 原计划中的
files、verify、summary已不再匹配当前仓库事实
建议在 internal/plan/validate.go 中增加两类校验:
- 结构校验
- 是否只有一个
in_progress -
blocked步骤是否带BlockReason -
completed步骤是否仍排在当前步骤之前
- 是否只有一个
- 时效性校验
- 当前步骤引用的文件是否仍存在
- 计划中的目标文件是否被外部修改
- 当前步骤的验证命令是否仍可解析
推荐增加:
type ValidationResult struct {
OK bool
Warnings []string
RequiresReplan bool
}
func ValidateState(state State) ValidationResult
func ValidateStepFreshness(state State, workspace string) ValidationResult其中 ValidateStepFreshness(...) 第一版不必做复杂 diff,只需做最低成本校验:
- 文件是否存在
- 文件修改时间是否晚于
state.UpdatedAt - 当前
verify命令是否为空或明显非法
如果发现明显脱步:
- 不直接继续执行
- 先提示 agent 更新计划
- 必要时切回
Phase = drafting
Plan Mode 的价值不能只体现在页脚颜色变化,必须让用户“看见计划”。
建议 UI 目标:
-
Tab切换后,立刻能感受到进入 planning 工作流 - 计划生成后,主界面能稳定显示计划状态
- 回到 Build 模式后,计划仍然可见
- 窄屏时也不破版
当终端宽度 >= 120:
- 左侧:聊天时间线
- 右侧:计划侧栏
比例建议:
- 聊天区
70% - 计划区
30%
当终端宽度 < 120:
- 聊天区在上
- 计划卡片放在输入框上方或聊天区顶部
建议新增:
-
Mode BadgeBUILDPLAN
-
Plan Phasedrafting / ready / approved / executing / blocked / completed
Goal / Summary-
Step List- 状态图标
- 标题
- 文件范围
- 风险级别
Next Action-
Blocked Reason(如存在)
在 internal/tui/model.go 中新增:
renderPlanSidebar() stringrenderPlanCompactCard() stringrenderMainLayout() stringrenderPlanStep(step plan.Step) string
在 internal/tui/styles.go 中新增:
planPanelStyleplanStepPendingStyleplanStepActiveStyleplanStepDoneStyleplanStepBlockedStyle
建议把 toggleMode() 改成:
- 修改本地
m.mode - 写回
m.sess.Mode - 立即
store.Save(m.sess) - 更新
statusNote
推荐状态文案:
- 切到 Plan:
Switched to Plan mode. Read-only planning is active. - 切到 Build:
Switched to Build mode. Implementation tools are active.
当前测试明确不希望 command palette 出现 /plan 命令。
这个方向是对的,建议继续保持:
- 主入口仍是
Tab - 不引入额外的 plan slash command
- 让模式切换足够直觉
建议定义以下用户路径:
- 用户按
Tab切到Plan - 输入需求
- Agent 用只读工具扫描仓库
- Agent 调用
update_plan - TUI 右侧实时显示计划步骤
- Agent 最终输出:
- Plan
- Risks
- Verification
- Next Action
- 用户留在
Plan - 输入“把第 3 步拆细”“把风险高的步骤后置”
- Agent 重新调用
update_plan - TUI 刷新计划
- 用户按
Tab回到Build - 用户输入“按当前计划执行第一步”或“按计划继续”
- Agent 在 Build 模式中带着计划继续执行
- 每完成一步,就更新 step 状态
若执行中遇到缺信息、权限、测试失败等情况:
- 将当前 step 标为
blocked - 写入
BlockReason - 最终回答只问最小必要问题
blocked 不是终点,必须定义回退闭环。
建议第一版明确支持两条路径:
- 人工驱动 re-plan
- 用户按
Tab回到Plan - 输入“基于当前阻塞重新规划”
- Agent 更新原计划
Phase: blocked -> drafting -> ready
- 用户按
- Agent 驱动 re-plan
- Build 模式下检测到当前计划明显脱步或无法继续
- Agent 先调用
update_plan - 仅对当前阻塞步骤及其紧邻后续步骤做局部重排
- 将当前阻塞步骤改写、拆分,或补入少量必要前置步骤
- 不在 Build 模式下整份推翻并重做全局计划
- 再向用户明确说明“已根据阻塞情况局部重排计划,请确认是否继续执行”
第一版的边界建议是:
- Build 模式下允许 agent 更新计划
- 但不允许 agent 在
blocked后自动继续大规模实施 - agent 完成 re-plan 后,应该停在“等待用户确认继续执行”
换句话说:
-
blocked后可以自动“修计划” - 但不能自动“继续猛做”
这里要特别区分两种场景:
- Build 模式下的 re-plan
- 只做执行期的小范围修正
- 目标是让当前任务从阻塞中恢复
- Plan 模式下的 re-plan
- 可以做全局重规划
- 适合目标变化、范围变化、架构变化等大调整
一句话定义:
- Build 模式的 re-plan = 局部重排计划
- Plan 模式的 re-plan = 全局重规划
推荐状态流转:
executing -> blocked -> drafting -> ready -> approved -> executing
目标:
- 模式切换持久化
- 工具权限按 mode 过滤
- Plan 面板可见
- session 使用新计划结构
涉及文件:
-
internal/plan/*新增 internal/session/store.gointernal/tools/registry.gointernal/tools/run_shell.gointernal/agent/runner.gointernal/tui/model.gointernal/tui/styles.go
目标:
- Build 模式按当前计划推进
- 当前步骤高亮
- step 完成后自动更新状态
- blocked 状态可回显
- blocked 后可进入 re-plan 闭环
涉及文件:
internal/agent/runner.gointernal/agent/prompt.gointernal/agent/prompts/block-plan.mdinternal/tui/model.go
目标:
-
update_plan支持更丰富字段 - plan block 输出更稳定
- 计划生成失败有软校验
- 增加计划相关测试矩阵
涉及文件:
internal/tools/update_plan.gointernal/agent/prompts/mode-plan.mdinternal/agent/runner.gointernal/plan/validate.go
新增测试建议:
-
internal/plan- 计划状态合法性校验
- 旧格式 session 迁移
-
internal/tools- Plan 模式下看不到写工具
- Plan 模式下运行修改型 shell 被拒绝
- Build 模式工具集合不受影响
-
internal/agent- Plan 模式必须产生结构化计划
- Build 模式会携带已有计划继续执行
-
internal/tui- 宽屏显示侧栏
- 窄屏显示 compact card
-
Tab切换后 mode 会持久化
建议补两个端到端场景:
Tab -> Plan -> 生成计划 -> 更新 session -> 恢复会话Plan 生成计划 -> Tab 回 Build -> 按计划执行 -> 更新步骤状态Build 执行中 blocked -> update_plan 重规划 -> 用户确认后继续执行长计划压缩渲染 -> prompt 仍保持可控长度
必须覆盖:
- Plan 模式下要求改代码时,模型不能真的写文件
- Plan 模式下可以读文件、搜文本、跑只读 shell
- Build 模式下仍然能正常编辑和验证
- 恢复旧 session 不崩溃
- 计划面板在 Windows 终端宽窄变化下可读
- Plan 模式下脚本执行、重定向、管道命令都会被拒绝
- 切到 Build 但未明确批准前,不会隐式开始执行计划
对于当前 ByteMind,最合适的落地方式不是重写一个“planner executor 系统”,而是在现有 runner + session + TUI + prompts + tools 架构上做四个关键升级:
- 引入
internal/plan作为计划领域模型 - 让
Tab切换真正驱动工具权限与 session 模式 - 让计划在 TUI 中稳定可见,而不是只存在于消息流里
- 让 Build 模式能够消费并持续更新已有计划
一句话总结:
ByteMind 的 Plan Mode 最佳实现路径,是把当前已有的“prompt层计划能力”升级成“模式切换 + 结构化状态 + 工具约束 + 可视化面板 + 执行接管”的完整 Go 工作流。