Skip to main content

Overview

The message types in ff-ai extend the Vercel AI SDK’s ModelMessage with additional metadata for persistence and identification.

ConversationMessage

The ConversationMessage namespace provides types and utilities for working with conversation messages.

Type Definition

import type * as Ai from 'ai';
import type * as v from 'valibot';

export namespace ConversationMessage {
  export type Id = string & v.Brand<'ff-ai/ConversationMessageId'>;

  export type Type = Ai.ModelMessage & {
    id: Id;
    createdAt: Date;
  };
}

Message Structure

A ConversationMessage includes:
id
ConversationMessage.Id
required
UUID v7 identifier for the message. Generated automatically using the uuid package.
createdAt
Date
required
Timestamp when the message was created. Used for ordering messages chronologically.
role
'user' | 'assistant' | 'tool' | 'system'
required
The role of the message sender (inherited from AI SDK)
content
string | ContentPart[]
required
The message content. Can be a simple string or an array of content parts (text, images, tool calls, etc.)

Creating Messages

Messages are typically created automatically when using createTurnHandler. The turn handler’s saveUserMessage and onStep methods handle message creation internally. If you need to manually create a message (e.g., for custom storage implementations), construct it with the required fields:
import type { ConversationMessage } from 'ff-ai';
import { v7 as createUuid } from 'uuid';

const message: ConversationMessage = {
  id: createUuid() as any, // UUID v7
  createdAt: new Date(),
  role: 'user',
  content: 'Hello, world!'
};
Most users should rely on createTurnHandler to manage message creation automatically rather than creating messages manually.

Converting Messages

convertToUIMessage

Convert a ConversationMessage to the AI SDK’s UIMessage format for rendering in user interfaces:
import { convertToUIMessage } from 'ff-ai';

const conversationMessage = {
  id: '01940b3e-...',
  createdAt: new Date(),
  role: 'assistant',
  content: 'Hello! How can I help you?'
};

const uiMessage = convertToUIMessage(conversationMessage);

// Result:
// {
//   id: '01940b3e-...',
//   role: 'assistant',
//   parts: [
//     { type: 'text', text: 'Hello! How can I help you?' }
//   ]
// }
Signature:
function convertToUIMessage(
  message: ConversationMessage.Type
): Ai.UIMessage
Behavior:
  • Extracts text parts from the message content
  • Maps tool role to assistant (UI convention)
  • Preserves message ID for React keys
  • Returns an array of UIMessagePart objects

Message Roles

Messages can have different roles based on their source:
Messages sent by the end user. These are the primary input to the AI model.
{
  role: 'user',
  content: 'What is the weather today?'
}
Messages generated by the AI model. Can include text responses or tool calls.
{
  role: 'assistant',
  content: 'The weather today is sunny with a high of 75°F.'
}
Results from tool executions. These provide context back to the model.
{
  role: 'tool',
  content: [
    {
      type: 'tool-result',
      toolCallId: 'call_123',
      toolName: 'getWeather',
      result: { temp: 75, condition: 'sunny' }
    }
  ]
}
System instructions that guide the model’s behavior. Not stored in conversation history by default.
{
  role: 'system',
  content: 'You are a helpful weather assistant.'
}

Content Types

Messages can contain different types of content:

Text Content

Simple string content:
const message = {
  role: 'user',
  content: 'Hello!'
};

Structured Content

Array of content parts for complex messages:
const message = {
  role: 'user',
  content: [
    {
      type: 'text',
      text: 'What is in this image?'
    },
    {
      type: 'image',
      image: 'data:image/png;base64,...'
    }
  ]
};

Tool Calls

Assistant messages requesting tool execution:
const message = {
  role: 'assistant',
  content: [
    {
      type: 'tool-call',
      toolCallId: 'call_123',
      toolName: 'getWeather',
      args: { location: 'San Francisco' }
    }
  ]
};

Working with Messages

Complete Example

import { ConversationMessage, convertToUIMessage } from 'ff-ai';
import { Effect } from 'effect';

const program = Effect.gen(function* () {
  // Create a user message
  const userMessage = ConversationMessage.fromModelMessage({
    role: 'user',
    content: 'Tell me a joke'
  });

  console.log('Message ID:', userMessage.id);
  console.log('Created at:', userMessage.createdAt);

  // Create an assistant response
  const assistantMessage = ConversationMessage.fromModelMessage({
    role: 'assistant',
    content: 'Why did the chicken cross the road? To get to the other side!'
  });

  // Convert for UI rendering
  const uiMessages = [
    convertToUIMessage(userMessage),
    convertToUIMessage(assistantMessage)
  ];

  return uiMessages;
});

Multi-part Messages

import { ConversationMessage } from 'ff-ai';

const message = ConversationMessage.fromModelMessage({
  role: 'user',
  content: [
    { type: 'text', text: 'Analyze this data:' },
    { type: 'text', text: 'Sales: $1M, Growth: 15%' }
  ]
});

console.log('Content parts:', message.content.length);

Tool Result Messages

import { ConversationMessage } from 'ff-ai';

const toolResult = ConversationMessage.fromModelMessage({
  role: 'tool',
  content: [
    {
      type: 'tool-result',
      toolCallId: 'call_abc123',
      toolName: 'getWeather',
      result: {
        location: 'San Francisco',
        temperature: 68,
        condition: 'Partly Cloudy'
      }
    }
  ]
});

Message IDs

Message IDs use UUID v7, which provides:
  • Time-ordered: IDs are sortable by creation time
  • Unique: Globally unique across distributed systems
  • Performance: Efficient for database indexing
import { v7 as createUuid } from 'uuid';

const id = createUuid();
// '01940b3e-5c5a-7b9c-9c3e-f1a2b3c4d5e6'
The first part of the UUID encodes the timestamp, making it naturally ordered.

Type Safety

The ConversationMessage.Id type is branded using Valibot:
const idSchema = v.pipe(
  v.string(),
  v.uuid(),
  v.brand('ff-ai/ConversationMessageId'),
);
This prevents accidental mixing of message IDs with other string types:
function processMessage(id: ConversationMessage.Id) {
  // TypeScript ensures `id` is a valid message ID
}

// Error: Type 'string' is not assignable to type 'ConversationMessage.Id'
processMessage('not-a-valid-id');

// OK: Created through proper channels
const msg = ConversationMessage.fromModelMessage({ role: 'user', content: 'Hi' });
processMessage(msg.id);

Best Practices

Don’t manually construct ConversationMessage objects. Use fromModelMessage to ensure IDs and timestamps are properly generated:
// Good
const message = ConversationMessage.fromModelMessage({
  role: 'user',
  content: 'Hello'
});

// Bad - missing id and createdAt
const message = {
  role: 'user',
  content: 'Hello'
};
When displaying messages in a UI, always convert them first:
const uiMessages = messages.map(convertToUIMessage);
return <MessageList messages={uiMessages} />;
Store complete messages including metadata. Don’t discard id and createdAt as they’re needed for message ordering and deduplication.
Always check if content is a string or array:
if (typeof message.content === 'string') {
  // Handle simple text
} else {
  // Handle content parts
  for (const part of message.content) {
    if (part.type === 'text') {
      // Handle text part
    }
  }
}

Next Steps