Understanding Message Types
VoltAgent works with three different message type formats, each serving a specific purpose in the agent lifecycle. Understanding when and how to use each type is essential for building effective AI applications.
Overview​
The three message types are:
- MessageContent - Raw content format for LLM communication (from AI SDK)
- UIMessage - Frontend-friendly format with metadata (from AI SDK)
- VoltAgentTextStreamPart - Streaming format with SubAgent support (VoltAgent extension)
These types come from the underlying AI SDK architecture and VoltAgent's multi-agent features. Rather than being redundant, they serve different layers of your application.
The Three Message Types​
1. MessageContent (AI SDK Content Type)​
MessageContent represents the raw content that gets sent to language models. It's the content property of a ModelMessage.
Type Definition​
type MessageContent = UserContent | AssistantContent | ToolContent | string;
// Examples:
type UserContent = string | Array<TextPart | ImagePart | FilePart>;
type AssistantContent = string | Array<TextPart | ToolCallPart>;
type ToolContent = Array<ToolResultPart>;
Format​
// Simple string
const content: MessageContent = "Hello, how can I help you?";
// Structured array with multiple parts
const content: MessageContent = [
{ type: "text", text: "Check this image:" },
{ type: "image", image: "data:image/png;base64,..." },
{ type: "text", text: "What do you see?" },
];
When to Use​
- ✅ Creating
ModelMessageobjects for AI SDK - ✅ Direct LLM provider calls
- ✅ Low-level message manipulation
- ✅ Working with
message.contentproperty
Common Patterns​
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
// Using MessageContent in direct AI SDK calls
const result = await generateText({
model: openai("gpt-4"),
messages: [
{
role: "user",
content: "Hello!", // MessageContent (string)
},
{
role: "user",
content: [
// MessageContent (array)
{ type: "text", text: "Describe this:" },
{ type: "image", image: imageData },
],
},
],
});
2. UIMessage (AI SDK UI Format)​
UIMessage is the modern format designed for frontend applications, chat UIs, and memory systems. It includes metadata like message IDs and uses a parts array instead of content.
Type Definition​
interface UIMessage {
id: string;
role: "user" | "assistant" | "system";
parts: Array<TextUIPart | FileUIPart | ToolUIPart>;
createdAt?: Date;
metadata?: Record<string, unknown>;
}
Format​
const message: UIMessage = {
id: "msg-123",
role: "user",
parts: [
{ type: "text", text: "Hello, " },
{ type: "file", url: "image.png", mediaType: "image/png" },
{ type: "text", text: "what is this?" },
],
createdAt: new Date(),
metadata: { source: "mobile" },
};
When to Use​
- ✅ Memory operations (
memory.getMessages(),memory.addMessage()) - ✅ Agent hooks (
onPrepareMessages,onAfterGeneration) - ✅ Chat UI rendering and state management
- ✅ Message history and conversation management
- ✅ Frontend/backend communication
Common Patterns​
import { Agent } from "@voltagent/core";
const agent = new Agent({
memory: myMemory,
hooks: {
// UIMessages in hooks
onPrepareMessages: async ({ messages }) => {
// messages is UIMessage[]
const enhanced = messages.map((msg) => ({
...msg,
parts: [...msg.parts, { type: "text", text: " [verified]" }],
}));
return { messages: enhanced };
},
},
});
// Getting messages from memory
const conversationHistory = await memory.getMessages(userId, conversationId);
// Returns UIMessage[]
conversationHistory.forEach((msg) => {
console.log(`[${msg.id}] ${msg.role}:`, msg.parts);
});
3. VoltAgentTextStreamPart (Streaming Extension)​
VoltAgentTextStreamPart extends AI SDK's TextStreamPart with SubAgent metadata, enabling multi-agent coordination during streaming.
Type Definition​
type VoltAgentTextStreamPart<TOOLS extends Record<string, any> = Record<string, any>> =
TextStreamPart<TOOLS> & {
/**
* Optional identifier for the subagent that generated this event
*/
subAgentId?: string;
/**
* Optional name of the subagent that generated this event
*/
subAgentName?: string;
};
// TextStreamPart types include:
// - text-delta
// - tool-call
// - tool-result
// - finish
// - error
// etc.
Format​
// Regular streaming part
const streamPart: VoltAgentTextStreamPart = {
type: "text-delta",
textDelta: "Hello",
};
// With SubAgent metadata
const subAgentStreamPart: VoltAgentTextStreamPart = {
type: "text-delta",
textDelta: "The answer is 42",
subAgentId: "agent-calculator",
subAgentName: "Calculator Agent",
};
When to Use​
- ✅ Streaming responses (
agent.streamText()) - ✅ Real-time UI updates
- ✅ Multi-agent coordination and event forwarding
- ✅ Identifying which SubAgent is responding
- ✅ Building collaborative agent UIs
Common Patterns​
import { Agent } from "@voltagent/core";
const agent = new Agent({
subAgents: [writerAgent, editorAgent],
});
// Streaming with SubAgent tracking
const stream = await agent.streamText("Write and edit a blog post");
for await (const part of stream.fullStream) {
if (part.type === "text-delta") {
// Check which SubAgent is responding
if (part.subAgentId) {
console.log(`[${part.subAgentName}]: ${part.textDelta}`);
} else {
console.log(`[Main Agent]: ${part.textDelta}`);
}
}
}
Message Flow Diagram​
Here's how messages flow through a typical VoltAgent application:
Converting Between Types​
VoltAgent and AI SDK provide utilities for converting between message formats:
MessageContent ↔ UIMessage​
import { convertToModelMessages } from "ai";
import type { UIMessage } from "ai";
// UIMessage[] → ModelMessage[] (which contains MessageContent)
const uiMessages: UIMessage[] = await memory.getMessages(userId, conversationId);
const modelMessages = convertToModelMessages(uiMessages);
// Access MessageContent from ModelMessage
const content = modelMessages[0].content; // MessageContent
// Extract text from either format using helpers
import { extractText } from "@voltagent/core";
const text1 = extractText(content); // From MessageContent
const text2 = extractText(uiMessages[0]); // From UIMessage (overloaded!)
Working with Both Formats​
VoltAgent's messageHelpers utilities support both formats:
import { extractText, hasImagePart, extractImageParts } from "@voltagent/core";
// Works with MessageContent
const content: MessageContent = [
{ type: "text", text: "Hello" },
{ type: "image", image: "data:..." },
];
const text = extractText(content); // "Hello"
const hasImage = hasImagePart(content); // true
// Also works with UIMessage!
const message: UIMessage = {
id: "msg-1",
role: "user",
parts: [
{ type: "text", text: "Hello" },
{ type: "file", url: "image.png", mediaType: "image/png" },
],
} as UIMessage;
const text2 = extractText(message); // "Hello"
const hasImage2 = hasImagePart(message); // true
const images = extractImageParts(message); // FileUIPart[]
See Message Helpers for the complete API.
Common Patterns and Use Cases​
Pattern 1: Memory + UIMessage​
Use Case: Store and retrieve conversation history
import { Agent } from "@voltagent/core";
const agent = new Agent({
memory: myMemory,
});
// Generate response (automatically stores UIMessages in memory)
await agent.generateText({
prompt: "Hello!",
userId: "user-123",
conversationId: "conv-456",
});
// Retrieve conversation history
const history = await myMemory.getMessages("user-123", "conv-456");
// Returns UIMessage[]
// Display in UI
history.forEach((msg) => {
const text = extractText(msg);
console.log(`${msg.role}: ${text}`);
});
Pattern 2: Streaming + VoltAgentTextStreamPart​
Use Case: Real-time UI updates with SubAgent identification
import { Agent } from "@voltagent/core";
const coordinator = new Agent({
subAgents: [researchAgent, writerAgent],
});
const stream = await coordinator.streamText("Research and write about AI");
// Track which agent is responding
let currentAgent = null;
for await (const part of stream.fullStream) {
if (part.type === "text-delta") {
// Detect agent switches
if (part.subAgentId && part.subAgentId !== currentAgent) {
currentAgent = part.subAgentId;
console.log(`\n[Switching to: ${part.subAgentName}]`);
}
process.stdout.write(part.textDelta);
}
}
Pattern 3: Hooks + UIMessage Transformation​
Use Case: Add context or filter sensitive data
import { Agent, messageHelpers } from "@voltagent/core";
const agent = new Agent({
hooks: {
onPrepareMessages: async ({ messages }) => {
// Add timestamps to user messages
const enhanced = messages.map((msg) => {
if (msg.role === "user") {
return messageHelpers.addTimestampToMessage(msg);
}
return msg;
});
return { messages: enhanced };
},
onAfterGeneration: async ({ response }) => {
// Filter sensitive data from assistant responses
const filtered = messageHelpers.mapMessageContent(response, (text) => {
// Redact SSN patterns
return text.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");
});
return { response: filtered };
},
},
});
Decision Tree: Which Type Should I Use?​
Are you working with...
┌─ Memory operations (store/retrieve)?
│ └─> Use UIMessage
│
├─ Agent hooks (transform messages)?
│ └─> Use UIMessage
│
├─ Streaming responses?
│ └─> Use VoltAgentTextStreamPart
│ └─ Need SubAgent tracking?
│ └─> Yes: Check subAgentId/subAgentName properties
│
├─ Direct AI SDK calls (generateText/streamText)?
│ └─> Use MessageContent (in ModelMessage.content)
│
├─ Chat UI rendering?
│ └─> Use UIMessage
│
└─ Message content manipulation?
└─> Use messageHelpers (supports both MessageContent and UIMessage!)
FAQ​
Q: Why not just one unified message type?​
A: These types come from different layers of the AI SDK and serve different purposes:
- MessageContent is optimized for LLM communication (minimal structure)
- UIMessage is optimized for UIs and storage (includes metadata, IDs)
- VoltAgentTextStreamPart is optimized for streaming (real-time, event-based)
Unifying them would lose these optimizations and break compatibility with AI SDK.
Q: Do I need to convert between types manually?​
A: Usually not! VoltAgent handles conversions automatically:
- Agent automatically converts UIMessages to ModelMessages when calling LLMs
- Memory automatically stores responses as UIMessages
messageHelpersutilities work with both MessageContent and UIMessage
You only need manual conversion for advanced use cases.
Q: Which type does memory.getMessages() return?​
A: Always UIMessage[]. Memory operations use UIMessage format for:
- Message IDs (tracking individual messages)
- Metadata (timestamps, sources, etc.)
- Frontend compatibility
Q: Can I use MessageContent with message helpers?​
A: Yes! All extractor and checker functions in messageHelpers support both:
import { extractText } from "@voltagent/core";
// Both work!
extractText(messageContent); // MessageContent
extractText(uiMessage); // UIMessage
Q: How do I know which SubAgent is responding in a stream?​
A: Check the subAgentId and subAgentName properties:
for await (const part of stream.fullStream) {
if (part.subAgentId) {
console.log(`SubAgent: ${part.subAgentName}`);
} else {
console.log("Main Agent");
}
}
Q: Are these types specific to VoltAgent?​
A: No! MessageContent and UIMessage come from Vercel's AI SDK. Only VoltAgentTextStreamPart is a VoltAgent extension (adding subAgentId and subAgentName to AI SDK's TextStreamPart).
Related Documentation​
- Message Helpers - Utilities for working with both MessageContent and UIMessage
- Memory Guide - How memory uses UIMessage format
- Agent Hooks - UIMessage transformation in hooks
- API Streaming - Working with VoltAgentTextStreamPart
Summary​
| Type | Source | Primary Use Case | Key Features |
|---|---|---|---|
| MessageContent | AI SDK | LLM communication | String or array, minimal structure |
| UIMessage | AI SDK | UI & Memory | Has id, parts, metadata |
| VoltAgentTextStreamPart | VoltAgent | Streaming | Has subAgentId, subAgentName |
Golden Rule: Let VoltAgent handle conversions automatically. Use messageHelpers for content manipulation, and check the documentation for each API to know which type it expects.