1. 标准流程

计划模式Plan Mode)的核心是 将 Agent 所有复杂任务统一拆分为「规划阶段 + 人工审批 + 执行阶段」,并非仅限代码开发,适用于所有多步骤、高风险、长周期的智能体任务。

1.1 规划阶段(PLAN 模式)

全程仅允许信息调研、梳理逻辑、制定方案,禁止一切新增、修改、删除等变更操作,从根源避免边想边做、盲目执行。

标准流程:

  1. 进入计划模式,通过只读工具全面调研现状、收集信息、梳理任务细节;
  2. 完整梳理可落地的任务方案,固化为 PLAN.md 计划文档;
  3. 提交方案,等待人工审核确认。

1.2 执行阶段(BUILD 执行模式)

仅在人工审批通过后解锁全部操作权限,有序落地任务,保障过程可控、可追溯。

标准流程:

  1. 审核通过后,退出只读规划状态,解除操作限制;
  2. 将整体方案拆解为精细化、可分步执行的待办任务;
  3. 按清单稳步推进执行,全程贴合既定方案,避免任务跑偏、出错。

2. 相关工具

2.1 三大核心工具

工具名 参数 作用
plan_enter 正式切入规划只读阶段
plan_write content 将方案写入PLAN.md,专用写入API,规避通用文件写入带来的安全漏洞
plan_exit rationale(可选) 结束规划,弹出HITL人工审批,审批通过才解除工具限制

如果把通用文件写入工具 write_file 开放给规划阶段,模型可以自定义文件路径,能向服务器里任意文件写入内容,存在越权篡改、覆盖配置文件的安全漏洞。一旦模型失控,会破坏系统文件。

专门定制只用于规划模式的写入 plan_write 工具,只能写入固定文件 plans/PLAN.md,规划阶段模型只能修改计划文档,不能操作其他任何文件,彻底杜绝任意文件写入的风险。

示例,修改项目接口代码任务中,会依次调用:

  1. 调用 plan_enter :进入只读规划模式,不能改文件,只能查看代码。
  2. 多次调用 plan_write,生成 PLAN.md ,全程只写入这一个计划文件,碰不到业务代码。
  3. 调用 plan_exit 弹出人工确认(HITL
    • 同意:退出规划,进入执行 BUILD 阶段,放开文件编辑权限。
    • 驳回:继续留在规划模式,只能修改 PLAN.md,啥代码都改不了。

2.2 shell 工具

命令行工具 execute(shell) 具备双重能力:

  • 只读查询lscatgrepgit log,只查看代码,不修改数据;
  • 变更写入rm、重定向写入、提交代码、安装依赖,会改动文件。

框架仅依靠工具名称做权限控制,没办法识别你本次执行命令是查询还是修改。为严格保证「规划阶段只能调研、不能修改代码」,默认情况下,计划阶段默认直接禁用 shell,守住只读底线

写靠谱的执行计划,往往需要遍历目录、查看源码、查阅 git 历史。shell 是最高效的代码调研手段。为了提升规划质量,可以手动开启开关:

.allowShellInPlanMode()

开启后的权限规则:

  1. execute 允许在规划阶段调用,模型可以执行 shell 命令来勘察项目;
  2. 系统会增加提示词,强制约束模型仅使用只读指令(ls/cat/grep/git查询类),禁止执行删改、写入类变更命令;
  3. 文件写入类工具 write_fileedit_file 依旧保持封禁。文件编辑入口全程锁住,只放开命令行查询通道。

安全取舍与风险:

  • 安全性相比默认配置有所下降:提示词约束不可靠,模型依然有可能拼出 shell 写入语句篡改文件;
  • 最佳配套方案:必须搭配沙箱文件系统,把 Agent 运行环境隔离在沙箱内。就算出现违规写操作,破坏范围也被限制在沙箱内部,不会污染主机环境。

这套权限逻辑对齐 OpenCode 规划 Agent:开放 shell 用于代码调研 + 彻底禁用文件编辑工具 + 依靠提示词约束 shell 为只读操作。


2.3 todo_write 工具

todo_write 工具是 Agent Scope Core 包提供的工具,对标 OpenCodetodowrite ,用于在执行阶段中:

  • 全量覆盖式更新结构化待办列表;
  • 强约束:同一时刻只能有一条 in_progress (当前正在执行)任务,不允许同时两条任务都在进行中,保证串行干活,不能多开;
  • 待办清单存入 Agent 状态,每一轮推理自动提醒模型。

示例,在规划阶段生成的 PLAN.md 文件内容:

整体计划:
1. 打开Controller源码
2. 修改入参校验逻辑
3. 补充单元测试
4. 运行测试验证结果

用户同意进入执行阶段时,模型执行调用 todo_write,一次性把整条计划拆成结构化清单(全量覆盖):

{
  "todos": [
    {"content":"打开Controller源码","status":"in_progress"},
    {"content":"修改入参校验逻辑","status":"pending"},
    {"content":"补充单元测试","status":"pending"},
    {"content":"运行测试验证结果","status":"pending"}
  ]
}

会先执行【打开Controller源码】任务,打开文件操作后,再次调用 todo_write(必须把整张列表完整重写),把第一条改成 completed,第二条变成进行中:

{
  "todos": [
    {"content":"打开Controller源码","status":"completed"},
    {"content":"修改入参校验逻辑","status":"in_progress"},
    {"content":"补充单元测试","status":"pending"},
    {"content":"运行测试验证结果","status":"pending"}
  ]
}

继续往下推进,直到所有任务全部 completed


3. 计划模式四种终态

是否开启计划模式由模型自主决策。仅通过 isPlanModeActive() == false 无法区分结果,极易造成指标统计歧义。必须同时观测:工具调用记录 + 计划文件是否存在 + 当前模式状态,才能区分 4 种运行终态。

终态分类 plan_enter调用 PLAN.md文件 PlanMode状态 结果说明
从未进入规划 不存在 关闭 直接执行,未走规划流程
规划中(已存文档) 存在 开启 写完计划,等待审批放行
规划中(无文档) 不存在 开启 无效规划,只构思不存档
正常退出闭环 存在 关闭 规划流程完整执行成功

3.1 从未进入计划模式

模型自主跳过整个规划环节,直接进入 BUILD 执行模式开始干活。

这是合法行为,常见诱因:

  • 任务步骤简单,无需撰写正式方案;
  • 当前代码工作区与任务目标不匹配,模型放弃编写计划;
  • 简短问答类任务,规划无实际意义。

判定条件(三者同时成立):

  1. 无任何 plan_enter 工具调用事件;
  2. 会话状态:isPlanModeActive() = false
  3. 文件系统:不存在 plans/PLAN.md

该状态属于“跳过规划直接执行”,不能算作规划流程执行成功,需要和走完规划再退出的场景分开统计。


3.2 进入计划模式

进入 Plan Mode 存在两条独立入口,权限约束一致,但日志观测方式完全不同。

3.2.1 方式1:模型自主调用工具

模型自主调用工具:

plan_enter

执行后,PlanModeManager 打开规划模式。

  • 触发时机:模型判断任务复杂,需要先写方案再动手;
  • 运行效果:会话切换至只读规划环境,所有文件修改、破坏性 Shell 命令全部被中间件拦截,仅允许执行 plan_enter / plan_write / plan_exit 三个工具。
  • 可观测证据:会捕获到 ToolCallStartEvent 事件,可以在代码里埋点记录本次会话开启过规划模式。
  • 使用场景:正常人机对话,由 LLM 自主判断任务复杂度来决定是否开启规划。

3.2.2 方式2:外部代码强制开启

适用场景:自动化评测、批量基准测试、管理后台手动管控会话,强制任务必须进入规划阶段。

(1)Java 原生API
RuntimeContext ctx = RuntimeContext.builder().sessionId("my-session").build();
agent.enterPlanMode(ctx);
  • 等价效果:等同于模型调用 plan_enter
  • 关键区别:不会产生任何工具调用事件,无法通过工具事件捕获。
(2)Admin 远程HTTP接口

引入 agentscope-admin-spring-boot-starter 后,可通过 HTTP 远程切换会话状态:

POST /v1/admin/sessions/{sessionId}:enter-plan-mode
(3)观测手段

只能依靠状态查询:

agent.isPlanModeActive(ctx);

3.3 停留在计划模式(未退出)

进入Plan Mode之后,任务可能中途终止,形成两类截然不同的未退出终态。

3.3.1 子终态1:已起草计划,文件已落地

判定条件:

  1. 已执行 plan_enter 进入规划;
  2. 调用过 plan_write
  3. isPlanModeActive() = true
  4. plans/PLAN.md 文件真实存在。

业务含义:

  • 整体方案已经撰写完成并保存到文档,只是没有调用 plan_exit 提交审批。
  • 会话仍然锁定在只读规划阶段,无法执行代码修改。
    后续在同一会话发送消息,即可人工批准进入执行阶段。

3.3.2 子终态2:仅有构思,没有写出计划文件(无效规划)

判定条件:

  1. 成功进入Plan Mode
  2. isPlanModeActive() = true
  3. 从未调用 plan_write
  4. 磁盘不存在 PLAN.md

业务含义:

  • 模型只在对话文本里口头构思方案,始终没有把计划落盘到文件。
  • 属于不合格的规划行为:“只思考、不存档”,没有产出正式计划文档。

优化方案:

  1. 增强系统提示词,强制要求必须调用 plan_write 保存方案;
  2. 校验规则:进入规划模式后必须产出 PLAN.md,否则判定为流程不达标;
  3. 更换与任务匹配的代码仓库,减少模型敷衍行为。

3.4 退出计划模式

退出同样分为模型主动申请、程序强制关闭两条路径,二者在审批流程上有本质区别。

3.4.1 方式1:模型调用 plan_exit

模型调用工具 plan_exit,附带可选的计划摘要。

工具内置权限拦截,一定会触发 HITL 人工确认弹窗:

  • 用户审批通过:解除只读限制,退出 Plan Mode,切换到 BUILD执行模式;
  • 用户驳回:继续停留在规划模式,只能修改 PLAN.md,不能执行改动操作。

完整成功链路:plan_enter → 多次 plan_write 迭代方案 → plan_exit + 人工审批放行。

3.4.2 方式2:代码程序化强制退出

(1)Java API
agent.exitPlanMode(ctx);
(2)Admin HTTP接口
  1. 核心特性:直接绕过HITL人工审批,无弹窗、无需人工确认;
  2. 适用场景:自动化测试、后台批量任务、管理端一键放行会话;
  3. 状态变化:isPlanModeActive() 直接变为 false。
POST /v1/admin/sessions/{sessionId}:exit-plan-mode

3.4.3 完整闭环成功的严格判定条件

只有同时满足下面全部条件,才能认定本次规划流程完整执行成功:

  1. 会话成功进入规划模式(有 enter 调用或状态为 true);
  2. 调用过 plan_write,磁盘存在 plans/PLAN.md
  3. 最终 isPlanModeActive() = false
  4. 流程走完:规划起草 → 保存文档 → 审批退出。

3.5 代码埋点实现方案

  1. 订阅 ToolCallStartEvent,记录会话内是否触发 plan_enterplan_write
  2. 任务结束后检查文件路径,判断 PLAN.md 是否生成;
  3. 读取 PlanMode 激活状态;
  4. 按照上表四分支做枚举分类,禁止仅依靠布尔开关判断成功与否。

4. 状态持久化

4.1 运行时状态持久化机制

Plan Mode 开关属于会话运行时状态,保存在 AgentState 内部。

  1. 只要 Agent 状态开启自动落盘,规划模式激活状态会跟着一起持久保存;
  2. 会话中断、应用进程重启、服务节点切换、多副本负载均衡恢复会话上下文时,会完整还原上一次的 Plan 模式状态:
    • 中断时处于 PLAN 模式,恢复后依然保持只读规划环境;
    • 中断时已经退出到 BUILD 执行模式,恢复后依旧保持可写入权限。

4.2 两部分数据分开存储

4.2.1 模式开关(内存状态)

  • 存储载体:AgentState → 由 AgentStateStore 统一持久化;
  • 保存内容:isPlanModeActive 布尔标记;
  • 恢复效果:会话断点续跑时,阶段不会错乱。

4.2.2 计划文档(磁盘文件)

plan_write 生成的 PLAN.md 写入工作目录 plans/,不受会话状态存储约束,完全跟随文件存储策略:

  1. 本地部署:直接保存在本地磁盘目录;
  2. 沙箱环境:文件隔离在沙箱文件系统内;
  3. 分布式集群:可对接远端对象存储、KV数据库,文件在多节点之间共享读取。

4.3 分布式断点续跑能力

  1. 会话上下文(规划模式开关)存入状态存储,多实例之间可以无缝迁移;
  2. 计划文档独立存入共享文件存储;
  3. 任意节点拉起同一会话,既能还原“处于规划中/已退出规划”的运行阶段,又能读取到已经写好的 PLAN.md
  4. 不会出现“模式恢复了,但是计划文件丢失”或者“文件还在,环境权限已经放开”的数据不一致问题。

4.4 对四种终态判断的影响

  1. 进程重启不会丢失 PlanMode 状态标记,断点恢复后依然可以准确区分:未进入规划、处于规划中、已退出;
  2. 只要文件存储配置一致,PLAN.md 是否存在这一判断条件在会话恢复后依然有效;
  3. 批量评测、分布式任务场景下,前后两次读取到的终态结论保持一致,不会因为服务重启导致统计结果失真。

4.5 关键总结

  1. 阶段开关(Plan 是否激活)跟着 AgentState 自动持久化,支持会话断点恢复;
  2. 计划文件独立存储在 plans/,跟随文件存储方案实现多节点共享;
  3. 状态+文件两套数据同时可恢复,保证跨实例、跨重启之后,规划阶段上下文完整不丢失。

5. 与子 Agent 的关系

5.1 状态隔离基本原则

  1. Plan Mode单个Agent会话独立状态
    Agent开启规划只读环境,该限制不会自动向下传递。
  2. 父子Agent拥有完全独立的 AgentState、独立的规划开关,互不干扰。
  3. 主线 todo_write 待办清单,与子Agent的 task_list / task_output 后台任务队列分属两套体系,数据完全隔离。

5.2 当前版本存在的缺口(重点)

  • Agent正处于 Plan Mode(只读模式),此时调用 agent_spawn 拉起子Agent
    Agent不会自动继承父级的只读权限限制
    -即便父进程被禁止修改文件,子Agent依然拥有完整的文件写入、命令执行权限,存在权限逃逸风险。

5.3 两种临时解决方案(手动管控子Agent权限)

方案一:限制子Agent工具白名单

在创建子Agent时,主动过滤工具列表,只保留只读类工具:

  • 只开放:read_filegrep、只读shell查询命令;
  • 直接剔除所有修改类工具:write_fileedit_file、文件覆盖类能力。保证子Agent全程只能查看代码,不能产生任何变更。

方案二:给子Agent也开启规划模式

构建子 Agent 时开启规划能力:

builder.enablePlanMode()
  • 再由代码主动调用程序化接口,让子Agent自行进入Plan只读环境。
  • Agent被强制锁定在规划阶段,和父Agent保持一致的只读约束。

5.4 版本演进规划

后续新版本会实现权限自动传播机制:父会话一旦进入 Plan Mode,所有通过 agent_spawn 派生出来的子Agent,会自动继承只读策略,不需要人工额外配置。

5.5 补充边界说明

  1. Plan Mode 只管控文件修改权限,父子Agent的任务调度互不影响;
  2. 父在规划阶段只能写PLAN.md,但子不受管控是当前版本的安全漏洞,必须手动加固;
  3. Agent独立的PlanMode不会反向影响父Agent的执行状态。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐