Skip to content

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 ​

mermaid
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
    end

Deployment Modes ​

The v7 chat handler works in two deployment modes:

ModeWhen to UseBackend
DirectAPI routes run in same process as agentRedis, Memory, or direct DO binding
DO ClientFrontend separate from Durable ObjectsHTTP to AgentServer DO

Direct Mode is simpler - your API route has direct access to stores:

typescript
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:

typescript
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:

mermaid
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: finish

Benefits 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 ChunkAI SDK EventNotes
text_deltatext-deltaToken-by-token text
thinkingreasoning-deltaReasoning traces
tool_starttool-input-availableTool call complete
tool_endtool-output-availableTool result
customdata-{eventName}Custom events
errorerrorError events
outputdata-outputFinal structured output

Quick Start ​

Backend Setup ​

typescript
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 ​

tsx
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).

typescript
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.

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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 ​

Released under the MIT License.