Skip to content

Message Parts Architecture

Viber uses a parts-based message architecture aligned with AI SDK v6. This enables rich, structured messages that can contain multiple content types while supporting real-time streaming.

Viber defines two message types for different contexts:

Used internally by the viber package for LLM interactions and persistence:

import type { XMessagePart } from "@viber/core";
interface XMessage {
id: string;
role: "system" | "user" | "assistant" | "tool" | "data";
parts: XMessagePart[];
metadata?: {
agentName?: string;
timestamp?: number;
[key: string]: unknown;
};
content?: string; // Backward compatibility
}

Used by @viber/react for UI rendering:

import type { UIMessagePart } from "ai";
interface XChatMessage {
id: string;
role: "user" | "assistant" | "system" | "tool" | "data";
parts: UIMessagePart[];
createdAt?: Date;
metadata?: Record<string, unknown>;
content?: string; // Backward compatibility
}
// Text content
interface TextPart {
type: "text";
text: string;
}
// Tool call request
interface ToolCallPart {
type: "tool-call";
toolCallId: string;
toolName: string;
args: Record<string, unknown>;
}
// Tool execution result
interface ToolResultPart {
type: "tool-result";
toolCallId: string;
toolName: string;
result: unknown;
isError?: boolean;
}
// Agent reasoning (for transparency)
interface ReasoningPart {
type: "reasoning";
content: string;
}
// File attachments
interface FilePart {
type: "file";
data: string | Uint8Array;
mimeType: string;
}
// Multi-step operation markers
interface StepStartPart {
type: "step-start";
stepId: string;
stepName?: string;
}
// Artifact references
interface ArtifactPart {
type: "artifact";
artifactId: string;
title: string;
version?: number;
preview?: string;
}
// Plan updates
interface PlanUpdatePart {
type: "plan-update";
planId: string;
action: "created" | "updated" | "completed" | "failed";
details?: Record<string, unknown>;
}
import {
getTextFromParts,
createTextMessage,
hasPendingApproval,
getToolCalls,
getArtifacts,
normalizeMessage,
} from "@viber/core";
// Extract text content
const text = getTextFromParts(message.parts);
// Create a simple text message
const msg = createTextMessage("assistant", "Hello!");
// Check for pending tool approvals
if (hasPendingApproval(message)) {
// Handle approval flow
}
import {
getMessageText,
messageNeedsApproval,
getPendingApprovals,
createUserMessage,
isStatusLoading,
} from "@viber/react";
// Get display text from message
const displayText = getMessageText(message);
// Check if message needs approval
if (messageNeedsApproval(message)) {
const pending = getPendingApprovals(message);
// Show approval UI
}
// Create user message for sending
const userMsg = createUserMessage("Hello, agent!");

Messages are streamed part-by-part for responsive UI:

sequenceDiagram
participant UI
participant Hook
participant API
participant Agent
UI->>Hook: sendMessage()
Hook->>API: POST /api/chat
API->>Agent: invoke
loop Streaming
Agent-->>API: TextPart delta
API-->>Hook: Stream chunk
Hook-->>UI: Update message.parts
end
Agent-->>API: ToolCallPart
API-->>Hook: Stream chunk
Hook-->>UI: Show tool call
API->>Agent: Execute tool
Agent-->>API: ToolResultPart
API-->>Hook: Stream chunk
Hook-->>UI: Show tool result
API-->>Hook: Message complete
Hook-->>UI: Final render
type XChatStatus =
| "idle" // No active request
| "submitted" // Request sent, waiting
| "streaming" // Receiving response
| "awaiting-approval" // Tool needs approval
| "error"; // Error occurred
import { useXChat, type XChatMessage } from "@viber/react";
function Chat({ spaceId }: { spaceId: string }) {
const {
messages,
input,
setInput,
append,
status,
isLoading,
approveToolCall,
} = useXChat({ spaceId });
// Render messages with parts
return (
<div>
{messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
{status === "awaiting-approval" && (
<ApprovalUI
message={messages[messages.length - 1]}
onApprove={(id) => approveToolCall(id, true)}
onReject={(id) => approveToolCall(id, false)}
/>
)}
</div>
);
}
function Message({ message }: { message: XChatMessage }) {
return (
<div className={message.role === "user" ? "user-msg" : "assistant-msg"}>
{message.parts.map((part, i) => (
<Part key={i} part={part} />
))}
</div>
);
}
function Part({ part }: { part: UIMessagePart }) {
switch (part.type) {
case "text":
return <p>{part.text}</p>;
case "tool-call":
return <ToolCallCard toolCall={part} />;
case "tool-result":
return <ToolResultCard result={part} />;
case "artifact":
return <ArtifactPreview artifact={part} />;
default:
return null;
}
}
  1. Progressive Rendering: UI updates smoothly as content streams
  2. Structured Data: Maintains proper message/part relationships
  3. Tool Transparency: Shows tool calls and results inline with text
  4. Type Safety: Full TypeScript types for all message structures
  5. Multi-modal Support: Easily extends to images, files, and custom types
  6. Backward Compatible: The content field provides plain text fallback
  7. Approval Flow: Native support for human-in-the-loop tool execution