Steer 不会修改正在传输的 HTTP/SSE 请求,也不会立即创建第二个 Turn。它在 ActiveTurn 锁下核对目标,把输入追加到当前 TurnState,并由 run_turn 在下一个安全采样边界写入历史。
接受 Steer 的四个前置条件
- ActiveTurn 存在且其中仍有 RunningTask;
- 调用者给出的 expected Turn ID(若有)与活动 id 一致;
- TaskKind 是 Regular;
- 用户 input 非空。
任一条件失败都不修改 TurnState。NoActiveTurn 会把原始 input 返还给上层,使 UserInput handler 能用它创建新 Turn;ID mismatch 和非 steerable task 则明确报错,不能降级为“随便启动一轮”。
async def steer(input, expected_id=None):
async with active_turn.lock() as active:
if active is None or active.task is None:
raise NoActiveTurn(return_input=input)
task = active.task
if expected_id is not None and expected_id != task.turn.id:
raise ExpectedTurnMismatch(expected_id, task.turn.id)
if task.kind != REGULAR:
raise ActiveTurnNotSteerable(task.kind)
if not input:
raise EmptyInput()
extra = await merge_additional_context()
pending = [ResponseItem(x) for x in extra]
pending.append(UserInput(input))
await queue.extend_and_accept_mailbox(active.turn_state, pending)
return task.turn.id
源码明确接受“持有 ActiveTurn lock 时再取得 SessionState/TurnState”的组合,并用 lint expectation 记录原因:目标检查和队列更新必须原子。其他路径若采用相反锁顺序就可能死锁,因此这是需要全局保持的锁序约束。
Steer 在模型哪里生效
正在运行的 stream 使用已经构造的 Prompt 和 StepContext,不会被中途改写。采样完成后 has_pending_input 使 needs_follow_up 为真;下一轮开头 get_pending_input 排空,hooks 处理并把内容记录进 ContextManager,然后才构建新 Prompt。
# 当前 request 已发出,不修改
current_result = await consume_current_stream()
if input_queue.has_pending_input(turn):
steers = await input_queue.get_pending_input(turn)
await hooks_and_record_inputs(steers)
next_step = await capture_step_context(turn)
await sample(history_including(steers), next_step)
AdditionalContext 先与 Session 跨 Turn accumulator merge,再转换为 ResponseItems排在 UserInput 之前。可选 Responses API client metadata 写入 TurnMetadataState,使后续请求携带最新调用方元数据,但不会更换 Turn id。
expected Turn ID 解决什么竞态
客户端看到 Turn A 正运行后发 steer;网络延迟期间 A 已完成、B 已开始。没有 expected id,文字会被合法地写入 B,语法上成功、语义上错误。expected id 把客户端观察转成 compare-and-set:只有活动 id 仍等于 A 才提交。
Review 和 Compact 拒绝 Steer 是输入契约,而非技术限制。Review 使用合成 rubric 和受限子线程;Compact 重写历史。把普通用户输入混进它们会破坏输出解析或压缩边界。
评论
登录后即可评论