桌面 Agent 正在扫描整个项目。用户突然补充:
不用分析全部文件,只看最近修改的。
另一个用户可能输入:
当前任务完成后,再帮我生成一份总结。
两句话都发生在 Agent 运行中,但语义不同:
- 第一条应该尽快影响下一步工作;
- 第二条应该等当前任务自然结束后再开始。
Pi 用 Steering 和 Follow-up 表达这两种队列。
1. 为什么运行中不能再次 prompt
基础 Agent.prompt() 检查:
if (this.activeRun) {
throw new Error(
"Agent is already processing a prompt. " +
"Use steer() or followUp() to queue messages, " +
"or wait for completion.",
);
}
一个 Agent Run 正在维护:
- 当前 Context;
- 当前模型流;
- Tool Call 批次;
- AbortSignal;
- Event 顺序;
- 本轮新消息。
再次启动独立 prompt() 会产生两个并发 Run,状态和 Session 写入无法保持清晰顺序。因此运行中输入必须进入队列。
2. Steering 与 Follow-up 的区别
flowchart TD
I["运行中收到用户消息"] --> Q{"希望何时生效?"}
Q -->|"当前 Assistant Turn 和工具结束后"| S["Steering"]
Q -->|"当前任务本来要停止时"| F["Follow-up"]
Steering
agent.steer(message);
当前 Assistant Turn 完成,并执行完它已经产生的 Tool Call 后,在下一次模型调用前注入。
Follow-up
agent.followUp(message);
只有当前 Agent 已经没有 Tool Call,也没有 Steering Message,本来要结束时才注入。
3. Steering 不是打断当前 Tool
假设当前时序:
Assistant 已请求 read_many_files
→ Tool 正在读取 100 个文件
→ 用户发送“只看最近修改”
Steering Message 会入队,但不会撤销已经开始的工具:
sequenceDiagram
participant U as 用户
participant A as Agent Loop
participant T as Tool
participant M as 模型
A->>T: execute read_many_files
U->>A: steer("只看最近修改")
Note over A: 消息进入队列
T-->>A: Tool Result
A-->>A: turn_end
A->>A: 注入 Steering
A->>M: Tool Result + Steering
若必须尽快停止当前工具,应请求 Abort,并等待当前 Run settle,再提交新 Prompt。Abort
也是协作式取消:Pi 把 AbortSignal 传给 Provider 和 Tool,只有它们正确监听并响应时,
操作才会尽快停止。Steering 代表“下一次判断时改变方向”,不是抢占式中断。
4. Turn 边界的真实顺序
一轮 Assistant 和工具结束后,Loop 依次:
- 发出
turn_end; - 调用
prepareNextTurn; - 调用
shouldStopAfterTurn; - 抽取 Steering Queue;
- 若仍有 Tool Call 或 Steering,进入下一 Turn;
- 当前工作本来要结束时,再抽取 Follow-up Queue;
- 都为空才
agent_end。
flowchart TD
T["turn_end"] --> P["prepareNextTurn"]
P --> STOP{"shouldStopAfterTurn?"}
STOP -->|true| END["agent_end"]
STOP -->|false| S["drain Steering"]
S --> MORE{"仍有 Tool Call 或 Steering?"}
MORE -->|是| NEXT["下一 Turn"]
MORE -->|否| F["drain Follow-up"]
F --> HAS{"有 Follow-up?"}
HAS -->|是| NEXT
HAS -->|否| END
shouldStopAfterTurn 返回 true 时会在抽取 Steering/Follow-up 前结束当前 Run,队列中的消息仍等待后续处理。
5. 桌面 Agent 的最小接法
function createUserMessage(text: string): AgentMessage {
return {
role: "user",
content: [{
type: "text",
text,
}],
timestamp: Date.now(),
};
}
function sendWhileRunning(
text: string,
behavior: "steer" | "followUp",
) {
const message = createUserMessage(text);
if (behavior === "steer") {
agent.steer(message);
} else {
agent.followUp(message);
}
}
UI 不应只显示“已发送”,更准确的是:
已排队:下一 Turn 生效
或:
已排队:当前任务完成后执行
6. QueueMode 控制一次取几条
type QueueMode =
| "all"
| "one-at-a-time";
基础 Agent 的 Steering 和 Follow-up 默认都是:
one-at-a-time
可以设置:
agent.steeringMode = "all";
agent.followUpMode = "one-at-a-time";
one-at-a-time
每次 Drain 只取最早的一条:
队列:[A, B, C]
本次取:[A]
剩余:[B, C]
all
一次取出当前全部:
队列:[A, B, C]
本次取:[A, B, C]
剩余:[]
7. QueueMode 怎样改变模型请求
one-at-a-time:
sequenceDiagram
participant Q as Steering Queue
participant M as 模型
Q-->>M: A
M-->>Q: Assistant Response
Q-->>M: B
M-->>Q: Assistant Response
all:
sequenceDiagram
participant Q as Steering Queue
participant M as 模型
Q-->>M: A + B
M-->>Q: 一个 Assistant Response
选择建议:
- 用户连续修正同一指令:
all可减少额外 Turn; - 每条 Follow-up 都应得到独立回答:
one-at-a-time; - 消息之间可能互相冲突:保留顺序并逐条处理更清晰。
8. Loop 开始时也会检查 Steering
runLoop() 在第一次模型调用前就执行:
let pendingMessages =
await config.getSteeringMessages?.() || [];
因此,基础 Agent.steer() 即使在 idle 时也能入队;下一次 prompt() 开始后,它可能与新 Prompt 一起进入第一次模型请求。
但通用 AgentHarness.steer() 明确要求当前 Phase 不是 idle。两层 API 不同:
| API | idle 时 steer |
|---|---|
基础 Agent |
允许排队 |
AgentHarness |
抛 invalid_state |
coding-agent AgentSession |
允许排队,但不会自行启动 Run |
桌面应用使用哪一层,就遵守哪一层的契约。
9. Follow-up 为什么在外层循环
Loop 的内层处理:
- 当前 Assistant 的 Tool Call;
- Steering Message。
当内层已经没有继续条件,才轮询 Follow-up。若有 Follow-up,就把它设为 Pending Message,重新进入内层。
这保证:
当前任务的工具链
→ 所有应优先生效的 Steering
→ 当前任务停止点
→ Follow-up
10. 多条 Steering 的实际效果
默认 one-at-a-time:
用户排队:
A:只看最近修改
B:排除测试文件
Turn N 完成
→ 注入 A
→ 模型响应并执行工具
→ Turn N+1 完成
→ 注入 B
如果模式为 all:
Turn N 完成
→ 同时注入 A、B
→ 模型看到两条连续 User Message
Pi 不会自动把它们拼成一个字符串;它们仍是独立 Agent Message。
11. Agent.continue() 是什么
await agent.continue();
它表示从已有 Transcript 继续,不追加新的显式 Prompt。
要求:
- 当前没有 Active Run;
- Transcript 非空;
- 若最后一条不是 Assistant,直接从现有 Context 继续;
- 最后一条最终必须能转换成 User 或 Tool Result,Provider 才能接受。
如果最后一条是 Assistant,正常情况下不能直接 Continue。但源码会先尝试处理队列:
- Drain Steering;
- 没有 Steering 时 Drain Follow-up;
- 有消息则以这些消息启动新 Prompt Run;
- 两个队列都空才抛错。
12. 为什么 Continue 优先 Steering
最后一条是 Assistant 时:
flowchart TD
C["agent.continue()"] --> S{"Steering Queue 有消息?"}
S -->|有| RS["以 Steering 启动 Run"]
S -->|无| F{"Follow-up Queue 有消息?"}
F -->|有| RF["以 Follow-up 启动 Run"]
F -->|无| E["Cannot continue from assistant"]
Steering 语义上更紧急,所以优先级更高。
基础 Agent 处理从 Steering 启动的 Continue 时,会跳过 Loop 开头的第一次额外 Steering Poll,避免刚 Drain 一条后又在首个模型响应前再 Drain 一次。
13. Prompt 与 Continue 的区别
| API | 是否追加新输入 | 典型用途 |
|---|---|---|
prompt(text) |
是 | 用户发起新请求 |
continue() |
不追加显式 Prompt;可能消费队列 | Transcript 以 User/Tool Result 结尾时继续;若以 Assistant 结尾则消费未交付队列 |
steer(message) |
先排队 | 当前任务下一 Turn 改方向 |
followUp(message) |
先排队 | 当前任务结束后追加任务 |
不要把 Continue 当成“重试刚才按钮”。它对最后消息角色和队列有严格要求。
14. AgentHarness 的队列 API
通用 Harness 提供更适合桌面调用的文字 API:
await harness.steer(
"只看最近修改的文件",
);
await harness.followUp(
"完成后生成总结",
);
两者都要求 Harness 不在 idle。
Harness 还提供:
await harness.nextTurn(
"下一次显式 Prompt 前先加入这条上下文",
);
nextTurn 与 Follow-up 不同:
- 它不会自己触发 Agent Run;
- 可以在 idle 时排队;
- 下一次调用
prompt()时,队列全部取出; - 这些消息放在新的显式 User Prompt 之前。
nextTurn 是 Harness 扩展能力,不是基础 Agent 的第三个队列。
15. nextTurn 的顺序
await harness.nextTurn("先考虑兼容性");
await harness.nextTurn("再检查迁移成本");
await harness.prompt("给出最终方案");
首次 Prompt Message 数组是:
User:先考虑兼容性
User:再检查迁移成本
User:给出最终方案
随后 before_agent_start Hook 提供的 Custom Message 再追加到这个数组之后。
16. Queue Update 怎样驱动 UI
Harness 发出:
type QueueUpdateEvent = {
type: "queue_update";
steer: AgentMessage[];
followUp: AgentMessage[];
nextTurn: AgentMessage[];
};
桌面 UI 可以渲染三个区域:
下一 Turn
· 只看最近修改
任务完成后
· 生成总结
下次显式请求
· 先检查迁移成本
不要只保存计数。用户需要知道排队了什么,也可能需要清空或重新提交。
17. 排队不等于已经持久化
Steering 和 Follow-up 在被 Loop 取出后,才发出 message_start / message_end 并进入 Session。
入队时:
- 只在内存 Queue 中;
- Harness 发
queue_update; - 尚未成为 Message Entry。
因此应用崩溃可能丢失尚未交付的队列消息。若产品要求 Queue 跨重启恢复,需要单独持久化待处理队列,并在交付后去重。
18. Abort 对队列的影响
基础 Agent.abort() 只触发当前 Run 的 AbortSignal,不清空队列。
通用 AgentHarness.abort():
- 复制并清空 Steering Queue;
- 复制并清空 Follow-up Queue;
- Abort 当前普通 Run;
- 等待 idle;
- 发出
abortEvent,包含被清除的两组消息。
当前代码不会在这个路径清空 nextTurnQueue。
flowchart LR
A["harness.abort()"] --> S["清空 steer"]
A --> F["清空 followUp"]
A --> R["Abort 当前 Run"]
A -.保留.-> N["nextTurn"]
真实事件顺序是:
清空 steer/followUp
→ 触发当前 Run 的 AbortSignal
→ queue_update
→ 等待 Run idle
→ abort Event
因此桌面 UI 会先收到清空后的 queue_update,当前 Run settle 后再收到包含清除内容的
abort Event。
三层取消语义不要混用:
| 层 | Abort 是否清队列 |
|---|---|
基础 Agent |
否 |
通用 AgentHarness |
清 Steering/Follow-up,保留 nextTurn |
coding-agent AgentSession |
否;clearQueue() 是独立操作 |
coding-agent 的 _handlePostAgentRun() 会在 Run 结束后检查剩余队列并可能继续
agent.continue()。如果产品中的“取消”意味着同时取消 Steering/Follow-up 待办,应先
处理 session.clearQueue(),再 session.abort();不要假设 Abort 自动清空。
这仍不是“清空所有未来输入”:Extension 用 deliverAs: "nextTurn" 放入的 Custom Message
保存在 _pendingNextTurnMessages,不属于 clearQueue() 的范围,当前也没有对应的公共
清理 API。
19. coding-agent 的 prompt 行为
完整 coding-agent 中,运行中的普通输入若要进入 Agent 队列,必须显式选择:
streamingBehavior:
| "steer"
| "followUp";
真正走到普通输入的运行中分支且没有指定时才会抛错。两个前置入口是例外:
- 已注册的 Extension Command 会立即执行并返回;
inputExtension Handler 可以返回handled并提前结束。
直接调用 session.steer() 或 session.followUp() 时,会先:
- 拒绝不能排队执行的 Extension Command;
- 展开 Skill Command;
- 展开 Prompt Template;
- 再把 User Message 入队。
这为后面 Skills 和 Extensions 章节埋下伏笔:队列里保存的是展开后的输入,不一定是用户原始输入字符串。
20. 怎样选择 Steering 或 Follow-up
| 用户意图 | 选择 |
|---|---|
| “停止分析全部文件,只看最近修改” | Steering |
| “刚才的工具结束后不要继续删除” | Steering;若必须尽快停止则请求 Abort,并等待 settle |
| “完成重构后再写测试” | Follow-up |
| “做完后给我一份总结” | Follow-up |
| “下次我点发送时先带上这条背景” | Harness nextTurn |
| “现在另开一个并发任务” | 新 Agent/Session,不是同一队列 |
21. 完整例子
sequenceDiagram
participant U as 用户
participant UI as 桌面 UI
participant H as AgentHarness
participant M as 模型
participant T as Tool
U->>H: prompt("分析整个项目")
M-->>H: Tool Call
H->>T: 扫描文件
U->>UI: "只看最近修改"
UI->>H: steer
U->>UI: "完成后生成总结"
UI->>H: followUp
T-->>H: Tool Result
H->>M: Tool Result + Steering
M-->>H: 调整后的回答
H->>M: Follow-up
M-->>H: 总结
Steering 与 Follow-up 都是新的 User Message,会进入 Transcript 和后续 Context。
22. 常见误区
“Steering 会立刻杀死当前工具”
错误。它在当前 Assistant Turn 和工具结束后交付。
“Follow-up 会等到整个应用空闲后另开一个 Session”
错误。它仍在同一个 Agent Run 的外层循环中继续。
“QueueMode all 会把文字拼起来”
错误。它一次注入多条独立 Agent Message。
“消息调用 steer 后已经写入 Session”
错误。交付并发出 Message Lifecycle 后才持久化。
“abort 会清空所有三种 Harness 队列”
错误。当前 Harness Abort 清空 Steering 和 Follow-up,不清空 nextTurn。
“运行中再次调用 prompt 就会自动变成 Steering”
基础 Agent 不会。完整 coding-agent 的普通排队输入也要求显式
streamingBehavior;Extension Command 与被 input Handler 处理的输入是前置例外。
23. 本章小结
- 运行中不能启动第二个基础
prompt(); - Steering 在当前 Turn 和工具结束后、下一次模型调用前生效;
- Follow-up 在 Agent 本来要停止时生效;
- Steering 不会抢占当前工具;Abort 也是依赖 Provider/Tool 响应 Signal 的协作式取消;
- QueueMode 决定每次 Drain 一条还是全部;
- Turn 后顺序是 prepare、stop 判断、Steering、Follow-up;
continue()可从现有 Context 或排队消息继续,Steering 优先;- Harness 增加了
nextTurn队列; - Queue Update 适合驱动桌面待处理列表;
- 未交付队列只在内存,持久化需额外设计;
- Harness Abort 先发清空后的 Queue Update,settle 后发 Abort Event,并保留 nextTurn;
- coding-agent Abort 不清队列;取消 Steering/Follow-up 待办需组合
clearQueue(),且 Extension nextTurn Custom Message 仍会保留; - coding-agent 运行中的普通 Prompt 排队需要显式选择
steer或followUp。
24. 自测
- Steering 和 Follow-up 分别在哪个边界被抽取?
- Steering 能否阻止已经开始的 Tool Call?
one-at-a-time与all怎样改变模型调用次数?shouldStopAfterTurn和 Steering 哪个先执行?- 最后一条是 Assistant 时,
continue()怎样处理队列? - Harness
nextTurn与 Follow-up 有什么区别? - 入队消息什么时候进入 Session?
- Harness Abort 会清空哪些队列?