@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
npm install @helix-agents/ai-sdkv7 surface map
| Export | Module | Purpose |
|---|---|---|
handleChatStream | @helix-agents/ai-sdk | Server-side seven-path orchestrator. Canonical v7 chat entry point. |
extractResumeIntent | @helix-agents/ai-sdk | Parse resume signals from incoming AI SDK v6 messages |
findExpiredPending | @helix-agents/ai-sdk | Surface client-tool calls past their deadline |
prepareHelixChatRequest | @helix-agents/ai-sdk/client | PrepareSendMessagesRequest for DefaultChatTransport |
prepareHelixReconnectRequest | @helix-agents/ai-sdk/client | PrepareReconnectToStreamRequest for DefaultChatTransport |
useResumeClientTools | @helix-agents/ai-sdk/react | React hook auto-dispatching client-executed tool calls |
createHelixChatTransport | @helix-agents/ai-sdk | ChatTransport that talks directly to @helix-agents/agent-server |
buildSnapshot | @helix-agents/ai-sdk | Standalone SSR snapshot helper (replaces FrontendHandler.getSnapshot) |
getUIMessages | @helix-agents/ai-sdk | Standalone message-history loader (replaces FrontendHandler.getMessages) |
createCloudflareChatHandler | @helix-agents/ai-sdk/cloudflare | Factory wiring handleChatStream to a Cloudflare Durable Object backend |
pipeWebResponseToExpress | @helix-agents/ai-sdk | Pipe a web Response (from handleChatStream) into an Express response |
createWebResponseExpressMiddleware | @helix-agents/ai-sdk | Express middleware adapter for handleChatStream-style handlers |
StreamTransformer | @helix-agents/ai-sdk | Helix chunk → AI SDK Data Stream event converter |
convertToAISDKMessages | @helix-agents/ai-sdk | Helix Message[] → AI SDK v6 UIMessage[] converter |
loadUIMessages / loadAllUIMessages | @helix-agents/ai-sdk | Direct loaders from a SessionStateStore |
Recovery hooks (useResumableChat, useAutoResync, ...) | @helix-agents/ai-sdk/react | Snapshot 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
function handleChatStream(
deps: HandleChatStreamDeps,
params: HandleChatStreamParams
): Promise<Response>;HandleChatStreamDeps
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
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
- Fresh new session →
executor.execute() - Continuing session, new user message →
executor.execute()withsessionId - Resume after tool / approval submit →
submitToolResult× N, thenexecutor.resume() - Abandonment recovery → fail every pending tool, then path 2
- Active-stream attach → passive subscriber on a running run
- Already-completed retry → terminal stream replay
- Stale runId rejection → SSE response with
data-resume-rejected
Example
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
function prepareHelixChatRequest<UI_MESSAGE extends UIMessage = UIMessage>(
opts: PrepareHelixChatRequestOptions
): PrepareSendMessagesRequest<UI_MESSAGE>;PrepareHelixChatRequestOptions
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
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
function prepareHelixReconnectRequest(
opts: PrepareHelixChatRequestOptions
): PrepareReconnectToStreamRequest;Same options shape as prepareHelixChatRequest.
Example
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
function useResumeClientTools(options: UseResumeClientToolsOptions): void;UseResumeClientToolsOptions
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.messagesfor tool parts instate: '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-requestedparts (those route throughchat.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 })plusonErrorcallback if provided.
Example
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
function extractResumeIntent(
messages: readonly UIMessage[],
sessionState: SessionState | null,
currentRunId: string | undefined
): ExtractResumeIntentResult;Result types
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, andapproval-respondedpart states qualify as resume signals. Other states are skipped silently. runIdpriority: parsed from${runId}::${toolCallId}for approvals, thencallProviderMetadata.helix.runIdfor client-tool results, thencurrentRunId. 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
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
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
function createHelixChatTransport(opts: HelixChatTransportOptions): HelixChatTransport;HelixChatTransportOptions
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
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
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
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
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;
}
BuildSnapshotDepsomitsexecutorandagent(this is a read-only helper that never invokes the executor or routes by agent config). A caller sharing a single deps object withhandleChatStreamcan still pass the larger object — structural typing tolerates excess properties.
FrontendSnapshot
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
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
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
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.
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.
StreamTransformer
Transforms Helix stream chunks to AI SDK UI events.
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
| Option | Type | Description |
|---|---|---|
generateMessageId | (agentId: string) => string | Generate unique message IDs |
includeStepEvents | boolean | Include step-start/finish events (default: false) |
chunkFilter | (chunk: StreamChunk) => boolean | Filter chunks before transformation |
startMetadata | Record<string, unknown> | (agentId: string) => Record<string, unknown> | Metadata for start event |
finishMetadata | Record<string, unknown> | (agentId: string) => Record<string, unknown> | Metadata for finish event |
logger | Logger | Optional logger instance |
Event Mapping
See Frontend AI SDK guide for the full mapping table.
Message Converter
Convert Helix messages to AI SDK v6 UIMessage format.
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)
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-pending'
| 'approval-resolved';Store Utilities
loadUIMessages
Paginated loading.
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
loadAllUIMessagesfor guaranteed merging.
loadAllUIMessages
Load all messages (auto-paginates).
import { loadAllUIMessages } from '@helix-agents/ai-sdk';
const allMessages = await loadAllUIMessages(stateStore, sessionId, {
includeReasoning: true,
includeToolResults: true,
});LoadUIMessagesOptions
interface LoadUIMessagesOptions {
offset?: number;
limit?: number;
includeReasoning?: boolean;
includeToolResults?: boolean;
generateId?: (index: number, message: Message) => string;
}createUIMessageStore
Wrapper for repeated access.
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
import { mapToolStateToAISDK } from '@helix-agents/ai-sdk';
mapToolStateToAISDK('pending'); // 'input-available'
mapToolStateToAISDK('executing'); // 'input-available'
mapToolStateToAISDK('completed'); // 'output-available'
mapToolStateToAISDK('error'); // 'output-error'React Hooks Reference
Import from @helix-agents/ai-sdk/react:
| Hook | Signature | Purpose |
|---|---|---|
useResumeClientTools | (options: UseResumeClientToolsOptions) => void | Auto-dispatch client-executed tools |
useStreamResync | (data, options: UseStreamResyncOptions) => void | Manual resync handling via callback |
useAutoResync | (data, options: UseAutoResyncOptions) => void | Auto-handle resyncs from snapshot URL |
useCheckpointSnapshot | (options) => UseCheckpointSnapshotResult<TState> | Load a specific checkpoint snapshot |
useResyncState | (data) => UseResyncStateResult | Track resync count without auto-handling |
useResumableChat | (data, options) => UseResumableChatResult<TState> | Turnkey snapshot + resync solution |
See Recovery hooks for usage examples.
SSE Response Builder
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
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-AtErrors
import {
FrontendHandlerError,
ValidationError, // 400
StreamNotFoundError, // 404
StreamReaderError, // 500
StreamFailedError, // 410
ConfigurationError, // 501
ExecutionError, // 500
StreamCreationError, // 500
HelixStreamError,
} from '@helix-agents/ai-sdk';FrontendHandlerError
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.
const error = HelixStreamError.fromEvent({
errorText: 'Provider overloaded',
code: 'provider_overloaded',
recoverable: true,
});
error.retryable; // true (maps from 'recoverable' on the wire)| Property | Type | Description |
|---|---|---|
message | string | Error description |
code | string | undefined | Error code from backend ErrorCode taxonomy |
retryable | boolean | Whether the operation can be safely retried |
Types
Configuration Types
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
import type {
AISDKUIEvent,
AISDKStartEvent,
AISDKFinishEvent,
AISDKMessageMetadataEvent,
AISDKTextStartEvent,
AISDKTextDeltaEvent,
AISDKTextEndEvent,
AISDKReasoningStartEvent,
AISDKReasoningDeltaEvent,
AISDKReasoningEndEvent,
AISDKToolInputAvailableEvent,
AISDKToolOutputAvailableEvent,
AISDKStartStepEvent,
AISDKFinishStepEvent,
AISDKDataEvent,
AISDKErrorEvent,
AISDKStreamResyncEvent,
} from '@helix-agents/ai-sdk';