UI Messages
UI Messages are the frontend-ready format for displaying agent conversations.
Overview
Helix stores messages in a storage format optimized for persistence and LLM context building. The UI format transforms these for frontend display:
- Merges tool calls with their results
- Provides explicit state tracking
- Omits
role: 'tool'messages (merged into parts)
AI SDK v6 UIMessage Format
Helix uses the Vercel AI SDK v6 UIMessage format for all frontend integrations:
import type { UIMessage } from 'ai';
interface UIMessage {
id: string;
role: 'user' | 'assistant' | 'system';
parts: UIMessagePart[];
metadata?: Record<string, unknown>;
}
type UIMessagePart =
| { type: 'text'; text: string }
| { type: 'reasoning'; text: string }
| {
type: 'dynamic-tool';
toolName: string;
toolCallId: string;
input: Record<string, unknown>;
state: ToolInvocationState;
output?: unknown;
errorText?: string;
};
type ToolInvocationState =
| 'input-streaming'
| 'input-available'
| 'output-available'
| 'output-error';Loading Messages
From State Store
import { loadUIMessages, loadAllUIMessages } from '@helix-agents/ai-sdk';
// Paginated loading
const { messages, hasMore } = await loadUIMessages(stateStore, sessionId, {
offset: 0,
limit: 50,
includeReasoning: true,
includeToolResults: true,
});
// Load all messages (auto-paginates)
const allMessages = await loadAllUIMessages(stateStore, sessionId);Important: When using paginated loading (
loadUIMessages), tool results may not be merged correctly if the tool call and its result span different pages. UseloadAllUIMessageswhen you need guaranteed tool result merging.
Using UIMessageStore Wrapper
For repeated access, wrap your state store:
import { createUIMessageStore } from '@helix-agents/ai-sdk';
const uiStore = createUIMessageStore(stateStore);
const { messages, hasMore } = await uiStore.getUIMessages(sessionId);
const all = await uiStore.getAllUIMessages(sessionId);From FrontendHandler
const { messages, hasMore } = await handler.getMessages(sessionId, {
offset: 0,
limit: 50,
includeReasoning: true,
includeToolResults: true,
});Converting Messages
Storage to AI SDK Format
import { convertToAISDKMessages } from '@helix-agents/ai-sdk';
const uiMessages = convertToAISDKMessages(helixMessages, {
generateId: (index, msg) => `msg-${index}`,
includeReasoning: true,
mergeToolResults: true,
});Tool State Transitions
During Streaming
pending → executing → completed
↘ errorIn Stored Messages
When loading from store, tools are either:
pending(no result saved yet)completed(result available)error(execution failed)
The executing state is transient and only exists during live streaming.
State Mapping
| Internal State | AI SDK State | Description |
|---|---|---|
| pending | input-available | Awaiting execution |
| executing | input-available | Currently running |
| completed | output-available | Finished successfully |
| error | output-error | Execution failed |
Edge Cases
Messages Without Tool Results
When mergeToolResults: false or tool results aren't stored yet:
// Tool part in 'input-available' state without output
{
type: 'dynamic-tool',
toolName: 'search',
toolCallId: 'tc1',
input: { query: 'test' },
state: 'input-available',
// No output field
}Empty Assistant Messages
Assistant messages with only tool calls (no text):
{
id: 'msg-1',
role: 'assistant',
parts: [
{ type: 'dynamic-tool', toolName: 'search', toolCallId: 'tc1', ... }
],
// No text part
}Parallel Tool Calls
Multiple tools called in same message:
{
id: 'msg-1',
role: 'assistant',
parts: [
{ type: 'text', text: 'Let me search and calculate...' },
{ type: 'dynamic-tool', toolName: 'search', state: 'output-available', toolCallId: 'tc1', input: {...}, output: {...} },
{ type: 'dynamic-tool', toolName: 'calculate', state: 'output-available', toolCallId: 'tc2', input: {...}, output: {...} },
],
}Malformed Tool Results
When tool result JSON is invalid, the raw string is preserved:
{
type: 'dynamic-tool',
state: 'output-available',
output: 'invalid json string',
}Orphaned Tool Results
Tool results without matching tool calls are dropped during conversion. This can happen if messages are partially loaded or corrupted.
React Integration
import { useChat } from 'ai/react';
import type { UIMessage } from 'ai';
function Chat() {
const { messages } = useChat({ api: '/api/chat' });
return (
<div>
{messages.map((message: UIMessage) => (
<Message key={message.id} message={message} />
))}
</div>
);
}
function Message({ message }: { message: UIMessage }) {
return (
<div className={`message ${message.role}`}>
{message.parts.map((part, i) => (
<Part key={i} part={part} />
))}
</div>
);
}
function Part({ part }) {
if (part.type === 'text') {
return <p>{part.text}</p>;
}
if (part.type === 'reasoning') {
return (
<details>
<summary>Thinking</summary>
<pre>{part.text}</pre>
</details>
);
}
if (part.type === 'dynamic-tool') {
return <ToolPart part={part} />;
}
return null;
}
function ToolPart({ part }) {
return (
<div className={`tool ${part.state}`}>
<strong>{part.toolName}</strong>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
{part.state === 'output-available' && (
<div className="output">
<pre>{JSON.stringify(part.output, null, 2)}</pre>
</div>
)}
{part.state === 'output-error' && (
<div className="error">{part.errorText}</div>
)}
</div>
);
}Loading Existing Conversations
import { loadAllUIMessages } from '@helix-agents/ai-sdk';
function ChatPage({ sessionId }) {
const [initialMessages, setInitialMessages] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadAllUIMessages(stateStore, sessionId)
.then((messages) => {
setInitialMessages(messages);
setLoading(false);
});
}, [sessionId]);
const { messages } = useChat({
api: '/api/chat',
initialMessages,
});
if (loading) return <Spinner />;
return (
<div>
{messages.map((m) => (
<Message key={m.id} message={m} />
))}
</div>
);
}API Reference
Loading Functions
| Function | Returns |
|---|---|
loadUIMessages() | { messages: UIMessage[], hasMore: boolean } |
loadAllUIMessages() | UIMessage[] |
Conversion Function
| Function | Description |
|---|---|
convertToAISDKMessages(messages, options) | Convert Helix Message[] to AI SDK UIMessage[] |
Options
interface LoadUIMessagesOptions {
offset?: number; // Starting position (default: 0)
limit?: number; // Max messages to return (default: 50)
includeReasoning?: boolean; // Include thinking content (default: true)
includeToolResults?: boolean; // Merge tool results (default: true)
generateId?: (index: number, message: Message) => string;
}Next Steps
- AI SDK Package - Full package reference
- React Integration - Building chat UIs
- Streaming - Real-time updates