---
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
const { send } = useOctavusChat({...});

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/client-sdk';

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/client-sdk';

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, pass existing messages:

```tsx
// Fetch session state from your backend
const sessionState = await fetchSession(sessionId);

// Pass to hook
const { messages } = useOctavusChat({
  initialMessages: sessionState.messages,
  onTrigger: ...
});
```

## Callbacks

```tsx
useOctavusChat({
  onTrigger: ...,
  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);
  },
});
```
