游乐游手机版
首页/AI教程/文章详情

深度全面剖析V2双循环与V1版本核心差异对比

时间:2026-07-15 22:31
V2 版本的双循环架构构成了这次重构的核心框架。它将原先混杂在一起的一层循环拆解为职责清晰的两层结构。让我们深入分析其具体实现——外层循环负责管理用户输入的排队机制,内层循环则专注于执行每一轮对话。这种分层设计使得“用户何时发送指令”与“模型如何生成回复”实现了彻底的解耦。 04-V2 双循环与 V

V2 版本的双循环架构构成了这次重构的核心框架。它将原先混杂在一起的一层循环拆解为职责清晰的两层结构。让我们深入分析其具体实现——外层循环负责管理用户输入的排队机制,内层循环则专注于执行每一轮对话。这种分层设计使得“用户何时发送指令”与“模型如何生成回复”实现了彻底的解耦。

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

一、双循环结构总览

// packages/core/src/session/runner/llm.ts:383-406const 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) returnyield* failInterruptedTools(input.sessionID)let promotion: SessionInput.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefinedlet shouldRun = input.force || hasSteer || hasQueuewhile (shouldRun) { // ← 外层:队列驱动let needsContinuation = truelet step = 1while (needsContinuation) {// ← 内层:轮次驱动const result = yield* runTurn(input.sessionID, promotion, step)needsContinuation = result.needsContinuationstep = result.step + 1promotion = "steer"// ← 每轮结束后默认检查 steerif (!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 版本:V1 仅有一层 while(true) 循环,所有涉及“继续”的逻辑——包括工具调用、compaction、overflow 处理——都混杂在一起,需要依靠 outcome: "break" | "continue" 和 task 分支进行控制。V2 通过这种分层设计,代码的可维护性得到了显著提升。

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

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

2.1 steer 与 queue 的二态输入模型

V2 在 SessionInput 层将用户输入划分为两种投递方式:

// 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 版本完全没有的能力。

这项设计的精髓在于,steer 消息的注入时机被精确限定在 turn 之间的短暂窗口内。

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

steer 的注入操作发生在 runTurnAttempt 的开头阶段,具体是在加载历史和构建 request 之前:

// packages/core/src/session/runner/llm.ts:187-196if (promotion) {const cutoff = yield* EventV2.latestSequence(db, session.id)let promoted = 0if (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. 内层循环默认检查 steer:代码第 400 行的 promotion = "steer" 表明,内层每轮结束后,下一轮默认会检查是否有 steer 消息。这意味着用户可以在 agent 执行工具的过程中进行插队,插队消息会在当前 turn 流结束后、下一个 turn 开始前被注入。

2.3 与 V1 的对比

V1 的用户消息处理模式是“调用方驱动”的——外部代码调用 prompt() 后,runLoop 才会启动。如果 agent 正在执行工具(处于 while(true) 迭代中),用户的新消息只能等待循环退出后,通过下一次 prompt() 调用才能进入系统。在 V1 中,根本没有“插队”这个概念。

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

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

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

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

// 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 事件——它只负责持久化,并不负责执行。

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

// packages/core/src/session/runner/llm.ts:232-271const providerStream = llm.stream(request).pipe(Stream.runForEach((event) =>Effect.gen(function* () {// ...yield* publish(event)// ← 先持久化 tool-callif (event.type !== "tool-call" || event.providerExecuted) return// ← provider 执行的跳过needsContinuation = trueconst 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-resultLLMEvent.toolResult({id: event.id, name: event.name,result: settlement.result, output: settlement.output,}),settlement.outputPaths ?? [],),),),).pipe(FiberSet.run(toolFibers))// ← eager start 为 fiber}),),)

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 作为“跳转信号”:

// packages/core/src/session/runner/llm.ts:152-166type 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 抛出:

// 触发点 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 的递归重启

runTurn 作为 runTurnAttempt 的包装层,通过 Effect.catchDefect 捕获 TurnTransitionError 并进行递归调用:

// packages/core/src/session/runner/llm.ts:369-381const 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.yieldNowif (defect.transition._tag === "ContinueAfterOverflowCompaction")return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)return yield* runTurn(sessionID, undefined, defect.transition.step) // ← 递归重启}),),)})

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

// packages/core/src/session/runner/llm.ts:355-367const runAfterOverflowCompaction: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {return yield* runTurnAttempt(sessionID, promotion, step).pipe( // ← 注意:没有传 recoverOverflowEffect.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.yieldNowreturn yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)}),),)})

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

关键区别在于 defect(不可恢复)与 failure(可恢复)的不同语义:

  • Effect.fail 会被 Stream.runForEach 内部的错误处理机制捕获,可能导致流部分消费后进入错误状态。
  • Effect.die 作为 defect,会立即中止当前 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 个工具调用,它们会被串行执行:

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 立即启动,然后继续消费流:

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

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

在流结束后,统一等待所有 fiber 完成:

// packages/core/src/session/runner/llm.ts:141-142, 296const 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:谁先完成就使用谁的结果。

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

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,避免出现孤儿进程:
// packages/core/src/session/runner/llm.ts:295, 306if (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 流:

// 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.toLLMEvents(位于 ai-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 流:

// packages/core/src/session/runner/llm.ts:232-234const 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 与 locally-executed 工具区分
// llm.ts:243if (event.type !== "tool-call" || event.providerExecuted) return

LLMEvent.tool-call 携带 providerExecuted 标志——如果 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
// packages/core/src/session/compaction.ts:195-203const 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.ts 中的 stream),需要独立的适配逻辑。

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

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.dieFiberSetuninterruptibleMask 这三个 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
来源:https://juejin.cn/post/7662054174280908863
上一篇飞算JavaAI工程流水线,告别手搓CRUD高效开发 下一篇如何设计Agent Harness:完整指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Vibe Coding出海首月获取100用户赚美金的方法
AI教程 · 2026-07-15

Vibe Coding出海首月获取100用户赚美金的方法

VibeCoding出海获客的关键在于精准找到付费用户,而非写代码。通过竞品情报监听、定向搜索潜在客户、多维画像筛选、个性化开发信触达以及持续跟进动态,跑通五关链路,提升获客效率与转化率。

程序员Agent协作方式实践指南
AI教程 · 2026-07-15

程序员Agent协作方式实践指南

基于TRAE与DeepSeek,以工具、记忆、Skill三系统构建Agent协作体系。工具系统执行确定性任务,记忆系统实现分层记忆与增量构建,Skill系统处理复杂推理。通过实践验证,该方案提升了可观测性与交互性。

大模型微调全自动工厂:基座模型成为AI专家
AI教程 · 2026-07-15

大模型微调全自动工厂:基座模型成为AI专家

LlamaFactory开源高效微调框架,提供WebUI和命令行,集成LoRA、QLoRA、FlashAttention等,支持超100种模型,低显存下完成微调、偏好训练及模型导出,大幅降低大模型定制门槛。

Hermes上下文压缩架构:长任务Agent不失忆的关键设计
AI教程 · 2026-07-15

Hermes上下文压缩架构:长任务Agent不失忆的关键设计

Hermes上下文压缩架构通过可替换引擎、双层压缩及预检、响应后、错误恢复三触发器管理当前窗口。先剪枝降噪后生成结构化handoff摘要,阈值计算预留输出空间,避免长窗口过早压缩。该机制保障长任务Agent的上下文完整性,不替代长期记忆。

年首批被AI取代的测试员已送外卖
AI教程 · 2026-07-15

年首批被AI取代的测试员已送外卖

去年冬天,前同事老周发了一条朋友圈,定位在望京的一处外卖站点。配文只有一句话:“以前跑测试用例,现在跑单,都是跑。” 我问他近况。他说被裁三个月了,面试了十几家公司,手工测试岗位要么早就招满了,要么上来就问“你会不会调 prompt 做自动化测试”。 他苦笑,写了八年测试用例,如今连面试机会都拿不到