@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-sdkFrontendHandler
Main handler for frontend requests.
createFrontendHandler
Factory function to create a handler.
typescript
import { createFrontendHandler } from '@helix-agents/ai-sdk';
const handler = createFrontendHandler({
streamManager, // StreamManager instance
executor, // AgentExecutor instance
agent, // Agent configuration
stateStore, // Optional: for getMessages()
transformerOptions, // Optional
logger, // Optional
});handleRequest
Handle incoming HTTP requests.
typescript
// POST - Execute new agent
const response = await handler.handleRequest({
method: 'POST',
body: {
message: 'Hello, agent!',
state: { userId: 'user-123' }, // Optional initial state
},
});
// GET - Stream existing execution
const response = await handler.handleRequest({
method: 'GET',
streamId: 'run-123',
resumeAt: 100, // Optional: resume from sequence
});Returns:
typescript
interface FrontendResponse {
status: number;
headers: Record<string, string>;
body: ReadableStream<Uint8Array> | string;
}getMessages
Load conversation history for useChat initialMessages.
typescript
const { messages, hasMore } = await handler.getMessages(runId, {
offset: 0,
limit: 50,
includeReasoning: true,
includeToolResults: true,
generateId: (index, msg) => `msg-${index}`,
});Returns: GetUIMessagesResult
typescript
interface GetUIMessagesResult {
messages: UIMessage[];
hasMore: boolean;
}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,
});
// Transform chunks
for await (const chunk of helixStream) {
const { events, sequence } = transformer.transform(chunk);
for (const event of events) {
yield event;
}
}
// Finalize (closes open blocks)
const { events } = transformer.finalize();Event Mapping
| Helix Chunk | AI SDK Events |
|---|---|
text_delta | text-start, text-delta |
thinking | reasoning-start, reasoning-delta, reasoning-end |
tool_start | text-end (if needed), tool-input-available |
tool_end | tool-output-available |
subagent_start | data-subagent-start |
subagent_end | data-subagent-end |
custom | data-{eventName} |
state_patch | data-state-patch |
error | error |
output | data-output |
Message Converter
Convert Helix messages to AI SDK v5 UIMessage format.
typescript
import { convertToUIMessages } from '@helix-agents/ai-sdk';
const uiMessages = convertToUIMessages(helixMessages, {
generateId: (index, msg) => `msg-${index}`,
includeReasoning: true,
includeToolResults: true,
});UIMessage Format (AI SDK v5)
typescript
interface UIMessage {
id: string;
role: 'user' | 'assistant' | 'system';
parts: UIMessagePart[];
}
type UIMessagePart = UIMessageTextPart | UIMessageReasoningPart | UIMessageToolInvocationPart;
interface UIMessageTextPart {
type: 'text';
text: string;
}
interface UIMessageReasoningPart {
type: 'reasoning';
text: string;
}
interface UIMessageToolInvocationPart {
type: `tool-${string}`; // e.g., 'tool-search'
toolCallId: string;
input: Record<string, unknown>;
state: ToolInvocationState;
output?: unknown;
errorText?: string;
}
type ToolInvocationState =
| 'input-streaming'
| 'input-available'
| 'output-available'
| 'output-error';SSE Response Builder
Build Server-Sent Events responses.
typescript
import { createSSEStream, createSSEHeaders, buildSSEResponse } from '@helix-agents/ai-sdk';
// Full response
const response = buildSSEResponse(eventsGenerator, {
headers: { 'X-Custom': 'value' },
});
// Manual construction
const headers = createSSEHeaders({ 'X-Custom': 'value' });
const stream = createSSEStream(eventsGenerator);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 Last-Event-ID
const lastEventId = request.headers.get('Last-Event-ID');
const resumeAt = extractResumePosition(lastEventId);
// Check if request is from AI SDK UI
const isAISDK = request.headers.get(AI_SDK_UI_HEADER) === AI_SDK_UI_HEADER_VALUE;Errors
All errors extend FrontendHandlerError:
typescript
import {
FrontendHandlerError,
ValidationError, // 400: Missing/invalid params
StreamNotFoundError, // 404: Stream doesn't exist
StreamReaderError, // 500: Reader creation failed
StreamFailedError, // 410: Stream has failed
ConfigurationError, // 501: Missing configuration
ExecutionError, // 500: Agent execution failed
StreamCreationError, // 500: Stream creation failed
} from '@helix-agents/ai-sdk';
try {
await handler.handleRequest(req);
} catch (error) {
if (error instanceof FrontendHandlerError) {
return Response.json({ error: error.message, code: error.code }, { status: error.statusCode });
}
throw error;
}Error Properties
typescript
interface FrontendHandlerError extends Error {
code: string; // e.g., 'VALIDATION_ERROR'
statusCode: number; // HTTP status code
}Types
Event Types
typescript
import type {
AISDKUIEvent,
AISDKStartEvent,
AISDKFinishEvent,
AISDKTextStartEvent,
AISDKTextDeltaEvent,
AISDKTextEndEvent,
AISDKReasoningStartEvent,
AISDKReasoningDeltaEvent,
AISDKReasoningEndEvent,
AISDKToolInputAvailableEvent,
AISDKToolOutputAvailableEvent,
AISDKStartStepEvent,
AISDKFinishStepEvent,
AISDKDataEvent,
AISDKErrorEvent,
} from '@helix-agents/ai-sdk';Configuration Types
typescript
import type {
StreamTransformerOptions,
FrontendHandlerOptions,
FrontendRequest,
FrontendResponse,
TransformResult,
SequencedEvent,
MessageConvertOptions,
} from '@helix-agents/ai-sdk';Complete Example
Backend (Hono)
typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createFrontendHandler, FrontendHandlerError } 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';
import { MyAgent } from './agent.js';
const stateStore = new InMemoryStateStore();
const streamManager = new InMemoryStreamManager();
const executor = new JSAgentExecutor(stateStore, streamManager, new VercelAIAdapter());
const handler = createFrontendHandler({
streamManager,
executor,
agent: MyAgent,
stateStore,
});
const app = new Hono();
app.use(
'/api/*',
cors({
origin: ['http://localhost:3000'],
allowHeaders: ['Content-Type', 'Last-Event-ID'],
exposeHeaders: ['X-Stream-Id'],
})
);
app.post('/api/chat', async (c) => {
try {
const body = await c.req.json();
const response = await handler.handleRequest({
method: 'POST',
body: { message: body.message, state: body.state },
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
} catch (error) {
if (error instanceof FrontendHandlerError) {
return c.json({ error: error.message, code: error.code }, error.statusCode);
}
throw error;
}
});
app.get('/api/messages/:runId', async (c) => {
const runId = c.req.param('runId');
const { messages, hasMore } = await handler.getMessages(runId);
return c.json({ messages, hasMore });
});
export default app;Frontend (React)
tsx
import { useChat } from 'ai/react';
function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
});
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.parts.map((part, i) => (
<MessagePart key={i} part={part} />
))}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} disabled={isLoading} />
<button type="submit">Send</button>
</form>
</div>
);
}