Frontend Integration Overview
v7 canonical entry point
v7 ships a new chat-handler quartet: handleChatStream (server), prepareHelixChatRequest + prepareHelixReconnectRequest (client), and useHelixChat (React — recommended) / useResumeClientTools (React — lower-level). Examples on this page that reference the legacy useChat({ initialMessages, isLoading, handleInputChange }) AI SDK v4 API are kept for archival reference only — see Frontend / @helix-agents/ai-sdk for the canonical v7 wiring (DefaultChatTransport + the prepare-helpers + useHelixChat).
Frontend integration connects your Helix Agents backend to user interfaces. The @helix-agents/ai-sdk package transforms Helix's streaming protocol into the Vercel AI SDK UI Data Stream format, enabling real-time chat UIs with useChat and other AI SDK hooks.
Why Frontend Integration Matters
AI agents produce:
- Streaming text - Token-by-token responses
- Tool invocations - Tools being called and their results
- Thinking/reasoning - Internal reasoning traces
- Custom events - Application-specific data
- Final outputs - Structured results
Frontend integration delivers these events to your UI in real-time, enabling:
- Progressive text rendering
- Tool call visualization
- Thinking/reasoning displays
- Status indicators
- Error handling
Architecture
graph TB
subgraph Backend ["Backend"]
Executor["<b>Agent Executor</b><br/>Helix Chunks"]
StreamMgr["<b>Stream Manager</b><br/>Helix Chunks"]
Handler["<b>handleChatStream</b><br/>Transform ↓<br/>AI SDK Events"]
Executor --> StreamMgr --> Handler
end
Handler -->|"SSE (Server-Sent Events)"| UseChat
subgraph Frontend ["Frontend"]
UseChat["<b>useChat Hook (Vercel AI SDK)</b><br/>messages, input, handleSubmit, ..."]
subgraph Components ["React Components"]
C1["Message list"]
C2["Tool call displays"]
C3["Input form"]
end
UseChat --> Components
endDeployment Modes
The v7 chat handler works in two deployment modes:
| Mode | When to Use | Backend |
|---|---|---|
| Direct | API routes run in same process as agent | Redis, Memory, or direct DO binding |
| DO Client | Frontend separate from Durable Objects | HTTP to AgentServer DO |
Direct Mode is simpler - your API route has direct access to stores:
import { handleChatStream, buildSnapshot } from '@helix-agents/ai-sdk';
const deps = { executor, stateStore, streamManager, agent: MyAgent };
const dispatchChat = (params) => handleChatStream(deps, params);
const getSnapshot = (sessionId) => buildSnapshot(deps, { sessionId });DO Mode uses the Cloudflare chat-handler factory:
import { createCloudflareChatHandler } from '@helix-agents/ai-sdk/cloudflare';
const chat = createCloudflareChatHandler({
namespace: env.AGENTS,
agentName: 'chat',
});
const dispatchChat = (params) => chat.handleChat(params);
const getSnapshot = (sessionId) => chat.getSnapshot({ sessionId });See AI SDK Package for detailed setup.
Server-Sent Events (SSE)
SSE provides real-time, server-to-client streaming:
sequenceDiagram
participant Server
participant Client
Server->>Client: text-delta: "Hello "
Server->>Client: text-delta: "world"
Server->>Client: tool-input-available
Server->>Client: tool-output-available
Server->>Client: finishBenefits over WebSockets:
- Simpler (one-way communication)
- Automatic reconnection
- Works through proxies/CDNs
- Native browser support
Stream Protocol Transformation
Helix internal chunks are transformed to AI SDK UI events:
| Helix Chunk | AI SDK Event | Notes |
|---|---|---|
text_delta | text-delta | Token-by-token text |
thinking | reasoning-delta | Reasoning traces |
tool_start | tool-input-available | Tool call complete |
tool_end | tool-output-available | Tool result |
custom | data-{eventName} | Custom events |
error | error | Error events |
output | data-output | Final structured output |
Quick Start
Backend Setup
import { handleChatStream, buildSnapshot } from '@helix-agents/ai-sdk';
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
// Create stores and executor
const stateStore = new InMemoryStateStore();
const streamManager = new InMemoryStreamManager();
const executor = new JSAgentExecutor(stateStore, streamManager, new VercelAIAdapter());
// Shared deps bundle — feeds every server helper.
const deps = {
executor,
stateStore,
streamManager,
agent: MyAgent,
} as const;
// Chat dispatch (path: POST /api/chat/:id) — returns a web Response.
export const dispatchChat = (params) => handleChatStream(deps, params);
// SSR / hydrate snapshot (path: GET /api/chat/:id/snapshot).
export const getSnapshot = (sessionId) => buildSnapshot(deps, { sessionId });
// Use with your framework
// See Framework Examples for Express, Hono, etc.Frontend Setup
import { useMemo, useState } from 'react';
import { DefaultChatTransport } from 'ai';
import {
prepareHelixChatRequest,
prepareHelixReconnectRequest,
snapshotResumeOptions,
} from '@helix-agents/ai-sdk/client';
import { useHelixChat } from '@helix-agents/ai-sdk/react';
import type { FrontendSnapshot } from '@helix-agents/ai-sdk';
function ChatComponent({
sessionId,
snapshot,
}: {
sessionId: string;
snapshot: FrontendSnapshot<unknown>;
}) {
const [input, setInput] = useState('');
// Seed and resume from the snapshot: while it is live, the seed leaves out the
// in-flight message and the resume replays its committed prefix.
const resumeOptions = useMemo(() => snapshotResumeOptions(snapshot), [snapshot]);
const transport = useMemo(() => {
const api = `/api/chat/${sessionId}`;
return new DefaultChatTransport({
api,
prepareSendMessagesRequest: prepareHelixChatRequest({ api }),
prepareReconnectToStreamRequest: prepareHelixReconnectRequest({
api,
resumeFromSequence: resumeOptions.resumeFromSequence,
existingMessageId: resumeOptions.existingMessageId,
replayPrefix: resumeOptions.replayPrefix,
}),
});
}, [sessionId, resumeOptions]);
const chat = useHelixChat({
id: sessionId,
transport,
messages: resumeOptions.messages,
resume: resumeOptions.resume,
resync: { snapshotUrl: `/api/chat/${sessionId}/snapshot`, snapshot },
});
return (
<div>
{chat.messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
<form
onSubmit={(e) => {
e.preventDefault();
chat.sendMessage({ text: input });
setInput('');
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit" disabled={chat.status === 'streaming'}>
Send
</button>
</form>
</div>
);
}useHelixChat returns UseHelixChatResult: useChat's helpers plus resyncError. See AI SDK Package for the client wiring and Resumable Streams for the snapshot and resume contract.
Package Contents
The @helix-agents/ai-sdk package provides:
handleChatStream
The canonical v7 chat dispatcher — accepts the parsed { sessionId, messages } body (plus the incoming Request) and returns a web Response carrying an SSE body. Dispatches one of seven paths internally (fresh turn, continuing turn, resume after tool/approval, abandonment recovery, active-stream attach, terminal-stream replay, stale-runId rejection).
import { handleChatStream } from '@helix-agents/ai-sdk';
const deps = { executor, stateStore, streamManager, agent: MyAgent };
// Same function handles POST (new message) and GET (resume).
const response = await handleChatStream(deps, {
sessionId,
messages: body.messages ?? [],
request: req,
});buildSnapshot
Standalone SSR-hydration helper. Returns a FrontendSnapshot read from one checkpoint: the committed messages up to a cursor (streamSequence), the state at that cursor, a status decided by the current run, and, while the run is live, resumeMessageId. It never carries in-flight content (the content-replay and partial-content options are removed; see the upgrade guide). It returns null only for a missing session and throws a typed state_history_incomplete for a failed read.
import { buildSnapshot } from '@helix-agents/ai-sdk';
const snapshot = await buildSnapshot({ stateStore, streamManager }, { sessionId });On the client, snapshotResumeOptions(snapshot) (from @helix-agents/ai-sdk/client) turns it into what to seed and how to resume, and fetchSnapshotWithRetry(url) fetches it with retries (4 fetches, 250 / 500 / 1000 ms backoff), throwing a typed SnapshotUnavailableError after exhaustion.
StreamTransformer
Converts Helix chunks to AI SDK events:
import { StreamTransformer } from '@helix-agents/ai-sdk';
const transformer = new StreamTransformer();
for await (const chunk of helixStream) {
const { events } = transformer.transform(chunk);
for (const event of events) {
yield event;
}
}Message Converter
Converts Helix messages to AI SDK v6 UIMessage format:
import { convertToUIMessages } from '@helix-agents/ai-sdk';
const helixMessages = await stateStore.getMessages(sessionId);
const uiMessages = convertToUIMessages(helixMessages.messages);
// Seed useChat (AI SDK v6) with them
const { messages } = useChat({ messages: uiMessages });Typed Errors
Structured error handling:
import {
chatErrorResponse,
AiSdkHandlerError,
ValidationError,
StreamNotFoundError,
StreamFailedError,
} from '@helix-agents/ai-sdk';
try {
return await dispatchChat({
sessionId,
messages: body.messages ?? [],
request: req,
});
} catch (error) {
// handleChatStream answers every HelixError itself (typed JSON). What reaches
// here is an AiSdkHandlerError or the route's own failure.
return chatErrorResponse(error, { sessionId, logger: console });
}handleChatStream answers every HelixError as a typed JSON response instead of throwing: its refusals (for example a send while a run is already active is 409 { error, code: 'state_already_running', retryable: true }) and any failed read (404 for a missing session or checkpoint, 503 when retryable, 409 for a non-retryable state_* error; never 503 once the request's input is committed). On the client, parseHelixChatError(chat.error) reads the body back, and chat.regenerate() retries the send. See Wire errors.
Framework Compatibility
handleChatStream returns a standard web Response (status + headers + SSE body). Hono / Fastify / Cloudflare Workers can return it directly; Express uses the pipeWebResponseToExpress / createWebResponseExpressMiddleware adapters from @helix-agents/ai-sdk/adapters/express.
This works with any HTTP framework:
- Express
- Hono
- Fastify
- Cloudflare Workers
- Vercel Functions
- AWS Lambda
Stream Resumability
A page refresh renders the snapshot, then resumes the stream after its cursor. handleChatStream reads the resume from the reconnect's headers: X-Resume-From-Sequence (or Last-Event-ID), X-Existing-Message-Id, and X-Helix-Replay-Prefix: 1 when the client seeded without the in-flight message. It then replays that message's committed prefix and streams the tail:
// The AI SDK's reconnect is a GET with the resume headers (prepareHelixReconnectRequest).
// handleChatStream picks path 5 (active-stream attach) or path 6 (terminal replay).
const response = await handleChatStream(deps, {
sessionId,
messages: [],
request: req,
});useHelixChat({ resync: { snapshotUrl } }) re-anchors on the latest snapshot after a rewind (data-stream-resync), a retried attempt (data-attempt-superseded) or a tool result for a call an earlier message holds (data-tool-output-elsewhere). The full contract, including the run-start window, wire errors and per-chat stream control (withHelixStreamControl), is in Resumable Streams.
Next Steps
- AI SDK Package - Deep dive into the package
- React Integration - Building React UIs
- Framework Examples - Express, Hono, etc.
@helix-agents/ai-sdkREADME — composability ladder —useHelixChat,useHelixSendAutomaticallyWhen, andcreateHelixSendAutomaticallyWhen