Aviary
Packages

@dudousxd/nestjs-agent-react

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

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

@dudousxd/nestjs-agent-react connects a React app to the /agent endpoints. useAgentChat wraps the Vercel AI SDK v7 useChat with a transport that speaks the backend's SSE protocol, then layers on threads, quota, cancel, and human-in-the-loop approve/reject. Components ship unstyled — every class is yours via classNames.

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

function Assistant() {
  const chat = useAgentChat({
    getHeaders: () => ({ 'x-actor-id': 'u1', 'x-actor-role': 'ADMIN' }),
  });

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

Exports

ExportWhat it is
useAgentChat(options)The hook — the AI SDK useChat result plus threads/quota/cancel/approve/reject.
AgentClientThe framework-agnostic REST client the hook is built on; usable standalone.
AgentChatTransportThe useChat transport implementing the /agent/chat SSE protocol.
ChatInputThe composer — Enter submits, Shift+Enter newlines, optional dictation.
MessageListWindows the thread; typing indicator, error+retry banner, follow-up chips, tool-part rendering.
MessageItemA single message, if you want to lay out the list 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.

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). Use it to seed initialMessages:

const thread = await client.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 }) now dispatches a turn's model call and tool executions as routed durable steps by default (dispatchedSteps defaults ON, opt out with dispatchedSteps: false) — 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.

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 Getting Started.

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

On this page