Learn Agents from Pi
中文
On this page

07. Interruption, Steering, and Follow-up Tasks

Real users do not wait for the agent to finish before speaking. They add information while the model is streaming, realize the direction is wrong while a tool is running, or append the next thing just as the current task is about to finish. The agent needs to distinguish three kinds of input: stop, steering, and follow-up tasks. Treating them all as new user prompts will corrupt the runtime state.

Three semantics

Stop cancels the current work. It should trigger the AbortSignal, halt the provider request or tool execution, and write the aborted state to the log.

Steering means "the current direction needs adjustment." It does not immediately kill every tool; instead, it inserts a new user message at a safe moment so the next model turn sees it. A typical example is "don't change the tests, fix the implementation."

A follow-up means "do this after the current task finishes." It should wait in the queue until the current task ends naturally, then start the next round as a new user message. A typical example is "after the fix, also update the changelog."

These three semantics are different, and the UI copy, queue behavior, and log records should differ accordingly.

Why not interrupt the tool batch immediately

Suppose the model has already launched two tools: reading a file and running the tests. The user steers at this moment: "hold off on the tests." If you kill the running tools immediately, you may leave behind half a log entry, half a process, or an unfinished file write. A safer strategy is to let the current tool batch reach a consistent boundary, then inject the steering into the next turn.

A consistent boundary usually means:

  • The current assistant message is complete.
  • All tools that were started have finished or been cancelled in a controlled way.
  • Tool results have been written to the log.
  • The next model request has not yet started.

This timing also has an extra benefit: the next model request contains the tool results and the user's steering together. Seeing "the tests failed" alongside "hold off on the tests," the model can adjust its plan in one shot, instead of being interrupted with incomplete information. This is not the fastest-responding strategy, but it makes recovery, auditing, and testing more reliable. For a coding agent, recoverability usually matters more than millisecond-level steering.

The dual-queue model

The runtime can maintain two queues:

type QueuedInput = {
  id: string;
  text: string;
  createdAt: string;
};

type AgentQueues = {
  steering: QueuedInput[];
  followUp: QueuedInput[];
};

The loop consumes the queues at two fixed checkpoints: after each turn ends (with tool results already in history and before the next model request) it checks the steering queue; only when "there are no more tool calls and the steering queue is empty" — when the task is about to end naturally — does it check the follow-up queue. If a follow-up exists, the run does not end but continues with it as a new user message. The ordering of these two checkpoints is what guarantees the "after" semantics of follow-up: as long as the task is still advancing, a follow-up always waits behind it.

The consumption policy itself is worth exposing as configuration. Mature systems offer two drain modes: "take one at a time" and "take all at once." Taking one at a time lets the model handle user inputs in order, with each getting a full reasoning turn; taking all at once saves request round-trips and suits the case where the user types several inputs in quick succession. Taking one at a time is the safer default — when two contradictory steering messages are injected at the same time, the model's behavior is hard to predict.

When merging steering, preserve the user's original wording and timing. Do not compress multiple user inputs into one vague summary, or you will lose intent. You can use an injection format like this:

User provided steering while you were working:
1. Do not edit tests.
2. Keep the public API unchanged.
Continue from the current state and adjust your plan.

There is one implementation detail that is easy to trip over: if prompt() itself works by putting the user message into the queue and then starting the loop, the loop's first queue check must not consume that message again, or the same input will be processed twice. Whatever mechanism you use (skipping the first poll, or distinguishing the initial message from a steering message), cover this scenario in a test.

Events and the UI

Queue changes should also be events:

queue_updated steering=1 followUp=0
steering_applied count=1
follow_up_started id=...

The UI needs to tell the user "queued; will apply after the current tool finishes" rather than staying silent. Otherwise the user will type the same thing again, and the model will receive multiple copies of the same instruction. Interactive interfaces usually show queued messages above the input box, while machine-readable modes (JSON, RPC) expose steer and followUp as separate commands — one kernel queue, many input entry points.

Failure modes

The most dangerous failure is appending steering directly to messages while the current assistant message is still streaming. The next turn's context can then contain an interleaved sequence of a half-finished assistant message, the user's steering message, and trailing tool results. Many providers are not tolerant of that ordering, and the model will be confused as well.

The second failure is a follow-up preempting the current task. The user says "update the docs when you're done," and the agent goes off to write documentation before the bug is even fixed — the task order is broken. The point of a follow-up is "after," not "also now."

The third failure is silently dropping the queue on abort. The user queued three follow-ups and then aborted the current task — should those three inputs be kept, cleared, or the user asked? There is no single correct answer, but there must be an explicit policy that the user can see. A queue that is silently cleared makes the user think the task will still continue.

Exercise

Add steer(text) and followUp(text) to the agent.

Acceptance criteria:

  • Calling steer while the model is streaming does not directly modify the request being sent.
  • After the current tool batch ends, steering enters the next turn's context and appears in the same request as the tool results.
  • A follow-up starts only after the current task settles naturally, and either does not trigger a new agent_start (staying within the same run) or has clear new-run semantics — pick one and write down your reasoning.
  • With several steer calls in a row, both "take one at a time" and "take all at once" modes work and behave as defined.
  • The UI or event subscribers can see the queue lengths change.
  • After a user abort, the policy for handling steering and follow-ups is explicit — keep them, clear them, or ask the user; they must not be dropped silently.