🔁 runLoop 主循环

这是 OpenCode 的大脑——session/prompt.ts:1081while(true) 循环。本章逐段拆解它,把 F3 章的 Agent 循环理论在生产代码里对号入座。你会真切看到一个真实产品如何实现那个"思考-行动-观察"循环。

本章目标

  • 逐段理解 runLoop 的 while(true) 循环结构
  • 看清"判断继续/停止"的生产实现(hasToolCalls + finish)
  • 理解 subtask / compaction 等特殊任务的处理
  • 对照 F3 章三阶段模型(Plan/Execute/Update),完成"理论→代码"闭环
  • 认识 Effect 框架在循环中的具体用法
📚 最关键的章节

如果你只能读 OpenCode 一个文件,就是 session/prompt.ts 的 runLoop。它是整个产品的核心,也是F3 章 Agent 循环理论最完整的生产实现。本章会把理论和代码逐行挂钩。

循环全景

runLoop 是一个 Effect.fn 包裹的生成器函数,核心是 while (true)

📄 session/prompt.ts:1081 · runLoop 函数签名与循环骨架 typescript
// Effect.fn("SessionPrompt.run") 是 Effect 的命名函数封装
const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> =
  Effect.fn("SessionPrompt.run")(function* (sessionID: SessionID) {
    const ctx = yield* InstanceState.context   // 获取运行上下文
    let structured: unknown
    let step = 0                                // 步数计数器(对应 F3 的步数上限)
    const session = yield* sessions.get(sessionID).pipe(Effect.orDie)

    while (true) {                              // ★ 主循环!对应 F3 的 Agent 循环
      yield* status.set(sessionID, { type: "busy" })
      // ... 每轮的逻辑(下面逐段拆解)
    }

    yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope))
    return yield* lastAssistant(sessionID)
  })
💡 对照 F3 章

这个 while(true) 就是 F3 章画的Agent 循环。LC8 的 AgentExecutor 用 for 循环实现,LangGraph 用图的 super-step 实现,OpenCode 用 while(true) 实现——本质完全一样,只是工程形态不同。OpenCode 选择了最直接的命令式写法。

第 1 段:读取消息历史(Plan 阶段开始)

📄 prompt.ts:1092-1110 · 读取并分析消息 typescript
while (true) {
  yield* status.set(sessionID, { type: "busy" })   // 标记会话为忙碌

  // ① 读取过滤后的消息历史(LG2 的 messages 列表,对应 Checkpointer 加载)
  let msgs = yield* MessageV2.filterCompactedEffect(sessionID)

  // ② 找出最新的 user / assistant 消息
  const { user: lastUser, assistant: lastAssistant,
          finished: lastFinished, tasks } = MessageV2.latest(msgs)

  // ③ 判断最后的 assistant 是否还有未处理工具调用
  const hasToolCalls =
    lastAssistantMsg?.parts.some(
      (part) => part.type === "tool"
        && !part.metadata?.providerExecuted
        && !isOrphanedInterruptedTool(part),
    ) ?? false
⚠️ hasToolCalls 是核心判断

注意 hasToolCalls 的计算——它检查最后一条 assistant 消息的 parts 里有没有未执行的 tool。这就是 LC7 章 AgentAction(有 tool_calls)vs AgentFinish(无)的二态判断!OpenCode 用"parts 里有没有 tool 类型且未执行"来表达。LG8 的 tools_condition 也是这个逻辑。

第 2 段:退出条件判断(Finish 检测)

📄 prompt.ts:1116-1131 · break 退出循环 typescript
  // ★ 退出条件:assistant 已 finish + 不是 tool-calls + 没有待执行工具 + user 在前
  if (
    lastAssistant?.finish &&                              // 模型说停了
    !["tool-calls"].includes(lastAssistant.finish) &&     // 且不是因为要调工具
    !hasToolCalls &&                                      // 且没有待执行工具
    lastUser.id < lastAssistant.id                        // 且 assistant 是最新的
  ) {
    // (处理孤儿中断工具的边缘情况...)
    yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
    break                          // ★ 退出循环!对应 AgentFinish
  }

  step++                          // 步数 +1(F3 的步数计数)

这段就是 F3 章"停止条件①:自然完成"的实现。当 LLM 输出 finish 且没有 tool_calls,循环 break。对比:

框架"完成"的判断方式
LangChain(LC7/LC8)解析出 AgentFinish(无 tool_calls)
LangGraph(LG8)tools_condition 返回 END(无 tool_calls)
OpenCode(本章)finish && !hasToolCallsbreak

第 3 段:特殊任务处理

📄 prompt.ts:1142-1168 · subtask / compaction typescript
  const task = tasks.pop()   // 取出待处理任务

  // ① 子任务(task 工具派生的子 agent)
  if (task?.type === "subtask") {
    yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
    continue                  // 处理完进入下一轮
  }

  // ② 上下文压缩任务(token 溢出时触发,对应 F2 的记忆管理)
  if (task?.type === "compaction") {
    const result = yield* compaction.process({ messages: msgs, ... })
    if (result === "stop") break
    continue
  }

  // ③ 自动检测 token 溢出 → 触发 compaction
  if (lastFinished && lastFinished.summary !== true
      && (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model }))) {
    yield* compaction.create({ sessionID, ..., auto: true })
    continue
  }
🧭 这是 OpenCode 独有的工程实践

LangChain/LangGraph 的循环是"纯 LLM + 工具"。OpenCode 在循环里额外处理两类特殊任务subtask(子 agent,OC5 详讲)和 compaction(上下文压缩,OC5 详讲)。这体现了生产级 Agent 的复杂度——真实产品要在循环里处理资源管理、上下文溢出等工程问题。

第 4 段:执行 LLM(Execute 阶段核心)★

这是循环最核心的部分——组装所有输入,调用 LLM:

📄 prompt.ts:1170-1285 · 组装 + 调用 LLM(精简) typescript
  // ① 取 agent 配置(提示 + 模型 + 权限,对应 LG8 的 agent 定义)
  const agent = yield* agents.get(lastUser.agent)
  const maxSteps = agent.steps ?? Infinity     // 步数上限(F3 的停止条件②)
  const isLastStep = step >= maxSteps

  // ② 应用 plan 模式提醒等
  msgs = yield* SessionReminders.apply({ messages: msgs, agent, session })

  // ③ 创建空 assistant 消息(待填充)
  const msg: SessionV1.Assistant = { role: "assistant", ... }
  yield* sessions.updateMessage(msg)

  // ④ 创建 processor handle(处理 LLM 流事件)
  const handle = yield* processor.create({ assistantMessage: msg, sessionID, model })

  // ⑤ 主逻辑
  const outcome = yield* Effect.gen(function* () {
    // 解析工具(OC3 详讲:把 Tool.Def 包成 AI SDK tool)
    const tools = yield* SessionTools.resolve({ agent, session, model, processor: handle, ... })

    // 组装 system prompt(OC5:env + instructions + mcp + skills)
    const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([
      sys.skills(agent), sys.environment(model), instruction.system(),
      sys.mcp(agent, session.permission), MessageV2.toModelMessagesEffect(msgs, model),
    ])
    const system = [...env, ...instructions, ...(mcpInstructions ? [mcpInstructions] : []),
                     ...(skills ? [skills] : [])]

    // ★ 调用 LLM!handle.process 内部调 LLM.Service.stream(OC1 的双运行时)
    const result = yield* handle.process({
      user: lastUser, agent, permission: session.permission,
      system, messages: [...modelMsgs, ...(isLastStep ? [MAX_STEPS_PROMPT] : [])],
      tools, model,
    })

    // 根据 result 决定 break 还是 continue
    if (result === "stop") return "break"
    if (result === "compact") {
      yield* compaction.create({ sessionID, ..., auto: true, overflow: !handle.message.finish })
    }
    return "continue"
  })

  if (outcome === "break") break    // 退出
  continue                          // 否则进入下一轮

完整对照 F3 章三阶段

runLoop 一次迭代 vs F3 章三阶段 ① Plan 规划 读消息历史 filterCompacted 取 agent 配置 组装 system prompt 解析工具 SessionTools.resolve ② Execute 执行 handle.process() → LLM.Service.stream() → processor 处理流 (工具在流中执行) ③ Update 更新 判断 hasToolCalls + finish result: continue/stop/compact 工具结果落库 Session → break 或 continue 对应 Pregel prepare_next_tasks 对应 PregelRunner 对应 apply_writes 同一套三阶段,三种实现 LangChain: for 循环 + AgentExecutor LangGraph: Pregel super-step(BSP) OpenCode: while(true) + Effect(本章)
图 OC2.1 · ★ runLoop 一次迭代完美对应 F3 章的 Plan→Execute→Update 三阶段

result 三态:比二态更丰富

注意 OpenCode 的 result 不只是 continue/stop,还有第三态 compact

result含义对应 LC7 概念
"continue"有工具要执行,继续循环AgentAction
"stop"模型完成,退出循环AgentFinish
"compact"token 溢出,先压缩历史再继续(OpenCode 独有,生产需求)
💡 compact 态是生产级特征

教学框架(LangChain/LangGraph)的循环只有 continue/stop 两态。OpenCode 加了 compact 第三态——当检测到 token 溢出,循环不退出也不直接继续,而是先压缩历史再继续。这是真实产品处理 F2 章"context window 限制"的工程方案(OC5 详讲)。

Effect 在循环中的具体用法

看 runLoop 如何用 Effect 表达副作用。这对理解 TS Agent 实现很重要:

📄 Effect 在 runLoop 的典型用法 typescript
// 1. yield* —— Effect 版的 await(在生成器里)
const agent = yield* agents.get(lastUser.agent)

// 2. Effect.gen —— 把生成器变成 Effect(类似 async function)
const outcome = yield* Effect.gen(function* () { ... })

// 3. Effect.all —— 并发执行多个 Effect(对应 RunnableParallel)
const [skills, env, instructions, mcpInstructions, modelMsgs] =
  yield* Effect.all([sys.skills(agent), sys.environment(model), ...])

// 4. .pipe(Effect.onInterrupt(...)) —— 中断时的清理(对应 AbortController)
const handle = yield* processor.create({...})
  .pipe(Effect.onInterrupt(() => finalizeInterruptedAssistant))

// 5. Effect.forkIn —— 后台异步执行(不阻塞主循环)
yield* title({...}).pipe(Effect.ignore, Effect.forkIn(scope))
🧭 Effect vs LangChain Runnable

对比两者:Effect.gen ≈ RunnableLambda(包装工作单元),Effect.all ≈ RunnableParallel(并发),yield* ≈ await(串联)。但 Effect 更强大——原生支持中断、重试、资源管理(onInterrupt)。LangChain 的 Runnable 用 config 的 callbacks 实现类似功能,但不如 Effect 类型安全。

串行保证:ensureRunning

runLoop 不直接被调用,而是通过 loop()ensureRunning,保证一个会话同时只有一个循环在跑

📄 prompt.ts:1342 · loop() 保证串行 typescript
const loop = Effect.fn("SessionPrompt.loop")(function* (input) {
  // ensureRunning:如果该 session 已有循环在跑,排队等待
  // 这保证了【单会话串行】,不会并发修改状态
  return yield* state.ensureRunning(
    input.sessionID,
    lastAssistant(input.sessionID),
    runLoop(input.sessionID),   // 真正的 runLoop
  )
})

这对应 LangGraph 的"单线程执行一个 thread"——状态不能并发修改。OpenCode 用 SessionRunState 状态机(idle/busy)实现。

与框架的深度对照

runLoop 中的元素对应框架概念
while(true)F3 Agent 循环 / LG8 super-step 循环
hasToolCalls 判断LC7 AgentAction/Finish 二态
finish && !hasToolCalls → breakLG8 tools_condition 返回 END
filterCompactedEffect 读消息LG6 Checkpointer.get 加载状态
agents.get 取配置LG8 create_react_agent 的 model+tools+prompt
SessionTools.resolve 解析工具LG8 ToolNode
handle.process 调 LLMLC3 model.invoke
result "continue/stop/compact"条件边路由(多了 compact 态)
ensureRunning 串行单 thread 串行执行
Effect.onInterrupt 清理LG6 interrupt + 恢复

小结

下一章

你看懂了 Agent 的"大脑循环"。下一章 OC3 · 工具系统:看 OpenCode 如何用 Tool.define 统一所有工具——从 46 行的 todo.tstool.ts 的 wrap 装饰器。对应 LC6 的 @tool 和 LG8 的 ToolNode。