🔁 runLoop 主循环 ★
这是 OpenCode 的大脑——session/prompt.ts:1081 的 while(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):
// 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)
})
这个 while(true) 就是 F3 章画的Agent 循环。LC8 的 AgentExecutor 用 for 循环实现,LangGraph 用图的 super-step 实现,OpenCode 用 while(true) 实现——本质完全一样,只是工程形态不同。OpenCode 选择了最直接的命令式写法。
第 1 段:读取消息历史(Plan 阶段开始)
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 的计算——它检查最后一条 assistant 消息的 parts 里有没有未执行的 tool。这就是 LC7 章 AgentAction(有 tool_calls)vs AgentFinish(无)的二态判断!OpenCode 用"parts 里有没有 tool 类型且未执行"来表达。LG8 的 tools_condition 也是这个逻辑。
第 2 段:退出条件判断(Finish 检测)
// ★ 退出条件: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 && !hasToolCalls → break |
第 3 段:特殊任务处理
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
}
LangChain/LangGraph 的循环是"纯 LLM + 工具"。OpenCode 在循环里额外处理两类特殊任务:subtask(子 agent,OC5 详讲)和 compaction(上下文压缩,OC5 详讲)。这体现了生产级 Agent 的复杂度——真实产品要在循环里处理资源管理、上下文溢出等工程问题。
第 4 段:执行 LLM(Execute 阶段核心)★
这是循环最核心的部分——组装所有输入,调用 LLM:
// ① 取 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 章三阶段
result 三态:比二态更丰富
注意 OpenCode 的 result 不只是 continue/stop,还有第三态 compact:
| result | 含义 | 对应 LC7 概念 |
|---|---|---|
"continue" | 有工具要执行,继续循环 | AgentAction |
"stop" | 模型完成,退出循环 | AgentFinish |
"compact" | token 溢出,先压缩历史再继续 | (OpenCode 独有,生产需求) |
教学框架(LangChain/LangGraph)的循环只有 continue/stop 两态。OpenCode 加了 compact 第三态——当检测到 token 溢出,循环不退出也不直接继续,而是先压缩历史再继续。这是真实产品处理 F2 章"context window 限制"的工程方案(OC5 详讲)。
Effect 在循环中的具体用法
看 runLoop 如何用 Effect 表达副作用。这对理解 TS Agent 实现很重要:
// 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.gen ≈ RunnableLambda(包装工作单元),Effect.all ≈ RunnableParallel(并发),yield* ≈ await(串联)。但 Effect 更强大——原生支持中断、重试、资源管理(onInterrupt)。LangChain 的 Runnable 用 config 的 callbacks 实现类似功能,但不如 Effect 类型安全。
串行保证:ensureRunning
runLoop 不直接被调用,而是通过 loop() → ensureRunning,保证一个会话同时只有一个循环在跑:
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 → break | → | LG8 tools_condition 返回 END |
| filterCompactedEffect 读消息 | → | LG6 Checkpointer.get 加载状态 |
| agents.get 取配置 | → | LG8 create_react_agent 的 model+tools+prompt |
| SessionTools.resolve 解析工具 | → | LG8 ToolNode |
| handle.process 调 LLM | → | LC3 model.invoke |
| result "continue/stop/compact" | → | 条件边路由(多了 compact 态) |
| ensureRunning 串行 | → | 单 thread 串行执行 |
| Effect.onInterrupt 清理 | → | LG6 interrupt + 恢复 |
小结
- runLoop 的
while(true)就是 F3 章 Agent 循环的生产实现。 - Plan:读消息、取 agent、组装 system、解析工具;Execute:
handle.process()调 LLM;Update:判断 result,break 或 continue。 hasToolCalls+finish是"继续/停止"的判断,对应 LC7 的二态。- result 多了 compact 第三态——处理 token 溢出(生产级特征)。
- Effect 框架(gen/all/yield*/onInterrupt)是 TS 版的 Runnable,思想相通。
你看懂了 Agent 的"大脑循环"。下一章 OC3 · 工具系统:看 OpenCode 如何用 Tool.define 统一所有工具——从 46 行的 todo.ts 到 tool.ts 的 wrap 装饰器。对应 LC6 的 @tool 和 LG8 的 ToolNode。