Skip to content
AdaptersAG-UI adapterMenu

AG-UI adapter

Render any AG-UI agent with chatcn: a pure reducer from protocol events to messages, and a hook around HttpAgent with interrupts as approvals.

The chatbot block on a real HttpAgent, with a recorded SSE stream and an interrupt.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/use-chatcn-ag-ui.json

This installs the hook and the chatcn-ag-ui reducer it builds on. Install only chatcn-ag-ui if you feed events yourself.

Usage

tsx
"use client"

import { HttpAgent } from "@ag-ui/client"
import { useMemo } from "react"
import { Chatbot } from "@/components/chatcn/chatbot/chatbot"
import { useChatcnAgUi } from "@/hooks/use-chatcn-ag-ui"

export function AgentChat() {
  const agent = useMemo(() => new HttpAgent({ url: "/api/agent" }), [])
  const { messages, status, error, actions } = useChatcnAgUi({ agent })

  return (
    <Chatbot
      messages={messages}
      actions={actions}
      status={status === "running" ? "streaming" : status === "error" ? "error" : "ready"}
      error={error ? new Error(error.message) : null}
    />
  )
}

Keep the agent stable (useMemo or module scope): a new instance starts a new conversation. agentState holds the shared state from StateSnapshot and StateDelta, and state.step names the running step for a status line.

Actions

ChatcnActionsAG-UI
send(text, { files })agent.addMessage(user) then agent.runAgent(). Files go as image, audio, video or document content parts
stop()agent.abortRun(). Unfinished tool calls render as cancelled
approve(id), deny(id, reason)Answers the interrupt id. Once every open interrupt has an answer, the run resumes with resume: [{ interruptId, status: "resolved", payload }]
submitToolOutput(toolCallId, output)Adds a tool message for a frontend tool, then runs again when no other call waits. Pass an Error to report a failure
regenerate(messageId?)Trims the agent's history to the last user message and runs again
editMessage(id, text)Replaces the user message, drops what followed and runs again

The default resume payload is { approved, reason }. If your agent expects another shape, pass resumePayload: (decision, interrupt) => ....

The reducer on its own

ts
import { agUiReducer, createAgUiState, reduceAgUiEvents } from "@/lib/chatcn-ag-ui"

// Fold a recorded stream, for tests or server rendering.
const state = reduceAgUiEvents(events)

// Or step it from any event source.
let next = createAgUiState()
agent.subscribe({ onEvent: ({ event }) => { next = agUiReducer(next, event) } })

agUiReducer(state, event) is pure, matches events by their type string (plain JSON from an SSE parser works), and returns the same state for events it does not render. Helpers: fromAgUiMessages for history and MessagesSnapshot, answerAgUiInterrupt, toAgUiResume, stopAgUiRun, rewindAgUiState and applyJsonPatch.

Event mapping

EventResult
RunStartedStatus running. A new assistant message, or the last one when a run resumes after an interrupt
TextMessageStart/Content/End, TextMessageChunkA text part that streams in place. Role user or system starts its own message
ReasoningStart/MessageStart/Content/EndA reasoning part. Encrypted reasoning with no text leaves an empty part that renders as redacted
ToolCallStart/Args/End, ToolCallChunkA tool part: input-streaming with partial JSON input, then input-available
ToolCallResultoutput-available with JSON content parsed. A denied call keeps showing the denial
RunFinished with outcome: interruptStatus interrupted. Each interrupt puts its tool call in approval-requested, or adds a dynamic-tool part when it names no call
RunFinished with outcome: cancelledStatus stopped
RunErrorStatus error; unfinished tool calls fail with the message
StateSnapshot, StateDeltaagentState, patched with JSON Patch. A patch that does not apply keeps the last good state
MessagesSnapshotMessages rebuilt from the snapshot, open interrupts kept
ActivitySnapshot/DeltaA data-activity part, reconciled by message id
SubagentStarted/Finished/ErrorA data-subagent part whose messages hold the subagent's own reduced transcript
StepStarted/Finishedstate.step
Custom, Raw, anything newerIgnored

Examples

Protocol in, components out

Recorded AG-UI events folded by the reducer into chatcn messages, approval included.

Both demos replay recorded events. The chat runs a real HttpAgent whose fetch answers with an SSE stream, so the client's parser and verifier see a real stream with no server.

Notes

  • AG-UI requires every open interrupt to be answered in one resume. The hook waits for all answers, then resumes once.
  • AG-UI has no approval scope. Persist "always allow" in onApprovalScope.
  • Leaf components never import this adapter; only your app and blocks do.