Steering lets a client redirect an agent while the agent is still working, so a follow-up message reshapes the answer in flight instead of cancelling the Run and starting over, or waiting for it to finish.
In the AI Transport SDK, a Run encapsulates the agent's output for a single turn (including multiple iterations around an agentic loop). The last release, v0.5.0, made a single agent turn survive a crash by splitting a Run into re-attemptable Steps. This one is about what happens when the user changes their mind while the agent is still processing.
Changing your mind mid-run
A user asks the agent to plan a 3-day trip to Lisbon. Halfway through the response they decide they want 5 days, under £800. Today you have two options.
You can cancel the Run and start a new one with the combined prompt. The agent throws away every token and every piece of reasoning and research it produced so far, and the user watches the answer restart from scratch.
Or, you can wait for the Run to finish and then send the follow-up as a second turn. The user sits through a full answer they already know is wrong before they get to correct it.
Steering is the third option. The client sends the follow-up into the Run that's already going, the agent picks it up on its next loop iteration, and it keeps the tokens, research, and reasoning it already has.
Steering is hard to build without a transport like Ably behind the conversation.
For steering to work, the client has to get a message to the agent while the agent is still responding. Over a traditional HTTP and SSE transport, there's no way for the client to send data to the server after the initial request. There's no way for the client to send a follow-up prompt to the agent to shape its output. You can open a second HTTP request for the follow-up, but then your server has to route it to the exact process running the agent's loop, and where that process lives depends on how you deploy.
Using the AI Transport SDK the agent's loop reads its input from an Ably channel, and the client publishes the steering message onto that channel, so it stops mattering which process the loop runs in or whether the client that sent the prompt is still connected. The same client and agent code works on a serverless function, a long-lived server, or a Temporal or WDK workflow.
Send a steering message
A Run you started with session.view.send returns the active Run. You call steer(...) on it to send a follow-up prompt into the same Run, which is routed to the exact same agent process that started the Run:
const activeRun = await session.view.send(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Plan a 3-day trip to Lisbon.' }],
}));
const { published, outcome } = activeRun.steer(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Make it 5 days, and keep it under £800.' }],
}));
const { serial } = await published;
const { consumed, runTerminalReason } = await outcome;
steer(...) gives you back two promises. published resolves once the steering message reaches the channel, with the serial Ably assigned it. outcome resolves once you know whether the agent acted on it: consumed is true when the agent saw the message before it sent its response, and false when the Run ended first. If it comes back false, tell the user the message didn't land rather than dropping it silently.
What the agent does
The agent runs a loop. After each response it checks whether it has any new input it hasn't answered yet, and a steering message counts as input:
const run = session.createRun(invocation, { signal: req.signal });
while (run.view.hasOlder()) await run.view.loadOlder();
await run.start();
while (run.hasInput()) {
const conversation = run.view.getMessages().map(({ message }) => message);
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
messages: await convertToModelMessages(conversation),
abortSignal: run.abortSignal,
});
await run.pipe(result.toUIMessageStream());
}
await run.end({ reason: 'complete' });
run.hasInput() returns true while there's an unanswered message, whether that's the original prompt or a steering message that arrived while the last response was streaming. Each pass rebuilds the conversation from run.view.getMessages(), so the steering message is already in the list the model sees. run.pipe records which steering messages the agent consumed on that pass, which is what resolves the client's consumed flag.
Interrupt the model call the moment it arrives
The loop above finishes the current model call before it picks up a steering message. If you want the agent to stop generating and react straight away, pass an onSteer(...) hook that aborts the model call without ending the Run:
let stepAbort = new AbortController();
const run = session.createRun(invocation, {
signal: req.signal,
onSteer: () => stepAbort.abort(),
});
while (run.hasInput()) {
stepAbort = new AbortController();
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
messages: await convertToModelMessages(conversation),
abortSignal: AbortSignal.any([run.abortSignal, stepAbort.signal]),
});
const pipeResult = await run.pipe(result.toUIMessageStream());
const steerInterrupted =
pipeResult.reason === 'complete' &&
stepAbort.signal.aborted &&
!run.abortSignal.aborted;
if (steerInterrupted) {
result.finishReason.catch(() => {});
continue;
}
}
onSteer(...) triggers the stepAbort cancellation signal, which cancels only the in-flight model call. The Run's own abortSignal stays untouched, so the Run lives on and the loop restarts with the steering message in the conversation. The two signals do different jobs on purpose: run.abortSignal ends the whole Run, and stepAbort ends one model call.
Steer, cancel, or send alongside
Steering is one of three ways a client can act on a Run that's already processing. Which one you want depends on whether the earlier direction still stands:
- Steer keeps the Run and its output. Use it when the new message refines where the agent is already heading, like turning a 3-day plan into a 5-day one under budget.
- Cancel and re-prompt ends the Run and starts a new one.
session.cancel(runId)fires the Run'sabortSignal, streaming stops, and the Run ends withreason: 'cancelled'. Use it when the earlier direction was wrong and you want to start over. - Send alongside starts a second Run next to the first, with its own
runId. Both stream at once. Use it for a quick follow-up where you want to keep both answers, like "/btw what's the weather in Lisbon".
What the client sees
Steering travels over the Ably channel like everything else in AI Transport, so it inherits the same guarantees the rest of the conversation has:
- Every tab and device on the conversation sees the steering message and the redirected response.
- A client that reconnects after a drop or joins late catches up from channel history, steering messages included.
- The
consumedflag tells the sender whether the agent acted on the message before the Run ended, so the UI can flag a steering message that didn't land instead of dropping it, or can trigger a brand new Run for that follow-up prompt.
Get started
Install the SDK:
npm install @ably/ai-transport
- Read the docs
- Read the steering and interruption guide
- See what AI Transport is
- Read the source and demos
- Sign up free: you need an Ably account and an API key to run a session, and the free tier covers everything you need to start.



