Files
zk-data-agent/docs/technical-architecture/12-runtime-guidance-queue.md
T
2026-06-12 18:15:52 +08:00

6.1 KiB
Raw Blame History

运行中输入队列与 Runtime Guidance 注入

背景

用户在一个会话运行中继续输入,是 Agent 产品的基本能力。这个输入不能直接当成普通 user message 写入当前模型历史,否则会产生三个问题:

  1. 串台:前端切换 session 或 URL 状态滞后时,新输入可能被写进旧 session。
  2. 取消误伤:新建任务或继续输入会触发新的 run,从而取消当前正在运行的 run。
  3. 上下文污染:运行中的输入如果直接进入 model_messages,会破坏当前 tool_use/tool_result 顺序,甚至触发 Bedrock/Anthropic 的 tool_result 校验错误。

正确做法是把运行中输入先作为 UI 和 runtime 的外部事件持久化,等 Agent loop 进入安全边界时再决定如何注入。

主流方案对比

方案 关键机制 对本项目的启发
OpenAI Codex long-horizon tasks 长任务依赖计划、验证、修复和可持续的外部状态,而不是单轮大 prompt 会话运行态要可恢复;用户中途修正不能重置整轮任务
Claude Code hooks UserPromptSubmitPreToolUsePostToolUseStop 等生命周期点允许注入上下文或阻断动作 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_messagesmodel_messagesrun_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,在每轮模型调用前消费 pending guidance

agent loop boundary
  -> consume_session_guidance(session_id, run_id)
  -> append hidden user message:
     <runtime-guidance>
     用户在任务运行中补充了以下引导...
     </runtime-guidance>
  -> display=false
  -> 继续模型调用

这个注入点必须在 tool_result 已经写回之后、下一次模型调用之前,不能插在 tool_use 和 tool_result 中间。

前端交互

  1. 当前 session idle:输入框 Enter 仍然正常发送。
  2. 当前 session running:输入框不禁用;Enter 写入 queue,清空输入框。
  3. pending 输入显示为 composer 上方 chip
    • 编辑:取消 queue item,把文本恢复到输入框。
    • 引导:改成 kind=guidance 并绑定 active run_id
    • 删除:取消 queue item。
  4. 停止 run 时,后端同时取消该 session 下 pending queue item,避免下一轮误消费。

不做的事

  • 不在运行中输入时自动创建新 run。
  • 不把 pending 输入直接写入 display_messages
  • 不把 guidance 展示成普通用户消息;它是运行中的控制信号,不是对话历史。
  • 不依赖前端内存判断最终状态;刷新后必须能从 DB 完整恢复。

验收点

  1. 同一个账号同时打开两个 session,分别运行任务,输入不会串台。
  2. A session 运行中切到 B session 输入,B 的输入只进入 B 的 queue。
  3. A session 运行中输入后刷新,pending chip 仍存在。
  4. 点击“引导”后,下一次模型调用前能消费 guidance,并在活动区记录注入事件。
  5. 点击停止后,对应 run 的进程和 pending queue 都被取消。
  6. 触发 compact 后,UI 仍能从 display_messages 回放完整历史;模型只使用 compact 后的 model_messages