Skip to content

Put a chat on an AG-UI agent

Connect an agent served over AG-UI — Agno, LangGraph, Mastra, CrewAI, Pydantic AI — and the chat draws its replies, its reasoning, its tool calls and its approvals while the agent runs on its own server.

On this page
bash
npm install @aparte/runner-ag-ui @aparte/core
ts
import { AparteClient } from '@aparte/core';
import { createAgUiRunner } from '@aparte/runner-ag-ui';

new AparteClient({
  streamRunner: createAgUiRunner({ endpoint: 'https://agents.example.com/agui' }),
}).start();

AG-UI is the protocol many agent frameworks serve their agents over: Agno, LangGraph, Mastra, CrewAI, Pydantic AI, and TanStack AI. The agent runs its loop and its tools on its server, so the page registers no provider and picks no model. The runner is the client’s streamRunner: the chat, Stop, the conversation manager and the approval panel work as they do with aparté’s own loop.

The agent sendsThe chat shows
TEXT_MESSAGE_*the reply’s text
REASONING_*a reasoning block
TOOL_CALL_START, TOOL_CALL_ARGS or TOOL_CALL_CHUNK, TOOL_CALL_ENDa tool row, drawn with its whole arguments once the call ends
TOOL_CALL_RESULTthe row’s result
RUN_FINISHEDthe end of the reply, with its usage — or, with an interrupt, a decision to make or a tool the page runs
RUN_ERRORthe reply’s error, with the agent’s message
an HTTP errorthe reply’s error, with the start of the response body — keep secrets out of it

Each model step of the run is a turn of its own in the reply, and the usage of every step is added up and reported with the reply. A stream that ends without RUN_FINISHED ends the reply with an error. STEP_*, STATE_*, MESSAGES_SNAPSHOT, ACTIVITY_*, RAW, CUSTOM and sub-agent events are not drawn. The runner reads the answer as server-sent events, or as NDJSON when its content type says so.

A tool registered in the page — ask_user through setupAskUser(), your own through registerTool — is sent to the agent as an AG-UI frontend tool. When the agent calls it, the run pauses with the call open; the page runs the tool, shows its result in the row, and calls the agent again with it. For an agent server, the pause is an interrupt whose reason contains client_tool, or a call the agent leaves without a result. The page runs the tool as soon as the agent calls it: its needsApproval and your approval policy are not consulted, so a tool that needs a person asks in its own handler, as ask_user does.

When the agent pauses on a call that needs a person — an interrupt whose reason is tool_call or confirmation — the page asks with the same approval panel as for its own tools, and your approval policy can answer first. Every answer goes back to the agent in one resume, and the run goes on on the agent’s server, in the same reply. The pause is part of the conversation, so a saved conversation brings the question back after a reload.

ts
import { uuid } from '@aparte/core';
import { createAgUiRunner } from '@aparte/runner-ag-ui';

let conversationId = uuid();   // set it when your app opens a conversation

const runner = createAgUiRunner({
  endpoint: '/agui',
  headers: async () => ({ authorization: `Bearer ${sessionStorage.getItem('token') ?? ''}` }),
  threadId: () => conversationId,   // read on every turn
});
OptionWhat it does
endpointthe agent’s AG-UI URL; required
headerssent with every request; a function is called for each, for a fresh token
fetchthe fetch the runner calls; the global one by default
credentialsfor a cross-origin endpoint that authenticates by cookie
threadIdthe AG-UI thread a turn runs in: a string, or a function of the run’s options
forwardedPropsRunAgentInput.forwardedProps: settings your agent reads from the request
contextRunAgentInput.context: { description, value } entries for the agent

These are the AgUiRunnerOptions. By default every turn runs in a new thread with the whole history, which suits a stateless agent; give threadId when the agent keeps state per thread, as Agno does with its session — the conversation’s id is the usual choice. A paused run resumes in its own thread, whatever threadId answers. An agent on another origin than the page has to allow it: answer the preflight (OPTIONS) with Access-Control-Allow-Origin (the page’s origin) and Access-Control-Allow-Headers: content-type plus the headers you send, and with Access-Control-Allow-Credentials: true when credentials is 'include'.

py
from agno.agent.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.os.interfaces.agui import AGUI

chat_agent = Agent(model=OpenAIResponses(id="gpt-5.4"))
agent_os = AgentOS(agents=[chat_agent], interfaces=[AGUI(agent=chat_agent)])
app = agent_os.get_app()  # POST /agui

Agno serves an agent or a team over AG-UI with the AGUI interface of its AgentOS, at POST /agui; give that URL to createAgUiRunner. This is Agno’s AG-UI example.

  • A person’s message goes as text: images and files are left out.
  • An interrupt the page cannot answer ends the reply with an error that names its reason: one such as input_required, or one that names no toolCallId.
  • A refusal reaches the agent as { approved: false }, without the words the person wrote.