Docs menu
Chat UI: client, useChat & AG-UI
Three ways to put a durable agent in front of a user: a typed REST/SSE client, Vercel AI SDK useChat compatibility, and an AG-UI adapter — none of which asks you to write your own UI library.
What it is for#
The server already exposes every agent over REST and SSE. What is left is the client half, and there is no single right answer: some apps want a typed client, some already run useChat, some plug into an existing AG-UI surface. All three read the same stream.
One concern is shared by all of them and is worth stating up front. A durable tool can return an internal sentinel instead of a result — the run suspended for approval, or a guard blocked the call. Those carry internal fields and are not a wire contract, so they must never reach a browser verbatim. The masking that replaces them lives in one helper shared by the live stream and by history reconstruction, so the two cannot drift.
Typed client (@gnldev/client)#
GnlClient wraps the server's endpoints: listAgents, run, resume (same runId plus approvals — the suspended tool is released), stream as an async generator, streamTo with handler callbacks, plus listRuns and getRun for the journal timeline. It is framework-agnostic; @gnldev/client/react adds hooks.
import { GnlClient } from '@gnldev/client';
const client = new GnlClient({
baseUrl: 'http://localhost:3000/api',
headers: { authorization: `Bearer ${token}` },
});
const { runId } = await client.run('support', { prompt: 'Where is my order?' });
for await (const ev of client.stream('support', { runId, prompt: 'and the refund?' })) {
if (ev.event === 'text-delta') process.stdout.write(ev.data.text);
}
// an approval came back from the operator — same runId, the suspended tool is released
await client.resume('support', runId, { 'call-7': true });The React entry point exposes useGnlAgent and a useChat hook built on the client's own accumulator, so message state is assembled for you.
import { useChat, useGnlAgent } from '@gnldev/client/react';
const { messages, send, streaming } = useChat(client, 'support');Vercel AI SDK useChat (@gnldev/chat-adapter)#
If your UI already runs the AI SDK's useChat (v5), this package speaks its protocol. toUIMessageStreamResponse turns a durable run into a UI message stream, and createChatRoute gives you the endpoint in one line.
toUIMessages is the other half: it rebuilds the message history from the journal, so a page reload does not lose the conversation — the transcript is reconstructed from what actually happened, not from client-side state. An interrupt surfaces as a data-gnl-interrupt chunk, which is how the UI knows to render an approval prompt instead of a result.
import { createChatRoute, toUIMessages } from '@gnldev/chat-adapter';
app.route('/chat', createChatRoute(config, { agent: 'support' }));
// restore the conversation after a reload — rebuilt from the journal
const history = await toUIMessages(journal, runId);AG-UI / CopilotKit (@gnldev/agui)#
AG-UI is the protocol CopilotKit and several other front-ends speak. createAguiRoute exposes a single endpoint an AG-UI HttpAgent can post to; pipeAguiStream is the lower-level piece, converting the run's stream into AG-UI events. The package defines the event types itself and takes no @ag-ui/* dependency.
import { createAguiRoute } from '@gnldev/agui';
app.route('/agui', createAguiRoute(config));
// POST /agui/agents/support/run — an AG-UI HttpAgent can consume this directlyAPI#
GnlClientlistAgents, run, resume, stream (async generator), streamTo (callbacks), listRuns, getRun. Options: baseUrl, headers, fetch (for Node <18 or test mocks).
useGnlAgentReact hook over GnlClient for a single agent.
useChatReact chat hook built on the client's accumulator — message state is assembled for you.
toUIMessageStreamResponseTurns a durable run into an AI SDK useChat (v5) UI message stream response.
toUIMessagesRebuilds chat history from the journal, so a reload restores the conversation.
createChatRouteA ready Hono chat endpoint for useChat.
maskSentinelOutputReplaces the internal suspend/blocked sentinels with a client-safe shape; shared by the live stream and history so they cannot drift.
createAguiRouteA single POST endpoint an AG-UI HttpAgent can consume.
pipeAguiStreamStreams a run as AG-UI events (the AG-UI counterpart of pipeAgentStream).
__gnl_suspend sentinel and a blocked one its own marker. Both carry internal detail and raw guard reasons. maskSentinelOutput replaces them with { pending: 'approval' } / { blocked: true } and hands the raw interrupt back to the caller separately, so the UI can render an approval prompt without the internals leaking into it.pipeAgentStream rather than importing it. That file carries the resumable-stream id contract and is pinned by process-kill and exactly-once tests; adding a sink parameter to it would have been the riskier change. The copy is covered by parallel tests so the two shapes stay in step — and it is written down here because a reader deserves to know it is a copy.