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