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-1489packages/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. 最小源码路径

  1. packages/opencode/src/cli/cmd/run.ts:791-798:CLI 调用 client.session.prompt
  2. packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts:279-290:API handler 调用 promptSvc.prompt
  3. packages/opencode/src/session/prompt.ts:1211-1229:创建 user message 并调用 loop
  4. packages/opencode/src/session/prompt.ts:1240-1481:执行核心 runLoop
  5. packages/opencode/src/session/processor.ts:779-847:消费 LLM stream。
  6. packages/opencode/src/session/llm.ts:402-493:调用 AI SDK/native runtime。
  7. 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-1477packages/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-1489packages/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 的 discriminator status:判别联合类型。来源: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-1117packages/opencode/src/session/prompt.ts:1252-1254

Tool

SessionTools.resolve 解析工具并提供 ctx.askcompleteToolCall 写回 tool result。来源: packages/opencode/src/session/tools.ts:24-116packages/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-60packages/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 句话复述

  1. SessionPrompt.prompt 做了什么?
  2. runLoop 为什么要 while?
  3. tool result 是如何让模型继续推理的?

如果解释不出来,常见卡点

  • 把 CLI handler 当成真正 agent runtime。
  • 以为模型一次返回最终答案,不理解 tool call 会触发下一轮。
  • 分不清 SessionTools.resolve 和具体 tool execute。
  • 分不清 LLM.streamSessionProcessor.process

13. 练习题

入门题

  1. 找到 SessionPrompt.prompt,写出它调用的三个关键动作。
  2. 找到 MessageV2.latest,说明它为什么按 message id 判断最新消息。
  3. 找到 SessionProcessor.process,说明它返回哪三种结果。

进阶题

  1. 解释为什么 lastAssistant.finish 存在时,OpenCode 仍然可能继续 loop。
  2. 解释 SessionTools.resolve 为什么需要 processor
  3. 解释 ToolStatePending -> Running -> Completed/Error 对 UI 有什么价值。

小实现题

写一个内存版 agent loop:messages 存在数组里,tool 只有 echo,假 LLM 第一次返回 tool-call,第二次返回 text。

14. 源码追踪任务

  1. packages/opencode/src/cli/cmd/run.ts:791-798 追到 SessionPrompt.prompt
  2. packages/opencode/src/session/prompt.ts:1429-1440 追到 SessionProcessor.process
  3. packages/opencode/src/session/processor.ts:789-795 追到 LLM.stream
  4. packages/opencode/src/session/llm/ai-sdk.ts:191-218 追到 processor 的 tool-call/tool-result case。
  5. SessionTools.resolve 追一个具体工具,例如 read 或 edit。

15. 面试式自测

  1. OpenCode 的 agent loop 和普通 chat completion 最大区别是什么?
  2. 为什么 agent loop 需要持久化 message parts?
  3. SessionProcessor 为什么不直接返回字符串?
  4. OpenCode 如何处理模型返回了 tool call 的情况?
  5. 什么情况下 loop 会返回 compact
  6. 为什么 tool 执行上下文里要有 ask
  7. 如果要避免同一 session 并发跑两个 loop,你会怎么设计?

16. 下一步阅读建议

建议下一章生成 “Tool 调用系统”。理由:Agent 核心循环里最难理解的下一跳就是 SessionTools.resolve,它连接模型 tool schema、具体工具执行、权限系统、plugin hook 和 tool result 回填。

  • packages/opencode/src/tool/tool.ts
  • packages/opencode/src/tool/registry.ts
  • packages/opencode/src/session/tools.ts
  • packages/opencode/src/tool/read.ts
  • packages/opencode/src/tool/edit.ts