8.7 KiB
8.7 KiB
运行中输入队列与 Runtime Guidance 注入
背景
用户在一个会话运行中继续输入,是 Agent 产品的基本能力。这个输入不能直接当成普通 user message 写入当前模型历史,否则会产生三个问题:
- 串台:前端切换 session 或 URL 状态滞后时,新输入可能被写进旧 session。
- 取消误伤:新建任务或继续输入会触发新的 run,从而取消当前正在运行的 run。
- 上下文污染:运行中的输入如果直接进入
model_messages,会破坏当前 tool_use/tool_result 顺序,甚至触发 Bedrock/Anthropic 的 tool_result 校验错误。
正确做法是把运行中输入先作为 UI 和 runtime 的外部事件持久化,等 Agent loop 进入安全边界时再决定如何注入。
主流方案对比
| 方案 | 关键机制 | 对本项目的启发 |
|---|---|---|
| OpenAI Codex long-horizon tasks | 长任务依赖计划、验证、修复和可持续的外部状态,而不是单轮大 prompt | 会话运行态要可恢复;用户中途修正不能重置整轮任务 |
| Claude Code hooks | UserPromptSubmit、PreToolUse、PostToolUse、Stop 等生命周期点允许注入上下文或阻断动作 |
runtime guidance 应只在明确生命周期边界注入,不直接改写当前消息流 |
| Building AI Coding Agents for the Terminal | Agent harness 把输入层、工具层、上下文层和执行层拆开;输入可通过线程安全队列进入执行循环 | 运行中输入应先入队,再由 Agent loop 主线程消费 |
| Event-driven agentic loops | 用户输入、LLM chunk、tool call、interrupt 都是事件;UI、LLM、持久化各自投影 | display_messages、model_messages、run_events 必须分离,避免一个状态源服务所有场景 |
目标设计
用户输入
-> 如果当前 session idle: 正常发送,创建 run
-> 如果当前 session running: 写入 agent_input_queue
-> UI 展示 pending chip
-> 用户可编辑、删除、引导
-> 引导: kind=guidance,绑定当前 run_id
-> Agent loop 在安全插入点消费 guidance
-> guidance 以 display=false 的 user message 注入 model_messages
数据分层
| 数据 | 作用 | 是否允许 compact 覆盖 |
|---|---|---|
model_messages |
给模型推理用,可压缩、可摘要、可隐藏注入 | 允许 |
display_messages / agent_display_messages |
给 UI 回放用,append-only,不因为 compact 丢历史 | 不允许 |
run_states |
当前 run 的状态、耗时、取消能力 | 不允许用前端内存替代 |
run_events |
右侧活动区事件流 | 不允许只存在 SSE 内存里 |
agent_input_queue |
运行中输入、guidance、待处理后续输入 | 不允许直接写进 display/model messages |
后端实现约定
状态读取
前端优先读取:
GET /api/sessions/{session_id}/state
返回:
messages: DB 中agent_display_messages的增量或全量。activity_events: DB 中run_events的增量或全量。run:run_states最新状态。input_queue: 当前 pending 输入队列。
旧接口 GET /api/sessions/{session_id} 只作为兼容兜底,不应该再作为实时 UI 的主状态源。
输入队列
POST /api/sessions/{session_id}/input-queue
GET /api/sessions/{session_id}/input-queue
PATCH /api/sessions/{session_id}/input-queue/{item_id}
DELETE /api/sessions/{session_id}/input-queue/{item_id}
字段约定:
| 字段 | 说明 |
|---|---|
kind=next_turn |
运行中输入的默认状态,只展示在 composer 上方,不进入模型 |
kind=guidance |
用户显式点击“引导”后进入当前 run |
run_id |
guidance 应绑定当前 active run;未绑定时由后端尝试绑定 latest active run |
status=pending |
UI 可见,等待处理 |
status=consumed |
已被 runtime 注入 |
status=cancelled |
用户编辑/删除/取消 run 后不再处理 |
Runtime 注入
LocalCodingAgent 提供 runtime_guidance_provider,在 Agent loop 的安全边界消费 pending guidance:
agent loop safe boundary
-> consume_session_guidance(session_id, run_id)
-> append hidden user message:
<runtime-guidance>
用户在任务运行中补充了以下引导...
</runtime-guidance>
-> display=false
-> 继续模型调用
可用插入点:
| 插入点 | 时机 | 处理策略 |
|---|---|---|
before_model |
每次模型调用前 | 常规消费,适合上一轮工具完成后的补充 |
before_tools |
模型已经给出工具计划,但工具还没开始执行 | 运行时先为上一批未执行工具补 synthetic tool_result,标记为 runtime_guidance_replan,再注入 guidance,让模型重新判断是否继续原计划、调整参数或换计划 |
during_tool_interrupted |
工具已经开始执行,且用户引导明显要求停止、改目标、换参数、纠错 | 运行时给当前工具传入单工具 interrupt event,并通过 process registry 终止当前工具进程;当前工具结果落盘后注入 guidance,让模型重规划 |
after_tool |
工具执行期间或刚完成后收到补充型 guidance,或工具没有中间输出无法及时中断 | 保留当前工具结果,停止继续执行同批旧工具计划,注入 guidance 让模型判断继续、补充或重跑 |
before_finish |
模型准备给最终回复前 | 注入 guidance,让模型判断是修正最终输出、补充信息,还是转为后续任务 |
约束:
- 不把 guidance 插在
tool_use和tool_result中间。 before_tools不直接删除 assistant 的工具计划,而是补一组“未执行、被 runtime 跳过”的 tool result,保证 Anthropic/Bedrock 的消息顺序合法。during_tool_interrupted不复用整轮 run cancel event,而是构造“整轮取消 OR 当前工具中断”的组合 cancel event 传给当前工具,避免把用户引导误判成整轮取消。- 立即中断依赖工具合作:bash / python / Jupyter / 远端执行等接入 cancel_event 或 process registry 的工具可以被终止;纯同步且没有中间输出的工具只能在返回后进入
after_tool重规划。
引导策略判断
前端不暴露复杂按钮,用户仍然只点击“引导”。系统内部按安全点自动处理:
| 用户引导类型 | 默认策略 |
|---|---|
| 工具未开始前的纠偏、改目标、改参数 | before_tools 重规划 |
| 工具完成后的补充要求 | before_model 注入下一次模型调用 |
| 即将结束前的格式、总结、补充输出要求 | before_finish 注入并继续一轮 |
| 已经运行中的长工具纠偏 | 明显停止/改目标/纠错类引导触发 during_tool_interrupted;补充输出类引导进入 after_tool |
模型负责在收到 <runtime-guidance> 后判断如何吸收:继续原计划、调整计划、说明冲突或转为后续任务;运行时只负责选择合法插入点和维护消息协议。
前端交互
- 当前 session idle:输入框 Enter 仍然正常发送。
- 当前 session running:输入框不禁用;Enter 写入 queue,清空输入框。
- pending 输入显示为 composer 上方 chip:
- 编辑:取消 queue item,把文本恢复到输入框。
- 引导:改成
kind=guidance并绑定 activerun_id。 - 删除:取消 queue item。
- 停止 run 时,后端同时取消该 session 下 pending queue item,避免下一轮误消费。
不做的事
- 不在运行中输入时自动创建新 run。
- 不把 pending 输入直接写入
display_messages。 - 不把 guidance 展示成普通用户消息;它是运行中的控制信号,不是对话历史。
- 不依赖前端内存判断最终状态;刷新后必须能从 DB 完整恢复。
验收点
- 同一个账号同时打开两个 session,分别运行任务,输入不会串台。
- A session 运行中切到 B session 输入,B 的输入只进入 B 的 queue。
- A session 运行中输入后刷新,pending chip 仍存在。
- 点击“引导”后,最近的安全插入点能消费 guidance,并在活动区记录注入事件。
- 如果 guidance 在工具执行前到达,旧工具计划不执行,并产生
runtime_guidance_replan_before_tools活动事件。 - 点击停止后,对应 run 的进程和 pending queue 都被取消。
- 触发 compact 后,UI 仍能从
display_messages回放完整历史;模型只使用 compact 后的model_messages。