GNL
Docs menu
Core · Free@gnldev/client

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.

run, stream, resume
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.

React hooks
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.

a chat endpoint plus history
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.

an AG-UI endpoint
import { createAguiRoute } from '@gnldev/agui';

app.route('/agui', createAguiRoute(config));
// POST /agui/agents/support/run an AG-UI HttpAgent can consume this directly

API#

classGnlClient

listAgents, run, resume, stream (async generator), streamTo (callbacks), listRuns, getRun. Options: baseUrl, headers, fetch (for Node <18 or test mocks).

fnuseGnlAgent

React hook over GnlClient for a single agent.

fnuseChat

React chat hook built on the client's accumulator — message state is assembled for you.

fntoUIMessageStreamResponse

Turns a durable run into an AI SDK useChat (v5) UI message stream response.

fntoUIMessages

Rebuilds chat history from the journal, so a reload restores the conversation.

fncreateChatRoute

A ready Hono chat endpoint for useChat.

fnmaskSentinelOutput

Replaces the internal suspend/blocked sentinels with a client-safe shape; shared by the live stream and history so they cannot drift.

fncreateAguiRoute

A single POST endpoint an AG-UI HttpAgent can consume.

fnpipeAguiStream

Streams a run as AG-UI events (the AG-UI counterpart of pipeAgentStream).

Internal sentinels never reach the browser
A suspended tool returns a __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.
Two copies of one event mapping, on purpose
The AG-UI adapter converts stream parts with a switch copied from inside 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.