0. 本章学习目标
- 解释用户输入如何进入
SessionPrompt.prompt。 - 解释 OpenCode 为什么需要
runLoop,而不是只调用一次模型。 - 说明
runLoop如何选择 agent、model、tools 和 system prompt。 - 说明
SessionProcessor如何把 LLM 流式事件转成 message parts。 - 说明 tool result 如何回到 message history,并触发下一轮推理。
1. 一句话讲明白
Agent 核心循环就是一个会持续运行的会话状态机:它读取最近的用户消息,准备模型和工具, 调用 LLM,处理模型产生的文本或工具调用,把工具结果写回消息历史,然后决定继续下一轮还是停止。
来源:packages/opencode/src/session/prompt.ts:1240-1489、
packages/opencode/src/session/processor.ts:779-847。
2. 它在 OpenCode agent 中的位置
源码路径卡片
packages/opencode/src/session/prompt.ts:prompt、loop、runLoop。packages/opencode/src/session/processor.ts:消费 LLM stream,更新 text/tool/reasoning parts。packages/opencode/src/session/tools.ts:把 registry/MCP tools 包装成 AI SDK tools。packages/opencode/src/session/llm.ts:统一 LLM stream 入口。packages/opencode/src/session/message-v2.ts:message、part、tool state 类型。
CLI/API
-> SessionPrompt.prompt
-> SessionPrompt.runLoop
-> SessionTools.resolve
-> SessionProcessor.process
-> LLM.stream
-> SessionProcessor.handleEvent
-> message parts
-> runLoop next round
3. 生活类比
把 agent loop 想成项目经理处理任务:用户给需求,项目经理看项目记录,决定找哪个专家和哪些工具; 专家可能要求查文件或跑命令,项目经理把结果贴回记录,再让专家继续判断,直到完成。
这个过程对应源码里的 while (true)。来源:packages/opencode/src/session/prompt.ts:1248-1477。
4. Java 开发者类比
如果用 Java 写,runLoop 很像 Application Service + State Machine:
while (true) {
List<Message> messages = messageRepository.loadContext(sessionId);
Latest latest = MessageV2.latest(messages);
Agent agent = agentService.get(latest.user().agent());
Model model = providerService.getModel(latest.user().model());
Map<String, Tool> tools = toolRegistry.resolve(agent, model, session);
LlmResult result = llmGateway.stream(system, messages, tools);
processor.apply(result, session);
if (result.stop()) break;
}
差异是 OpenCode 大量使用 Effect.gen、对象字面量和 union/literal type,而不是 Spring Service class 风格。
5. 最小源码路径
packages/opencode/src/cli/cmd/run.ts:791-798:CLI 调用client.session.prompt。packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts:279-290:API handler 调用promptSvc.prompt。packages/opencode/src/session/prompt.ts:1211-1229:创建 user message 并调用loop。packages/opencode/src/session/prompt.ts:1240-1481:执行核心runLoop。packages/opencode/src/session/processor.ts:779-847:消费 LLM stream。packages/opencode/src/session/llm.ts:402-493:调用 AI SDK/native runtime。packages/opencode/src/session/processor.ts:376-500:处理 tool-call/tool-result。
6. 用户输入到 agent 行动的整体链路
6.1 用户输入
const result = await client.session.prompt({
sessionID,
agent,
model,
variant: args.variant,
parts: [...files, { type: "text", text: message }],
})
路径:packages/opencode/src/cli/cmd/run.ts:791-798
6.2 session / message
const message = yield* createUserMessage(input)
yield* sessions.touch(input.sessionID)
if (input.noReply === true) return message
return yield* loop({ sessionID: input.sessionID })
路径:packages/opencode/src/session/prompt.ts:1211-1229
6.3 agent 决策
let msgs = yield* MessageV2.filterCompactedEffect(sessionID)
const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)
const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)
const agent = yield* agents.get(lastUser.agent)
路径:packages/opencode/src/session/prompt.ts:1252-1317
6.4 LLM 调用
const result = yield* handle.process({
user: lastUser,
agent,
sessionID,
system,
messages: [...modelMsgs],
tools,
model,
})
路径:packages/opencode/src/session/prompt.ts:1429-1440
6.5 tool call
case "tool-call": {
const toolCall = yield* ensureToolCall(value)
const input = toolInput(value.input)
yield* updateToolCall(value.id, (match) => ({
...match,
tool: value.name,
state: { status: "running", input, time: { start: Date.now() } },
}))
}
路径:packages/opencode/src/session/processor.ts:376-421
6.6 tool result
yield* session.updatePart({
...match.part,
state: {
status: "completed",
input: match.part.state.input,
output: output.output,
metadata: output.metadata,
title: output.title,
time: { start: match.part.state.time.start, end: Date.now() },
},
})
路径:packages/opencode/src/session/processor.ts:169-193
6.7 再次推理
if (result === "stop") return "break" as const
if (result === "compact") {
yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
}
return "continue" as const
路径:packages/opencode/src/session/prompt.ts:1461-1471
不确定点
本章未展开 MessageV2.toModelMessagesEffect 如何把 tool parts 转成 provider message。
“tool parts 会被下一轮模型上下文消费”的判断来自每轮重新读取 messages 并调用
MessageV2.toModelMessagesEffect(msgs, model)。来源:
packages/opencode/src/session/prompt.ts:1248-1477、
packages/opencode/src/session/prompt.ts:1420-1425。
7. 核心源码逐段讲解
7.1 prompt:入口不直接问模型
prompt 先写 user message,再进入 loop。这保证 CLI、API、TUI 都能复用同一个状态入口。
来源:packages/opencode/src/session/prompt.ts:1211-1229。
7.2 loop:同一 session 不能随便并发跑
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
来源:packages/opencode/src/session/prompt.ts:1485-1489、packages/opencode/src/session/run-state.ts:87-93。
7.3 runLoop:真正的状态机
runLoop 每轮设置 busy,读取 compacted messages,找到最新 user/assistant/tasks,
然后选择 agent、model、tools 并调用 processor。来源:packages/opencode/src/session/prompt.ts:1248-1440。
7.4 退出条件:不是所有 stop 都能停
OpenCode 明确处理 provider 返回 stop 但 assistant message 仍含 tool calls 的情况,这时要继续 loop。
来源:packages/opencode/src/session/prompt.ts:1261-1276。
7.5 Processor:事件处理器
Processor 调用 llm.stream,用 Stream.tap 逐个处理事件。
来源:packages/opencode/src/session/processor.ts:779-795。
8. 关键 TypeScript 语法复习
Effect.gen(function* () { ... }):把 Effect 异步流程写成同步风格。来源:packages/opencode/src/session/prompt.ts:1240-1243。const { user: lastUser, ... } = MessageV2.latest(msgs):对象解构并重命名。来源:packages/opencode/src/session/prompt.ts:1254。export type Result = "compact" | "stop" | "continue":字符串 literal union。来源:packages/opencode/src/session/processor.ts:36。Record<string, AITool>:对象 map 类型。来源:packages/opencode/src/session/tools.ts:34。{ ...result, attachments: ... }:对象 spread 浅拷贝并补字段。来源:packages/opencode/src/session/tools.ts:93-102。ToolState的 discriminatorstatus:判别联合类型。来源:packages/opencode/src/session/message-v2.ts:299-320。
9. 涉及的设计模式和架构思想
- State Machine:
runLoop根据 stop/continue/compact/subtask 切换状态。 - Application Service:
SessionPrompt协调 session、agent、provider、processor、tools。 - Strategy / Registry:工具由 registry 解析,具体实现由
item.execute执行。 - Gateway / Adapter:
LLM.stream隐藏 AI SDK/native runtime 差异。 - Event Processor:
SessionProcessor消费 LLMEvent 并更新消息 parts。 - Policy / Interceptor:
ctx.ask把权限检查插入工具执行上下文。
10. 它如何和 Tool、Provider、Session、文件系统协作
Session
prompt 写入 user message;runLoop 每轮读取 messages;
processor 写入 assistant text/tool parts。来源:
packages/opencode/src/session/prompt.ts:1116-1117、
packages/opencode/src/session/prompt.ts:1252-1254。
Tool
SessionTools.resolve 解析工具并提供 ctx.ask;
completeToolCall 写回 tool result。来源:
packages/opencode/src/session/tools.ts:24-116、
packages/opencode/src/session/processor.ts:169-193。
Provider / LLM
LLM.StreamInput 包含 model、agent、system、messages、tools;
LLM.stream 调用 streamText 并映射事件。来源:
packages/opencode/src/session/llm.ts:39-60、
packages/opencode/src/session/llm.ts:402-493。
文件系统
Agent loop 本身不直接写文件,而是通过 read/edit/write/shell tools 间接操作文件系统。 具体文件读写在后续章节展开。
11. 如果自己实现 mini agent,这一章对应什么代码
type ToolState = "pending" | "running" | "completed" | "error"
async function runLoop(sessionID: string) {
while (true) {
const messages = await sessionStore.loadMessages(sessionID)
const latestUser = findLatestUser(messages)
const tools = toolRegistry.resolve()
const result = await llm.stream({ messages, tools })
await processor.apply(sessionID, result)
if (result.finish === "stop" && !hasUnresolvedToolCalls(sessionID)) break
if (result.finish === "tool-calls") continue
}
}
先不要实现 LSP、compaction、subtask、plugin。先把 user -> llm -> tool -> result -> next loop 跑通。
12. 费曼复述区
请用 3 句话复述
SessionPrompt.prompt做了什么?runLoop为什么要 while?- tool result 是如何让模型继续推理的?
如果解释不出来,常见卡点
- 把 CLI handler 当成真正 agent runtime。
- 以为模型一次返回最终答案,不理解 tool call 会触发下一轮。
- 分不清
SessionTools.resolve和具体 tool execute。 - 分不清
LLM.stream和SessionProcessor.process。
13. 练习题
入门题
- 找到
SessionPrompt.prompt,写出它调用的三个关键动作。 - 找到
MessageV2.latest,说明它为什么按 message id 判断最新消息。 - 找到
SessionProcessor.process,说明它返回哪三种结果。
进阶题
- 解释为什么
lastAssistant.finish存在时,OpenCode 仍然可能继续 loop。 - 解释
SessionTools.resolve为什么需要processor。 - 解释
ToolStatePending -> Running -> Completed/Error对 UI 有什么价值。
小实现题
写一个内存版 agent loop:messages 存在数组里,tool 只有 echo,假 LLM 第一次返回 tool-call,第二次返回 text。
14. 源码追踪任务
- 从
packages/opencode/src/cli/cmd/run.ts:791-798追到SessionPrompt.prompt。 - 从
packages/opencode/src/session/prompt.ts:1429-1440追到SessionProcessor.process。 - 从
packages/opencode/src/session/processor.ts:789-795追到LLM.stream。 - 从
packages/opencode/src/session/llm/ai-sdk.ts:191-218追到 processor 的 tool-call/tool-result case。 - 从
SessionTools.resolve追一个具体工具,例如 read 或 edit。
15. 面试式自测
- OpenCode 的 agent loop 和普通 chat completion 最大区别是什么?
- 为什么 agent loop 需要持久化 message parts?
SessionProcessor为什么不直接返回字符串?- OpenCode 如何处理模型返回了 tool call 的情况?
- 什么情况下 loop 会返回
compact? - 为什么 tool 执行上下文里要有
ask? - 如果要避免同一 session 并发跑两个 loop,你会怎么设计?