从 OpenCode 反推 mini coding agent
a3647eb025c7 · 官方 Release
Agent 生成档案
Section titled “Agent 生成档案”- 章节 ID:
14-mini-coding-agent - 章节摘要:把 CLI、session、LLM、tool、permission 与 processor 的最小机制重组为一个可实现、可观察、可设安全边界的 mini coding agent。
- 教程版本:
v1.18.16 - 源码基线:
a3647eb025c7615159d417dcc49fc39fdaeba65b - 章节元数据:/versions/v1-18-16/data/chapters.json
- 源码映射:/versions/v1-18-16/data/source-map.json
主要源码路径
Section titled “主要源码路径”packages/opencode/src/cli/cmd/run.tspackages/opencode/src/session/prompt.tspackages/opencode/src/session/llm.tspackages/opencode/src/tool/tool.tspackages/opencode/src/session/tools.tspackages/opencode/src/permission/index.tspackages/opencode/src/tool/read.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/shell.tspackages/opencode/src/session/processor.tspackages/opencode/src/server/routes/instance/httpapi/handlers/event.ts
源码基线:
v1.18.16(提交a3647eb025c7)。本章的 mini agent 是教学性设计;OpenCode 源码负责证明机制,但示例代码不是 OpenCode 的可直接运行摘录。
0. 本章学习目标
Section titled “0. 本章学习目标”读完本章,你应该能:
- 从成熟 OpenCode 中剥离出 coding agent 的最小闭环。
- 画出消息账本、LLM、tool registry、permission 与 processor 的边界。
- 沿着“读取
package.json再回答”走完一轮 tool call。 - 写出有停止条件、参数校验、审批和错误回填的 loop 伪代码。
- 排出一个可交付 mini agent 的实现与测试顺序。
1. 一句话讲明白
Section titled “1. 一句话讲明白”一个最小 coding agent 是一台有账本的循环机:模型从消息决定下一步,工具把外部世界的结果写回账本,循环根据明确条件继续或停止,权限闸门则阻止不该自动发生的动作。
中心问题是:从 OpenCode 删除 UI、MCP、插件、LSP、多 provider 和分布式接口之后,哪些机制再删一个,agent 就不再是真的?
v1.18.16 相比旧基线改变了什么
Section titled “v1.18.16 相比旧基线改变了什么”- 最新 loop 把 finish reason 与真实 ToolPart 一起判断,提醒 mini agent 不能只相信 provider 的一个字符串就退出。
- Tool wrapper 已统一承担参数解码和输出截断,说明 mini agent 至少需要一个执行 adapter,而不是直接调用业务函数。
- Permission 契约下沉到 core,runtime 用 Deferred 等待审批;这进一步验证“能力规则”与“交互界面”应当分层。
mini agent 的循环与退出证据
packages/opencode/src/session/prompt.ts:1081-1130
退出条件必须同时考虑 finish reason 和 tool 状态。
1081 const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(agent 核心循环。1082 function* (sessionID: SessionID) {定义一段可复用逻辑。1083 const ctx = yield* InstanceState.context等待 Effect 结果。1084 let structured: unknown1085 let step = 01086 const session = yield* sessions.get(sessionID).pipe(Effect.orDie)Effect 异步工作流。10871088 while (true) {agent 核心循环。1089 yield* status.set(sessionID, { type: "busy" })等待 Effect 结果。1090 yield* Effect.logInfo("loop", { "session.id": sessionID, step })Effect 异步工作流。10911092 let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(会话消息片段结构。1093 Effect.provideService(Database.Service, database),Effect 异步工作流。1094 )10951096 const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)会话消息片段结构。10971098 if (!lastUser) throw new Error("No user message found in stream. This should never happen.")按条件进入分支。10991100 const lastAssistantMsg = msgs.findLast(1101 (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,1102 )1103 // Some providers return "stop" even when the assistant message contains1104 // tool calls. Keep the loop running so tool results can be sent back to1105 // the model, but ignore cleanup-marked interrupted orphans.1106 const hasToolCalls =1107 lastAssistantMsg?.parts.some(1108 (part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),选择模型或 provider。1109 ) ?? false11101111 if (按条件进入分支。1112 lastAssistant?.finish &&1113 !["tool-calls"].includes(lastAssistant.finish) &&1114 !hasToolCalls &&1115 lastAssistant.parentID === lastUser.id1116 ) {1117 const orphan = lastAssistantMsg?.parts.find(1118 (part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),会话消息片段结构。1119 )1120 if (orphan) {按条件进入分支。1121 yield* Effect.logWarning("loop exit with orphaned interrupted tool", {Effect 异步工作流。1122 "session.id": sessionID,1123 messageID: lastAssistant.id,1124 tool: orphan.tool,1125 callID: orphan.callID,1126 })1127 }1128 yield* Effect.logInfo("exiting loop", { "session.id": sessionID })Effect 异步工作流。1129 break1130 }
mini tool adapter 的证据
packages/opencode/src/tool/tool.ts:102-171
schema、执行、截断和 metadata 是稳定边界。
102 truncate: Truncate.Interface,103 agents: Agent.Interface,104) {105 return () =>返回给上一层。106 Effect.gen(function* () {Effect 异步工作流。107 const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }等待 Effect 结果。108 // Compile the parser closure once per tool init; `decodeUnknownEffect`109 // allocates a new closure per call, so hoisting avoids re-closing it for110 // every LLM tool invocation.111 const decode = Schema.decodeUnknownEffect(toolInfo.parameters)定义并校验数据形状。112 const execute = toolInfo.execute113 toolInfo.execute = (args, ctx) => {114 const attrs = {115 "tool.name": id,116 "session.id": ctx.sessionID,117 "message.id": ctx.messageID,118 ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}),119 }120 return Effect.gen(function* () {Effect 异步工作流。121 const decoded = yield* decode(args).pipe(等待 Effect 结果。122 Effect.mapError(Effect 异步工作流。123 (error) =>124 new InvalidArgumentsError({125 tool: id,126 detail: toolInfo.formatValidationError ? toolInfo.formatValidationError(error) : String(error),127 }),128 ),129 )130 const result = yield* execute(decoded as Schema.Schema.Type<Parameters>, ctx)工具真正执行入口。131 if (result.metadata.truncated !== undefined) {按条件进入分支。132 return result返回给上一层。133 }134 const agent = yield* agents.get(ctx.agent)等待 Effect 结果。135 const truncated = yield* truncate.output(result.output, {}, agent)等待 Effect 结果。136 return {返回给上一层。137 ...result,138 output: truncated.content,139 metadata: {140 ...result.metadata,141 truncated: truncated.truncated,142 ...(truncated.truncated && { outputPath: truncated.outputPath }),143 },144 }145 }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))Effect 异步工作流。146 }147 return toolInfo返回给上一层。148 })149}150151export function define<对外暴露模块成员。152 Parameters extends Schema.Decoder<unknown>,定义并校验数据形状。153 Result extends Metadata,154 R,155 ID extends string = string,156>(157 id: ID,158 init: Effect.Effect<Init<Parameters, Result>, never, R>,Effect 异步工作流。159): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {Effect 异步工作流。160 return Object.assign(返回给上一层。161 Effect.gen(function* () {Effect 异步工作流。162 const resolved = yield* init等待 Effect 结果。163 const truncate = yield* Truncate.Service等待 Effect 结果。164 const agents = yield* Agent.Service等待 Effect 结果。165 return { id, init: wrap(id, resolved, truncate, agents) }返回给上一层。166 }),167 { id },168 )169}170171export function init<P extends Schema.Decoder<unknown>, M extends Metadata>(定义并校验数据形状。
2. 先做减法:内核与产品层
Section titled “2. 先做减法:内核与产品层”必须保留的内核
User Input -> Message Ledger -> LLM(messages, tool schemas) -> text OR tool calls -> validate + permission + execute -> tool results back to ledger -> continue / stop
OpenCode 的成熟产品层
多入口、持久化数据库、多 provider、插件、MCP、LSP、compaction、subagent、结构化输出、事件兼容迁移、成本统计、分享、桌面 UI……“mini”不等于写成一次 llm(prompt)。只调用一次模型的是聊天封装;coding agent 必须能让模型观察工具结果后继续决策。
3. 最小架构地图
Section titled “3. 最小架构地图”| 模块 | 最小职责 | OpenCode 证据 |
|---|---|---|
| CLI | 解析输入,调用 session service | packages/opencode/src/cli/cmd/run.ts |
| Session | 保存 user/assistant/tool 消息 | packages/opencode/src/session/prompt.ts |
| Agent loop | 选择继续、停止或进入工具轮 | packages/opencode/src/session/prompt.ts |
| LLM gateway | 接收 messages、system、tools 并流式返回 | packages/opencode/src/session/llm.ts |
| Tool registry | 暴露 schema 与 execute | packages/opencode/src/tool/tool.ts |
| Permission | allow/deny/ask,等待用户回复 | packages/opencode/src/permission/index.ts |
| Processor | 把流事件收敛成 message/tool 状态 | packages/opencode/src/session/processor.ts |
Java 类比可以帮助定位:CLI 像 Controller,Session 像 aggregate repository,LLM/Tool 像 outbound ports,loop 像状态机。但模型输出是流,tool call 会把控制权在模型与程序之间往返,不能简单等同一次 service transaction。
4. 最小机制:先写对循环,再加框架
Section titled “4. 最小机制:先写对循环,再加框架”append(userMessage)
for step in 1..maxSteps: response = llm(messages, toolSchemas) append(response.text and toolCalls)
if response has no tool calls: return finalAnswer
for call in response.toolCalls: tool = registry.require(call.name) args = tool.validate(call.args) permission.check(tool, args) result = tool.execute(args) append(toolResult(call.id, result))
return stoppedBecauseMaxSteps这个骨架故意没有 streaming UI、MCP 和 plugin。它仍然保留四个不能省的约束:
- tool call 与 result 用
call.id对应; - 参数先校验再执行;
- 工具结果进入下一轮 messages;
- 循环有确定的停止上限。
OpenCode 的 loop 更复杂,但 while (true)、完成条件、step 与 tool resolution 清晰可见于 packages/opencode/src/session/prompt.ts。
5. 先定义稳定的数据形状
Section titled “5. 先定义稳定的数据形状”教学版不必复制 OpenCode 的全部 MessageV2,可以从下面开始:
1type Message =定义数据结构约束。2 | { role: "user"; content: string }3 | { role: "assistant"; content: string; toolCalls?: ToolCall[] }4 | { role: "tool"; callID: string; name: string; output: string; isError?: boolean }56type ToolCall = {定义数据结构约束。7 id: string8 name: string9 args: unknown10}1112type Tool = {定义数据结构约束。13 name: string14 description: string15 validate(args: unknown): Record<string, unknown>16 execute(args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>工具真正执行入口。17}
OpenCode 的真实工具接口还带 session/message/agent、AbortSignal、metadata 更新和 ask(...),并要求返回 title、metadata、output 与可选 attachments,见 packages/opencode/src/tool/tool.ts。
为什么 context 不能只有 cwd?因为一次工具执行还必须能被取消、归属某个 call/message、发起权限请求,并把进度写回正确位置。
6. 一条具体旅程:读取 package.json 再回答
Section titled “6. 一条具体旅程:读取 package.json 再回答”用户输入:
读取 package.json,告诉我项目使用什么包管理器。下面是根据 OpenCode 机制整理的典型路径,不代表本章真的向模型发过这条请求。
第一步:CLI 把输入交给 session
Section titled “第一步:CLI 把输入交给 session”OpenCode 非交互 CLI 组装 text/file parts,然后调用 client.session.prompt(...),见 packages/opencode/src/cli/cmd/run.ts。
mini agent 的 CLI 同样应保持薄:
1const session = await sessions.create()2const answer = await agent.prompt(session.id, userText)3process.stdout.write(answer)
不要在 CLI 里解析模型 tool call,否则未来 Web/API 入口只能复制这段循环。
第二步:先落 user message,再进入 loop
Section titled “第二步:先落 user message,再进入 loop”SessionPrompt.prompt 读取 session、清理 revert 状态、创建 user message、更新 session,然后才调用 loop,见 packages/opencode/src/session/prompt.ts。
顺序保证:即使模型失败,用户请求仍在账本里,诊断与重试有依据。
第三步:loop 选择模型、agent 与工具
Section titled “第三步:loop 选择模型、agent 与工具”OpenCode 每轮读取压缩后的消息,判断上一条 assistant 是否真的完成。源码特别处理 provider 声称 stop、但 message 中仍有本地 tool call 的情况,见 packages/opencode/src/session/prompt.ts。
这是一个重要停止条件:不能只信 finish 字符串,还要检查有没有尚需回填的 tool call。
随后创建 assistant message、processor handle,并调用 SessionTools.resolve(...),见 packages/opencode/src/session/prompt.ts。
第四步:模型看到 read 工具 schema
Section titled “第四步:模型看到 read 工具 schema”Tool definition 提供 id、description、parameters schema 与 execute,见 packages/opencode/src/tool/tool.ts。OpenCode wrapper 会在执行前 decode args,失败时给出“重写输入以满足 schema”的错误,见 packages/opencode/src/tool/tool.ts。
模型可能返回:
1{2 "id": "call_1",3 "name": "read",4 "args": { "filePath": "package.json" }5}
这只是教学示例。真正字段名和可用 schema 应以 packages/opencode/src/tool/read.ts 当前定义为准。
第五步:权限在执行路径内,而不是 UI 提示框里
Section titled “第五步:权限在执行路径内,而不是 UI 提示框里”SessionTools.resolve 创建的 tool context 把 agent 与 session ruleset 合并,并通过 Permission service 执行 ask,见 packages/opencode/src/session/tools.ts。
Permission service 对每个 pattern 求值:
- 任一
deny立即失败; - 全部
allow直接返回; - 存在
ask就发布事件并等待 Deferred reply。
见 packages/opencode/src/permission/index.ts。
因此,UI 只是审批的一个呈现者。真正闸门必须在工具执行路径里,否则 CLI/API 可绕过安全提示。
第六步:执行 read,并把结果绑定到 call id
Section titled “第六步:执行 read,并把结果绑定到 call id”OpenCode 为 registry tool 包装 plugin before/after hooks,再调用 item.execute(args, ctx),见 packages/opencode/src/session/tools.ts。
mini agent 第一版可以不做 hooks,但必须保留:
validate(call.args) -> permission.check -> read.execute -> ToolResult(call.id, output)不要把 read 输出拼进一段 system prompt;它应成为与 call_1 对应的 tool message,让模型和日志都知道这是谁的结果。
第七步:processor 把 tool 流事件收敛成账本状态
Section titled “第七步:processor 把 tool 流事件收敛成账本状态”OpenCode 在 tool-call 事件把 part 设为 running,并检测重复同一工具/输入的 doom loop;在 tool-result 事件规范化附件并完成对应 call,见 packages/opencode/src/session/processor.ts。
mini agent 即使不做 token streaming,也应该让 tool call 经历:
pending -> running -> completed | error否则取消、重试、UI 展示和崩溃恢复都没有可靠状态。
第八步:结果回到模型,模型才生成答案
Section titled “第八步:结果回到模型,模型才生成答案”加入 tool result 后重新调用 LLM。第二轮模型看到 package.json 内容,才回答包管理器。没有这次回环,工具执行只是旁路脚本,不是 agent observation。
如果第二轮不再产生 tool call,loop 返回 final answer;如果继续调用工具,就重复以上过程,直到完成、失败、取消或达到 max steps。
7. 停止与失败:最小 agent 也必须认真处理
Section titled “7. 停止与失败:最小 agent 也必须认真处理”7.1 正常完成
Section titled “7.1 正常完成”没有本地待处理 tool call,模型给出非 tool-calls finish。OpenCode 的判断见 packages/opencode/src/session/prompt.ts。
7.2 达到步数上限
Section titled “7.2 达到步数上限”OpenCode 读取 agent.steps,在最后一步向模型追加限制提示,见 packages/opencode/src/session/prompt.ts、:1435-1439。mini agent 可更简单:达到上限就停止并返回结构化原因,不能无限循环。
7.3 权限拒绝
Section titled “7.3 权限拒绝”拒绝不是空字符串结果。它应该成为明确 error/denied 状态,让模型知道动作没有发生,也让用户知道文件或 shell 没被执行。
7.4 工具参数错误
Section titled “7.4 工具参数错误”schema validation 必须早于副作用。把可修复错误写回模型,模型可能重试;连续相同错误则应计入循环保护。
7.5 重复工具调用
Section titled “7.5 重复工具调用”OpenCode 检查最近若干 part 是否对同一工具重复同样 input,并通过 doom_loop permission 决定是否继续,见 packages/opencode/src/session/processor.ts。
mini agent 可以先用更透明的策略:同一 name + stableJson(args) 连续出现 N 次就停止,并报告重复轨迹。
7.6 取消
Section titled “7.6 取消”Tool context 带 AbortSignal,见 packages/opencode/src/tool/tool.ts。mini agent 应把同一个 signal 传给 LLM 与工具;只让 UI 显示“已取消”而后台进程继续运行是错误实现。
8. 第一版应该保留什么,延后什么
Section titled “8. 第一版应该保留什么,延后什么”| 第一版保留 | 可以延后 | 原因 |
|---|---|---|
| 内存 message ledger | 数据库与 session 分享 | 先证明闭环 |
| 一个 provider adapter | 多 provider transform | 先稳定内部协议 |
| read + 明确审批的 shell | edit/write/patch/LSP | 降低不可逆风险 |
| schema validation | MCP/plugin tool | 先稳定 registry |
| max steps + cancel + duplicate guard | compaction/subagent | 先防失控 |
| text event logger | TUI/Desktop | 先获得可观测性 |
注意:如果目标明确要求“coding agent 能修改代码”,那么 edit 不能永远延后;但可以在只读闭环验证后再加入,并把 diff/审批/原子写入作为独立里程碑。
9. 一个可实现的目录骨架
Section titled “9. 一个可实现的目录骨架”src/ cli.ts # 输入输出,不含 loop agent-loop.ts # 状态机与停止条件 messages.ts # ledger 类型与 append llm.ts # provider-neutral port permissions.ts # allow/deny/ask tools/ tool.ts # schema + execute contract registry.ts read.ts shell.ts events.ts # 可观察但不拥有真相test/ agent-loop.test.ts permission.test.ts read.test.ts shell.test.ts这里的关键不是文件数量,而是依赖方向:CLI 依赖 loop,loop 依赖 ports,具体 provider/tool 实现 ports;任何 UI 都不应被 tool import。
10. 实现顺序:每一步都形成可验证能力
Section titled “10. 实现顺序:每一步都形成可验证能力”里程碑一:无工具的消息闭环
Section titled “里程碑一:无工具的消息闭环”- 定义 Message 与 LLM port。
- user message 落账后调用 fake deterministic LLM。
- 断言 assistant message 也落账。
里程碑二:一个只读工具
Section titled “里程碑二:一个只读工具”- 定义 Tool 与 registry。
- LLM 返回 read call。
- 参数校验、执行、result 回填、第二轮回答。
里程碑三:停止与错误
Section titled “里程碑三:停止与错误”- max steps。
- unknown tool。
- invalid args。
- repeated identical call。
- AbortSignal。
里程碑四:权限与 shell
Section titled “里程碑四:权限与 shell”- 默认拒绝危险 shell。
- allow/deny/ask 三条路径。
- 审批只允许本次与持久批准分开。
- stdout/stderr/exit code 明确建模。
里程碑五:真实 provider 与事件
Section titled “里程碑五:真实 provider 与事件”- 用 adapter 接真实流式 API。
- 将 provider events 归一化。
- 事件 logger 展示 step、call、result、finish,不泄漏 secret。
这种顺序让每一步都可演示、可测试。先做漂亮 TUI 会掩盖循环与安全边界尚未成立的问题。
11. 最小测试矩阵
Section titled “11. 最小测试矩阵”| 场景 | 关键断言 |
|---|---|
| 模型直接回答 | 只调用一次 LLM,loop stop |
| read 后回答 | 两次 LLM,tool result 带原 call id |
| 参数非法 | 工具未执行,错误可观察 |
| permission deny | 副作用未发生,状态为 denied/error |
| permission ask | 回复前暂停,回复后只继续一次 |
| 相同调用重复 | 达阈值停止,不无限循环 |
| 用户取消 | LLM/tool 收到 abort,账本记录 interrupted |
| 工具抛错 | error result 回填或按策略终止 |
尽量用真实临时目录测试 read/shell,而不是 mock 掉文件系统后只测试自己复制的逻辑。这个思想也与 OpenCode AGENTS.md 的“测试真实实现、尽量少 mock”一致,但 mini 项目仍需根据速度和隔离选择测试层次。
12. OpenCode 的选择:哪些不应照抄
Section titled “12. OpenCode 的选择:哪些不应照抄”OpenCode 当前 loop 还处理 compaction、subtask、结构化输出、plugin transform、provider executed tools、summary、事件迁移等,见 packages/opencode/src/session/prompt.ts。
照抄的代价是:你会在还没跑通 read loop 前,就背负成熟产品的兼容与扩展复杂度。
| OpenCode 机制 | mini agent 决策 |
|---|---|
| Effect service/layer | 可先用显式构造函数依赖注入 |
| 持久化 MessageV2 | 先内存 ledger,类型保留扩展空间 |
| plugin + MCP tools | 先单一 registry |
| compaction | 先限制最大历史/token,明确报错 |
| SSE/UI event system | 先 callback/async iterator 日志 |
| 多级 permission rules | 先按 tool 的 allow/deny/ask |
“先简化”不等于删掉安全与停止条件;它只删产品规模,不删 agent 正确性。
13. 可以带走的方法
Section titled “13. 可以带走的方法”方法一:用账本驱动循环
Section titled “方法一:用账本驱动循环”每次决定都从 messages 恢复,而不是依赖散落的局部变量。这样重试、回放和 UI 投影才有基础。
验证问题:进程在 tool result 写入后重启,能否知道下一步该把什么发给模型?
方法二:把副作用封进统一 Tool contract
Section titled “方法二:把副作用封进统一 Tool contract”schema、permission、abort、result/error 状态必须包住每个工具,而不是由工具作者自由发挥。
验证问题:新增 shell 时,是否天然经过与 read 相同的 validation 与 event 路径?
方法三:停止条件与能力一起设计
Section titled “方法三:停止条件与能力一起设计”每增加一种“继续”能力,就增加对应上限:工具轮需要 max steps,自动重试需要 retry budget,压缩需要失败出口。
验证问题:模型持续产生合法但无进展的调用时,系统在哪里停止?
14. 费曼复述与最终练习
Section titled “14. 费曼复述与最终练习”请关闭本章,用两分钟讲清:
- chat wrapper 与 coding agent 的分界是什么?
- 为什么 tool result 必须进入 message ledger?
- permission 为什么必须在 execute path 内?
- mini agent 哪些 OpenCode 能力可以延后,哪些不能?
最终练习:
- 第一关:画出“读
package.json”的八步序列,并标出两次 LLM 调用。 - 第二关:写出
Tool、Message、runLoop三个最小接口。 - 第三关:补齐 invalid args、deny、abort、重复调用四个测试。
- 第四关:加入 shell,但证明未批准命令绝不会启动进程。
- 迁移关:选择 OpenCode 的一个产品能力,说明它应在哪个边界加入,而不是塞进 loop。
如果你的实现还不能回答“工具失败后账本里有什么”,请先不要做 UI:回到第 5–7 节补齐状态模型。
最后复盘:小,但必须是真的
Section titled “最后复盘:小,但必须是真的”最终闭环只有一句伪代码:
用户消息入账 -> 模型决定 -> 工具受控执行 -> 结果入账 -> 模型再决定 -> 明确停止你从 OpenCode 学到的不是一份可缩写的文件清单,而是三条架构纪律:消息是可恢复的事实,工具是受控副作用,循环必须证明为什么继续以及为什么停止。
教程到这里结束,但源码学习的下一步很具体:亲手实现 read 闭环,然后用真实 trace 对照 SessionPrompt、SessionTools 和 SessionProcessor。当你的设计与 OpenCode 不同时,先问“我省略的是产品规模,还是正确性边界?”