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

对应问题 Q3:逐行追踪 prompt.ts:1081-1632runLoop 一次完整 while(true) 迭代,覆盖消息加载、latest 判断、工具组装、指令发现、流处理、三个 continue 站点与 break 条件。 勘察依据:packages/opencode/src/session/prompt.tsprocessor.tsmessage-v2.tstools.tsinstruction.tscompaction.ts

一、runLoop 函数签名与入口

ts 复制代码
// 1081:1086:opencode/packages/opencode/src/session/prompt.ts
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
    const 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)

二、while(true) 迭代的 7 个阶段

ts 复制代码
// 1088:1130:opencode/packages/opencode/src/session/prompt.ts
while (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),
    ) ?? false

  if (
    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 如何加载消息历史

ts 复制代码
// 574:576:opencode/packages/opencode/src/session/message-v2.ts
export 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 后的模型消费。核心逻辑:

ts 复制代码
// 521:572:opencode/packages/opencode/src/session/message-v2.ts
export function filterCompacted(msgs: Iterable<WithParts>) {
  const result = [] as WithParts[]
  const completed = new Set<string>()
  let retain: MessageID | undefined
  for (const msg of msgs) {
    result.push(msg)
    if (retain) {
      if (msg.info.id === retain) break
      continue
    }
    if (msg.info.role === "user" && completed.has(msg.info.id)) {
      const part = msg.parts.find((item): item is CompactionPart => item.type === "compaction")
      if (!part) continue
      if (!part.tail_start_id) break
      retain = part.tail_start_id
      if (msg.info.id === retain) break
      continue
    }
    // ...
    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 的用途

ts 复制代码
// 585:601:opencode/packages/opencode/src/session/message-v2.ts
export function latest(msgs: WithParts[]) {
  let user: User | undefined
  let assistant: Assistant | undefined
  let finished: Assistant | undefined
  for (const msg of msgs) {
    const info = msg.info
    if (info.role === "user" && (!user || info.id > user.id)) user = info
    if (info.role === "assistant" && (!assistant || info.id > assistant.id)) assistant = info
    if (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 退出判断

ts 复制代码
// 1106:1130:opencode/packages/opencode/src/session/prompt.ts
const hasToolCalls =
  lastAssistantMsg?.parts.some(
    (part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
  ) ?? false

if (
  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 分发

ts 复制代码
// 1132:1168:opencode/packages/opencode/src/session/prompt.ts
step++
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") break
  continue
}

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 行)

ts 复制代码
// 1144:1147:opencode/packages/opencode/src/session/prompt.ts
if (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 行)

ts 复制代码
// 1149:1159:opencode/packages/opencode/src/session/prompt.ts
if (task?.type === "compaction") {
  const result = yield* compaction.process({
    messages: msgs, parentID: lastUser.id, sessionID, auto: task.auto, overflow: task.overflow,
  })
  if (result === "stop") break
  continue
}

触发条件tasks 中有 type === "compaction" 的 part------这是之前 compaction.create() 插入的压缩任务。compaction.process 用 compaction agent 生成摘要,返回 "stop"(压缩失败,break)或继续。continue 让循环用压缩后的消息重新开始。

站点 3:overflow create continue(第 1167 行)

ts 复制代码
// 1161:1168:opencode/packages/opencode/src/session/prompt.ts
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
}

触发条件lastFinished(最新已完成的助手消息)存在,且不是 summary 消息,且 isOverflow 判断 token 超限。compaction.create 只插入一个 compaction part(不立即执行),continue 让循环在下一轮的"站点 2"处理它。这是"检测 → 创建任务 → 下一轮执行"的两步模式。

isOverflow 的判断逻辑

ts 复制代码
// 168:178:opencode/packages/opencode/src/session/compaction.ts
const 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 解析 + 提醒注入 + 助手消息创建

ts 复制代码
// 1170:1219:opencode/packages/opencode/src/session/prompt.ts
const agent = yield* agents.get(lastUser.agent)
// ... agent not found error handling ...
const maxSteps = agent.steps ?? Infinity
const isLastStep = step >= maxSteps
msgs = 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) return
  msg.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 流处理

ts 复制代码
// 1221:1286:opencode/packages/opencode/src/session/prompt.ts
const 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") ?? false
  const 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 配置组装工具

ts 复制代码
// 41:134:opencode/packages/opencode/src/session/tools.ts
export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
  agent: Agent.Info
  model: Provider.Model
  session: Session.Info
  processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
  bypassAgentCheck: boolean
  messages: 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

ts 复制代码
// 155:169:opencode/packages/opencode/src/session/instruction.ts
const 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 函数):

ts 复制代码
// 60:68:opencode/packages/opencode/src/session/instruction.ts
const 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"原则

ts 复制代码
// 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 分发:

ts 复制代码
// 627:683:opencode/packages/opencode/src/session/processor.ts
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
  ctx.needsCompaction = false
  ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true

  return yield* Effect.gen(function* () {
    yield* Effect.gen(function* () {
      ctx.currentText = undefined
      ctx.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):

ts 复制代码
// 278:537:opencode/packages/opencode/src/session/processor.ts
const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) {
  switch (value.type) {
    case "reasoning-start":   // 创建 reasoning part,写入 SQLite
    case "reasoning-delta":   // 增量追加文本,updatePartDelta
    case "reasoning-end":     // 结束 reasoning,设 time.end

    case "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 part
    case "step-finish":       // 捕获快照,计算 usage/cost,写入 step-finish part + patch,异步 summary,检测 overflow
    case "text-start":        // 创建 text part
    case "text-delta":        // 增量追加文本
    case "text-end":          // 触发 plugin "experimental.text.complete",结束 text part

    case "finish":            // 空操作(finish 信息在 step-finish 中处理)
  }
})

关键事件处理细节

tool-call 的 DOOM_LOOP 检测

ts 复制代码
// 356:380:opencode/packages/opencode/src/session/processor.ts
const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)  // DOOM_LOOP_THRESHOLD = 3

if (
  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 检测

ts 复制代码
// 477:483:opencode/packages/opencode/src/session/processor.ts
if (
  !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 的返回值

ts 复制代码
// 679:681:opencode/packages/opencode/src/session/processor.ts
if (ctx.needsCompaction) return "compact"
if (ctx.blocked || ctx.assistantMessage.error) return "stop"
return "continue"
  • "compact" ------ overflow 了,需要压缩
  • "stop" ------ 权限拒绝(blocked)或出错(error)
  • "continue" ------ 正常完成,继续下一轮

阶段 8:outcome 判断与循环控制

ts 复制代码
// 1288:1336:opencode/packages/opencode/src/session/prompt.ts
if (structured !== undefined) {
  handle.message.structured = structured
  handle.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 const
if (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 ------ 正常继续下一轮
ts 复制代码
// 1330:1336:opencode/packages/opencode/src/session/prompt.ts
}).pipe(
  Effect.ensuring(instruction.clear(handle.message.id)),
  Effect.onInterrupt(() => finalizeInterruptedAssistant),
)
if (outcome === "break") break
continue
  • Effect.ensuring(instruction.clear(handle.message.id)) ------ 清理指令文件声明跟踪
  • Effect.onInterrupt(finalizeInterruptedAssistant) ------ 中断时标记 AbortedError
  • outcome === "break"break 退出 while
  • 否则 continue 进入下一轮

阶段 9:循环退出后

ts 复制代码
// 1338:1339:opencode/packages/opencode/src/session/prompt.ts
yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope))
return yield* lastAssistant(sessionID)
  • compaction.prune ------ 异步清理过期 compaction(Effect.ignore 忽略错误,Effect.forkIn 不阻塞)
  • lastAssistant(sessionID) ------ 返回最新助手消息(带 parts)

三、完整迭代流程图

bash 复制代码
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 被写入。

相关推荐
科技大视界1 小时前
投资AI项目,传统尽调不够用了——李章虎律师拆解算法、数据、算力三大雷区
人工智能·算法·数据挖掘
纳米体育数据2 小时前
体育数据API接口怎么选?纳米数据一站式接入18+项目,助力产品快速上线
人工智能
SL-staff2 小时前
(十九)「JVS-Rules规则引擎 V2.5」— 简单评分卡节点
人工智能·低代码·规则引擎·jvs-rules·决策流·信用评分卡·简单评分卡节点
xd1855785552 小时前
电影匹配引擎-基于鸿蒙的心情电影推荐应用开发实践
人工智能·安全·华为·harmonyos·鸿蒙
sramdram2 小时前
离线语音识别芯片|智能离线语音SoC芯片语音识别方案
人工智能·语音识别·语音识别芯片·离线语音识别芯片
MemOS2 小时前
Memory 系统全面横评:谁让 Agent 具备更强长期智能?
人工智能
delishcomcn2 小时前
AI视觉+分切机:烫金箔废边自动寻优算法实战
人工智能
秦任之2 小时前
Gliding Horse 整体架构拼图:当 AI Agent 有了自己的操作系统
人工智能·架构
CIO_Alliance2 小时前
企业级AI化转型基础认知(2):大语言模型、RAG、Agent与微调核心概念一次厘清
人工智能·深度学习·机器学习·语言模型·ipaas·系统集成·企业cio联盟