Skip to content

@helix-agents/ai-sdk ​

Vercel AI SDK UI binding layer for Helix Agents. Transforms Helix internal streaming protocol to AI SDK UI Data Stream protocol for use with useChat and other AI SDK React hooks.

Installation ​

bash
npm install @helix-agents/ai-sdk

v7 surface map ​

ExportModulePurpose
handleChatStream@helix-agents/ai-sdkServer-side seven-path orchestrator. Canonical v7 chat entry point.
extractResumeIntent@helix-agents/ai-sdkParse resume signals from incoming AI SDK v6 messages
findExpiredPending@helix-agents/ai-sdkSurface client-tool calls past their deadline
prepareHelixChatRequest@helix-agents/ai-sdk/clientPrepareSendMessagesRequest for DefaultChatTransport
prepareHelixReconnectRequest@helix-agents/ai-sdk/clientPrepareReconnectToStreamRequest for DefaultChatTransport
useResumeClientTools@helix-agents/ai-sdk/reactReact hook auto-dispatching client-executed tool calls
createHelixChatTransport@helix-agents/ai-sdkChatTransport that talks directly to @helix-agents/agent-server
buildSnapshot@helix-agents/ai-sdkStandalone SSR snapshot helper (replaces FrontendHandler.getSnapshot)
getUIMessages@helix-agents/ai-sdkStandalone message-history loader (replaces FrontendHandler.getMessages)
createCloudflareChatHandler@helix-agents/ai-sdk/cloudflareFactory wiring handleChatStream to a Cloudflare Durable Object backend
pipeWebResponseToExpress@helix-agents/ai-sdkPipe a web Response (from handleChatStream) into an Express response
createWebResponseExpressMiddleware@helix-agents/ai-sdkExpress middleware adapter for handleChatStream-style handlers
StreamTransformer@helix-agents/ai-sdkHelix chunk → AI SDK Data Stream event converter
convertToAISDKMessages@helix-agents/ai-sdkHelix Message[] → AI SDK v6 UIMessage[] converter
loadUIMessages / loadAllUIMessages@helix-agents/ai-sdkDirect loaders from a SessionStateStore
Recovery hooks (useResumableChat, useAutoResync, ...)@helix-agents/ai-sdk/reactSnapshot loading + resync handling

handleChatStream ​

The canonical v7 entry point. Takes the parsed { sessionId, messages } body (plus the incoming Request) and dispatches to one of seven paths, returning a web Response with an SSE body.

Signature ​

typescript
function handleChatStream(
  deps: HandleChatStreamDeps,
  params: HandleChatStreamParams
): Promise<Response>;

HandleChatStreamDeps ​

typescript
interface HandleChatStreamDeps {
  executor: MinimalExecutor;
  stateStore: MinimalStateStore;
  streamManager: MinimalStreamManager;
  /**
   * `AnyAgentConfig` for in-process integrations; pass a
   * `MinimalAgentConfig` (`{ name, type }`) when the agent runs in a
   * remote runtime and only routing info is available.
   */
  agent: AnyAgentConfig | MinimalAgentConfig;
  /** Optional structured logger. Defaults to noop. */
  logger?: Logger;
  /** Forwarded to the StreamTransformer when streaming back to the client. */
  transformerOptions?: StreamTransformerOptions;
  /** Max accepted request body size (default 10 MB). Checked against Content-Length. */
  maxRequestBytes?: number;
}

HandleChatStreamParams ​

typescript
interface HandleChatStreamParams {
  sessionId: string;
  /** Incoming AI SDK v6 messages. Only the tail is inspected. */
  messages: readonly UIMessage[];
  /** Forwarded to executor.execute as `userId`. */
  userId?: string;
  /** Forwarded to executor.execute / resume as metadata. */
  metadata?: Record<string, string>;
  /**
   * Optional incoming Request. When provided, the orchestrator extracts
   * `X-Resume-From-Sequence` / `Last-Event-ID` / `X-Existing-Message-Id`
   * from its headers — required for path 5 (active-stream attach) and
   * path 6 (already-completed retry).
   */
  request?: Request;
  /** Explicit override for `X-Resume-From-Sequence`. */
  resumeFromSequence?: number;
  /** Explicit override for `X-Existing-Message-Id`. */
  existingMessageId?: string;
}

The seven dispatch paths ​

  1. Fresh new session → executor.execute()
  2. Continuing session, new user message → executor.execute() with sessionId
  3. Resume after tool / approval submit → submitToolResult × N, then executor.resume()
  4. Abandonment recovery → fail every pending tool, then path 2
  5. Active-stream attach → passive subscriber on a running run
  6. Already-completed retry → terminal stream replay
  7. Stale runId rejection → SSE response with data-resume-rejected

Example ​

typescript
import { handleChatStream } from '@helix-agents/ai-sdk';

export async function POST(req: Request, { params }: { params: { sessionId: string } }) {
  const body = await req.json();
  return handleChatStream(
    { executor, stateStore, streamManager, agent: MyAgent },
    {
      sessionId: params.sessionId,
      messages: body.messages ?? [],
      request: req,
    }
  );
}

prepareHelixChatRequest ​

A PrepareSendMessagesRequest factory for use with the AI SDK v6 DefaultChatTransport. Forwards AI SDK fields (id, messages, trigger, messageId) onto the request body and stamps X-Resume-From-Sequence / X-Existing-Message-Id headers the chat orchestrator uses to dispatch.

Signature ​

typescript
function prepareHelixChatRequest<UI_MESSAGE extends UIMessage = UIMessage>(
  opts: PrepareHelixChatRequestOptions
): PrepareSendMessagesRequest<UI_MESSAGE>;

PrepareHelixChatRequestOptions ​

typescript
interface PrepareHelixChatRequestOptions {
  /** API endpoint path. Mirrors DefaultChatTransport's `api` field. */
  api: string;
  /** Sequence to resume from (X-Resume-From-Sequence). */
  resumeFromSequence?: number;
  /** Assistant message id to continue (X-Existing-Message-Id). */
  existingMessageId?: string;
  /** Static body fields merged underneath the AI SDK fields. */
  body?: Record<string, unknown>;
}

Example ​

typescript
import { DefaultChatTransport } from 'ai';
import { prepareHelixChatRequest } from '@helix-agents/ai-sdk/client';

const transport = new DefaultChatTransport({
  api: '/api/chat/abc',
  prepareSendMessagesRequest: prepareHelixChatRequest({
    api: '/api/chat/abc',
    resumeFromSequence: snapshot.streamSequence,
    existingMessageId: lastAssistantId,
  }),
});

prepareHelixReconnectRequest ​

A PrepareReconnectToStreamRequest factory. The Helix chat handler reuses ONE path for both POST (new messages) and GET (stream resume), so the AI SDK's default reconnect URL — ${api}/${chatId}/stream — does not match the route. Without overriding the reconnect, page-refresh-during-stream silently 404s and any tool that hadn't completed stays pending in the UI forever.

Signature ​

typescript
function prepareHelixReconnectRequest(
  opts: PrepareHelixChatRequestOptions
): PrepareReconnectToStreamRequest;

Same options shape as prepareHelixChatRequest.

Example ​

typescript
import { DefaultChatTransport } from 'ai';
import { prepareHelixChatRequest, prepareHelixReconnectRequest } from '@helix-agents/ai-sdk/client';

const helixOptions = {
  api: '/api/chat/abc',
  resumeFromSequence: snapshot.streamSequence,
  existingMessageId: lastAssistantId,
};

const transport = new DefaultChatTransport({
  api: '/api/chat/abc',
  prepareSendMessagesRequest: prepareHelixChatRequest(helixOptions),
  prepareReconnectToStreamRequest: prepareHelixReconnectRequest(helixOptions),
});

useResumeClientTools ​

React hook that auto-dispatches client-executed tool calls surfaced by AI SDK v6 useChat and reports the results back to the chat. Replaces ~280 LOC of consumer dispatcher boilerplate.

Signature ​

typescript
function useResumeClientTools(options: UseResumeClientToolsOptions): void;

UseResumeClientToolsOptions ​

typescript
interface UseResumeClientToolsOptions {
  chat: ResumeClientToolsChat; // Returned by useChat()
  toolHandlers: Record<string, ResumeClientToolHandler>;
  onError?: (err: unknown, ctx: { toolName: string; toolCallId: string }) => void;
}

type ResumeClientToolHandler = (
  input: unknown,
  ctx: { toolCallId: string; abortSignal: AbortSignal }
) => Promise<unknown>;

Behavior ​

  • Watches chat.messages for tool parts in state: 'input-available' on the LAST assistant message.
  • Dispatches handlers in parallel; each call dispatches at most once per Chat instance.
  • On Chat-instance identity change (e.g. session switch), in-flight handlers are aborted and dispatch state is cleared.
  • Skips approval-requested parts (those route through chat.addToolApprovalResponse) and parts whose tool name has no registered handler.
  • On success: chat.addToolOutput({ state: 'output-available', tool, toolCallId, output }).
  • On failure: chat.addToolOutput({ state: 'output-error', tool, toolCallId, errorText }) plus onError callback if provided.

Example ​

tsx
import { useChat } from '@ai-sdk/react';
import { useResumeClientTools } from '@helix-agents/ai-sdk/react';

const chat = useChat({ id: sessionId, transport });

useResumeClientTools({
  chat,
  toolHandlers: {
    editContent: async (input, { toolCallId, abortSignal }) => {
      const result = await runOnClient(input, abortSignal);
      return result;
    },
  },
  onError: (err, ctx) => console.error(`tool ${ctx.toolName} failed`, err),
});

extractResumeIntent ​

Parse incoming AI SDK v6 messages to determine which (if any) are resume signals — client-tool results or approval responses — for currently-pending tool calls.

Signature ​

typescript
function extractResumeIntent(
  messages: readonly UIMessage[],
  sessionState: SessionState | null,
  currentRunId: string | undefined
): ExtractResumeIntentResult;

Result types ​

typescript
interface ExtractResumeIntentResult {
  intents: ResumeIntent[];
  rejected: ResumeIntentRejection[];
  hasTrailingUserMessage: boolean;
}

type ResumeIntent =
  | {
      kind: 'client-tool-result';
      runId: string;
      toolCallId: string;
      output?: unknown;
      errorText?: string;
    }
  | {
      kind: 'approval-response';
      runId: string;
      toolCallId: string;
      approvalId: string;
      approved: boolean;
      reason?: string;
    };

type ResumeIntentRejection =
  | { reason: 'stale-run'; runId: string; toolCallId: string; currentRunId: string }
  | { reason: 'unknown-tool-call'; toolCallId: string }
  | { reason: 'malformed-approval-id'; approvalId: string };

Behavior notes ​

  • Only the LAST assistant message is inspected (Mastra invariant — stale resumes from earlier turns are ignored).
  • Only output-available, output-error, and approval-responded part states qualify as resume signals. Other states are skipped silently.
  • runId priority: parsed from ${runId}::${toolCallId} for approvals, then callProviderMetadata.helix.runId for client-tool results, then currentRunId. Mismatches surface as 'stale-run' rejections.

handleChatStream calls this internally; consumers calling it directly are wiring their own custom server.

findExpiredPending ​

Sweep a pendingClientToolCalls map for entries past their deadline.

Signature ​

typescript
function findExpiredPending(
  pendingClientToolCalls: Record<string, PendingClientToolCall>
): string[];

Returns an array of toolCallIds whose deadlineAt has elapsed. Defensive against malformed entries: non-finite deadlineAt values are not treated as expired.

Example ​

typescript
import { findExpiredPending } from '@helix-agents/ai-sdk';

const expired = findExpiredPending(sessionState.pendingClientToolCalls);
for (const toolCallId of expired) {
  await executor.submitToolResult({
    sessionId,
    toolCallId,
    error: 'client_tool_deadline_exceeded',
  });
}

handleChatStream calls this internally before resume detection (deadline-sweep step).

createHelixChatTransport ​

A Vercel AI SDK v6 ChatTransport that talks directly to the HTTP endpoints exposed by @helix-agents/agent-server (/start, /resume, /submit-tool-result, /status, /sse). Use this when you don't want to implement a single chat route on top of handleChatStream.

Signature ​

typescript
function createHelixChatTransport(opts: HelixChatTransportOptions): HelixChatTransport;

HelixChatTransportOptions ​

typescript
interface HelixChatTransportOptions {
  /** Base URL for the agent-server, e.g. '/api/agent'. */
  endpoint: string;
  /** Session identifier. Stable across the chat lifetime. */
  sessionId: string;
  /** Agent type (required on first /start; ignored on /resume). */
  agentType?: string;
  /** Per-request auth headers. */
  getAuthHeaders?: () => Record<string, string>;
  /** Override fetch (for tests). */
  fetch?: typeof globalThis.fetch;
}

Returned HelixChatTransport ​

typescript
interface HelixChatTransport {
  sendMessages(options: {
    messages: UIMessage[];
    trigger: 'submit-message' | 'regenerate-message';
    chatId: string;
    messageId: string | undefined;
    abortSignal: AbortSignal | undefined;
  }): Promise<ReadableStream<UIMessageChunk>>;
  reconnectToStream(options: {
    chatId: string;
    abortSignal?: AbortSignal;
  }): Promise<ReadableStream<UIMessageChunk> | null>;
  /**
   * Bypass AI SDK's `addToolOutput` auto-fire timing constraints to
   * submit a client-tool result while the SSE stream is still active.
   */
  submitToolResult(
    params: { sessionId?: string; toolCallId: string; result?: unknown; error?: string },
    abortSignal?: AbortSignal
  ): Promise<SubmitToolResultResponse>;
}

Example ​

tsx
import { useChat } from '@ai-sdk/react';
import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
import { createHelixChatTransport } from '@helix-agents/ai-sdk';

const transport = createHelixChatTransport({
  endpoint: '/api/agent',
  sessionId: 'my-session',
  agentType: 'my-agent',
});

const { messages, addToolOutput } = useChat({
  transport,
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
  async onToolCall({ toolCall }) {
    if (toolCall.toolName === 'editContent') {
      const result = await runOnClient(toolCall.input);
      addToolOutput({
        tool: 'editContent',
        toolCallId: toolCall.toolCallId,
        output: result,
      });
    }
  },
});

This transport is separate from the prepareHelixChatRequest / prepareHelixReconnectRequest helpers, which are for DefaultChatTransport against the in-framework chat handler endpoints (those handlers already serve UIMessageChunks directly).

buildSnapshot ​

Standalone SSR-hydration snapshot helper. Replaces the v7 FrontendHandler.getSnapshot method. Implements the "sequence last" pattern, content replay (so partial mid-stream content isn't double-rendered), and stream-status detection.

Signature ​

typescript
function buildSnapshot<TState>(
  deps: BuildSnapshotDeps,
  params: { sessionId: string; includeThinking?: boolean } & Partial<SnapshotOptions>
): Promise<FrontendSnapshot<TState> | null>;

Returns null when the session doesn't exist.

BuildSnapshotDeps ​

typescript
interface BuildSnapshotDeps {
  stateStore: MinimalStateStore;
  streamManager: MinimalStreamManager;
  logger?: Logger;
  /**
   * When true, follow-up turns (currentRun.startSequence > 0) skip
   * including partial content in the snapshot (the live stream provides
   * it). First turns always include partial content.
   */
  contentReplayEnabled?: boolean;
  transformerOptions?: StreamTransformerOptions;
}

BuildSnapshotDeps omits executor and agent (this is a read-only helper that never invokes the executor or routes by agent config). A caller sharing a single deps object with handleChatStream can still pass the larger object — structural typing tolerates excess properties.

FrontendSnapshot ​

typescript
interface FrontendSnapshot<TState = unknown> {
  sessionId: string;
  state: TState | null;
  messages: UIMessage[];
  streamSequence: number;
  timestamp: number;
  status: 'active' | 'paused' | 'ended' | 'failed';
  checkpointId: string | null;
  stepCount: number;
  startSequence?: number;
  startMessageCount?: number;
}

Example ​

typescript
import { buildSnapshot } from '@helix-agents/ai-sdk';

const deps = { executor, stateStore, streamManager, agent: MyAgent, contentReplayEnabled: true };

const snapshot = await buildSnapshot<MyState>(deps, { sessionId });
if (!snapshot) {
  // session does not exist
}

getUIMessages ​

Standalone paginated message-history loader. Replaces the v7 FrontendHandler.getMessages method.

Signature ​

typescript
function getUIMessages(
  deps: GetUIMessagesDeps,
  params: GetUIMessagesParams
): Promise<GetUIMessagesResult>;

interface GetUIMessagesDeps {
  stateStore: MinimalStateStore;
  logger?: Logger;
}

interface GetUIMessagesParams {
  sessionId: string;
  offset?: number;
  limit?: number;
  includeThinking?: boolean;
  generateId?: (index: number, message: Message) => string;
}

Example ​

typescript
import { getUIMessages } from '@helix-agents/ai-sdk';

const { messages, hasMore } = await getUIMessages(
  { stateStore },
  { sessionId, offset: 0, limit: 50, includeThinking: true }
);

createCloudflareChatHandler ​

Factory wrapping handleChatStream / buildSnapshot / getUIMessages with Cloudflare Durable Object adapter clients. Imported from the @helix-agents/ai-sdk/cloudflare subpath.

typescript
import { createCloudflareChatHandler } from '@helix-agents/ai-sdk/cloudflare';

const chat = createCloudflareChatHandler({
  namespace: env.AGENTS,
  agentName: 'chat-agent',
});

// POST /chat
const response = await chat.handleChat({
  sessionId,
  messages,
  request,
});

// GET /snapshot
const snapshot = await chat.getSnapshot({ sessionId });

// GET /messages
const { messages, hasMore } = await chat.getMessages({ sessionId, offset: 0, limit: 50 });

See CloudflareChatHandlerOptions / CloudflareChatHandler in @helix-agents/ai-sdk/cloudflare for the full type surface.

Fixed: dropped stream frames on re-attach and replay (RM-51)

Before @helix-agents/runtime-cloudflare 5.20.0, DOStreamManagerClient.createResumableReader yielded only the first SSE frame of each network read, so an attached, re-attached or replayed message could arrive truncated when the DO's writes were coalesced. This is fixed in 5.20.0; upgrade if you see truncated attached or replayed messages. See RM-51 in the conformance findings.

StreamTransformer ​

Transforms Helix stream chunks to AI SDK UI events.

typescript
import { StreamTransformer } from '@helix-agents/ai-sdk';

const transformer = new StreamTransformer({
  generateMessageId: (agentId) => `msg-${agentId}`,
  includeStepEvents: false,
  chunkFilter: (chunk) => chunk.type !== 'state_patch',
  logger: console,
  startMetadata: { requestId: 'req-123', source: 'web-ui' },
  finishMetadata: { model: 'claude-3', totalTokens: 150 },
});

for await (const chunk of helixStream) {
  const { events, sequence } = transformer.transform(chunk);
  for (const event of events) yield { event, sequence };
}

// Always finalize.
const { events } = transformer.finalize();

StreamTransformerOptions ​

OptionTypeDescription
generateMessageId(agentId: string) => stringGenerate unique message IDs
includeStepEventsbooleanEmit bare start-step / finish-step UI chunks at LLM-step boundaries (default: false). Carries no stepId/usage/finishReason (ai's schema forbids them); only adapters that report steps produce them — llm-vercel does not today (RM-48)
chunkFilter(chunk: StreamChunk) => booleanFilter chunks before transformation
startMetadataRecord<string, unknown> | (agentId: string) => Record<string, unknown>Metadata for start event
finishMetadataRecord<string, unknown> | (agentId: string) => Record<string, unknown>Metadata for finish event
loggerLoggerOptional logger instance

includeStepEvents depends on the adapter and runtime (RM-48, RM-49)

The option only forwards step chunks the runtime writes, and what gets written depends on the adapter and runtime:

  • With @helix-agents/llm-vercel on runtime-js (and the DO), Temporal or Cloudflare Workflows, no step chunks are written, so the option emits nothing and no per-step usage or finishReason reaches the stream (RM-48).
  • On DBOS, under every adapter (llm-vercel included), each step writes a step_start but never a step_end, so you get start-step without a matching finish-step (RM-49).
  • With an adapter that does report step boundaries, the chunks are emitted bare ({ type }), so every supported ai version accepts them (DI-20, fixed; see the UI-chunk wire contract).

See the conformance findings.

Event Mapping ​

See Frontend AI SDK guide for the full mapping table, and UI-chunk wire contract: every emitted event parses with ai's uiMessageChunkSchema on ai 6.0.0+, so Helix extras ride on data-* events or providerMetadata.helix.

Message Converter ​

Convert Helix messages to AI SDK v6 UIMessage format.

typescript
import { convertToAISDKMessages } from '@helix-agents/ai-sdk';

const uiMessages = convertToAISDKMessages(helixMessages, {
  generateId: (index, msg) => `msg-${index}`,
  includeReasoning: true,
  mergeToolResults: true,
});

UIMessage shape (AI SDK v6) ​

typescript
interface UIMessage {
  id: string;
  role: 'user' | 'assistant' | 'system';
  parts: UIMessagePart[];
  metadata?: Record<string, unknown>;
}

type UIMessagePart = UIMessageTextPart | UIMessageReasoningPart | UIMessageToolInvocationPart;

interface UIMessageToolInvocationPart {
  type: `tool-${string}` | 'dynamic-tool';
  toolCallId: string;
  input: Record<string, unknown>;
  state: ToolInvocationState;
  output?: unknown;
  errorText?: string;
}

type ToolInvocationState =
  | 'input-streaming'
  | 'input-available'
  | 'output-available'
  | 'output-error'
  | 'approval-requested'
  | 'approval-responded'
  | 'output-denied';

Store Utilities ​

loadUIMessages ​

Paginated loading.

typescript
import { loadUIMessages } from '@helix-agents/ai-sdk';

const { messages, hasMore } = await loadUIMessages(stateStore, sessionId, {
  offset: 0,
  limit: 50,
  includeReasoning: true,
  includeToolResults: true,
  generateId: (index, msg) => `msg-${index}`,
});

Note: Paginated loading may not correctly merge tool results when a tool call and its result span different pages. Use loadAllUIMessages for guaranteed merging.

loadAllUIMessages ​

Load all messages (auto-paginates).

typescript
import { loadAllUIMessages } from '@helix-agents/ai-sdk';

const allMessages = await loadAllUIMessages(stateStore, sessionId, {
  includeReasoning: true,
  includeToolResults: true,
});

LoadUIMessagesOptions ​

typescript
interface LoadUIMessagesOptions {
  offset?: number;
  limit?: number;
  includeReasoning?: boolean;
  includeToolResults?: boolean;
  generateId?: (index: number, message: Message) => string;
}

createUIMessageStore ​

Wrapper for repeated access.

typescript
import { createUIMessageStore } from '@helix-agents/ai-sdk';

const uiStore = createUIMessageStore(stateStore);
const { messages, hasMore } = await uiStore.getUIMessages(sessionId);
const all = await uiStore.getAllUIMessages(sessionId);

State Mapping ​

typescript
import { mapToolStateToAISDK } from '@helix-agents/ai-sdk';

mapToolStateToAISDK('pending'); // 'input-available'
mapToolStateToAISDK('executing'); // 'input-available'
mapToolStateToAISDK('completed'); // 'output-available'
mapToolStateToAISDK('error'); // 'output-error'

RemotePatchFilter ​

Consumer-side helper implementing rules 3–4 of the §7 consumer reconstruction contract: agentId filtering, provenance dedup, and unpinned-baseline / foreign-provenance suppression for a parent stream that multiplexes root and forwarded remote-child data-state-patch parts. Rules 1–2 (get a baseline via getSnapshot; attach the stream at that baseline's own streamSequence) remain the caller's responsibility — the filter cannot detect a misaligned attach from the wire.

Signatures ​

typescript
class RemotePatchFilter {
  constructor(options: RemotePatchFilterOptions);
  /** Whether an incoming `data-state-patch` part should be applied locally. */
  shouldApply(part: StatePatchPart): boolean;
  /** Seed/update the checkpoint-pinned baseline for one child document. */
  setChildBaseline(
    childSessionId: string,
    baseline: { streamId: string; streamSequence: number }
  ): void;
  /** Clear dedup keys and latched unalignable-baseline signals. */
  reset(): void;
}

function createRemotePatchFilter(options: RemotePatchFilterOptions): RemotePatchFilter;

function isStatePatchPart(item: unknown): item is StatePatchPart;

interface StatePatchPart {
  type: 'data-state-patch';
  data: StatePatchPartData;
}

interface StatePatchPartData {
  patches: StatePatchChunk['patches'];
  /** The session (root or sub-agent) this patch belongs to. */
  agentId: string;
  /** Present only on chunks forwarded from a remote child. */
  remoteSource?: { childSessionId: string; childSequence: number; streamId: string };
}

type UnalignableBaselineReason = 'unpinned_baseline' | 'nested_descendant';

One-line semantics ​

ExportSemantics
RemotePatchFilterStateful per-document patch filter. shouldApply() drops patches for a different document, dedups forwarded child patches on (childSessionId, childSequence), and suppresses patches it cannot align (unpinned baseline, or provenance naming another session), firing onUnalignableBaseline when it does.
createRemotePatchFilter()Factory function; equivalent to new RemotePatchFilter(options).
isStatePatchPart()Type guard narrowing an arbitrary UI message part to StatePatchPart (type === 'data-state-patch' with an agentId-bearing data).
StatePatchPartThe typed shape of a data-state-patch UI message part.
StatePatchPartDataThe part's data: { patches, agentId, remoteSource? } — remoteSource is present only on chunks proxied from a remote child.
UnalignableBaselineReason'unpinned_baseline' (no stream position to align against — the -1 sentinel) or 'nested_descendant' (the patch's provenance names a session other than this document's, commonly a grandchild).

RemotePatchFilterOptions (constructor input): { documentAgentId: string; childBaselines?: Map<string, { streamId: string; streamSequence: number }>; onStreamIdChange?: (childSessionId: string) => void; onUnalignableBaseline?: (sessionId: string, reason: UnalignableBaselineReason) => void; logger?: Logger }.

React Hooks Reference ​

Import from @helix-agents/ai-sdk/react:

HookSignaturePurpose
useResumeClientTools(options: UseResumeClientToolsOptions) => voidAuto-dispatch client-executed tools
useStreamResync(data, options: UseStreamResyncOptions) => voidManual resync handling via callback
useAutoResync(data, options: UseAutoResyncOptions) => voidAuto-handle resyncs from snapshot URL
useCheckpointSnapshot(options) => UseCheckpointSnapshotResult<TState>Load a specific checkpoint snapshot
useResyncState(data) => UseResyncStateResultTrack resync count without auto-handling
useResumableChat(data, options) => UseResumableChatResult<TState>Turnkey snapshot + resync solution

See Recovery hooks for usage examples.

SSE Response Builder ​

typescript
import { createSSEStream, createSSEHeaders, buildSSEResponse } from '@helix-agents/ai-sdk';

const response = buildSSEResponse(eventsGenerator, {
  headers: { 'X-Custom': 'value' },
});

SSE Format ​

id: 1
data: {"type":"text-delta","id":"block-1","delta":"Hello"}

id: 2
data: {"type":"text-delta","id":"block-1","delta":" world"}

data: {"type":"finish"}

Header Utilities ​

typescript
import {
  AI_SDK_UI_HEADER, // 'X-AI-SDK-UI'
  AI_SDK_UI_HEADER_VALUE, // 'vercel-ai-sdk-ui'
  extractResumePosition,
} from '@helix-agents/ai-sdk';

// Extract resume position from request headers
const headers = Object.fromEntries(request.headers.entries());
const resumeAt = extractResumePosition(headers);
// Reads (in priority order):
//   X-Resume-From-Sequence
//   Last-Event-ID
//   X-Resume-At

Errors ​

typescript
import {
  FrontendHandlerError,
  ValidationError, // 400
  StreamNotFoundError, // 404
  StreamReaderError, // 500
  StreamFailedError, // 410
  ConfigurationError, // 501
  ExecutionError, // 500
  StreamCreationError, // 500
  HelixStreamError,
} from '@helix-agents/ai-sdk';

FrontendHandlerError ​

typescript
interface FrontendHandlerError extends Error {
  code: string; // e.g., 'VALIDATION_ERROR'
  statusCode: number; // HTTP status
}

HelixStreamError ​

Typed error class for reconstructing stream errors on the client. Provides code and retryable for programmatic error handling.

typescript
const error = HelixStreamError.fromEvent({
  errorText: 'Provider overloaded',
  code: 'provider_overloaded',
  recoverable: true,
});

error.retryable; // true (maps from 'recoverable' on the wire)
PropertyTypeDescription
messagestringError description
codestring | undefinedError code from backend ErrorCode taxonomy
retryablebooleanWhether the operation can be safely retried

Types ​

Configuration Types ​

typescript
import type {
  HandleChatStreamDeps,
  HandleChatStreamParams,
  ExtractResumeIntentResult,
  ResumeIntent,
  ResumeIntentRejection,
  PrepareHelixChatRequestOptions,
  StreamTransformerOptions,
  BuildSnapshotDeps,
  GetUIMessagesDeps,
  GetUIMessagesParams,
  GetUIMessagesResult,
  FrontendResponse,
  FrontendSnapshot,
  ResumableStreamStatus,
  TransformResult,
  SequencedEvent,
  MessageConvertOptions,
  // Replay types (for stream resumption)
  ReplayContent,
  ReplayToolCall,
  ReplayToolState,
  ContentReplayOptions,
  // Minimal interfaces (adapter implementations)
  MinimalExecutor,
  MinimalExecutionHandle,
  MinimalStreamManager,
  MinimalStateStore,
} from '@helix-agents/ai-sdk';

Event Types ​

typescript
import type {
  AISDKUIEvent,
  AISDKStartEvent,
  AISDKFinishEvent,
  AISDKMessageMetadataEvent,
  AISDKTextStartEvent,
  AISDKTextDeltaEvent,
  AISDKTextEndEvent,
  AISDKReasoningStartEvent,
  AISDKReasoningDeltaEvent,
  AISDKReasoningEndEvent,
  AISDKToolInputAvailableEvent,
  AISDKToolOutputAvailableEvent,
  AISDKFileEvent, // filename in providerMetadata.helix.filename
  AISDKStartStepEvent, // bare { type: 'start-step' }
  AISDKFinishStepEvent, // bare { type: 'finish-step' }
  AISDKDataEvent,
  AISDKErrorEvent,
  AISDKStreamResyncEvent,
} from '@helix-agents/ai-sdk';

See Also ​

Released under the MIT License.