active_item 是 UI 生命周期的暂态指针,不是正在拼装的权威历史项。权威内容来自 OutputItemDone;它会在工具开始执行前立即写入 History 与 Rollout,即使 Turn 随后取消,调用事实也不会消失。
具体问题与启用条件
本节统一说明活动项清理、完整项记录、工具 future 和 Completed 的边界,回答断流与取消时哪些事实已经提交。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| Item Added 可投影 | Message/Reasoning/WebSearch 等 | 建立 active TurnItem |
| Item Done | 完整 ResponseItem 可反序列化 | 进入记录和路由 |
| 工具调用 | ToolRouter 产生 Local ToolCall | 记录后再创建异步 future |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| active_item | 采样循环栈 | Added→Done | 用于 item_id 与 Started/Delta 对齐 |
| 完整 ResponseItem | ContextManager/Rollout | 对话历史 | Done 后立即记录 |
| in_flight FuturesOrdered | 采样循环 | 到响应结束后 drain | 工具结果保持调用顺序 |
| LastResponse.items_added | 传输映射层 | Completed 后 previous 基线 | 与 History 是不同用途的副本 |
机制调用链如下:
Item Added
→ active_item + optional ItemStarted
→ Delta 仅引用 active ID
→ Item Done
→ finish parsers/consumer + active_item.take
→ Tool call: 先 record item,再创建 tool future
→ Non-tool: finalize/ItemCompleted,再 record item
→ Completed
→ drain in-flight results
→ 工具 output 进入 History,下一采样
机制怎样工作
Added 阶段只保存可投影的 TurnItem;Function/Custom ToolCall 往往没有普通 TurnItem,但仍可建立参数 consumer。Done 时若服务端漏了 Item ID,Core会尝试从先前 active item 补齐,随后立即清空 active 状态。
本地 ToolCall 的完整 Item 先由 record_completed_response_item 写入 History/Rollout,再创建带 child cancellation token 的执行 future,设置 needs_follow_up=true。这条顺序很关键:工具即使取消或进程崩溃,History 里仍有与后续 aborted/error output 可配对的 call。非工具项先生成最终 TurnItem 与客户端 Completed,再记录原始 ResponseItem;最终事实还可能触发 memory citation 或 mailbox 语义。响应 Completed 后才 drain FuturesOrdered,记录工具结果。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
async def handle_done(item: ResponseItem, state: StreamState):
await state.finish_preview_and_text(state.active_item)
previous_ui_item, state.active_item = state.active_item, None
call = tool_router.try_build_call(item)
if call is not None:
await history.record(item) # 副作用开始前的事实屏障
future = asyncio.create_task(
tool_runtime.execute(call, state.cancel.child())
)
state.in_flight.append(future)
state.needs_follow_up = True
return
finalized = await finalize_turn_item(item)
if finalized is not None:
if previous_ui_item is None:
await emit_started(finalized)
await emit_completed(finalized)
await history.record(item)
async def finish_response(state: StreamState):
results = [await future for future in state.in_flight] # ordered drain
for result in results:
await history.record(result.to_response_item())
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| Added 后断流、无 Done | active/UI 暂态存在 | 不记录半项 | 是 | flush 展示状态并重试 |
| Done 工具项后取消 | Call 已记录,future 可能取消 | TurnAborted/aborted output | 下一 Turn 可继续 | 保持 call/output 配对 |
| ToolRouter RespondToModel | Call Item 已记录 | 追加错误 output 并 follow-up | 由模型修正 | 下一采样 |
| Fatal 路由错误 | 完整项可能已到达 | 终止采样/Turn | 否 | 上报 Fatal |
设计取舍与验证
将 History 提交点放在 Item Done,而非 Response Completed,使同一响应中已完成的工具调用在后续断流时仍可恢复;代价是一次失败响应可能留下若干已完成 Item。重试从最新 History 重建正是为此设计。active_item 只服务 UI,避免它与持久事实争夺权威。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| Call 在工具执行前进入 History | 取消工具后检查 Rollout | 仍存在完整 call |
| Added/Delta 不产生 History 项 | 中断半流 | 原始历史无半截 Message |
| 多个工具结果按调用顺序 drain | 并行完成顺序反转测试 | History 顺序仍稳定 |
Mini Codex 对照
Mini Codex 应定义 commit_completed_item() 作为唯一 History 入口。工具 future 必须在 call 提交之后创建;UI active state 放在采样局部对象,绝不能直接引用可变 History 项。
本节边界
本节关闭了内容事实边界。5.20 转向安全元数据,说明 Safety Buffering 在客户端究竟做什么、又不做什么。
评论
登录后即可评论