docs: add agent-loop paradigms note and OpenCode plan-mode analysis
agent-loop-notes.md: common agent-loop paradigms (ReAct, Plan-and-Execute, Reflection, Tool-use Loop, multi-agent, ToT/LATS) plus framework comparison of OpenClaw / OpenCode / Claude Code, and an Extended Thinking section corrected to adaptive thinking. opencode-plan-mode.md: source-read of how OpenCode implements Plan Mode as a named permission ruleset (agent), enforced via evaluate/disabled/ask. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# OpenCode Plan Mode 是怎么实现的
|
||||
|
||||
> 基于 sibling 仓库 `../opencode` 源码实读。关键文件:
|
||||
> `packages/opencode/src/agent/agent.ts`、`.../permission/index.ts`、
|
||||
> `.../session/llm/request.ts`、`.../tool/plan.ts`、`.../tool/edit.ts`。
|
||||
|
||||
## 一句话
|
||||
|
||||
OpenCode **没有专门的「plan mode」状态机**。Plan Mode 就是一个叫 `plan` 的 **agent**,
|
||||
本质是一套 **permission 规则集(Ruleset)**;它复用了 OpenCode 给每次工具调用做权限
|
||||
闸门的同一套机制。「进/出 plan」= 切换这一回合归属的 agent。
|
||||
|
||||
---
|
||||
|
||||
## 1. Mode 即 Agent:plan 是预置 agent 之一
|
||||
|
||||
`agent.ts:138` 起定义了四个 native agent,每个就是一个 permission Ruleset:
|
||||
|
||||
| agent | mode | 作用 |
|
||||
|-------|------|------|
|
||||
| `build` | primary | 默认 agent,按配置权限执行(可编辑) |
|
||||
| `plan` | primary | Plan mode,禁所有编辑工具 |
|
||||
| `general` | subagent | 通用并行子任务 |
|
||||
| `explore` | subagent | 只读探索代码库 |
|
||||
|
||||
`plan` 的定义(`agent.ts:154`):
|
||||
|
||||
```ts
|
||||
plan: {
|
||||
description: "Plan mode. Disallows all edit tools.",
|
||||
permission: Permission.merge(
|
||||
defaults, // "*": allow;plan_enter/plan_exit/question 默认 deny
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_exit: "allow", // 只有 plan agent 能退出
|
||||
task: { general: "deny" }, // 禁止派 general subagent(否则可绕道写文件)
|
||||
edit: {
|
||||
"*": "deny", // ★ 核心:禁所有编辑
|
||||
[".opencode/plans/*.md"]: "allow", // 但允许写计划 md
|
||||
[globalPlansDir + "/*.md"]: "allow",
|
||||
},
|
||||
}),
|
||||
user, // 用户 config 最后合并,可覆盖
|
||||
),
|
||||
mode: "primary", native: true,
|
||||
}
|
||||
```
|
||||
|
||||
对照 `build`(`agent.ts:139`):`plan_enter: "allow"` + `question: "allow"`,继承
|
||||
`"*": "allow"`,所以能编辑、也能进入 plan。
|
||||
|
||||
`defaults`(`agent.ts:117`)是所有 agent 的底:`"*": "allow"`、`doom_loop: "ask"`、
|
||||
`question: "deny"`、`plan_enter/plan_exit: "deny"`、`read` 对 `*.env` 类 `ask`、
|
||||
外部目录 `ask`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 规则集怎么被执行:last-match-wins + 两层闸门
|
||||
|
||||
### 规则求值
|
||||
|
||||
`evaluate()`(`permission/index.ts:39`)在合并后的 ruleset 里 `findLast` 命中
|
||||
`{permission, pattern}` 的规则——**后写覆盖先写**(所以 `user` 放在 merge 最后能
|
||||
覆盖默认),无命中则默认 `ask`。
|
||||
|
||||
### 第一层 · 整工具移除(发请求前)
|
||||
|
||||
`resolveTools()`(`session/llm/request.ts:198`)调 `Permission.disabled()`
|
||||
(`index.ts:215`):
|
||||
|
||||
```ts
|
||||
export function disabled(tools, ruleset) {
|
||||
const edits = ["edit", "write", "apply_patch"]
|
||||
return new Set(tools.filter((tool) => {
|
||||
const permission = edits.includes(tool) ? "edit" : tool
|
||||
const rule = ruleset.findLast((r) => Wildcard.match(permission, r.permission))
|
||||
return rule?.pattern === "*" && rule.action === "deny" // 仅当 "*" 全禁才删
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
一个工具若其**最后命中**规则是 `pattern === "*" && action === "deny"`,就整个从
|
||||
模型可见工具表里删掉(`edit/write/apply_patch` 都映射到 `edit` 这个 key)。
|
||||
|
||||
### 第二层 · 调用时逐次 ask(运行时)
|
||||
|
||||
工具执行时自己调 `ctx.ask(...)`。例如 `tool/edit.ts:102,145`:
|
||||
|
||||
```ts
|
||||
yield* ctx.ask({ permission: "edit", patterns: [文件路径], ... })
|
||||
```
|
||||
|
||||
`Permission.ask()`(`index.ts:78`)对每个 pattern 跑 `evaluate`:
|
||||
|
||||
- `deny` → 抛 `DeniedError`,该 tool call 被拒,错误回写给模型;
|
||||
- `allow` → 静默放行;
|
||||
- `ask` → 发 `Asked` 事件、阻塞在 Deferred 上,等用户回 once / always / reject。
|
||||
|
||||
### 为什么 plan 能「只读但能写计划文件」
|
||||
|
||||
关键细节:plan 给 `.opencode/plans/*.md` 留了 `allow`,所以 `disabled()` 的
|
||||
`findLast("edit")` 命中的是那条 **path 级 allow**(`pattern` 不是 `"*"`)——
|
||||
于是 **edit 工具不会被整个删掉**,而是保留下来、在运行时按路径逐个判:
|
||||
|
||||
- 写普通文件 → 命中 `edit "*": deny` → `DeniedError` 挡掉;
|
||||
- 写 `.opencode/plans/foo.md` → 命中 path allow → 放行。
|
||||
|
||||
这就是「只读分析、但能把计划落地写进 md」的实现方式。
|
||||
|
||||
---
|
||||
|
||||
## 3. 进/出 Plan:`plan_enter` / `plan_exit` 是受权限管的伪工具
|
||||
|
||||
切换不是 UI flag,而是两个权限动作:
|
||||
|
||||
- `build`:`plan_enter: allow`、`plan_exit: deny`(继承)→ 只能进
|
||||
- `plan`:`plan_exit: allow`→ 只能出
|
||||
|
||||
退出由 `plan_exit` 工具实现(`tool/plan.ts:15`):
|
||||
|
||||
1. `question.ask(...)` 弹确认:「计划已完成,要切到 build agent 开始实现吗?」
|
||||
(`plan.ts:30`)
|
||||
2. 选 **No** → 抛 `Question.RejectedError`,留在 plan(`plan.ts:46`)。
|
||||
3. 选 **Yes** → 注入一条 **synthetic user 消息**,把 `agent: "build"`
|
||||
(`plan.ts:53-61`)+ 合成文本「计划已批准,你现在可以编辑文件,执行计划」
|
||||
(`plan.ts:67`)。这一回合的归属 agent 翻成 `build`,其 ruleset 放开编辑——
|
||||
权限随之解锁。
|
||||
|
||||
```
|
||||
[plan agent] edit "*": deny ──plan_exit(用户确认 Yes)──▶ 注入 agent="build" 的 user 消息
|
||||
└─ [build agent] "*": allow,可编辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计要点
|
||||
|
||||
OpenCode 把 **plan-mode、只读 explore、各种 subagent、用户自定义 agent 全部统一成
|
||||
「命名的 permission ruleset」**。同一套:
|
||||
|
||||
- `evaluate`(last-match-wins)
|
||||
- `disabled`(整工具删除)
|
||||
- `ask`(运行时 deny / allow / 询问)
|
||||
|
||||
既做单次工具闸门,也实现了 Plan Mode。「换模式」= 换这一回合归属的 agent,
|
||||
经 `plan_enter` / `plan_exit` 且需用户确认。
|
||||
|
||||
> 对比 Claude Code:Claude Code 的 Plan Mode 是独立的 `EnterPlanMode`/`ExitPlanMode`
|
||||
> 门控;OpenCode 则把它压进通用 permission 体系里,代价更小、可被用户 config 复用同
|
||||
> 一套 allow/ask/deny 规则。
|
||||
|
||||
> 注:`../opencode/packages/core/src/plugin/agent.ts:117` 有一份 V1 core 等价镜像
|
||||
> 定义(`{action:"plan_enter", effect:"deny"}` 等),结构一致;上文以
|
||||
> `packages/opencode/src` 下现行实现为准。
|
||||
Reference in New Issue
Block a user