04-V2 双循环与 V1 的核心差异

04-V2 双循环与 V1 的核心差异

核心命题 :V1 的 runLoop 是一个自给自足的 while(true) 巨函数------AI SDK 既管流又管工具,compaction 是循环内的一个 task 分支,用户消息靠外部重新调 prompt() 驱动。V2 的 run() 拆成 外层队列循环 + 内层轮次循环 ,用队列驱动的 steer 插队、Effect.die 异常跳转、FiberSet 并发结算、原生 @opencode-ai/llm 流四个机制,把 V1 的"单体循环"重构为"编排者 + 协作者"的微内核。

一、双循环结构总览

ts 复制代码
// packages/core/src/session/runner/llm.ts:383-406
const run = Effect.fn("SessionRunner.run")(function* (input: {
  readonly sessionID: SessionSchema.ID
  readonly force: boolean
}) {
  const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, "steer")
  const hasQueue = hasSteer ? false : yield* SessionInput.hasPending(db, input.sessionID, "queue")
  if (!input.force && !hasSteer && !hasQueue) return
  yield* failInterruptedTools(input.sessionID)
  let promotion: SessionInput.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined
  let shouldRun = input.force || hasSteer || hasQueue
  while (shouldRun) {                           // ← 外层:队列驱动
    let needsContinuation = true
    let step = 1
    while (needsContinuation) {                  // ← 内层:轮次驱动
      const result = yield* runTurn(input.sessionID, promotion, step)
      needsContinuation = result.needsContinuation
      step = result.step + 1
      promotion = "steer"                        // ← 每轮结束后默认检查 steer
      if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer")
    }
    shouldRun = yield* SessionInput.hasPending(db, input.sessionID, "queue")
    promotion = shouldRun ? "queue" : undefined
  }
})

两层循环的职责切分

维度 外层 while(shouldRun) 内层 while(needsContinuation)
驱动源 queue 队列(用户排队消息) steer 队列 + 工具调用继续
退出条件 queue 待处理 无工具调用且无 steer
promotion 策略 "queue" → 提升一条排队消息 "steer" → 提升所有插队消息
step 重置 每次进入内层重置为 1 保持递增,受 agent.steps 上限约束
语义 "处理完用户的一个完整请求" "完成一个 provider turn 并决定是否继续"

V1 只有一层 while(true)------所有"继续"逻辑(工具调用、compaction、overflow)都混在同一层,靠 outcome: "break" | "continue" 和 task 分支控制。V2 的分层让"用户输入管理"和"轮次执行"彻底解耦。

二、Q1:外层队列循环如何支持异步用户 steer(插队)?

2.1 steer 与 queue 的二态输入模型

V2 在 SessionInput 层将用户输入分为两种投递方式(Delivery 类型):

ts 复制代码
// packages/core/src/session/input.ts:245-266 (promoteSteers)
export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
  db, events, sessionID, cutoff,
) {
  const rows = yield* db.select().from(SessionInputTable)
    .where(and(
      eq(SessionInputTable.session_id, sessionID),
      isNull(SessionInputTable.promoted_seq),   // ← 未提升
      eq(SessionInputTable.delivery, "steer"),    // ← steer 类型
      lte(SessionInputTable.admitted_seq, cutoff),// ← 在当前 turn 之前提交
    ))
    .orderBy(asc(SessionInputTable.admitted_seq))
    .all().pipe(Effect.orDie)
  return yield* publish(db, events, sessionID, rows)
})
  • queue :排队消息------用户发了一条新指令,等当前对话轮次完全结束后才接入。对应 V1 的"重新调用 prompt()"。
  • steer :转向消息------用户在 agent 正在工作时插入指令,在当前 turn 结束后立即注入,不等待 queue。这是 V1 完全不具备的能力。

2.2 steer 的注入时机:turn 之间的精确窗口

steer 的注入发生在 runTurnAttempt开头,在加载历史和构建 request 之前:

ts 复制代码
// packages/core/src/session/runner/llm.ts:187-196
if (promotion) {
  const cutoff = yield* EventV2.latestSequence(db, session.id)
  let promoted = 0
  if (promotion === "steer") promoted = yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
  if (promotion === "queue") {
    promoted += Number(yield* SessionInput.promoteNextQueued(db, events, session.id))
    promoted += yield* SessionInput.promoteSteers(db, events, session.id, cutoff)  // ← queue 也顺便提升 steer
  }
  if (promoted > 0) currentStep = 1   // ← 有新输入则 step 重置
}

关键设计:

  1. cutoff 快照latestSequence 取当前事件序列号,只有 admitted_seq <= cutoff 的 steer 才会被提升。这避免了"提升过程中用户又发了一条 steer"的竞态------新 steer 留到下一个 turn 窗口。
  2. step 重置if (promoted > 0) currentStep = 1------steer 注入后 step 归 1,意味着 agent 有完整的步数预算来响应新指令,不会因为"已用了 9 步"而立即触发 MAX_STEPS_PROMPT
  3. 内层循环默认 steerpromotion = "steer"(第 400 行)------内层每轮结束后,下一轮默认检查 steer。这意味着用户可以在 agent 执行工具的过程中插队,插队消息会在当前 turn 流结束后、下一个 turn 开始前被注入。

2.3 与 V1 的对比

V1 的用户消息处理是"调用方驱动"的------外部代码调用 prompt()runLoop 启动。如果 agent 正在执行工具(while(true) 迭代中),用户的新消息只能等循环退出后通过下一次 prompt() 调用进入。没有"插队"概念。

V2 的 steer 机制把"用户输入"从"函数调用参数"升级为"持久化队列项"------agent 运行期间用户可以随时 admit 一条 steer,runner 会在 turn 边界自动消费它。这是 V2 作为"可恢复的持久会话"的基础能力。

三、Q2:toolMaterialization.settle() 与 V1 AI SDK 工具调度的本质区别

3.1 V1:AI SDK 拥有工具执行权

V1 的 streamText() 调用把工具定义传给 AI SDK:

ts 复制代码
// packages/opencode/src/session/llm.ts:278-353 (V1 AI SDK 路径)
return {
  type: "ai-sdk" as const,
  result: streamText({
    // ...
    tools: prepared.tools,           // ← AI SDK 持有 execute 回调
    experimental_repairToolCall(failed) { /* ... */ },
    // ...
  }),
}

AI SDK 的 streamText 内部在收到 tool-call 事件后,自己调用 tool.execute() ,然后把 tool-result 也放进 fullStream。V1 的 processor.ts 收到的是"已经执行完"的 tool-result 事件------它只负责持久化,不负责执行。

scss 复制代码
V1 数据流:
  Provider → AI SDK streamText → [tool-call] → AI SDK 内部执行 execute() → [tool-result] → processor 持久化

3.2 V2:Runner 拥有工具执行权

V2 的 llm.stream() 只发射事件,不执行工具。tool-call 事件到达后,runner 自己调用 toolMaterialization.settle()

ts 复制代码
// packages/core/src/session/runner/llm.ts:232-271
const providerStream = llm.stream(request).pipe(
  Stream.runForEach((event) =>
    Effect.gen(function* () {
      // ...
      yield* publish(event)                                    // ← 先持久化 tool-call
      if (event.type !== "tool-call" || event.providerExecuted) return  // ← provider 执行的跳过
      needsContinuation = true
      const assistantMessageID = yield* publisher.assistantMessageID(event.id)
      yield* Effect.uninterruptibleMask((restore) =>
        restore(
          toolMaterialization.settle({                          // ← runner 自己执行工具
            sessionID: session.id,
            agent: agent.id,
            assistantMessageID,
            call: event,
          }),
        ).pipe(
          Effect.flatMap((settlement) =>
            publish(                                            // ← 执行完后再发 tool-result
              LLMEvent.toolResult({
                id: event.id, name: event.name,
                result: settlement.result, output: settlement.output,
              }),
              settlement.outputPaths ?? [],
            ),
          ),
        ),
      ).pipe(FiberSet.run(toolFibers))                          // ← eager start 为 fiber
    }),
  ),
)
scss 复制代码
V2 数据流:
  Provider → llm.stream → [tool-call] → runner 持久化 → runner 调用 settle() → [tool-result] → runner 持久化

3.3 本质区别总结

维度 V1 (AI SDK 执行) V2 (Runner 执行)
执行主体 AI SDK 内部循环 toolMaterialization.settle()
tool-result 来源 AI SDK 自动生成 Runner 手动 publish
provider-executed 区分 无(AI SDK 统一处理) 有(event.providerExecuted 跳过 settle)
执行时机 串行,阻塞流 并发(FiberSet),不阻塞流
中断安全 AI SDK 内部处理 uninterruptibleMask 保护 settle→publish 原子性
权限/授权 AI SDK 的 execute 回调内检查 settle 内部通过 PermissionV2 独立检查
输出存储 execute 返回字符串 ToolOutputStore.bound() 结构化存储 + outputPaths

核心动机 :V2 需要支持 provider-executed tools (如 DWS workflow 模型的服务端工具执行)。AI SDK 的模型只发射 tool-call,但工具可能由 provider 侧执行------这需要 runner 层面区分"谁来执行"。V1 的 AI SDK 黑盒无法表达这种语义。

四、Q3:TurnTransitionError + Effect.die 的异常流如何实现 compaction 跳转?

4.1 问题:compaction 需要中断当前 turn 并重跑

V2 有两个 compaction 触发点:

  1. compactIfNeeded(turn 开始前):如果预估 token 超限,先压缩历史再构建 request。
  2. compactAfterOverflow(turn 流失败后):如果 provider 返回 context overflow 错误,压缩历史后重跑。

两种情况都需要"放弃当前 turn 的部分工作,用压缩后的历史重新构建 request 重跑"。这在线性代码中很难优雅表达------你可能需要 return 一个特殊值,然后外层 if 判断,层层传递。

4.2 方案:用 Effect.die 抛出"语义异常"

V2 定义了 TurnTransitionError 作为"跳转信号":

ts 复制代码
// packages/core/src/session/runner/llm.ts:152-166
type TurnTransition =
  | { readonly _tag: "ContinueAfterCompaction"; readonly step: number }
  | { readonly _tag: "ContinueAfterOverflowCompaction"; readonly step: number }

class TurnTransitionError extends Error {
  constructor(readonly transition: TurnTransition) { super() }
}

const continueAfterCompaction = (step: number) => new TurnTransitionError({ _tag: "ContinueAfterCompaction", step })
const continueAfterOverflowCompaction = (step: number) =>
  new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })

runTurnAttempt 中,两个触发点用 Effect.die 抛出:

ts 复制代码
// 触发点 1:compactIfNeeded 成功(llm.ts:215-216)
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
  return yield* Effect.die(continueAfterCompaction(currentStep))

// 触发点 2:overflow 后 compactAfterOverflow 成功(llm.ts:286-288)
if (
  recoverOverflow &&
  !publisher.hasAssistantStarted() &&
  isContextOverflowFailure(overflowFailure ?? failure) &&
  (yield* restore(recoverOverflow({ sessionID: session.id, entries, model, request })))
)
  return yield* Effect.die(continueAfterOverflowCompaction(currentStep))

4.3 捕获层:Effect.catchDefect 的递归重启

runTurnrunTurnAttempt 的包装层,用 Effect.catchDefect 捕获 TurnTransitionError 并递归调用:

ts 复制代码
// packages/core/src/session/runner/llm.ts:369-381
const runTurn: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {
  return yield* runTurnAttempt(sessionID, promotion, step, compaction.compactAfterOverflow).pipe(
    Effect.catchDefect(
      Effect.fnUntraced(function* (defect) {
        if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect)  // ← 非 transition 重新抛出
        yield* Effect.yieldNow
        if (defect.transition._tag === "ContinueAfterOverflowCompaction")
          return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)
        return yield* runTurn(sessionID, undefined, defect.transition.step)   // ← 递归重启
      }),
    ),
  )
})

runAfterOverflowCompaction 是一个不带 recoverOverflow 的变体,防止二次 overflow 再次触发 compaction(死循环保护):

ts 复制代码
// packages/core/src/session/runner/llm.ts:355-367
const runAfterOverflowCompaction: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {
  return yield* runTurnAttempt(sessionID, promotion, step).pipe(   // ← 注意:没有传 recoverOverflow
    Effect.catchDefect(
      Effect.fnUntraced(function* (defect) {
        if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect)
        if (defect.transition._tag === "ContinueAfterOverflowCompaction")
          return yield* Effect.die("Post-compaction provider attempt cannot recover another overflow")
        yield* Effect.yieldNow
        return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)
      }),
    ),
  )
})

4.4 为什么用 Effect.die 而不是 Effect.fail?

关键区别在于 defect(不可恢复)vs failure(可恢复)

  • Effect.fail 会被 Stream.runForEach 内部的错误处理捕获,可能导致流部分消费后进入错误状态。
  • Effect.diedefect ,会立即中止当前 Effect 的所有正常错误处理,直接传播到 Effect.catchDefect。这保证了"跳转"语义的原子性 ------从 Effect.diecatchDefect 之间不会有任何中间逻辑误处理这个信号。

此外,Effect.die 穿透 uninterruptibleMask------即使 runTurnAttempt 的后半段被 uninterruptibleMask 包裹(llm.ts:277),Effect.die 仍能正常逃逸。如果用 Effect.fail,它可能被 uninterruptibleMask 内的 restore 块的 Effect.exit 捕获变成 Failure 退出值,需要额外判断。

4.5 对比 V1

V1 的 compaction 是循环内的 task 分支------task?.type === "compaction" 时调用 compaction.process(),结果通过 outcome 控制 break/continue。没有"跳转"语义,只是状态机流转。V1 的 overflow 同理:compaction.create() 插入 part,下一轮迭代在站点 2 执行。

V2 的异常跳转更接近 goto 语义------在任意深度抛出 Effect.die,在最外层捕获并重启。这让 runTurnAttempt 可以保持线性结构,不需要在每个可能触发 compaction 的点后面加 if (compacted) return 检查。

五、Q4:FiberSet 的 eager-start + 批量-await 模式解决了什么并发问题?

5.1 V1 的工具执行:串行阻塞

V1 通过 AI SDK 的 streamText 执行工具。AI SDK 在收到 tool-call 后,同步调用 tool.execute() ,等它返回后才继续消费流。如果模型一次输出 3 个工具调用,它们会被串行执行

scss 复制代码
V1 时间线(3 个工具调用):
  Stream → tool-call-1 → execute(1) [阻塞] → tool-result-1 → tool-call-2 → execute(2) [阻塞] → tool-result-2 → ...

工具执行期间,provider 流的消费完全停滞。如果一个工具耗时 30 秒(如大文件搜索),后续工具只能等。

5.2 V2 的 FiberSet 模式:eager-start + 批量-await

V2 在收到 tool-call 事件后,用 FiberSet.run(toolFibers)settle() 作为 fiber 立即启动,然后继续消费流:

ts 复制代码
// packages/core/src/session/runner/llm.ts:271
).pipe(FiberSet.run(toolFibers))   // ← 不 await,立即返回,继续消费流

FiberSet.run 的语义是"启动这个 Effect 作为独立 fiber,加入集合,不等待完成"。流消费继续处理下一个事件------可能是另一个 tool-call,也启动为 fiber。

在流结束后,统一 await 所有 fiber:

ts 复制代码
// packages/core/src/session/runner/llm.ts:141-142, 296
const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error>) =>
  Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers))
// ...
const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit)

Effect.raceFirst(FiberSet.join, FiberSet.awaitEmpty) 的语义:

  • FiberSet.join:等待所有 fiber 完成,如果任一失败则返回失败。
  • FiberSet.awaitEmpty:等待集合变空(所有 fiber 都被移除,无论成功失败)。
  • raceFirst: whichever completes first。

这个组合确保:无论所有工具成功还是有工具失败,只要全部结束就返回 。如果有工具失败,join 会返回 Failure;如果全部成功,awaitEmpty 先完成返回 Success。

sql 复制代码
V2 时间线(3 个工具调用):
  Stream → tool-call-1 → fiber-1 启动 → tool-call-2 → fiber-2 启动 → tool-call-3 → fiber-3 启动 → stream end
          ↓                    ↓                    ↓
          execute(1) 并行      execute(2) 并行      execute(3) 并行
          └──────── awaitToolFibers ────────┘

5.3 解决的三个并发问题

  1. 工具间并发:多个工具调用并行执行,总耗时约等于最慢的工具,而非所有工具之和。
  2. 工具与流的并发 :工具执行期间,流继续消费------后续的 text-deltareasoning-delta 等事件不被阻塞,用户能实时看到 assistant 的输出。
  3. 中断安全 :如果流被中断(Cause.hasInterrupts),FiberSet.clear(toolFibers) 会取消所有未完成的 fiber,避免孤儿进程:
ts 复制代码
// packages/core/src/session/runner/llm.ts:295, 306
if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers)
// ...
if ((stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) ||
    (settled._tag === "Failure" && Cause.hasInterrupts(settled.cause))) {
  yield* FiberSet.clear(toolFibers)
  yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
}

5.4 uninterruptibleMask 保护 settle→publish 原子性

单个工具的 settle → publish(toolResult)uninterruptibleMask 包裹(llm.ts:250-271)。这意味着即使 runner 被外部中断,已经在执行中的 settle 不会被中断------它会完成并 publish 结果,然后 fiber 才结束。这防止了"工具执行了但结果没持久化"的半完成状态。

对比 V1:AI SDK 的 execute() 回调没有这种保护------如果 streamText 被中断,正在执行的 execute() 可能被取消,结果丢失。

六、Q5:V2 用 @opencode-ai/llm 原生 stream 替代 V1 的 AI SDK streamText(),获得了什么?

6.1 V1 的 AI SDK 适配层:黑盒转换

V1 的 llm.ts 把 AI SDK 的 fullStream 适配为 LLMEvent 流:

ts 复制代码
// packages/opencode/src/session/llm.ts:372-378 (V1 AI SDK 适配)
const state = LLMAISDK.adapterState()
return Stream.fromAsyncIterable(result.result.fullStream, (e) => ...).pipe(
  Stream.mapEffect((event) => LLMAISDK.toLLMEvents(state, event)),
  Stream.flatMap((events) => Stream.fromIterable(events)),
)

LLMAISDK.toLLMEventsai-sdk.ts)是一个 200+ 行的适配器,把 AI SDK 的内部事件格式(text-deltatool-calltool-resultfinishstep-finish 等)翻译为 opencode 的 LLMEvent

问题:AI SDK 拥有 流消费和工具执行的完整生命周期。opencode 无法介入中间环节------无法在 tool-call 到达后但 tool-result 生成前做任何事情,因为 AI SDK 内部已经把这两者绑定了。

6.2 V2 的原生 stream:协议级归一

V2 直接调用 llm.stream(request)@opencode-ai/llm 包),获得原生 LLMEvent 流:

ts 复制代码
// packages/core/src/session/runner/llm.ts:232-234
const providerStream = llm.stream(request).pipe(
  Stream.runForEach((event) =>
    Effect.gen(function* () {
      // event 已经是 LLMEvent,无需适配

@opencode-ai/llm 是一个无状态协议库 ------它只负责把不同 provider 的响应归一化为 LLMEvent,不持有工具执行权、不管理重试、不做 repair。这让 V2 获得了以下 V1 不具备的能力:

6.3 V2 获得的五大能力

能力 1:provider-executed vs locally-executed 工具区分
ts 复制代码
// llm.ts:243
if (event.type !== "tool-call" || event.providerExecuted) return

LLMEvent.tool-callproviderExecuted 标志------如果 provider 侧已经执行了工具(如 DWS workflow),runner 跳过 settle(),只持久化结果。V1 的 AI SDK 无法表达"工具由 provider 执行"的语义。

能力 2:工具执行的完全控制权

Runner 可以在 tool-calltool-result 之间插入任意逻辑------权限检查(PermissionV2)、输出存储(ToolOutputStore.bound)、快照捕获、FiberSet 并发调度。V1 的 AI SDK 把这些逻辑锁死在 execute() 回调里,无法与流消费协同。

能力 3:compaction 复用同一个 llm.stream
ts 复制代码
// packages/core/src/session/compaction.ts:195-203
const summarized = yield* dependencies.llm.stream(
  LLM.request({
    model: input.model,
    messages: [Message.user(summaryPrompt)],
    tools: [],
    generation: { maxTokens: summaryOutput },
  }),
)

compaction 的摘要生成直接复用 llm.stream------同一个协议、同一个事件格式、同一个 publisher 模式。V1 的 compaction 走 AI SDK 的另一条路径(session/llm.tsstream),需要独立的适配逻辑。

能力 4:overflow 的精确检测与恢复
ts 复制代码
// llm.ts:236-240
if (LLMEvent.is.providerError(event)) {
  if (isContextOverflowFailure(event) && !publisher.hasAssistantStarted()) {
    overflowFailure = event    // ← 暂存,不立即 publish
    return
  }
}

V2 能在 providerError 事件到达时判断是否是 context overflow,如果是且 assistant 还没开始输出,就暂存错误 而不是立即 publish,然后在流结束后尝试 compactAfterOverflow 恢复。V1 的 AI SDK 把错误直接抛进流,opencode 无法在错误到达后"撤销"它来做恢复。

能力 5:多 provider 架构的统一基础

@opencode-ai/llm 作为独立协议包,可以被 V2 的 core 包直接消费,也可以被 V1 的 opencode 包通过 LLMClient.Service 消费(llm.ts:82)。这为 V1→V2 渐进迁移提供了基础------V1 可以通过 flags.experimentalNativeLlm 切换到原生运行时(llm.ts:226-269),V2 则直接使用。两条路径共享同一个 LLMEvent 协议,但 V2 摆脱了 AI SDK 的执行模型束缚。

七、V1→V2 迁移的本质动机

7.1 从"单体循环"到"编排者 + 协作者"

V1 的 prompt.ts 是一个 1600+ 行的单体循环------消息加载、工具组装、指令发现、流处理、compaction、overflow、中断处理全部耦合在 runLoop 内。V2 的 runner/llm.ts 只有 430 行,它是一个编排者

  • SessionInput 管理"用户输入何时进入"(steer/queue)
  • ToolRegistry 管理"工具如何执行"(materialize/settle)
  • SessionCompaction 管理"历史如何压缩"(compactIfNeeded/compactAfterOverflow)
  • createLLMEventPublisher 管理"事件如何持久化"(publish/flush/failUnsettledTools)
  • Snapshot 管理"文件状态如何捕获"(capture/files)

Runner 只负责把这些协作者串联成双循环,并用 Effect.die / FiberSet / uninterruptibleMask 三个 Effect 原语处理控制流。

7.2 从"AI SDK 中心"到"LLM 协议中心"

V1 的架构中心是 AI SDK------streamText 是黑盒,opencode 围着它转。V2 的架构中心是 @opencode-ai/llm 协议------llm.stream 是白盒,opencode 直接消费事件流并拥有全部控制权。这个转变的深层动机是:

  1. provider-executed tools:DWS 等服务端执行工具的模型需要 runner 区分"谁执行",AI SDK 无法表达。
  2. 持久化恢复:V2 的 runner 设计目标是"durable coding-agent Session"------每个 turn 都是可恢复的持久化单元。AI SDK 的黑盒执行模型无法提供这种粒度。
  3. 多项目并行 :V2 的 LayerNode 位置感知架构需要 llm.stream 是无状态的,可以在不同 location 间共享。AI SDK 的有状态 streamText 不适合这种模型。

7.3 迁移未完成项

runner/llm.ts 的文档注释(llm.ts:43-91)列出了大量 [ ] 未完成项------durable ownership、status 标记、runtime context assembly、snapshot/patch 持久化、cancellation settlement 等。V2 目前是"骨架已立、血肉未全"的状态。V1 仍然是生产路径,V2 是面向未来的重写。

八、附录:关键行号索引

概念 文件 行号
双循环入口 run() packages/core/src/session/runner/llm.ts 383-406
runTurn 递归包装 同上 369-381
runAfterOverflowCompaction 同上 355-367
runTurnAttempt 主体 同上 173-348
TurnTransitionError 定义 同上 152-166
Effect.die 触发点 1(compactIfNeeded) 同上 215-216
Effect.die 触发点 2(overflow) 同上 286-288
FiberSet eager-start 同上 271
awaitToolFibers 同上 141-142, 296
uninterruptibleMask 保护 settle 同上 250-271, 277
steer/queue 提升 packages/core/src/session/input.ts 245-288
hasPending 同上 170-189
toolMaterialization.settle packages/core/src/tool/registry.ts 50-82, 116-121
compactIfNeeded / compactAfterOverflow packages/core/src/session/compaction.ts 172-236
createLLMEventPublisher packages/core/src/session/runner/publish-llm-event.ts 54-423
V1 AI SDK 适配路径 packages/opencode/src/session/llm.ts 278-378
V1 native runtime 开关 同上 226-269
V1 AI SDK 适配器 packages/opencode/src/session/llm/ai-sdk.ts 76-80
相关推荐
江畔柳前堤1 小时前
roLabelImg 详细安装教程
开发语言·人工智能·后端·云原生
阿里云大数据AI技术2 小时前
分链路差异化设计的DSP准实时数仓|钛动科技基于阿里云实时计算 Flink 版 + DLF Paimon + EMR Serverless StarRocks 的实践
人工智能·flink
陕西企来客2 小时前
2026年7月AI智能搜索曝光趋势研判
大数据·人工智能·机器学习·ai智能搜索曝光
阿里云大数据AI技术2 小时前
从算力到智能体,面向 Agentic AI 的基础设施演进
人工智能·agent
hangyuekejiGEO3 小时前
GEO技术服务选型指南
大数据·人工智能·python
阿里云大数据AI技术3 小时前
EMR Serverless Spark AI Function 的双维降本实践
人工智能·sql·spark
维基框架4 小时前
GitHub源码处理提速 一趟扫描反而更慢
人工智能·github
冬奇Lab4 小时前
代码库知识库系列(05):向量检索 vs 知识图谱——加了调用图并没有变更好
人工智能
AKAMAI4 小时前
你的源服务器可能是你做出的最昂贵决定
运维·人工智能·云计算
冬奇Lab4 小时前
【无标题】
人工智能·开源