返回课程首页

14

在运行过程中改变 Agent 的方向

使用 Steering、Follow-up 和队列控制正在运行的 Agent。

源码基线:Pi v0.82.0 · commit 518855d

桌面 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 依次:

  1. 发出 turn_end
  2. 调用 prepareNextTurn
  3. 调用 shouldStopAfterTurn
  4. 抽取 Steering Queue;
  5. 若仍有 Tool Call 或 Steering,进入下一 Turn;
  6. 当前工作本来要结束时,再抽取 Follow-up Queue;
  7. 都为空才 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。但源码会先尝试处理队列:

  1. Drain Steering;
  2. 没有 Steering 时 Drain Follow-up;
  3. 有消息则以这些消息启动新 Prompt Run;
  4. 两个队列都空才抛错。

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;
  • 发出 abort Event,包含被清除的两组消息。

当前代码不会在这个路径清空 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 会立即执行并返回;
  • input Extension 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 排队需要显式选择 steerfollowUp

24. 自测

  1. Steering 和 Follow-up 分别在哪个边界被抽取?
  2. Steering 能否阻止已经开始的 Tool Call?
  3. one-at-a-timeall 怎样改变模型调用次数?
  4. shouldStopAfterTurn 和 Steering 哪个先执行?
  5. 最后一条是 Assistant 时,continue() 怎样处理队列?
  6. Harness nextTurn 与 Follow-up 有什么区别?
  7. 入队消息什么时候进入 Session?
  8. Harness Abort 会清空哪些队列?