Skip to content

Messages Package - Deep Reference

Core Architecture

MessageStore System

The MessageStore system provides multi-section message management for complex conversational scenarios:

class MessageStore {
    public Sections $sections;
    public Metadata $parameters;
}

// Usage patterns
MessageStore::fromSections($section1, $section2);
MessageStore::fromMessages($messages, 'section_name');

// Fluent API
$store->section('system')->appendMessages($messages);
$store->section('prompt')->setMessages($messages);
$store->section('examples')->remove();
$store->parameters()->setParameter('model', 'gpt-4');

Message System Contracts

interface CanProvideMessage {
    public function toMessage(): Message;
}

interface CanProvideMessages {
    public function toMessages(): Messages;
}

interface CanRenderMessages {
    public function renderMessages(Messages $messages, array $parameters = []): Messages;
}

Message Role System

enum MessageRole: string {
    case System = 'system';
    case Developer = 'developer';
    case User = 'user';
    case Assistant = 'assistant';
    case Tool = 'tool';
}

// Role utilities
MessageRole::fromString('user');           // Parse from string
MessageRole::fromAny($stringOrEnum);       // Flexible parsing
$role->is(MessageRole::User);              // Compare roles
$role->isNot(MessageRole::System);         // Negated comparison
$role->oneOf(MessageRole::User, MessageRole::Assistant);  // Multiple check
$role->isSystem();                         // System or Developer check
MessageRole::normalizeArray($mixedRoles);  // Normalize array of roles

Content Type Enumeration

enum ContentType: string {
    case Text = 'text';
    case Image = 'image_url';
    case File = 'file';
    case Audio = 'input_audio';
}

Message Type Enumeration

enum MessageType: string {
    case Text = 'text';                          // Regular text message (any role)
    case AssistantToolCalls = 'assistant_tool_calls'; // Assistant message with tool calls
    case ToolResult = 'tool_result';             // Tool role message with result
}

// Derived from message role + tool state via Message::type()
$message->type();                                // MessageType enum

Identity Types

MessageId

final readonly class MessageId {
    public function __construct(public string $value);

    MessageId::generate();                        // Generate UUID-based ID
    $id->toString();                              // string
    $id->__toString();                            // string (magic method)
    $id->equals(MessageId $other);                // bool
}

MessageSessionId

final readonly class MessageSessionId {
    public function __construct(public string $value);

    MessageSessionId::generate();                 // Generate UUID-based ID
    $id->toString();                              // string
    $id->__toString();                            // string (magic method)
    $id->equals(MessageSessionId $other);         // bool
}

Tool Types

ToolCallId

// Opaque external identifier for tool calls
final readonly class ToolCallId extends OpaqueExternalId {}

new ToolCallId('call_abc123');
$id->toString();                              // 'call_abc123'
$id->toNullableString();                      // 'call_abc123' or null

ToolCall

final readonly class ToolCall {
    public function __construct(
        string $name,
        array $arguments = [],
        ToolCallId|string|null $id = null,     // Accepts string, ToolCallId, or null
    );

    // Construction
    ToolCall::fromArray($data);                // From array (OpenAI format)
    ToolCall::none();                          // Sentinel for "a ToolCall is required but none exists"

    // Accessors
    $tc->id();                                 // ?ToolCallId
    $tc->idString();                           // string ('' if null)
    $tc->name();                               // string
    $tc->arguments();                          // array
    $tc->args();                               // array (alias for arguments)
    $tc->argumentsAsJson();                    // string (JSON-encoded)
    $tc->argsAsJson();                         // string (alias for argumentsAsJson)
    $tc->hasArgs();                            // bool
    $tc->isNone();                             // bool - true for ToolCall::none(); the
                                               // sentinel name is private, so test with
                                               // this rather than comparing name()
    $tc->hasValue(string $key);                // bool (checks if argument key exists)
    $tc->value(string $key, mixed $default = null); // mixed (argument value by key)
    $tc->toArray();                            // Serialized array
    $tc->toString();                           // String representation

    // Mutation (immutable)
    $tc->with($id, $name, $args);             // Replace fields (all optional, named params)
    $tc->withId($id);                          // Replace ID
    $tc->withName($name);                      // Replace name
    $tc->withArguments(string|array $args);    // Replace arguments (accepts JSON string or array)
    $tc->withArgs(string|array $args);         // Alias for withArguments
}

ToolCalls

final readonly class ToolCalls {
    public function __construct(ToolCall ...$toolCalls);

    // Construction
    ToolCalls::empty();
    ToolCalls::fromArray($array);              // From array of ToolCall/array/string
    ToolCalls::fromMapper($items, $mapper);    // Custom mapping

    // Access
    $tcs->all();                               // ToolCall[]
    $tcs->first();                             // ?ToolCall
    $tcs->last();                              // ?ToolCall
    $tcs->count();                             // int
    $tcs->isEmpty();                           // bool
    $tcs->hasAny();                            // bool (not empty)
    $tcs->hasNone();                           // bool (empty)
    $tcs->hasSingle();                         // bool (exactly one)
    $tcs->hasMany();                           // bool (more than one)

    // Iteration
    $tcs->each();                              // iterable<ToolCall>
    $tcs->map(fn(ToolCall $tc) => ...);        // array
    $tcs->filter(fn(ToolCall $tc) => ...);     // ToolCalls
    $tcs->reduce($callback, $initial);         // mixed

    // Mutation (immutable)
    $tcs->withAddedToolCall($name, $args);     // Add a tool call
    $tcs->withLastToolCallUpdated($name, $json); // Update last call's name/args

    // Serialization
    $tcs->toArray();                           // array
    $tcs->toString();                          // string
}

ToolResult

final readonly class ToolResult {
    public function __construct(
        string $content,
        ToolCallId|string|null $callId = null, // Accepts string, ToolCallId, or null
        ?string $toolName = null,
        bool $isError = false,
    );

    // Factory methods
    ToolResult::success($content, $callId, $toolName);
    ToolResult::error($content, $callId, $toolName);
    ToolResult::fromArray($data);

    // Accessors
    $tr->content();                            // string
    $tr->callId();                             // ?ToolCallId
    $tr->callIdString();                       // string ('' if null)
    $tr->toolName();                           // ?string
    $tr->isError();                            // bool
    $tr->toArray();                            // Serialized array
}

Message Class Structure

Core Message Class

final readonly class Message {
    public MessageId $id;                      // Generated UUID, preserved across mutations
    public DateTimeImmutable $createdAt;       // Timestamp, preserved across mutations

    protected string $role;
    protected string $name;
    protected Content $content;
    protected ToolCalls $toolCalls;
    protected ?ToolResult $toolResult;
    protected Metadata $metadata;
    protected ?MessageId $parentId;

    public const DEFAULT_ROLE = 'user';
}

Message Construction Patterns

// Basic construction
new Message($role, $content, $name = '', $metadata = [], $parentId = null, $toolCalls = null, $toolResult = null, $id = null, $createdAt = null);
new Message(MessageRole::User, 'Hello world');
new Message('', 'Content');  // Defaults to 'user' role ('' and null both mean default)

// Roles are validated at construction: an unrecognised role string throws
// InvalidArgumentException here rather than at the first role() call. The same applies
// to withRole(). Only '' and null fall back to DEFAULT_ROLE.
new Message('wizard', 'Content');  // throws InvalidArgumentException

// Factory methods
Message::empty();                          // Empty message
Message::make($role, $content, $name);     // Explicit construction
Message::asUser($message);                 // Force user role
Message::asAssistant($message);            // Force assistant role
Message::asSystem($message);               // Force system role
Message::asDeveloper($message);            // Force developer role
Message::asTool($message);                 // Force tool role

// Flexible construction
Message::fromAny($input, $role = null);    // Universal constructor
Message::fromString($content, $role);      // From string
Message::fromArray($messageArray);         // From OpenAI format
Message::fromContent($content, $role);     // From Content object
Message::fromContentPart($part, $role);    // From ContentPart
Message::fromInput($input, $role);         // From various inputs
Message::fromImage($image, $role);         // From Image object

Message Input Resolution

// fromAny() resolution logic
match(true) {
    is_string($message) => new Message(role: $role, content: $message),
    is_array($message) => Message::fromArray($message),
    $message instanceof Message => $message,
    $message instanceof Content => Message::fromContent($message),
    $message instanceof ContentPart => Message::fromContentPart($message),
    $message instanceof ContentParts => Message::fromContent(Content::fromParts($message)),
}

// Array validation and parsing
Message::isMessage($array);                // Check OpenAI format
Message::fromArray($array) supports:
- Standard format: ['role' => 'user', 'content' => 'text']
- Array of strings: ['Hello', 'World'] -> Content with multiple parts
- Metadata: ['role' => 'user', 'content' => 'text', '_metadata' => []]
- Tool calls: ['role' => 'assistant', 'tool_calls' => [...]]
- Tool result: ['role' => 'tool', 'content' => '...', 'tool_call_id' => '...']
- IDs: ['id' => '...', 'parentId' => '...', 'createdAt' => '...']

Content System Architecture

Content Class Structure

final readonly class Content {
    protected ContentParts $parts;

    // Content state classification
    public function isComposite(): bool;    // Multi-part or complex content
    public function isEmpty(): bool;        // All parts empty
}

ContentParts Collection

/** @implements IteratorAggregate<int, ContentPart> */
final readonly class ContentParts implements Countable, IteratorAggregate {
    /** @var ContentPart[] */
    private array $parts;

    public function __construct(ContentPart ...$parts);
    public static function empty(): self;
    public static function fromArray(array $parts): self;
    public function getIterator(): Traversable;   // foreach ($parts as $part)
    public function all(): array;                 // ContentPart[]
    public function add(ContentPart $part): self;
    public function replaceLast(ContentPart $part): self;
    public function first(): ?ContentPart;
    public function last(): ?ContentPart;
    public function get(int $index): ?ContentPart;
    public function count(): int;
    public function isEmpty(): bool;
    public function map(callable $callback): array;
    public function reduce(callable $callback, mixed $initial = null): mixed;
    public function filter(callable $callback): self;
    public function toArray(): array;
    public function toString(string $separator = "\n"): string;
    public function withoutEmpty(): self;
}

The collection is directly iterable, so foreach ($parts as $part) works and foreach ($parts->all() as $part) is no longer required.

Content Construction

// Factory methods
Content::empty();                          // No parts
Content::text($text);                      // Single text part
Content::texts(...$texts);                 // Multiple text parts

// Universal constructor
Content::fromAny($input);                  // Handles multiple input types
Content::fromParts($parts);                // From ContentParts collection

// Input resolution logic
match(true) {
    is_null($content) => new self(),
    is_string($content) => new self(ContentPart::text($content)),
    is_array($content) && Message::isMessage($content) => Content::fromAny($content['content'] ?? ''),
    // A keyed (non-list) array describes ONE content part, so it is passed to
    // ContentPart::fromArray() instead of being iterated as a parts collection
    is_array($content) && $content !== [] && !array_is_list($content)
        => new self(ContentPart::fromArray($content)),
    is_array($content) => Content::fromParts(ContentParts::fromArray($content)),
    $content instanceof Content => Content::fromParts($content->partsList()),
    $content instanceof ContentPart => new self($content),
    $content instanceof ContentParts => Content::fromParts($content),
}

A parts collection is always a LIST. That is what separates the two array arms: ['type' => 'image_url', 'image_url' => [...]] is a single part, while [['type' => 'text', ...], ['type' => 'text', ...]] is a collection of parts. Messages/MessageInput apply the same list-vs-keyed rule one level up: a keyed array is a message (recognised by its role / content / _metadata keys), and only a list of strings is treated as a sequence of text parts.

Content State Management

// Content introspection
$content->parts();                         // ContentPart[] (deprecated)
$content->partsList();                     // ContentParts collection
$content->toArray();                       // Serialize to array
$content->toString();                      // Extract text content
$content->normalized();                    // string|array based on complexity

// Content mutation (immutable)
$content->addContentPart($part);           // Add new part
$content->appendContentField($key, $value); // Add single field to last part

Content Complexity Logic

// Composite detection (delegates to hasSingleTextContentPart())
isComposite(): bool {
    return match(true) {
        $this->isNull() => false,
        ($this->parts->count() > 1) => true,
        $this->hasSingleTextContentPart() => false,
        default => true,
    };
}

// Normalization based on complexity
normalized(): string|array {
    return match(true) {
        $this->isNull() => '',
        $this->isSimple() => $this->firstContentPart()?->toString() ?? '',
        default => $this->parts->toArray(),
    };
}

ContentPart System

ContentPart Structure

final readonly class ContentPart {
    protected string $type;                // Content type identifier
    /** @var array<string, mixed> */
    protected array $fields;               // Type-specific data

    // Constructor filters out null/empty values
    public function __construct(string $type, array $fields = []);
}

ContentPart Factory Methods

// Basic types
ContentPart::text($text);                  // Text content part
ContentPart::imageUrl($url);               // Image from URL (simple format)
ContentPart::image($image);                // From Image object (nested OpenAI format)
ContentPart::file($file);                  // From File object
ContentPart::audio($audio);                // From Audio object

// Array construction
ContentPart::fromArray($array);            // Extract type and fields
ContentPart::fromAny($item);               // Universal constructor

// fromAny resolution
match(true) {
    is_string($item) => self::text($item),
    is_array($item) => self::fromArray($item),
    is_object($item) && $item instanceof self => $item,
    is_object($item) && $item instanceof Image => self::image($item),
    is_object($item) && $item instanceof File => self::file($item),
    is_object($item) && $item instanceof Audio => self::audio($item),
    default => throw new InvalidArgumentException('Unsupported content type'),
}

ContentPart API

$part->type();                            // Content type string
$part->fields();                          // All fields array
$part->withFields($fields);               // Replace all fields (immutable)
$part->withField($key, $value);           // Add/update single field (immutable)
$part->get($key, $default);               // Get field value
$part->has($key);                         // Check field existence

// Type checking
$part->isTextPart();                      // type === 'text'
$part->hasText();                         // Has 'text' field
$part->isEmpty();                         // All fields null/empty
$part->isSimple();                        // Single 'text' field only

// Serialization
$part->toArray();                         // Export with type filtering
$part->toString();                        // Extract text or empty string

ContentPart Export Filtering

// Field export logic in toArray()
private function shouldExport(string|int $key, mixed $value): bool {
    return !is_null($value)
        && ($value !== '')
        && ($value !== [])
        && (is_string($key) ? (str_starts_with($key, '_') === false) : true);
}
// Filters: null values, empty strings, empty arrays, underscore-prefixed keys

Media Utilities

Image Utility Class

class Image implements CanProvideMessages {
    protected string $base64bytes = '';    // Base64 data or empty
    protected string $url = '';            // HTTP URL or empty
    protected string $mimeType;            // MIME type
}

// Construction patterns
Image::fromFile($imagePath);               // Load from file system; throws RuntimeException
                                           // if the file is unreadable or its MIME type
                                           // cannot be detected
Image::fromBase64($base64string, $mimeType); // From base64 data: string (must start with 'data:' prefix)
Image::fromUrl($imageUrl, $mimeType);      // From HTTP URL

// Content integration
$image->toContentPart();                   // ContentPart with OpenAI image_url structure
$image->toContent();                       // Content with single image part
$image->toMessage();                       // Message with user role
$image->toMessages();                      // Messages collection

// Data access
$image->toImageUrl();                      // URL or base64 string
$image->getBase64Bytes();                  // Base64 data
$image->getMimeType();                     // MIME type

// OpenAI format
$image->toContentPart()->toArray() produces:
[
    'type' => 'image_url',
    'image_url' => [
        'url' => $this->url ?: $this->base64bytes
    ]
]

$image->toArray() produces:
[
    'role' => 'user',
    'content' => [
        [
            'type' => 'image_url',
            'image_url' => ['url' => $this->url ?: $this->base64bytes]
        ],
    ],
]

File Utility Class

class File implements CanProvideMessages {
    public function __construct(
        string $fileData = '',             // '' means "no inline data" (e.g. fileId only);
                                           // anything else MUST be a data: URI or the
                                           // constructor throws, same as fromBase64()
        string $fileName = '',             // Original filename
        string $fileId = '',               // File identifier (for uploaded files)
        string $mimeType = 'application/octet-stream',
    );
}

// Construction
File::fromFile($filePath);                 // Load from file system; also sets fileName from
                                           // basename($filePath). Throws RuntimeException if
                                           // unreadable or MIME type cannot be detected
File::fromBase64($base64string, $mimeType); // From base64 data: string (must start with 'data:' prefix)

// Content integration
$file->toContentPart();                    // ContentPart with file structure
$file->toMessage();                        // Message with user role
$file->toMessages();                       // Messages collection

// Data access
$file->getBase64Bytes();                   // Base64 data
$file->getMimeType();                      // MIME type

// ContentPart output structure - only the keys that are actually set are emitted
$file->toContentPart() produces:
new ContentPart('file', [
    'file' => array_filter([
        'file_data' => $this->base64bytes,  // Base64 data if available
        'file_name' => $this->fileName,     // Original filename
        'file_id' => $this->fileId,         // File ID for uploaded files
    ], fn(string $value) => $value !== ''),
])

// Supports both uploaded files (file_id) and inline files (file_data)
// OpenAI API compatible structure
//
// Empty fields are OMITTED, not sent as "": a file referenced only by file_id emits
// ['file' => ['file_id' => 'file-...']] with no empty file_data/file_name alongside it,
// because providers pass such keys through verbatim rather than ignoring them.

Audio Utility Class

class Audio {
    public function __construct(
        protected string $format,          // Audio format: 'wav', 'mp3', etc.
        protected string $base64bytes,     // Base64 encoded audio data
    );

    // Accessors
    $audio->format();                      // string
    $audio->getBase64Bytes();              // string

    // ContentPart integration
    $audio->toContentPart() produces:
    new ContentPart('input_audio', [
        'input_audio' => array_filter([
            'format' => $this->format,
            'data' => $this->base64bytes,
        ], fn(string $value) => $value !== ''),
    ]);

    // OpenAI API compatible input_audio structure
    // As with File, an empty format/data is omitted rather than sent as "".
}

Metadata Utility Class

// Note: Cognesy\Utils\Metadata (shared utility, not in messages package)
final readonly class Metadata implements Countable, IteratorAggregate {
    private array $metadata;

    // Construction
    Metadata::empty();                         // Empty metadata
    Metadata::fromArray($array);               // From array
    new Metadata($array);                      // Direct construction

    // Immutable operations
    $metadata->withKeyValue($key, $value);     // Add/update key-value pair
    $metadata->withoutKey($key);               // Remove key
    $metadata->withMergedData(array $data);    // Merge array into metadata

    // Data access
    $metadata->get($key, $default);            // Get value with default
    $metadata->hasKey($key);                   // Check key existence
    $metadata->keys();                         // All keys array
    $metadata->isEmpty();                      // Check if empty
    $metadata->count();                        // Number of entries (Countable)
    $metadata->toArray();                      // Convert to array

    // Usage patterns for OpenAI content enhancement
    $imageMetadata = Metadata::empty()
        ->withKeyValue('detail', 'high')
        ->withKeyValue('alt_text', 'Description');

    $audioMetadata = Metadata::fromArray([
        'transcription' => 'Hello world',
        'confidence' => 0.95,
        'language' => 'en'
    ]);

    $fileMetadata = $metadata->withKeyValue('page_count', 42);
}

OpenAI API Content Part Compliance

Canonical output uses nested payload keys for non-text parts (e.g. image_url, file, input_audio). Flat legacy inputs like ['type' => 'image_url', 'url' => '...'] are accepted, but outputs are normalized to the nested shape.

Supported Content Part Types

// Text content part
[
    'type' => 'text',
    'text' => 'Hello world'
]

// Image content part (URL format)
[
    'type' => 'image_url',
    'image_url' => [
        'url' => 'https://example.com/image.jpg',
        'detail' => 'high'  // Optional: auto, low, high
    ]
]

// Image content part (base64 format)
[
    'type' => 'image_url',
    'image_url' => [
        'url' => 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...'
    ]
]

// Audio input content part
[
    'type' => 'input_audio',
    'input_audio' => [
        'data' => 'UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAI...', // Base64
        'format' => 'wav'  // wav, mp3
    ]
]

// File content part (inline)
[
    'type' => 'file',
    'file' => [
        'file_data' => 'data:application/pdf;base64,JVBERi0xLjQ...',
        'file_name' => 'document.pdf'
    ]
]

// File content part (uploaded)
[
    'type' => 'file',
    'file' => [
        'file_id' => 'file-BK7bzQj3FfUp6VNGYLssxKcE',
        'file_name' => 'uploaded_document.pdf'
    ]
]

Multimodal Message Examples

// Complete multimodal message structure
$message = [
    'role' => 'user',
    'content' => [
        [
            'type' => 'text',
            'text' => 'What is in this image and analyze the audio file?'
        ],
        [
            'type' => 'image_url',
            'image_url' => [
                'url' => 'https://example.com/chart.png',
                'detail' => 'high'
            ]
        ],
        [
            'type' => 'input_audio',
            'input_audio' => [
                'data' => 'UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAI...',
                'format' => 'wav'
            ]
        ],
        [
            'type' => 'file',
            'file' => [
                'file_id' => 'file-abc123',
                'file_name' => 'report.pdf'
            ]
        ]
    ]
];

// Using utility classes to build content
$content = new Content(
    ContentPart::text('Analyze this content:'),
    ContentPart::image(Image::fromUrl('https://example.com/image.jpg', 'image/jpeg')),
    ContentPart::audio(new Audio('wav', $base64AudioData)),
    ContentPart::file(File::fromFile('/path/to/document.pdf'))
);

// Enhanced with metadata
$content = $content->appendContentField('analysis_type', 'comprehensive');

MessageStore System Architecture

MessageStore Structure

final readonly class MessageStore {
    public Sections $sections;                // Collection of named sections
    public Metadata $parameters;              // Key-value parameters
}

// Construction
MessageStore::empty();                           // Empty store
MessageStore::fromSections(Section ...$sections);
MessageStore::fromMessages(Messages $messages, string $section = 'messages');
MessageStore::fromArray(array $data);         // Deserialize from array

// Section management
$store->sections();                           // Get Sections collection
$store->withSection(string $name);            // Ensure section exists
$store->setSection(Section $section);         // Add or replace a section
$store->removeSection(string $name);          // Remove a section by name
$store->select(string|array $sections);       // Select specific sections
$store->merge(MessageStore $other);           // Merge with another store
$store->withoutEmpty();                       // Remove empty sections

// Serialization
$store->toMessages();                         // Flatten to Messages
$store->toArray();                            // Export structured store array
$store->toFlatArray();                        // Export flat messages array
$store->toString();                           // Text representation

// Storage integration
MessageStore::fromStorage($storage, $sessionId); // Load from CanStoreMessages
$store->toStorage($storage, $sessionId);         // Save to CanStoreMessages

toStorage() calls CanStoreMessages::save(), which REPLACES the session with exactly this store - it is not a merge. Messages absent from $store are gone from the session afterwards, including any sibling branch that was not on the loaded path, so a load-mutate-save round trip through one branch discards the others. Use append() (with navigateTo() to choose the branch) for additive writes. Both InMemoryStorage and JsonlStorage implement this same contract.

Section Operator API

// Section access and queries
$store->section('system')->name();           // Get section name
$store->section('system')->exists();         // Check if section exists
$store->section('system')->isEmpty();        // Check if section empty
$store->section('system')->isNotEmpty();     // Check if section has content
$store->section('system')->get();            // Get Section object
$store->section('system')->messages();       // Get section messages

// Section mutations (immutable)
$store->section('system')->appendMessages($messages);
$store->section('system')->setMessages($messages);
$store->section('system')->setSection($section);  // Replace section (renames if needed)
$store->section('system')->remove();         // Remove entire section
$store->section('system')->clear();          // Clear section messages

Parameter Operator API

// Parameter access and management
$store->parameters()->get();                 // Get all parameters
$store->parameters()->setParameter('key', 'value');
$store->parameters()->unsetParameter('key');
$store->parameters()->mergeParameters($params);
$store->parameters()->withParams($newParams);

Section Class Structure

final readonly class Section implements Countable, IteratorAggregate {
    public string $name;
    public Messages $messages;
}

// Construction
Section::empty('section_name');
Section::fromArray($data);                 // Deserialize from array

// Accessors
$section->name();                          // string
$section->isEmpty();                       // bool
$section->messages();                      // Messages
$section->count();                         // int (Countable)
$section->getIterator();                   // IteratorAggregate

// Mutation (immutable)
$section->appendMessages($messages);
$section->withMessages($messages);
$section->appendContentField($key, $value);

// Transformation
$section->toMergedPerRole();
$section->withoutEmptyMessages();
$section->toArray();
$section->toString();

Sections Collection

final readonly class Sections {
    // Collection management
    public function add(Section ...$sections): Sections;
    public function set(Section ...$sections): Sections;    // Add or replace
    public function has(string $name): bool;
    public function get(string $name): ?Section;
    public function select(array $names): Sections;         // Select by names
    public function filter(callable $callback): Sections;
    public function remove(callable $callback): Sections;   // Remove matching
    public function merge(Sections $other): Sections;       // Merge collections
    public function withoutEmpty(): Sections;                // Filter empty sections
    public function toMessages(): Messages;

    // Iteration and access
    public function all(): array;
    // IteratorAggregate - iterate with foreach ($sections as $section)
    public function count(): int;
    public function names(): array;
    public function map(callable $callback): array;
    public function reduce(callable $callback, mixed $initial): mixed;
}

Messages Collection System

Messages Class Structure

final readonly class Messages implements Countable, IteratorAggregate {
    private MessageList $messages;

    public function __construct(Message ...$messages);
    public static function empty(): static;
}

MessageList Collection

final readonly class MessageList {
    /** @var Message[] */
    private array $messages;

    public static function empty(): self;
    public static function fromArray(array $messages): self;
    public function all(): array;
    public function add(Message $message): self;
    public function addAll(self $messages): self;
    public function prependAll(self $messages): self;
    public function replaceLast(Message $message): self;
    public function removeHead(): self;
    public function removeTail(): self;
    public function reversed(): self;
    public function withoutEmpty(): self;
    public function first(): ?Message;
    public function last(): ?Message;
    public function get(int $index): ?Message;
    public function count(): int;
    public function isEmpty(): bool;
    public function toArray(): array;
}

Messages Construction Patterns

// Basic construction
new Messages(...$messageArray);
Messages::empty();

// Factory methods
Messages::fromString($content, $role = 'user'); // Single message
Messages::fromArray($messagesArray);            // Array of message arrays
Messages::fromList($messageList);               // MessageList collection
Messages::fromMessages($arrayOrMessages);       // Array of Message objects
Messages::fromAnyArray($mixedArray);           // Mixed array types
Messages::fromAny($input);                     // Universal constructor
Messages::fromInput($input);                   // Input with provider support

// Input resolution in fromAny()
match(true) {
    is_string($messages) => self::fromString($messages),
    is_array($messages) => self::fromAnyArray($messages),
    $messages instanceof Message => new Messages($messages),
    $messages instanceof Messages => $messages,
    $messages instanceof MessageList => Messages::fromList($messages),
}

Messages Access API

// Basic access
$messages->first();                        // First message (or empty Message if none)
$messages->last();                         // Last message (or empty Message if none)
$messages->all();                          // Message[] array (deprecated)
$messages->messageList();                  // MessageList collection
$messages->count();                        // Message count (Countable)

// Iteration
$messages->each();                         // Generator<Message>
foreach ($messages->each() as $message) { /* */ }

// Collection operations
$messages->map($callback);                 // Transform to array
$messages->filter($callback);              // Keep the messages the predicate accepts, and
                                           // nothing else - empty messages are NOT also
                                           // dropped, so filter(fn($m) => $m->isEmpty())
                                           // selects them. Compose with
                                           // withoutEmptyMessages() if you want both.
$messages->filter();                       // @deprecated alias for withoutEmptyMessages()
$messages->reduce($callback, $initial);    // Reduce to single value

// Partitioning
$messages->head();                         // First message as array (deprecated)
$messages->tail();                         // Last message as array (deprecated)
$messages->headList();                     // First message as MessageList
$messages->tailList();                     // Last message as MessageList

// State checking
$messages->isEmpty();                      // All messages empty
$messages->notEmpty();                     // Has non-empty messages
$messages->hasComposites();                // Any message is composite

// Role-specific access
$messages->firstRole();                    // MessageRole of first message
$messages->lastRole();                     // MessageRole of last message

// ID-based access
$messages->getById(MessageId $id);         // ?Message
$messages->hasId(MessageId $id);           // bool

Messages Mutation API

// Role-based fluent appending
$messages->asSystem($content);             // Append system message
$messages->asDeveloper($content);          // Append developer message
$messages->asUser($content);               // Append user message
$messages->asAssistant($content);          // Append assistant message
$messages->asTool($content);               // Append tool message

// Message management (immutable)
$messages->withMessages($messages);        // Replace all messages (pass [$message] for one)
$messages->appendMessage($message);        // Append single message
$messages->appendMessages($messages);      // Append multiple messages
$messages->prependMessages($messages);     // Prepend multiple messages
$messages->removeHead();                   // Remove first message
$messages->removeTail();                   // Remove last message
$messages->appendContentField($key, $value); // Append field to last message content

Messages Transformation API

// Role-based operations
$messages->forRoles($roles);               // Filter by roles
$messages->exceptRoles($roles);            // Exclude roles
$messages->headWithRoles($roles);          // Take while role matches
$messages->tailAfterRoles($roles);         // Skip while role matches
$messages->remapRoles($mapping);           // Transform roles

// Content operations
$messages->contentParts();                 // ContentParts collection from all messages
$messages->toMergedPerRole();              // Merge consecutive same-role messages
                                           // (see Role-Based Merging for the boundaries)

// Collection operations
$messages->reversed();                     // Reverse message order
$messages->withoutEmptyMessages();         // Remove empty messages

Messages Serialization

// Array conversion
$messages->toArray();                      // Message array format (filters empty)
$messages->toString($separator = "\n");    // Text with separator (no composites)

// Static conversion utilities
Messages::asString($messageArray, $separator, $renderer); // Custom rendering

// $separator applies to the DEFAULT rendering only. A custom $renderer owns its own
// separation and receives no separator - deliberately, so that a renderer producing its
// own framing (JSON lines, XML tags, a chat transcript) does not get an extra separator
// injected into markup it controls.

// Composite handling
if ($messages->hasComposites()) {
    // toString() throws RuntimeException
    // Use toArray() or custom rendering
}

Advanced Message Operations

Message State Management

// Message access
$message->id();                            // MessageId
$message->role();                          // MessageRole enum
$message->name();                          // Name string
$message->content();                       // Content object
$message->contentParts();                  // ContentParts collection
$message->contentParts()->all();           // ContentPart[] array
$message->parentId();                      // ?MessageId

// Role and type checking
$message->isUser();                        // role === 'user'
$message->isAssistant();                   // role === 'assistant'
$message->isTool();                        // role === 'tool'
$message->isSystem();                      // role is system or developer
$message->isDeveloper();                   // role === 'developer'
$message->hasRole(MessageRole ...$roles);  // Check against multiple roles
$message->type();                          // MessageType enum (Text, AssistantToolCalls, ToolResult)

// Tool accessors
$message->hasToolCalls();                  // Has non-empty ToolCalls
$message->toolCalls();                     // ToolCalls collection
$message->hasToolResult();                 // Has a ToolResult (not null)
$message->toolResult();                    // ?ToolResult

// State checking
$message->isEmpty();                       // Content empty and no metadata/tool data
$message->isComposite();                   // Complex content structure

// Metadata operations
$message->metadata();                      // Get Metadata object
$message->withMetadata($key, $value);      // Add metadata (immutable)

Message Mutation (Immutable)

$message->withContent($content);           // Replace content
$message->withRole($role);                 // Change role (throws on unknown role)
$message->withName($name);                 // Change name
$message->withToolCalls($toolCalls);       // Replace ToolCalls
$message->withToolResult($toolResult);     // Replace ToolResult
$message->withParentId($parentId);         // Set parent message ID
$message->addContentFrom($sourceMessage); // Merge content from another message
$message->addContentPart(string|array|ContentPart $part); // Add content part
$message->withMergedFrom($source);         // Fold $source into this message

withMergedFrom() is the full fold, where addContentFrom() only moves content: content parts are appended, tool calls concatenated, and $source's metadata keys win on conflict, while identity (id, parentId, createdAt), role and name stay with the receiver - so a merged message keeps its place in a stored parentId chain.

It throws InvalidArgumentException if either message carries a tool result: a tool result is bound to a single tool call id, and folding two together would silently drop that binding. This is what toMergedPerRole() uses.

Message Serialization

// Array format (OpenAI compatible)
$message->toArray() produces:
[
    'id' => 'uuid-v4',
    'createdAt' => '2026-02-01T12:00:00+00:00',
    'parentId' => 'uuid-v4',               // Optional
    'role' => $this->role,
    'name' => $this->name,                 // If not empty
    'content' => /* content based on complexity */,
    '_metadata' => $this->metadata,        // If not empty
]

// Content serialization logic
'content' => match(true) {
    $this->content->isEmpty() => '',
    $this->content->isComposite() => $this->content->toArray(),
    default => $this->content->toString(),
}

// Text extraction
$message->toString();                      // Content as string

Message Validation and Detection

Format Detection

// Message format validation
Message::isMessage($array): bool {
    return isset($array['role']) && (
        isset($array['content']) || isset($array['_metadata'])
    );
}

Message::isMessages($array): bool {
    // All items must be valid messages
    foreach ($array as $message) {
        if (!self::isMessage($message)) return false;
    }
    return true;
}

// State detection utilities
Message::becomesComposite($messageArray);  // Will be composite after parsing
Messages::becomesEmpty($input);            // Will be empty after parsing
Messages::becomesComposite($messageArray); // Contains composite messages

Conversion and Integration Patterns

Role-Based Merging

// Merge consecutive same-role messages
$merged = $messages->toMergedPerRole();
$array = $merged->toArray();

A run of same-role messages is folded with Message::withMergedFrom(), so the merged message keeps the FIRST message of the run's id, parentId and createdAt, and carries the tool calls and metadata of every message in the run. Runs break at a role change and at any tool-result message: a message carrying a ToolResult is never merged with a neighbour, in either direction, because that binding belongs to one tool call id.

Provider Pattern Integration

// CanProvideMessage implementations
class CustomClass implements CanProvideMessage {
    public function toMessage(): Message {
        return new Message('user', $this->getText());
    }
}

// CanProvideMessages implementations
class CustomCollection implements CanProvideMessages {
    public function toMessages(): Messages {
        return Messages::fromArray($this->getData());
    }
}

// Usage in factory methods
Messages::fromInput($input) handles:
- Messages => direct return
- CanProvideMessages => $input->toMessages()
- Message => wrap in Messages
- CanProvideMessage => $input->toMessage() -> wrap
- default => TextRepresentation::fromAny($input) -> wrap

TextRepresentation Integration

// Used for arbitrary input conversion
Message::fromInput($input, $role) uses:
match(true) {
    $input instanceof Message => $input,
    $input instanceof CanProvideMessage => $input->toMessage(),
    default => new Message($role, TextRepresentation::fromAny($input)),
}

Messages::fromInput($input) uses similar pattern for Messages

Performance and Memory Considerations

Immutable Design Patterns

// Core value objects use immutable operations
$newMessage = $message->withRole('assistant');     // Creates new instance
$newContent = $content->addContentPart($part);     // Creates new instance
$newMessages = $messages->appendMessage($message); // Creates new instance

Lazy Evaluation Patterns

// Content complexity calculated on demand
$content->isComposite();                          // Evaluates state rules
$content->normalized();                           // Returns appropriate format

// Message state evaluation
$message->isEmpty();                              // Checks content + metadata
$message->isComposite();                          // Delegates to content

// Collection operations with generators
foreach ($messages->each() as $message) {          // Generator-based iteration
    // Process one at a time
}

Memory-Efficient Operations

// Field filtering in ContentPart constructor and export
$fields = array_filter($fields, fn($value, $key) =>
    !is_null($value) && ($value !== []), ARRAY_FILTER_USE_BOTH
);

// Messages::toArray() preserves the collection as-is
foreach ($this->messages as $message) {
    $result[] = $message->toArray();
}

// Efficient role-based operations
$messages->forRoles($roles);                      // Creates new Messages without cloning filtered messages