Skip to main content

How to Add a Stop Button to an AG-UI Chat

· 6 min read
Jagdish Kumawat
Founder @ Dewiride

A long AI answer needs a way out: a Stop button that ends it where it is. With the AG-UI TypeScript client, the stop itself is one call, abortRun(). Around it, the page needs to know when a run is going, show Stop instead of Send, and not mistake the stop for an error. This guide adds all of that to a React chat and shows the result.

What abortRun Does​

HttpAgent from @ag-ui/client sends each run with fetch, tied to its own AbortController. abortRun() aborts that controller, which cancels the request and the stream of events. The client then ends the run with an event of its own — a RUN_ERROR with the code abort — and runAgent() returns normally instead of throwing. The words that arrived before the stop stay in the agent's messages.

Prerequisites​

Step 1: Know When a Run Is Going​

Keep a piece of state that's true from sending until the run ends — successfully, with an error, or stopped:

app/chat.tsx
const [isRunning, setIsRunning] = useState(false);

async function send(text: string) {
// … add the message …
setIsRunning(true);

try {
await agent.runAgent(/* … */);
} finally {
setIsRunning(false);
}
}

finally runs whatever happens, so the button always comes back.

Step 2: Stop the Run​

app/chat.tsx
function stop() {
agentRef.current?.abortRun();
}

Step 3: One Button That Changes​

While a run is going, the form shows Stop; otherwise Send:

app/chat.tsx
{isRunning ? (
<button type="button" onClick={stop}>
Stop
</button>
) : (
<button type="submit" disabled={!draft.trim()}>
Send
</button>
)}

macOS-style code window titled app/chat.tsx showing the stop function that calls abortRun on the agent, and a button that shows Stop with type button while isRunning is true and Send with type submit otherwise

Stop is type="button", so clicking it never submits the form.

Step 4: Don't Show the Stop as an Error​

If the page shows RUN_ERROR messages, skip the client's own abort event:

app/chat.tsx
await agent.runAgent(
{},
{
onMessagesChanged: ({ messages }) => setMessages([...messages]),
onRunErrorEvent: ({ event }) => {
// Stop ends the run with the client's own "abort" event: not an error.
if (event.code !== "abort") {
setError(event.message);
}
},
},
);

Without this check, pressing Stop puts the browser's wording for a cancelled request on screen — in Chrome, signal is aborted without reason (Fixed: signal is aborted without reason After Stopping an AG-UI Run).

Step 5: Stop the Work Behind the Page​

abortRun() cancels the browser's request. If that request goes to your own server route, which calls the agent in turn, pass the browser request's signal on to that call — then Stop reaches the agent too:

app/api/chat/route.ts
const agent = new HttpAgent({
url: getAgentSettings().url,
fetch: (url, init) => fetch(url, { ...init, signal: request.signal }),
});

(How to Cancel an Upstream Request When the Browser Disconnects in Next.js)

Step 6: Try It​

Ask for something long. While the answer streams in, Stop replaces Send:

A chat page while a long answer about setting up a VPN on Windows 11 streams in, with the Stop button next to the text box

Press it. The answer ends where it was, Send comes back, and no error appears:

The chat after pressing Stop: the VPN answer cut off mid-list, the Send button back and no error message

The two screenshots come from two runs, so the wording differs. The text that arrived before the stop stays in the conversation.

Common Mistakes​

MistakeWhat happensDo this instead
Setting isRunning back only after successAfter a stop or an error, the button stays on StopReset it in finally
A Stop button with the default type inside the formClicking it submits the formtype="button"
Showing every RUN_ERRORStop shows the browser's cancel message as an errorSkip code: "abort"
Not passing the signal on in the server routeThe agent keeps working after StopPass request.signal to the agent call

Troubleshooting​

SymptomLikely causeFix
signal is aborted without reason after StopThe abort event is shown as an errorSkip code: "abort" in onRunErrorEvent
Stop does nothingThe request doesn't use the agent's abort signal — for example a custom requestInit without signalReturn signal: this.abortController.signal (How to Customize the Request an AG-UI HttpAgent Sends)
The button stays on StopisRunning isn't reset in finallyMove setIsRunning(false) into finally

FAQ​

Can the person continue the conversation after Stop?​

Yes. The agent and its threadId stay the same, so the next message continues the conversation.

Is the stopped answer saved?​

The words that arrived stay in the agent's messages on the page. Whether the server keeps the partial answer depends on the agent behind it.

Conclusion​

A Stop button for an AG-UI chat is abortRun() on the agent, an isRunning state that swaps Send for Stop and resets in finally, a check that skips the client's abort event, and — when a server route sits in between — request.signal passed on to the agent call.

Additional Resources​