03-V1 主循环的完整生命周期
一、runLoop 函数签名与入口
// 1081:1086:opencode/packages/opencode/src/session/prompt.tsconst runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(function* (sessionID: SessionID) {const ctx = yield* InstanceState.contextlet structured: unknownlet step = 0const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
Effect.fn("SessionPrompt.run")—— 带追踪命名的入口函数,日志和 span 中可见InstanceState.context—— 获取当前工作区上下文(directory + worktree)structured—— 捕获结构化输出(json_schema 模式)step—— 循环步数计数器,从 0 开始session—— 加载会话元信息(permission、parentID 等)
调用链:CLI.RunCommand → session.prompt() → loop() → state.ensureRunning(sessionID, lastAssistant, runLoop(sessionID)) → runLoop 进入 while(true)。

二、while(true) 迭代的 7 个阶段
// 1088:1130:opencode/packages/opencode/src/session/prompt.tswhile (true) {yield* status.set(sessionID, { type: "busy" })yield* Effect.logInfo("loop", { "session.id": sessionID, step })let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(Effect.provideService(Database.Service, database),)const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)if (!lastUser) throw new Error("No user message found in stream. This should never happen.")const lastAssistantMsg = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,)const hasToolCalls =lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),) ?? falseif (lastAssistant?.finish &&!["tool-calls"].includes(lastAssistant.finish) &&!hasToolCalls &&lastUser.id < lastAssistant.id) {const orphan = lastAssistantMsg?.parts.find((part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),)if (orphan) {yield* Effect.logWarning("loop exit with orphaned interrupted tool", { ... })}yield* Effect.logInfo("exiting loop", { "session.id": sessionID })break}
阶段 1:状态置 busy + 加载消息历史
(1) MessageV2.filterCompactedEffect 如何加载消息历史
// 574:576:opencode/packages/opencode/src/session/message-v2.tsexport const filterCompactedEffect = Effect.fnUntraced(function* (sessionID: SessionID) {return filterCompacted(yield* stream(sessionID))})
它做两件事:
第一步:stream(sessionID) —— 从 SQLite 增量加载会话全部消息(按时间序),每条消息带 info(元信息)和 parts(内容片段:text/reasoning/tool/step-start/step-finish/patch/compaction/subtask)。
第二步:filterCompacted(msgs) —— 重排消息以适应 compaction 后的模型消费。核心逻辑:
// 521:572:opencode/packages/opencode/src/session/message-v2.tsexport function filterCompacted(msgs: Iterable<WithParts>) {const result = [] as WithParts[]const completed = new Set<string>()let retain: MessageID | undefinedfor (const msg of msgs) {result.push(msg)if (retain) {if (msg.info.id === retain) breakcontinue}if (msg.info.role === "user" && completed.has(msg.info.id)) {const part = msg.parts.find((item): item is CompactionPart => item.type === "compaction")if (!part) continueif (!part.tail_start_id) breakretain = part.tail_start_idif (msg.info.id === retain) breakcontinue}// ...if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error)completed.add(msg.info.parentID)}result.reverse()// 重排:[compaction-user, summary, ...retained tail..., continue-user]// ...}
重排后的消息结构为 [compaction-user, summary, ...retained tail..., continue-user]——compaction 摘要放前面,保留的近期 tail 放中间,最新用户消息放最后。这让模型看到的上下文是"摘要 + 近期对话"而非"完整历史"。
为什么用 fnUntraced:每次 while(true) 迭代都调用,是热路径,不需要 span 开销。
阶段 2:latest() 提取关键消息
(2) lastUser/lastAssistant/lastFinished/tasks 的用途
// 585:601:opencode/packages/opencode/src/session/message-v2.tsexport function latest(msgs: WithParts[]) {let user: User | undefinedlet assistant: Assistant | undefinedlet finished: Assistant | undefinedfor (const msg of msgs) {const info = msg.infoif (info.role === "user" && (!user || info.id > user.id)) user = infoif (info.role === "assistant" && (!assistant || info.id > assistant.id)) assistant = infoif (info.role === "assistant" && info.finish && (!finished || info.id > finished.id)) finished = info}const tasks = msgs.flatMap((m) =>finished && m.info.id <= finished.id? []: m.parts.filter((p): p is CompactionPart | SubtaskPart => p.type === "compaction" || p.type === "subtask"),)return { user, assistant, finished, tasks }}
为什么用 max id 而非数组位置:filterCompacted 重排了消息顺序,数组位置不再等于时间顺序。MessageID.ascending() 保证 ID 单调递增,所以用 info.id > user.id 找最新。
四个返回值的用途:
| 返回值 | 含义 | 用途 |
|---|---|---|
lastUser | 最新用户消息 | 提取 agent/model/format 配置,决定本轮用什么 Agent 和模型 |
lastAssistant | 最新助手消息 | 判断 finish 状态,决定是否 break 退出循环 |
lastFinished | 最新已完成的助手消息(有 finish 字段) | 判断 token 是否 overflow,触发压缩 |
tasks | 未处理的 compaction/subtask 部分(比 finished 更新的) | 驱动三个 continue 站点中的两个 |
tasks 的精确定义:finished 之后的消息中,类型为 compaction 或 subtask 的 part。即"最新已完成助手消息之后还堆积的待处理工作"。tasks 是数组,pop() 从尾部取最新的。
阶段 3:break 退出判断
// 1106:1130:opencode/packages/opencode/src/session/prompt.tsconst hasToolCalls =lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),) ?? falseif (lastAssistant?.finish &&!["tool-calls"].includes(lastAssistant.finish) &&!hasToolCalls &&lastUser.id < lastAssistant.id) {// ... orphan check ...yield* Effect.logInfo("exiting loop", { "session.id": sessionID })break}
(7) break 退出的完整条件
break 需要四个条件同时满足:
lastAssistant?.finish—— 最新助手消息有 finish 状态(非 undefined)!["tool-calls"].includes(lastAssistant.finish)—— finish 不是"tool-calls"(tool-calls 表示模型要调工具,需要继续循环)!hasToolCalls—— 助手消息中没有待执行的工具调用(排除 provider 执行的 和 orphaned interrupted 的)lastUser.id < lastAssistant.id—— 最新助手消息比最新用户消息更新(即助手已经响应当前用户消息)
hasToolCalls 的排除项:
!part.metadata?.providerExecuted—— 排除 provider 端执行的工具(如 OpenAI web_search)!isOrphanedInterruptedTool(part)—— 排除中断遗留的孤儿工具调用
orphan 检查:如果 break 时发现孤儿工具,记录 warning 但仍然 break——这是清理路径,不阻塞退出。
阶段 4:step 计数 + 模型解析 + task 分发
// 1132:1168:opencode/packages/opencode/src/session/prompt.tsstep++if (step === 1)yield* title({ session, modelID: lastUser.model.modelID, providerID: lastUser.model.providerID, history: msgs }).pipe(Effect.ignore, Effect.forkIn(scope))const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)const task = tasks.pop()if (task?.type === "subtask") {yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })continue}if (task?.type === "compaction") {const result = yield* compaction.process({messages: msgs, parentID: lastUser.id, sessionID, auto: task.auto, overflow: task.overflow,})if (result === "stop") breakcontinue}if (lastFinished &&lastFinished.summary !== true &&(yield* compaction.isOverflow({ tokens: lastFinished.tokens, model }))) {yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })continue}
step === 1时异步生成会话标题(Effect.forkIn不阻塞循环)getModel解析 provider + modelID 到具体模型实例tasks.pop()取最新的待处理 task
(6) 三个 continue 站点的触发条件
站点 1:subtask continue(第 1146 行)
// 1144:1147:opencode/packages/opencode/src/session/prompt.tsif (task?.type === "subtask") {yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })continue}
触发条件:tasks 中有 type === "subtask" 的 part——这是 TaskTool 创建的子 Agent 任务(前台/后台模式)。handleSubtask 创建助手消息、执行 TaskTool、更新 part 状态,然后 continue 让循环重新加载消息并检查后续状态。
站点 2:compaction process continue(第 1158 行)
// 1149:1159:opencode/packages/opencode/src/session/prompt.tsif (task?.type === "compaction") {const result = yield* compaction.process({messages: msgs, parentID: lastUser.id, sessionID, auto: task.auto, overflow: task.overflow,})if (result === "stop") breakcontinue}
触发条件:tasks 中有 type === "compaction" 的 part——这是之前 compaction.create() 插入的压缩任务。compaction.process 用 compaction agent 生成摘要,返回 "stop"(压缩失败,break)或继续。continue 让循环用压缩后的消息重新开始。
站点 3:overflow create continue(第 1167 行)
// 1161:1168:opencode/packages/opencode/src/session/prompt.tsif (lastFinished &&lastFinished.summary !== true &&(yield* compaction.isOverflow({ tokens: lastFinished.tokens, model }))) {yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })continue}
触发条件:lastFinished(最新已完成的助手消息)存在,且不是 summary 消息,且 isOverflow 判断 token 超限。compaction.create 只插入一个 compaction part(不立即执行),continue 让循环在下一轮的"站点 2"处理它。这是"检测 → 创建任务 → 下一轮执行"的两步模式。
isOverflow 的判断逻辑:
// 168:178:opencode/packages/opencode/src/session/compaction.tsconst isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input) {return overflow({cfg: yield* config.get(),tokens: input.tokens,model: input.model,outputTokenMax: flags.outputTokenMax,})})
它检查 tokens(input + output + cache)是否超过模型上下文窗口的可用容量。
阶段 5:Agent 解析 + 提醒注入 + 助手消息创建
// 1170:1219:opencode/packages/opencode/src/session/prompt.tsconst agent = yield* agents.get(lastUser.agent)// ... agent not found error handling ...const maxSteps = agent.steps ?? Infinityconst isLastStep = step >= maxStepsmsgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe(...)const msg: SessionV1.Assistant = {id: MessageID.ascending(),parentID: lastUser.id,role: "assistant",mode: agent.name,agent: agent.name,// ... cost/tokens/model/time ...}yield* sessions.updateMessage(msg)const finalizeInterruptedAssistant = Effect.gen(function* () {if (msg.time.completed) returnmsg.error ??= MessageV2.fromError(new DOMException("Aborted", "AbortError"), { ... aborted: true })msg.time.completed = Date.now()yield* sessions.updateMessage(msg)})const handle = yield* processor.create({ assistantMessage: msg, sessionID, model }).pipe(Effect.onInterrupt(() => finalizeInterruptedAssistant))
agents.get(lastUser.agent)—— 获取用户消息指定的 Agent 配置maxSteps/isLastStep—— Agent 步数限制,最后一步注入MAX_STEPS_PROMPT并禁工具SessionReminders.apply—— 注入结构化提醒(Plan 模式、步数限制提示等)- 创建助手消息
msg(初始 cost=0、tokens=0),写入 SQLite finalizeInterruptedAssistant—— 中断时的清理闭包,标记为 AbortedErrorprocessor.create—— 创建 Processor Handle,携带onInterrupt钩子
阶段 6:工具组装 + 指令发现 + LLM 流处理
// 1221:1286:opencode/packages/opencode/src/session/prompt.tsconst outcome: "break" | "continue" = yield* Effect.gen(function* () {const lastUserMsg = msgs.findLast((m) => m.info.role === "user")const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? falseconst promptOps = yield* ops()const tools = yield* SessionTools.resolve({agent, session, model, processor: handle, bypassAgentCheck, messages: msgs, promptOps,}).pipe(...)if (lastUser.format?.type === "json_schema") {tools["StructuredOutput"] = createStructuredOutputTool({ schema: lastUser.format.schema, onSuccess(output) { structured = output } })}// ... summary fork ...yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([sys.skills(agent),sys.environment(model),instruction.system().pipe(Effect.orDie),sys.mcp(agent, session.permission),MessageV2.toModelMessagesEffect(msgs, model),])const system = [...env, ...instructions, ...(mcpInstructions ? [mcpInstructions] : []), ...(skills ? [skills] : [])]const format = lastUser.format ?? { type: "text" as const }if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)const result = yield* handle.process({user: lastUser, agent, permission: session.permission, sessionID,parentSessionID: session.parentID, system, messages: [...modelMsgs, ...(isLastStep ? [MAX_STEPS_PROMPT_MSG] : [])],tools, model, toolChoice: format.type === "json_schema" ? "required" : undefined,})
(3) SessionTools.resolve 如何根据 Agent 配置组装工具
// 41:134:opencode/packages/opencode/src/session/tools.tsexport const resolve = Effect.fn("SessionTools.resolve")(function* (input: {agent: Agent.Infomodel: Provider.Modelsession: Session.Infoprocessor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">bypassAgentCheck: booleanmessages: SessionV1.WithParts[]promptOps: TaskPromptOps}) {const tools: Record<string, AITool> = {}// ...for (const item of yield* registry.tools({modelID: ModelV2.ID.make(input.model.api.id),providerID: input.model.providerID,agent: input.agent,permission: input.session.permission,})) {const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))tools[item.id] = tool({description: item.description,inputSchema: jsonSchema(schema),execute(args, options) {return run.promise(Effect.gen(function* () {const ctx = context(args, options)yield* plugin.trigger("tool.execute.before", { tool: item.id, ... }, { args })const result = yield* item.execute(args, ctx)// ... plugin.trigger("tool.execute.after") ...return output}))},})}// ... MCP resource tools ...
工具组装的四步流程:
registry.tools({ modelID, providerID, agent, permission })—— 从工具注册表查询可用工具。按 Agent 配置(agent.tools白名单/黑名单)和会话权限过滤ProviderTransform.schema—— 按模型 provider 转换工具 schema(不同 provider 的 JSON Schema 细节差异)tool({ description, inputSchema, execute })—— 包装成 AI SDK 的AITool对象。execute内部通过EffectBridge把 Effect 工具执行桥接到 Promise- MCP 资源工具 —— 如果有 MCP 服务器支持 resources,额外注入
list_mcp_resources、read_mcp_resource等工具
context 工厂为每个工具执行提供 Tool.Context:sessionID、abortSignal、messageID、callID、agent、messages、metadata 回调、ask(权限请求)。权限通过 Permission.merge(input.agent.permission, input.session.permission) 合并 Agent 级和会话级权限规则。
结构化输出:如果 lastUser.format?.type === "json_schema",额外注入 StructuredOutput 工具,强制模型调用它返回结构化数据,onSuccess 回调把结果存入 structured 变量。
(4) instruction.system() 如何发现 AGENTS.md
// 155:169:opencode/packages/opencode/src/session/instruction.tsconst system = Effect.fn("Instruction.system")(function* () {const config = yield* cfg.get()const paths = yield* systemPaths()const urls = (config.instructions ?? []).filter((item) => item.startsWith("https://") || item.startsWith("http://"))const files = yield* Effect.forEach(Array.from(paths), read, { concurrency: 8 })const remote = yield* Effect.forEach(urls, fetch, { concurrency: 4 })return [...Array.from(paths).flatMap((item, i) => (files[i] ? [`Instructions from: ${item}n${files[i]}`] : [])),...urls.flatMap((item, i) => (remote[i] ? [`Instructions from: ${item}n${remote[i]}`] : [])),]})
发现路径(systemPaths 函数):
// 60:68:opencode/packages/opencode/src/session/instruction.tsconst globalFiles = [path.join(global.config, "AGENTS.md"),...(!flags.disableClaudeCodePrompt ? [path.join(global.home, ".claude", "CLAUDE.md")] : []),]const instructionFiles = ["AGENTS.md",...(!flags.disableClaudeCodePrompt ? ["CLAUDE.md"] : []),"CONTEXT.md", // deprecated]
三层发现:
- 全局层:
~/.config/opencode/AGENTS.md(或~/.claude/CLAUDE.md),全局指令 - 项目层:从
ctx.directory向上findUp查找AGENTS.md/CLAUDE.md/CONTEXT.md,第一个匹配即停止(不叠加祖先目录) - 配置层:
opencode.json的instructions数组,支持本地路径(glob)和远程 URL(http/https)
读取:本地文件 fs.readFileString(并发 8),远程 URL http.execute + 5 秒超时(并发 4)。返回 ["Instructions from: <path>n<content>", ...] 字符串数组。
systemPaths 的"第一个项目级匹配 wins"原则:
// 122:132:opencode/packages/opencode/src/session/instruction.ts// The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor.if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {for (const file of instructionFiles) {const matches = yield* fs.findUp(file, ctx.directory, ctx.worktree).pipe(Effect.catch(() => Effect.succeed([])))if (matches.length > 0) {matches.forEach((item) => paths.add(path.resolve(item)))break}}}
system prompt 组装顺序:[...env, ...instructions, ...(mcpInstructions ? [mcpInstructions] : []), ...(skills ? [skills] : [])]——环境信息 + AGENTS.md 指令 + MCP 指令 + Skill 指令。
阶段 7:handle.process() 流处理与结果判断
(5) processor 如何按事件类型驱动
handle.process() 把 LLM 流交给 processor.ts。核心是 handleEvent 函数,按 LLMEvent.type 分发:
// 627:683:opencode/packages/opencode/src/session/processor.tsconst process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {ctx.needsCompaction = falsectx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== truereturn yield* Effect.gen(function* () {yield* Effect.gen(function* () {ctx.currentText = undefinedctx.reasoningMap = {}yield* status.set(ctx.sessionID, { type: "busy" })const stream = llm.stream(streamInput)yield* stream.pipe(Stream.tap((event) => handleEvent(event)),Stream.takeUntil(() => ctx.needsCompaction),Stream.runDrain,)}).pipe(Effect.onInterrupt(() => Effect.gen(function* () { aborted = true; if (!ctx.assistantMessage.error) yield* halt(...) })),Effect.catchCauseIf((cause) => !Cause.hasInterruptsOnly(cause), (cause) => Effect.fail(Cause.squash(cause))),Effect.retry(SessionRetry.policy({ provider: input.model.providerID, parse, set: ... })),Effect.catch(halt),Effect.ensuring(cleanup()),)if (ctx.needsCompaction) return "compact"if (ctx.blocked || ctx.assistantMessage.error) return "stop"return "continue"})})
流处理管道:
llm.stream(streamInput)—— 调用 LLM 获取事件流Stream.tap(handleEvent)—— 每个事件交给handleEvent处理Stream.takeUntil(() => ctx.needsCompaction)—— 检测到 overflow 时提前终止流Stream.runDrain—— 消费完整个流Effect.retry(SessionRetry.policy)—— 指数退避重试(provider 错误)Effect.catch(halt)—— 错误处理(ContextOverflowError 设needsCompaction)Effect.ensuring(cleanup)—— 清理(快照 patch、未完成工具标记 error)
handleEvent 的事件类型驱动(processor.ts:278-537):
// 278:537:opencode/packages/opencode/src/session/processor.tsconst handleEvent = Effect.fnUntraced(function* (value: StreamEvent) {switch (value.type) {case "reasoning-start": // 创建 reasoning part,写入 SQLitecase "reasoning-delta": // 增量追加文本,updatePartDeltacase "reasoning-end": // 结束 reasoning,设 time.endcase "tool-input-start":// ensureToolCall(创建/更新 tool part 为 pending)case "tool-input-delta":// ensureToolCall(流式工具输入)case "tool-input-end":// ensureToolCall(工具输入完成)case "tool-call": // 更新 tool part 为 running + input + DOOM_LOOP 检测case "tool-result": // completeToolCall(设 completed + output + attachments)case "tool-error":// failToolCall(设 error 状态)case "provider-error":// throw new Error(value.message)case "step-start":// 捕获快照,写入 step-start partcase "step-finish": // 捕获快照,计算 usage/cost,写入 step-finish part + patch,异步 summary,检测 overflowcase "text-start":// 创建 text partcase "text-delta":// 增量追加文本case "text-end":// 触发 plugin "experimental.text.complete",结束 text partcase "finish":// 空操作(finish 信息在 step-finish 中处理)}})
关键事件处理细节:
tool-call 的 DOOM_LOOP 检测:
// 356:380:opencode/packages/opencode/src/session/processor.tsconst recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)// DOOM_LOOP_THRESHOLD = 3if (recentParts.length !== DOOM_LOOP_THRESHOLD ||!recentParts.every((part) =>part.type === "tool" &&part.tool === value.name &&part.state.status !== "pending" &&JSON.stringify(part.state.input) === JSON.stringify(input),)) {return}const agent = yield* agents.get(ctx.assistantMessage.agent)yield* permission.ask({permission: "doom_loop",patterns: [value.name],sessionID: ctx.assistantMessage.sessionID,metadata: { tool: value.name, input },always: [value.name],ruleset: agent.permission,})
如果最近 3 个工具调用都是同名工具 + 相同输入 + 非 pending,触发 doom_loop 权限询问——防止 Agent 陷入无限循环。
step-finish 的 overflow 检测:
// 477:483:opencode/packages/opencode/src/session/processor.tsif (!ctx.assistantMessage.summary &&isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })) {ctx.needsCompaction = true}
step-finish 时检查 token 是否 overflow,如果是则设 ctx.needsCompaction = true,Stream.takeUntil 会在下一个事件前终止流。
process 的返回值:
// 679:681:opencode/packages/opencode/src/session/processor.tsif (ctx.needsCompaction) return "compact"if (ctx.blocked || ctx.assistantMessage.error) return "stop"return "continue"
"compact"—— overflow 了,需要压缩"stop"—— 权限拒绝(blocked)或出错(error)"continue"—— 正常完成,继续下一轮
阶段 8:outcome 判断与循环控制
// 1288:1336:opencode/packages/opencode/src/session/prompt.tsif (structured !== undefined) {handle.message.structured = structuredhandle.message.finish = handle.message.finish ?? "stop"yield* sessions.updateMessage(handle.message)return "break" as const}const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)if (finished && !handle.message.error) {if (handle.message.finish === "content-filter") {handle.message.error = new SessionV1.ContentFilterError({ ... }).toObject()yield* sessions.updateMessage(handle.message)yield* events.publish(Session.Event.Error, { sessionID, error: handle.message.error })return "break" as const}if (format.type === "json_schema") {handle.message.error = new SessionV1.StructuredOutputError({ ... }).toObject()yield* sessions.updateMessage(handle.message)return "break" as const}}if (result === "stop") return "break" as constif (result === "compact") {yield* compaction.create({sessionID, agent: lastUser.agent, model: lastUser.model, auto: true, overflow: !handle.message.finish,})}return "continue" as const
outcome 的判断顺序(短路求值):
structured !== undefined→ break —— 结构化输出成功捕获content-filter→ break —— 内容被 provider 过滤,报错json_schema未产出结构化 → break —— 结构化输出失败result === "stop"→ break —— processor 返回 stop(权限拒绝/错误)result === "compact"→ 创建 compaction task → continue —— processor 检测 overflow- 默认 → continue —— 正常继续下一轮
// 1330:1336:opencode/packages/opencode/src/session/prompt.ts}).pipe(Effect.ensuring(instruction.clear(handle.message.id)),Effect.onInterrupt(() => finalizeInterruptedAssistant),)if (outcome === "break") breakcontinue
Effect.ensuring(instruction.clear(handle.message.id))—— 清理指令文件声明跟踪Effect.onInterrupt(finalizeInterruptedAssistant)—— 中断时标记 AbortedErroroutcome === "break"→break退出 while- 否则
continue进入下一轮
阶段 9:循环退出后
// 1338:1339:opencode/packages/opencode/src/session/prompt.tsyield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope))return yield* lastAssistant(sessionID)
compaction.prune—— 异步清理过期 compaction(Effect.ignore忽略错误,Effect.forkIn不阻塞)lastAssistant(sessionID)—— 返回最新助手消息(带 parts)
三、完整迭代流程图
runLoop(sessionID) 入口│├── 加载 ctx / session / step=0│└── while(true) ──────────────────────────────────────────────────────┐││├── [1] status.set(busy) + logInfo("loop")│├── [1] msgs = MessageV2.filterCompactedEffect(sessionID) ││ └── stream() 从 SQLite 加载 → filterCompacted() 重排│││├── [2] {lastUser, lastAssistant, lastFinished, tasks} = latest() ││ └── 按 max id 找最新 user/assistant/finished│││├── [3] break 判断:││ lastAssistant.finish && ≠"tool-calls" && !hasToolCalls ││ && lastUser.id < lastAssistant.id││ → break ──────────────────────────────────────► 退出循环│││├── [4] step++ + (step==1 时异步 title) + getModel + task=pop()│││├── [4a] task?.type === "subtask"││ → handleSubtask() → continue ──────────────────────────►│││├── [4b] task?.type === "compaction" ││ → compaction.process() → result=="stop"?break:continue ─> │││├── [4c] lastFinished && !summary && isOverflow││ → compaction.create() → continue ───────────────────────► │││├── [5] agents.get(lastUser.agent) + maxSteps + SessionReminders ││ + 创建助手消息 msg + processor.create(handle)│││├── [6] SessionTools.resolve(agent, session, model, permission)││ → registry.tools() 过滤 → ProviderTransform.schema →││ → tool() 包装 + MCP 资源工具 + StructuredOutput 工具 │││├── [6] instruction.system() 发现 AGENTS.md/CLAUDE.md/CONTEXT.md││ → 全局层 + 项目层(findUp 第一个匹配) + 配置层(URL/glob) │││├── [6] handle.process({ system, messages, tools, model })││ └── llm.stream() → Stream.tap(handleEvent)││ ├── reasoning-start/delta/end → reasoning part ││ ├── text-start/delta/end → text part ││ ├── tool-input-start/delta/end → ensureToolCall││ ├── tool-call → 更新 running + DOOM_LOOP 检测││ ├── tool-result → completeToolCall ││ ├── tool-error → failToolCall││ ├── step-start → 快照││ ├── step-finish → usage/cost + patch + overflow 检测 ││ └── provider-error → throw ││ └── takeUntil(needsCompaction) + retry + catch(halt) ││ └── 返回 "compact" | "stop" | "continue" │││├── [7] outcome 判断:││ structured? → break││ content-filter? → break││ json_schema 失败? → break││ result=="stop"? → break││ result=="compact"? → compaction.create() → continue││ 默认 → continue │││└── outcome=="break" ? break : continue ──────────────────────────┘ │break ──► compaction.prune() + return lastAssistant(sessionID)
四、关键设计决策
1. "消息重排而非截断"的 compaction 策略
filterCompacted 不删除旧消息,而是把 compaction 摘要放前面、保留近期 tail 放中间、最新消息放后面。这让模型始终看到"摘要 + 近期上下文",比简单截断保留更多有效信息。
2. "检测 → 创建 task → 下一轮执行"的两步 compaction
overflow 检测(站点 3)只 compaction.create() 插入一个 part,不立即执行。continue 后下一轮迭代在站点 2 才真正 compaction.process()。这种分离让"检测"和"执行"解耦,避免在 overflow 状态下还强行执行压缩。
3. "DOOM_LOOP_THRESHOLD = 3"的轻量反思
没有 Claude Code 的异步复盘系统,但通过检测"连续 3 次同名工具 + 相同输入"触发 doom_loop 权限询问,提供最基本的循环检测。这是 V1 反思层的全部。
4. "事件类型驱动"的 processor 架构
handleEvent 用 switch(value.type) 按事件类型分发,每个事件类型独立处理。这让 LLM 流的解析与业务逻辑解耦——新增事件类型只需加一个 case,不影响其他事件处理。
5. "中断安全"的双重保障
finalizeInterruptedAssistant(阶段 5 创建)+ Effect.onInterrupt(阶段 7 注册)确保循环被中断时助手消息被正确标记为 AbortedError,不会留下半完成状态。cleanup()(Effect.ensuring)确保未完成工具被标记 error,快照 patch 被写入。
-
07.15
遗忘之海船员档案全解析:遗忘之海主要角色与背景故事详细介绍
-
07.15
伊莫隐藏宝箱地点在哪-伊莫隐藏宝箱的位置是哪
-
07.15
梦幻足球手游战术搭配方案:梦幻足球手游高胜率阵型与球员配置指南
-
07.15
三国天下归心藤甲机巧队配队方法-三国天下归心藤甲机巧队如何配队
-
07.15
世界之光开局怎么玩世界之光新手入门全流程攻略
-
07.15
王者荣耀世界东方曜培养方法-王者荣耀世界一图流教你培养东方曜
-
-
-
- 03-V1 主循环的完整生命周期
- 07.15
-
- crm软件如何用AI把事后复盘变成过程经营
- 07.15
-
-
- 06-LLM 多协议抽象 Protocol 管道
- 07.15
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏