Message Parts Architecture
Overview
Section titled “Overview”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.
Message Types
Section titled “Message Types”Viber defines two message types for different contexts:
Server-Side: XMessage
Section titled “Server-Side: XMessage”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}Client-Side: XChatMessage
Section titled “Client-Side: XChatMessage”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}Part Types
Section titled “Part Types”Core Parts (from AI SDK)
Section titled “Core Parts (from AI SDK)”// Text contentinterface TextPart { type: "text"; text: string;}
// Tool call requestinterface ToolCallPart { type: "tool-call"; toolCallId: string; toolName: string; args: Record<string, unknown>;}
// Tool execution resultinterface ToolResultPart { type: "tool-result"; toolCallId: string; toolName: string; result: unknown; isError?: boolean;}
// Agent reasoning (for transparency)interface ReasoningPart { type: "reasoning"; content: string;}
// File attachmentsinterface FilePart { type: "file"; data: string | Uint8Array; mimeType: string;}
// Multi-step operation markersinterface StepStartPart { type: "step-start"; stepId: string; stepName?: string;}Viber-Specific Parts
Section titled “Viber-Specific Parts”// Artifact referencesinterface ArtifactPart { type: "artifact"; artifactId: string; title: string; version?: number; preview?: string;}
// Plan updatesinterface PlanUpdatePart { type: "plan-update"; planId: string; action: "created" | "updated" | "completed" | "failed"; details?: Record<string, unknown>;}Utility Functions
Section titled “Utility Functions”Server-Side (@viber/core)
Section titled “Server-Side (@viber/core)”import { getTextFromParts, createTextMessage, hasPendingApproval, getToolCalls, getArtifacts, normalizeMessage,} from "@viber/core";
// Extract text contentconst text = getTextFromParts(message.parts);
// Create a simple text messageconst msg = createTextMessage("assistant", "Hello!");
// Check for pending tool approvalsif (hasPendingApproval(message)) { // Handle approval flow}Client-Side (@viber/react)
Section titled “Client-Side (@viber/react)”import { getMessageText, messageNeedsApproval, getPendingApprovals, createUserMessage, isStatusLoading,} from "@viber/react";
// Get display text from messageconst displayText = getMessageText(message);
// Check if message needs approvalif (messageNeedsApproval(message)) { const pending = getPendingApprovals(message); // Show approval UI}
// Create user message for sendingconst userMsg = createUserMessage("Hello, agent!");Streaming Architecture
Section titled “Streaming Architecture”Progressive Rendering
Section titled “Progressive Rendering”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 renderChat Status States
Section titled “Chat Status States”type XChatStatus = | "idle" // No active request | "submitted" // Request sent, waiting | "streaming" // Receiving response | "awaiting-approval" // Tool needs approval | "error"; // Error occurredReact Integration
Section titled “React Integration”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; }}Benefits
Section titled “Benefits”- Progressive Rendering: UI updates smoothly as content streams
- Structured Data: Maintains proper message/part relationships
- Tool Transparency: Shows tool calls and results inline with text
- Type Safety: Full TypeScript types for all message structures
- Multi-modal Support: Easily extends to images, files, and custom types
- Backward Compatible: The
contentfield provides plain text fallback - Approval Flow: Native support for human-in-the-loop tool execution