Aviary
Packages

@dudousxd/nestjs-agent-react

useAgentChat, the AgentChatTransport and AgentClient, the headless transcript model, styling-agnostic chat components, dictation, and the markdown subpath.

pnpm add @dudousxd/nestjs-agent-react ai @ai-sdk/react

@dudousxd/nestjs-agent-react connects a React app to the /agent endpoints. <AgentProvider> holds the connection once; useAgentChat wraps the Vercel AI SDK v7 useChat with a transport that speaks the backend's SSE protocol, loads a thread's history, re-attaches a streaming turn, gates on quota, and returns a bound transcript, composer and model picker. useChatTranscript turns the streamed messages into a renderable model, and the components are one rendering of it — they ship unstyled, every class yours via classNames.

import { useAgentChat, ChatInput, MessageList } from '@dudousxd/nestjs-agent-react';

function Assistant() {
  const chat = useAgentChat(); // same origin, `/agent`; <AgentProvider> for anything else

  return (
    <div>
      <MessageList messages={chat.messages} status={chat.status} queue={chat.queue} />
      <ChatInput onSubmit={(text) => chat.sendMessage({ text })} />
    </div>
  );
}

Exports

ExportWhat it is
useAgentUiCapabilities()The enclosing provider's uiCapabilities (or undefined), for your own send path.
AgentProvider / useAgentBackend()The connection (baseUrl, path, headers, credentials, upload strategy, onHttpError, genui, uiCapabilities) once, for every hook below it / the backend in scope.
useAgentChat(options?)The hook — the AI SDK useChat result plus transcript, composer, models, quota, queue, proposals, runError, cancel/approve/reject/answer/skip/fork. Sends made mid-turn queue on the thread (whileRunning: 'queue' | 'interrupt' | 'block') — Typing ahead. Independent action proposals are read and merged into messages by itself — Frontend.
useActionProposals(backend, threadId, setMessages, refreshKey, pollMs?, onAdmitted?)That proposal reconciliation as a standalone hook ({ items, error, refresh() }), for a chat on the SDK's useChat — onAdmitted(proposalIds) hears of outcomes newly written to the thread (the cue useAgentChat follows a server-started turn on, followServerTurn()); reconcileProposalMessages / proposalNeedsPolling are its pure pieces.
UiCapabilities{ components: { name, version }[] } — what uiCapabilities on the provider / useAgentChat takes (Which components this client can draw).
useAgentConfig(options?)GET /agent/config — attachment limits and upload mode, and which features are on; shared per backend.
AgentBackendThe interface every hook talks to — implement it over your own client (Your own backend).
AgentClientThe default AgentBackend: a framework-agnostic REST client over this library's routes; usable standalone.
AgentHttpErrorWhat AgentClient throws on an error answer: status, code, the parsed body, and the server's message as its own message — so a hook showing error.message shows the server's words. onHttpError (on the provider or the client) sees each one first, for app-wide reactions like "401 → sign in again".
useThreads(options?)The thread list, kept current by chats on the same backend; optimistic rename/remove.
useModels(options?) / useAgents(options?)The model catalog (grouped, flattened, find, defaultModel, locked when the agent always runs on one model) and the agent list (lockedModel, each agent's personasOf(agent) / defaultPersonaOf(agent)), for pickers. useAgentChat({ persona }) sends a persona per turn and reports the thread's pin as threadPersona.
useAttachments(options?)A composer's staged files, headless: validation, uploads with progress/cancel/retry, previews, and input/drop/paste handlers. messageFiles(message) reads files back off a message.
useQuota(options?)Budget windows (day/month), usage vs ceilings, blocked, and warning (the fullest window past its warnAt soft limit) — useAgentChat gates on it by itself.
useMessageFeedback(options?)Thumbs-up/down (+ comment) state and calls for messages, optimistic.
AgentChatTransportThe useChat transport implementing the /agent/chat SSE protocol.
agUiChatStream(request, { url })An openChatStream over an AG-UI 1.0 producer, re-framed into this protocol (AG-UI).
useApprovalCountdown(expiresAt) / approvalCountdown{ remainingMs, isExpired } for an approval's expiresAt.
useToolCatalog(options?) / ALL_AGENTSTool presentations for one agent, or with agent: ALL_AGENTS every tool the actor reaches through any agent.
useChatTranscript(options)The transcript model — messages normalized into items with their parts grouped into text/reasoning/tool blocks, per-message derived values, each action a state machine, plus list windowing and stick-to-bottom. Renders nothing.
useTranscriptItem(options)The same model for a single message, for a host that lays out the list itself.
useStickToBottom(options)Follow-the-stream-while-at-the-bottom, standalone. Also on transcript.scroll.
ChatInputThe composer — Enter submits, Shift+Enter newlines, optional dictation, optional stop.
MessageListWindows the thread; typing indicator, error+retry banner, follow-up chips, reasoning and files, tool calls through renderToolPart / renderToolGroup (no approval or question markup of its own); with queue={chat.queue}, the messages waiting in the thread's queue and a resume control.
MessageItemA single message, if you want to lay out the list yourself.
MessageItemViewThe default markup for one item you modelled yourself.
useSpeechRecognitionBrowser dictation hook backing ChatInput's optional mic button.
SPEECH_LANGUAGES / DEFAULT_SPEECH_LANGThe dictation language catalog and default.
storedThreadToUiMessages(messages)Converts a thread's full StoredMessage[] history into UIMessage[] for hydrating a reloaded chat — see Hydrating from history below.
storedMessageToUiMessageThe 1:1 row → message adapter storedThreadToUiMessages is built on; use it directly if you don't want turn-merging.

Subpath @dudousxd/nestjs-agent-react/markdown exports AgentMarkdown — a streaming-aware GFM + KaTeX + Mermaid renderer for renderText, behind its own peers so a plain-text app never pulls them in.

Subpath @dudousxd/nestjs-agent-react/genui exports GenuiProvider, GenerativeUI, useGenerativeUI, GenerativeUIScope and GenuiTree — the headless renderer for server-pushed components (registry, optional catalog validation, versioned resolveComponent, per-item error boundaries, genui:tree frames). Put <GenuiProvider> at the root and MessageItem draws pushed components with no renderUi. It also exports createReactComponentRegistry, one registry for the browser and server rendering. /genui/json-render exports the same provider with a jsonRender option; /genui/server (createReactServerRenderer) and /genui/server/playwright (createPlaywrightCaptureAdapter) render components outside the browser. See the Generative UI guide and Component rendering.

Subpath @dudousxd/nestjs-agent-react/media exports mediaAttachments(), createMediaUpload and MediaUploadError — resumable uploads through nestjs-media (File uploads).

Headless, in the TanStack sense

The components are style-agnostic, but style-agnostic is not headless: a classNames map lets you restyle the markup this package chose, not replace it. useChatTranscript is the model under that markup — grouped part runs, per-message derived values, actions as state machines, windowing, stick-to-bottom — with nothing drawn and no class named:

const transcript = useChatTranscript({
  messages: chat.messages,
  status: chat.status,
  onStop: () => chat.cancel(),
});

transcript.items[0].blocks;        // [{ kind: 'text' | 'reasoning' | 'tools' | 'files' | 'ui' | 'elicitation' | 'sources', … }]
transcript.items[0].copy;          // { available, copied, copy() }
transcript.items[0].edit;          // { isEditing, draft, canSave, save(), getTextareaProps() }
transcript.window;                 // { hiddenCount, canLoadEarlier, loadEarlier() }
transcript.scroll;                 // { isAtBottom, showJumpToLatest, getContainerProps() }
transcript.stop;                   // { available, isStopping, stop() }

MessageList and MessageItem are one rendering of exactly this and keep their existing props, so adopting the model is additive: a host can move to it screen by screen. The full walkthrough — blocks, reasoning, stopping a turn, stick-to-bottom — is in Bring your own UI.

Hydrating from history

storedMessageToUiMessage maps one persisted StoredMessage row to one UIMessage, but the loop persists one row per model iteration — a turn with tool calls is 2-3 rows (a "checking that..." row, tool-call rows, a final-answer row). Loading a thread with that adapter renders 2-3 separate assistant bubbles, each with its own cost/tokens footer, for what the live stream rendered as one continuous response.

storedThreadToUiMessages(messages) merges consecutive assistant rows (no user message between them) into a single UIMessage per turn, concatenating parts in step order — a reloaded thread matches the live stream bubble-for-bubble. The merged message carries metadata.usage with summed tokens and cost (costUsd is null only when every merged row's cost is unknown — unknown is never coerced to 0). useAgentChat({ threadId }) does this for you; use it yourself to seed initialMessages from history you already have (SSR, a cache):

const thread = await backend.getThread(threadId);
const chat = useAgentChat({ threadId, initialMessages: storedThreadToUiMessages(thread.messages) });

Reconciling after a run settles

useAgentChat({ onRunSettled }) fires exactly once per run — { runId, status: 'completed' | 'failed' } — when its stream settles, on both the send and the resume paths. By the time it fires the server has already persisted the thread's title and the run's terminal state, which is the signal onFinish doesn't give you: onFinish only means "a turn rendered here," not "the server is done writing." Use it to refetch thread/list queries so a header or sidebar showing "Untitled" picks up the derived title:

const chat = useAgentChat({
  threadId,
  onRunSettled: ({ status }) => {
    if (status === 'completed') queryClient.invalidateQueries({ queryKey: threadListQueryKey });
  },
});

This matters more than it used to: under a durable backend, AgentModule.forRoot({ durable: true }) dispatches a turn's model call and tool executions as routed durable steps — the actual LLM/tool work can run on a different worker than the pod holding this SSE connection, and results flow back through a cross-process token sink (e.g. Redis) instead of an in-process buffer. The /agent/chat SSE contract this package speaks doesn't change, but a run can keep progressing, or finish, on infrastructure this tab has no direct line to — onRunSettled (and resume, for reattaching to a run still in flight on mount) are the client-side hooks that make that invisible.

Peer dependencies

PeerRequired for
react (>=18)Always.
ai, @ai-sdk/reactThe hook — useAgentChat returns the SDK's own useChat result, extended.
streamdown, @streamdown/code, @streamdown/math, @streamdown/mermaid, react-markdown, remark-gfm, remark-math, rehype-katex, rehype-sanitize, katex, prism-react-renderer, unist-util-visitOptional — only if you import the /markdown subpath.
@json-render/react (>=0.21)Optional — only if you import the /genui/json-render subpath.
@dudousxd/nestjs-media-client (>=0.3)Optional — only if you import the /media subpath (mediaAttachments, createMediaUpload; see File uploads).
react-dom (>=18)Optional — only for the /genui/server subpath.
playwright-core (>=1.48)Optional — only for the /genui/server/playwright subpath.

When to use it

Reach for -react when the frontend is React and you want the streaming + HITL + thread machinery handled for you. Apps on another framework, or wanting full control over the wire protocol, can build directly against AgentClient or the raw /agent/* REST + SSE surface described in Endpoints.

See Frontend for the full hook option table, wiring headers to identity, rendering approve/reject, and reopening an existing thread.

On this page