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
相关推荐
拾起_3691 小时前
05-Event Sourcing 事件系统
人工智能·开源
精神底层1 小时前
AI 学习笔记:研究方法的演变
人工智能·笔记·学习
拾起_3691 小时前
03-V1 主循环的完整生命周期
人工智能·开源
科技大视界1 小时前
投资AI项目,传统尽调不够用了——李章虎律师拆解算法、数据、算力三大雷区
人工智能·算法·数据挖掘
纳米体育数据2 小时前
体育数据API接口怎么选?纳米数据一站式接入18+项目,助力产品快速上线
人工智能
SL-staff2 小时前
(十九)「JVS-Rules规则引擎 V2.5」— 简单评分卡节点
人工智能·低代码·规则引擎·jvs-rules·决策流·信用评分卡·简单评分卡节点
xd1855785552 小时前
电影匹配引擎-基于鸿蒙的心情电影推荐应用开发实践
人工智能·安全·华为·harmonyos·鸿蒙
sramdram2 小时前
离线语音识别芯片|智能离线语音SoC芯片语音识别方案
人工智能·语音识别·语音识别芯片·离线语音识别芯片
MemOS2 小时前
Memory 系统全面横评:谁让 Agent 具备更强长期智能?
人工智能