您的位置:首页 > 手游攻略 > 03-V1 主循环的完整生命周期

03-V1 主循环的完整生命周期

作者:互联网  时间: 2026-07-15 19:28:07  

一、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.RunCommandsession.prompt()loop()state.ensureRunning(sessionID, lastAssistant, runLoop(sessionID))runLoop 进入 while(true)

03-V1 主循环的完整生命周期

二、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 之后的消息中,类型为 compactionsubtask 的 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 需要四个条件同时满足:

  1. lastAssistant?.finish —— 最新助手消息有 finish 状态(非 undefined)
  2. !["tool-calls"].includes(lastAssistant.finish) —— finish 不是 "tool-calls"(tool-calls 表示模型要调工具,需要继续循环)
  3. !hasToolCalls —— 助手消息中没有待执行的工具调用(排除 provider 执行的 和 orphaned interrupted 的)
  4. 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 —— 中断时的清理闭包,标记为 AbortedError
  • processor.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 ...

工具组装的四步流程:

  1. registry.tools({ modelID, providerID, agent, permission }) —— 从工具注册表查询可用工具。按 Agent 配置(agent.tools 白名单/黑名单)和会话权限过滤
  2. ProviderTransform.schema —— 按模型 provider 转换工具 schema(不同 provider 的 JSON Schema 细节差异)
  3. tool({ description, inputSchema, execute }) —— 包装成 AI SDK 的 AITool 对象。execute 内部通过 EffectBridge 把 Effect 工具执行桥接到 Promise
  4. MCP 资源工具 —— 如果有 MCP 服务器支持 resources,额外注入 list_mcp_resourcesread_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]

三层发现:

  1. 全局层:~/.config/opencode/AGENTS.md(或 ~/.claude/CLAUDE.md),全局指令
  2. 项目层:从 ctx.directory 向上 findUp 查找 AGENTS.md/CLAUDE.md/CONTEXT.md,第一个匹配即停止(不叠加祖先目录)
  3. 配置层:opencode.jsoninstructions 数组,支持本地路径(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"})})

流处理管道:

  1. llm.stream(streamInput) —— 调用 LLM 获取事件流
  2. Stream.tap(handleEvent) —— 每个事件交给 handleEvent 处理
  3. Stream.takeUntil(() => ctx.needsCompaction) —— 检测到 overflow 时提前终止流
  4. Stream.runDrain —— 消费完整个流
  5. Effect.retry(SessionRetry.policy) —— 指数退避重试(provider 错误)
  6. Effect.catch(halt) —— 错误处理(ContextOverflowError 设 needsCompaction
  7. 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 = trueStream.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 的判断顺序(短路求值):

  1. structured !== undefined → break —— 结构化输出成功捕获
  2. content-filter → break —— 内容被 provider 过滤,报错
  3. json_schema 未产出结构化 → break —— 结构化输出失败
  4. result === "stop" → break —— processor 返回 stop(权限拒绝/错误)
  5. result === "compact" → 创建 compaction task → continue —— processor 检测 overflow
  6. 默认 → 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) —— 中断时标记 AbortedError
  • outcome === "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 架构

handleEventswitch(value.type) 按事件类型分发,每个事件类型独立处理。这让 LLM 流的解析与业务逻辑解耦——新增事件类型只需加一个 case,不影响其他事件处理。

5. "中断安全"的双重保障

finalizeInterruptedAssistant(阶段 5 创建)+ Effect.onInterrupt(阶段 7 注册)确保循环被中断时助手消息被正确标记为 AbortedError,不会留下半完成状态。cleanup()Effect.ensuring)确保未完成工具被标记 error,快照 patch 被写入。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4