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 重置
}
关键设计:
cutoff快照 :latestSequence取当前事件序列号,只有admitted_seq <= cutoff的 steer 才会被提升。这避免了"提升过程中用户又发了一条 steer"的竞态------新 steer 留到下一个 turn 窗口。- step 重置 :
if (promoted > 0) currentStep = 1------steer 注入后 step 归 1,意味着 agent 有完整的步数预算来响应新指令,不会因为"已用了 9 步"而立即触发MAX_STEPS_PROMPT。 - 内层循环默认 steer :
promotion = "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 触发点:
compactIfNeeded(turn 开始前):如果预估 token 超限,先压缩历史再构建 request。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 的递归重启
runTurn 是 runTurnAttempt 的包装层,用 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.die是 defect ,会立即中止当前 Effect 的所有正常错误处理,直接传播到Effect.catchDefect。这保证了"跳转"语义的原子性 ------从Effect.die到catchDefect之间不会有任何中间逻辑误处理这个信号。
此外,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 解决的三个并发问题
- 工具间并发:多个工具调用并行执行,总耗时约等于最慢的工具,而非所有工具之和。
- 工具与流的并发 :工具执行期间,流继续消费------后续的
text-delta、reasoning-delta等事件不被阻塞,用户能实时看到 assistant 的输出。 - 中断安全 :如果流被中断(
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.toLLMEvents(ai-sdk.ts)是一个 200+ 行的适配器,把 AI SDK 的内部事件格式(text-delta、tool-call、tool-result、finish、step-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-call 带 providerExecuted 标志------如果 provider 侧已经执行了工具(如 DWS workflow),runner 跳过 settle(),只持久化结果。V1 的 AI SDK 无法表达"工具由 provider 执行"的语义。
能力 2:工具执行的完全控制权
Runner 可以在 tool-call 和 tool-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.ts 的 stream),需要独立的适配逻辑。
能力 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 直接消费事件流并拥有全部控制权。这个转变的深层动机是:
- provider-executed tools:DWS 等服务端执行工具的模型需要 runner 区分"谁执行",AI SDK 无法表达。
- 持久化恢复:V2 的 runner 设计目标是"durable coding-agent Session"------每个 turn 都是可恢复的持久化单元。AI SDK 的黑盒执行模型无法提供这种粒度。
- 多项目并行 :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 |