---
title: Messages
description: Working with message state in the Client SDK.
---

# Messages

Messages represent the conversation history. The Client SDK tracks messages automatically and provides structured access to their content through typed parts.

## Message Structure

```typescript
interface UIMessage {
  id: string;
  role: 'user' | 'assistant';
  parts: UIMessagePart[];
  status: 'streaming' | 'done';
  createdAt: Date;
}
```

### Message Parts

Messages contain ordered `parts` that preserve content ordering:

```typescript
type UIMessagePart =
  | UITextPart
  | UIReasoningPart
  | UIToolCallPart
  | UIOperationPart;

// Text content
interface UITextPart {
  type: 'text';
  text: string;
  status: 'streaming' | 'done';
  thread?: string;  // For named threads (e.g., "summary")
}

// Extended reasoning/thinking
interface UIReasoningPart {
  type: 'reasoning';
  text: string;
  status: 'streaming' | 'done';
  thread?: string;
}

// Tool execution
interface UIToolCallPart {
  type: 'tool-call';
  toolCallId: string;
  toolName: string;
  displayName?: string;  // Human-readable name
  args: Record<string, unknown>;
  result?: unknown;
  error?: string;
  status: 'pending' | 'running' | 'done' | 'error';
  thread?: string;
}

// Internal operations (set-resource, serialize-thread)
interface UIOperationPart {
  type: 'operation';
  operationId: string;
  name: string;
  operationType: string;
  status: 'running' | 'done';
  thread?: string;
}
```

## Sending Messages

```tsx
import { useMemo } from 'react';
import { useOctavusChat, createHttpTransport } from '@octavus/react';

function Chat({ sessionId }: { sessionId: string }) {
  const transport = useMemo(
    () =>
      createHttpTransport({
        triggerRequest: (triggerName, input) =>
          fetch('/api/trigger', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ sessionId, triggerName, input }),
          }),
      }),
    [sessionId],
  );

  const { send } = useOctavusChat({ transport });

  async function handleSend(text: string) {
    // Add user message to UI and trigger agent
    await send(
      'user-message',
      { USER_MESSAGE: text },
      { userMessage: { content: text } },
    );
  }

  // ...
}
```

The `send` function:
1. Adds the user message to the UI immediately (if `userMessage` is provided)
2. Triggers the agent with the specified trigger name and input
3. Streams the assistant's response back

## Rendering Messages

### Basic Rendering

```tsx
function MessageList({ messages }: { messages: UIMessage[] }) {
  return (
    <div className="space-y-4">
      {messages.map((msg) => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
    </div>
  );
}

function MessageBubble({ message }: { message: UIMessage }) {
  const isUser = message.role === 'user';

  return (
    <div className={isUser ? 'text-right' : 'text-left'}>
      <div className="inline-block p-3 rounded-lg">
        {message.parts.map((part, i) => (
          <PartRenderer key={i} part={part} />
        ))}
      </div>
    </div>
  );
}
```

### Rendering Parts

```tsx
import { isOtherThread, type UIMessagePart } from '@octavus/react';

function PartRenderer({ part }: { part: UIMessagePart }) {
  // Check if part belongs to a named thread (e.g., "summary")
  if (isOtherThread(part)) {
    return <OtherThreadPart part={part} />;
  }

  switch (part.type) {
    case 'text':
      return <TextPart part={part} />;

    case 'reasoning':
      return (
        <details className="text-gray-500">
          <summary>Thinking...</summary>
          <pre className="text-sm">{part.text}</pre>
        </details>
      );

    case 'tool-call':
      return (
        <div className="bg-gray-100 p-2 rounded text-sm">
          🔧 {part.displayName || part.toolName}
          {part.status === 'done' && ' ✓'}
          {part.status === 'error' && ` ✗ ${part.error}`}
        </div>
      );

    case 'operation':
      return (
        <div className="text-gray-500 text-sm">
          {part.name}
          {part.status === 'done' && ' ✓'}
        </div>
      );

    default:
      return null;
  }
}

function TextPart({ part }: { part: UITextPart }) {
  return (
    <p>
      {part.text}
      {part.status === 'streaming' && (
        <span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1" />
      )}
    </p>
  );
}
```

## Named Threads

Content from named threads (like "summary") is identified by the `thread` property. Use the `isOtherThread` helper:

```tsx
import { isOtherThread } from '@octavus/react';

function PartRenderer({ part }: { part: UIMessagePart }) {
  if (isOtherThread(part)) {
    // Render differently for named threads
    return (
      <div className="bg-amber-50 p-2 rounded border border-amber-200">
        <span className="text-amber-600 text-sm">
          {part.thread}: {part.type === 'text' && part.text}
        </span>
      </div>
    );
  }

  // Regular rendering for main thread
  // ...
}
```

## Session Restore

When restoring a session, fetch messages from your backend and pass them to the hook:

```tsx
import { useMemo } from 'react';
import { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';

interface ChatProps {
  sessionId: string;
  initialMessages: UIMessage[];
}

function Chat({ sessionId, initialMessages }: ChatProps) {
  const transport = useMemo(
    () =>
      createHttpTransport({
        triggerRequest: (triggerName, input) =>
          fetch('/api/trigger', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ sessionId, triggerName, input }),
          }),
      }),
    [sessionId],
  );

  // Pass existing messages to restore the conversation
  const { messages } = useOctavusChat({
    transport,
    initialMessages,
  });

  // ...
}
```

On your backend, use `agentSessions.getMessages()` to fetch UI-ready messages:

```typescript
// Server-side
const session = await client.agentSessions.getMessages(sessionId);
// session.messages is UIMessage[] ready for the client
```

## Callbacks

```tsx
useOctavusChat({
  transport,
  onFinish: () => {
    console.log('Stream completed');
    // Scroll to bottom, play sound, etc.
  },
  onError: (error) => {
    console.error('Error:', error);
    toast.error('Failed to get response');
  },
  onResourceUpdate: (name, value) => {
    console.log('Resource updated:', name, value);
  },
});
```
