@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/reactnpm 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
| Export | What it is |
|---|---|
useAgentChat(options) | The hook — the AI SDK useChat result plus threads/quota/cancel/approve/reject. |
AgentClient | The framework-agnostic REST client the hook is built on; usable standalone. |
AgentChatTransport | The useChat transport implementing the /agent/chat SSE protocol. |
ChatInput | The composer — Enter submits, Shift+Enter newlines, optional dictation. |
MessageList | Windows the thread; typing indicator, error+retry banner, follow-up chips, tool-part rendering. |
MessageItem | A single message, if you want to lay out the list yourself. |
useSpeechRecognition | Browser dictation hook backing ChatInput's optional mic button. |
SPEECH_LANGUAGES / DEFAULT_SPEECH_LANG | The 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. |
storedMessageToUiMessage | The 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
| Peer | Required for |
|---|---|
react (>=18) | Always. |
ai, @ai-sdk/react | The 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-visit | Optional — 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.
@dudousxd/nestjs-agent-ai-sdk
aiSdkModel adapts any Vercel AI SDK v7 LanguageModel to the ModelProvider SPI — streaming, tool-call translation, cache-aware usage, and gateway cost, with zero provider code.
@dudousxd/nestjs-agent-data
createExecuteSqlTool — governed, read-only SQL as a prebuilt tool. AST-validated single SELECTs, a fail-closed table allowlist, tenant scoping, and a row cap.