Skip to content

Docs

Guides

AG-UI agents

Render LangGraph, CrewAI, Mastra and other AG-UI agents with the same components, interrupts included, through useAgUiAgent.

AG-UI is the open protocol that LangGraph, CrewAI, Mastra, Pydantic AI and other agent frameworks use to stream runs to a frontend. @dgesteves/agent-ui-kit/ag-ui turns an AG-UI agent into the same message parts the AI SDK produces, so every component works with it unchanged, approvals included.

Want to see it first? The components page runs a real @ag-ui/client agent replaying a LangGraph-style run, with an interrupt you can approve or deny.

Install

npm i @dgesteves/agent-ui-kit ai @ag-ui/client

ai is there for the message part types only. A custom agent, an AbstractAgent whose run() returns an RxJS Observable, needs rxjs too, at the version @ag-ui/client uses (rxjs@7.8.1 for 1.0). Add the styles as in Getting started. With the shadcn CLI, the adapter is the ag-ui item:

npx shadcn@latest add @agent-ui-kit/ag-ui

Render an agent

useAgUiAgent takes any @ag-ui/client agent, HttpAgent or a framework integration's own AbstractAgent, and returns what the components need:

app/agent-run.tsx
'use client';

import { HttpAgent } from '@ag-ui/client';
import { AgentMessage, AgentStatus, RunMeter, deriveAgentState } from '@dgesteves/agent-ui-kit';
import { useAgUiAgent } from '@dgesteves/agent-ui-kit/ag-ui';

const agent = new HttpAgent({ url: '/api/agent' });

export function AgentRun() {
  const { messages, status, usage, step, respond } = useAgUiAgent(agent);
  const last = messages.findLast((m) => m.role === 'assistant');
  const { state, detail } = deriveAgentState({ status, message: last });
  return (
    <>
      <AgentStatus state={state} detail={step ?? detail} />
      {last ? (
        <AgentMessage
          message={last}
          streaming={status === 'streaming'}
          active={state !== 'done' && state !== 'error'}
          onToolApproval={respond}
        />
      ) : null}
      <RunMeter usage={usage} />
    </>
  );
}

export async function send(text: string) {
  agent.addMessage({ id: crypto.randomUUID(), role: 'user', content: text });
  await agent.runAgent();
}

Create the agent once, outside the component or in a useMemo: the hook subscribes to the instance it's given.

ReturnsWhat it is
messagesThe conversation as AI SDK UIMessages: pass each to AgentMessage
statususeChat-style status, for deriveAgentState and AgentStatus
errorThe run's error, if it failed
usageToken usage summed over the runs seen, for RunMeter
stepThe step in progress, such as the LangGraph node running
interruptsOpen interrupts; those bound to a tool call show as approval cards
respondAnswers a tool-call interrupt; pass it as AgentMessage's onToolApproval
resolveAnswers any interrupt with your own payload
stopAborts the run in progress

How events map

AG-UI 1.0Becomes
user messageuser message with text parts, images and documents as file parts
the assistant, reasoning, tool and activity messages that follow itone assistant message, the way the AI SDK groups a multi-step run
TEXT_MESSAGE_*, REASONING_*text and reasoning parts, state: 'streaming' until their end event
TOOL_CALL_START, TOOL_CALL_ARGSdynamic-tool part, input-streaming, with the arguments parsed as they arrive
TOOL_CALL_ENDinput-available
TOOL_CALL_RESULT, tool messageoutput-available (JSON results parsed), or output-error with the tool message's error
RUN_FINISHED with an interrupt outcome bound to a tool callapproval-requested, the interrupt's message as approval.requestReason
respond({ id, approved, reason })approval-responded or output-denied; the agent resumes with payload: { approved, reason }
RUN_STARTED, content, RUN_FINISHED, RUN_ERRORstatus: submitted, streaming, then ready or error
usage on RUN_FINISHED and RUN_ERRORusage, summed over runs, with cache and reasoning tokens
STEP_STARTEDstep
activity messagea data-${activityType} part, rendered by renderData

Shared state (STATE_SNAPSHOT, STATE_DELTA) stays on agent.state, and a subagent's messages render inline in its parent's timeline.

Interrupts and approvals

AG-UI resumes every open interrupt in one run, so the hook waits until each has an answer before it calls runAgent({ resume }). A tool-call interrupt renders as an approval card, and respond answers it with { approved, reason }.

Interrupts that aren't tool approvals (input_required, or your own) are in interrupts. Answer them with resolve, matching the interrupt's responseSchema:

await resolve({ interruptId: interrupt.id, status: 'resolved', payload: { city: 'Lisbon' } });

LangGraph

LangGraph agents speak AG-UI through ag-ui-langgraph, which serves a compiled graph from FastAPI:

server.py
# pip install ag-ui-langgraph fastapi uvicorn
from fastapi import FastAPI
from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint

from my_agent import graph  # your compiled graph

app = FastAPI()
# emit_interrupt_outcome reports interrupts on RUN_FINISHED, where useAgUiAgent reads them. It's off by default.
add_langgraph_fastapi_endpoint(app, LangGraphAgent(name="agent", graph=graph, emit_interrupt_outcome=True), "/agent")

Point HttpAgent at it (new HttpAgent({ url: 'http://localhost:8000/agent' })), with FastAPI's CORSMiddleware or a rewrite in your app if the origins differ. An interrupt renders as an approval card when its value names the tool call it guards, and the hook resumes it with { approved, reason }:

from langgraph.types import interrupt

answer = interrupt({"message": f"Run `{command}`?", "tool_call_id": tool_call["id"]})
if not answer["approved"]:
    ...  # answer.get("reason") is what the user typed, if anything

Interrupts without a tool call are in interrupts, for resolve.

Other frameworks

CrewAI, Mastra, Pydantic AI and the protocol's other integrations each serve or wrap an AG-UI agent. Point an HttpAgent at the endpoint, or pass the integration's own AbstractAgent to useAgUiAgent; the components don't change. An interrupt becomes an approval card when it carries the toolCallId of the call it guards; how each framework sets that varies, so check its docs if you want approval cards rather than interrupts.

Without the hook

The pure pieces behind it, fromAgUiMessages, reduceAgUiRun, answerAgUiInterrupt and getAgUiResume, work with your own store or a recorded event log. The adapter has no dependency on @ag-ui/*: it reads their objects structurally, and its tests run a real @ag-ui/client agent through an interrupt and a resumed run.