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

126 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运行中输入队列与 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](https://developers.openai.com/blog/run-long-horizon-tasks-with-codex) | 长任务依赖计划、验证、修复和可持续的外部状态,而不是单轮大 prompt | 会话运行态要可恢复;用户中途修正不能重置整轮任务 |
| [Claude Code hooks](https://code.claude.com/docs/en/hooks-guide) | `UserPromptSubmit``PreToolUse``PostToolUse``Stop` 等生命周期点允许注入上下文或阻断动作 | runtime guidance 应只在明确生命周期边界注入,不直接改写当前消息流 |
| [Building AI Coding Agents for the Terminal](https://arxiv.org/html/2603.05344v1) | Agent harness 把输入层、工具层、上下文层和执行层拆开;输入可通过线程安全队列进入执行循环 | 运行中输入应先入队,再由 Agent loop 主线程消费 |
| [Event-driven agentic loops](https://boundaryml.com/podcast/2025-11-05-event-driven-agents) | 用户输入、LLM chunk、tool call、interrupt 都是事件;UI、LLM、持久化各自投影 | `display_messages``model_messages``run_events` 必须分离,避免一个状态源服务所有场景 |
## 目标设计
```text
用户输入
-> 如果当前 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 |
## 后端实现约定
### 状态读取
前端优先读取:
```text
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 的主状态源。
### 输入队列
```text
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
```text
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`