Skip to content

Persistence Reference

Atlas persistence is an optional layer that tracks conversations, executions, and assets. When enabled, Atlas automatically records every AI interaction with full observability.

Setup

env
ATLAS_PERSISTENCE_ENABLED=true
bash
php artisan vendor:publish --tag=atlas-migrations
php artisan migrate

Tables Overview

All tables are prefixed with atlas_ by default (configurable via persistence.table_prefix in config/atlas.php).

TablePurpose
atlas_conversationsConversation threads between users and agents
atlas_conversation_messagesIndividual messages within conversations
atlas_conversation_message_assetsLinks messages to generated files (images, audio, etc.)
atlas_executionsEvery AI provider call — agent or direct — with tokens and timing
atlas_execution_stepsEach round trip in the agent tool loop
atlas_execution_tool_callsIndividual tool invocations with arguments and results
atlas_assetsGenerated files stored on disk with tool call linkage
atlas_conversation_voice_callsVoice call sessions with complete transcripts

Conversations

What it stores: A thread of messages between one or more users and one or more agents, scoped to an owner model.

Why it exists: Agents need conversation history to maintain context across multiple interactions. The conversation record ties messages to an owner (user, team, or any Eloquent model) and tracks metadata like the title and which agent manages the thread.

ColumnTypeWhy
idbigintPrimary key
owner_typestring(255) nullablePolymorphic — the model that owns this conversation (User, Team, etc.)
owner_idunsignedBigInteger nullablePolymorphic — the owner's ID
agentstring(255) nullableWhich agent manages this conversation. Allows one owner to have separate conversations with different agents
titlestring(255) nullableAuto-generated from the first user message. Useful for conversation lists in a UI
summarytext nullableConsumer-provided or auto-generated summary of the conversation
metadatajson nullableConsumer-provided metadata from withMeta(). Stored on the conversation for app-specific context
created_attimestampWhen the conversation started
updated_attimestampWhen the conversation was last updated
deleted_attimestamp nullableSoft delete — conversations are never hard-deleted

Messages

What it stores: Every message in a conversation — user inputs, assistant responses, and system messages.

Why it exists: Messages are the core of conversation persistence. They store the full thread, support retry/sibling branching via parent_id and is_active, enable multi-agent conversations via the agent column, and link to execution data for tool call reconstruction.

ColumnTypeWhy
idbigintPrimary key
conversation_idbigintFK → conversations. Which conversation this message belongs to
parent_idbigint nullableFK → conversation_messages (self-reference). Links assistant responses to the user message they answer. Enables sibling tracking — multiple retry responses share the same parent
step_idunsignedBigInteger nullableFK → execution_steps. Links assistant messages to their execution step so tool calls can be reconstructed when loading history
execution_idunsignedBigInteger nullableFK → executions. Links this message to the execution that produced it
owner_typestring nullablePolymorphic — who sent this message (User model, etc.). Separate from role because multiple users can send user role messages
owner_idunsignedBigInteger nullablePolymorphic — the owner's ID
agentstring(255) nullableWhich agent authored this message. Enables multi-agent conversations where different agents respond in the same thread
rolestring(20)user, assistant, or system (backed by MessageRole enum). Determines how the message is sent to the AI provider
statusstring(20)delivered (normal), queued (waiting to be processed), or failed (backed by MessageStatus enum)
contenttext nullableThe message text
sequenceunsignedIntegerOrdering within the conversation (starts at 1). Unique per conversation — ensures consistent message order
is_activebooleanControls visibility in conversation history. When you retry a response, the old one is deactivated (false) and only the active sibling appears in future history loads
read_attimestamp nullableWhen the message was read. Enables unread message counts and read receipts
metadatajson nullableAdditional metadata
embeddingvector nullablePostgreSQL only — vector embedding for semantic search over message history
embedding_attimestamp nullablePostgreSQL only — when the embedding was generated
created_attimestampWhen the message was created
updated_attimestampWhen the message was last updated

Message Attachments

What it stores: Links between messages and generated assets (images, audio files, etc.).

Why it exists: When a tool generates an image or audio file during an agent execution, the asset needs to be associated with the assistant message so a UI can display it inline.

ColumnTypeWhy
idbigintPrimary key
message_idbigintFK → conversation_messages. The message this asset is attached to
asset_idbigintFK → assets. The generated file
metadatajson nullableAttachment context — which tool produced it, tool call ID
created_attimestamp nullableWhen the attachment was created

Executions

What it stores: A record of every AI provider interaction — both agent executions and direct modality calls (images, audio, etc.).

Why it exists: Full observability. Every call to an AI provider is tracked with the provider, model, token counts, timing, and status. This is the audit trail for cost tracking, debugging, and monitoring. Messages and voice calls link back to their execution via execution_id.

ColumnTypeWhy
idbigintPrimary key
conversation_idbigint nullableFK → conversations. Set when the execution is part of a conversation. Null for standalone direct calls
statusunsignedTinyIntegerLifecycle state as int-backed ExecutionStatus enum: 0 (Pending) → 1 (Queued) → 2 (Processing) → 3 (Completed) or 4 (Failed)
agentstring(255) nullableAgent key. Null for direct modality calls
typestring(30)What type of execution, backed by ExecutionType enum: text, structured, stream, image, image_to_text, audio, audio_to_text, video, video_to_text, music, sfx, speech, embed, moderate, rerank, voice
providerstring(50)Which provider was called (openai, anthropic, etc.)
modelstring(100)Which model was used (gpt-4o, claude-sonnet-4-20250514, etc.)
usagejson nullableToken usage data: {input_tokens, output_tokens, reasoning_tokens?, cached_tokens?, cache_write_tokens?}
errortext nullableError message when execution fails. Includes the exception message for debugging
metadatajson nullableConsumer metadata from withMeta()
started_attimestamp nullableWhen the execution began processing
completed_attimestamp nullableWhen the execution finished (success or failure)
duration_msunsignedInteger nullableWall-clock duration in milliseconds
created_attimestampWhen the execution record was created
updated_attimestampWhen the execution record was last updated

Execution Steps

What it stores: Each round trip in the agent's tool call loop.

Why it exists: An agent execution may involve multiple calls to the AI provider — the model responds, calls tools, gets results, and responds again. Each of these round trips is a step. Steps record the model's response at each point and the finish reason (did it stop, or does it want to call more tools?).

ColumnTypeWhy
idbigintPrimary key
execution_idbigintFK → executions. Which execution this step belongs to
sequenceunsignedIntegerStep number (1, 2, 3...). Represents the round-trip order in the tool loop
statusunsignedTinyIntegerInt-backed ExecutionStatus enum: 0 (Pending) → 2 (Processing) → 3 (Completed) or 4 (Failed)
contenttext nullableThe model's response text at this step. May be an intermediate response before tool calls, or the final response
reasoningtext nullableReasoning/thinking content from models that support it (e.g. Anthropic extended thinking, OpenAI o-series)
finish_reasonstring(30) nullableWhy the model stopped: stop (done), tool_calls (wants to call tools), length (hit token limit), content_filter (blocked)
errortext nullableError message if this step failed
metadatajson nullableAdditional context
started_attimestamp nullableWhen this step started
completed_attimestamp nullableWhen this step completed
duration_msunsignedInteger nullableHow long this provider call took
created_attimestampWhen the step record was created
updated_attimestampWhen the step record was last updated

Execution Tool Calls

What it stores: Each individual tool invocation within a step.

Why it exists: When the model requests tool calls, each tool runs independently. This table records what tool was called, what arguments it received, what it returned, and how long it took. Essential for debugging tool behavior and understanding the agent's decision-making process.

ColumnTypeWhy
idbigintPrimary key
execution_idbigintFK → executions. Top-level execution reference for fast querying
step_idbigint nullableFK → execution_steps. Which step triggered this tool call
tool_call_idstring(100)The provider's unique ID for this tool call (used to match results back to requests)
statusunsignedTinyIntegerInt-backed ExecutionStatus enum: 0 (Pending) → 2 (Processing) → 3 (Completed) or 4 (Failed)
namestring(100)Tool name (e.g. lookup_order, web_search)
typestring(20)local for user-defined tools, mcp for MCP tools, provider for native provider tools (backed by ToolCallType enum)
argumentsjson nullableThe arguments the model passed to the tool. Stored as JSON for inspection
resulttext nullableThe serialized return value from the tool. What was sent back to the model
started_attimestamp nullableWhen tool execution started
completed_attimestamp nullableWhen tool execution completed
duration_msunsignedInteger nullableHow long the tool took to execute
metadatajson nullableAdditional context
created_attimestampWhen the record was created
updated_attimestampWhen the record was last updated

Assets

What it stores: Generated files — images, audio, video — stored on disk with optional vector embeddings for semantic search.

Why it exists: When Atlas generates an image, audio clip, or video, the binary content is stored on a configured disk (local, S3, etc.) and tracked in this table. Assets can be linked to executions (which call produced them), to tool calls (which tool generated them), and to messages (for display in a conversation UI).

ColumnTypeWhy
idbigintPrimary key
execution_idunsignedBigInteger nullableFK → executions. Which execution produced this asset
tool_call_idunsignedBigInteger nullableFK → execution_tool_calls. Which tool call generated this asset. Null for direct modality calls and user uploads
owner_typestring nullablePolymorphic — who generated this asset. Derived from the execution's conversation owner
owner_idunsignedBigInteger nullablePolymorphic — the owner's ID
agentstring(255) nullableWhich agent generated this asset
typestring(20)Asset type backed by AssetType enum: image, audio, video, document, text, json, file
mime_typestring(100) nullableMIME type (e.g. image/png, audio/mpeg)
filenamestring(255)Generated filename (UUID-based)
pathstring(500)Storage path on disk
diskstring(50)Laravel filesystem disk name
size_bytesunsignedBigInteger nullableFile size in bytes
descriptiontext nullableOptional description
metadatajson nullableConsumer-provided metadata only. No internal Atlas data is stored here
created_attimestampWhen the asset was stored
updated_attimestampWhen the asset record was last updated
deleted_attimestamp nullableSoft delete
embeddingvector nullablePostgreSQL only — vector embedding for semantic search
embedding_attimestamp nullablePostgreSQL only — when the embedding was generated

Voice Calls

What it stores: A complete voice call session with its transcript stored as a JSON array. Voice transcripts are isolated from the messages table — they live here. Consumers listen for VoiceCallCompleted to post-process transcripts (create summaries, embed into memory, generate conversation messages).

Table name: atlas_conversation_voice_calls

ColumnTypeWhy
idbigintPrimary key
conversation_idbigint nullableFK → conversations
execution_idunsignedBigInteger nullableFK → executions. Links this voice call to its execution record
voice_session_idstring(100)Unique session ID from provider
owner_typestring nullablePolymorphic — who initiated this call
owner_idunsignedBigInteger nullablePolymorphic — the owner's ID
agentstring(255) nullableAgent key
providerstring(50)Provider name
modelstring(100)Model name
statusstring(20)active, completed, failed (backed by VoiceCallStatus enum)
transcriptjson nullable`[{role: 'user'
summarytext nullableConsumer-generated summary
duration_msunsignedInteger nullableWall-clock duration
metadatajson nullableCustom metadata
started_attimestamp nullableWhen the voice session started
completed_attimestamp nullableWhen the voice session ended
created_attimestampWhen the record was created
updated_attimestampWhen the record was last updated

Relationships

Conversation
├── has many ConversationMessages
├── has many Executions
└── belongs to Owner (polymorphic)

ConversationMessage
├── belongs to Conversation
├── belongs to Execution (optional)
├── belongs to ExecutionStep (via step_id, optional)
├── has many siblings (same parent_id)
├── has many responses (children where parent_id = this.id)
├── has many ConversationMessageAssets
└── belongs to Owner (polymorphic)

Execution
├── belongs to Conversation (optional)
├── has one ConversationMessage (via conversation_messages.execution_id)
├── has one VoiceCall (via conversation_voice_calls.execution_id)
├── has many ExecutionSteps
├── has many ExecutionToolCalls
└── has many Assets

ExecutionStep
├── belongs to Execution
└── has many ExecutionToolCalls

ExecutionToolCall
├── belongs to Execution
└── belongs to ExecutionStep

Asset
├── has many ConversationMessageAssets
├── belongs to Execution (optional)
└── belongs to ExecutionToolCall (optional, via tool_call_id)

VoiceCall
├── belongs to Conversation (optional)
└── belongs to Execution (optional)

Models

All persistence models live in the Atlasphp\Atlas\Persistence\Models namespace.

Conversation

Atlasphp\Atlas\Persistence\Models\Conversation

A conversation thread owned by a polymorphic model (User, Team, etc.).

RelationshipTypeDescription
owner()MorphToThe owning model
messages()HasMany → ConversationMessageAll messages in the conversation, ordered by sequence
executions()HasMany → ExecutionAll executions linked to this conversation
MethodDescription
recentMessages(int $limit)Get the last N active, delivered messages
nextSequence()Get the next message sequence number
ScopeDescription
forOwner(Model $owner)Filter by polymorphic owner
forAgent(string $agent)Filter by agent key

ConversationMessage

Atlasphp\Atlas\Persistence\Models\ConversationMessage

A single message in a conversation — user input, assistant response, or system message. Supports sibling branching for retry/regenerate.

RelationshipTypeDescription
conversation()BelongsTo → ConversationParent conversation
execution()BelongsTo → ExecutionThe execution that produced this message
step()BelongsTo → ExecutionStepLinked execution step (for tool call reconstruction)
parent()BelongsTo → selfThe message this is a response to
siblings()HasMany → selfAll messages sharing the same parent (retry alternatives)
responses()HasMany → selfChild messages (responses to this message)
assets()HasMany → ConversationMessageAssetLinked file assets (images, audio, documents)
owner()MorphToWho sent this message (from HasOwner trait)
MethodDescription
toAtlasMessage()Convert to a typed message object for the provider
toAtlasMessagesWithTools()Convert to AssistantMessage with tool calls reconstructed from the execution step
ownerInfo()Get unified owner info array for UI rendering
canRetry()Whether this message can be retried (no later user message exists)
siblingGroups()Group siblings by execution (multi-step responses stay together)
siblingCount()Number of sibling groups
siblingIndex()1-based index in the sibling list
markAsRead()Set read_at to now
markDelivered()Transition queued message to delivered
isFromUser()Whether this is a user role message
isFromAssistant()Whether this is an assistant role message
isSystem()Whether this is a system role message
isRead() / isUnread()Check read status
isDelivered() / isQueued()Check delivery status
ScopeDescription
active()Only active messages (is_active = true)
read() / unread()Filter by read status
delivered() / queued()Filter by delivery status

Execution

Atlasphp\Atlas\Persistence\Models\Execution

A tracked AI provider call — agent execution or direct modality call — with usage, timing, and status.

RelationshipTypeDescription
conversation()BelongsTo → ConversationLinked conversation (null for standalone calls)
message()HasOne → ConversationMessageThe message this execution produced
voiceCall()HasOne → VoiceCallThe voice call linked to this execution
steps()HasMany → ExecutionStepRound trips in the tool call loop, ordered by sequence
toolCalls()HasMany → ExecutionToolCallAll tool invocations across all steps
assets()HasMany → AssetGenerated files (images, audio, video)
MethodDescription
markQueued()Transition to queued status
markCompleted(?int $durationMs, ?Usage $usage)Transition to completed with duration and usage
markFailed(string $error, ?int $durationMs, ?Usage $usage)Transition to failed with error and usage
getUsageObject()Get the usage as a Usage DTO
getTotalTokensAttribute()Accessor for total token count (input + output)
ScopeDescription
pending() / processing() / completed() / failed()Filter by ExecutionStatus (from HasExecutionStatus trait)
queued()Filter for queued executions
forAgent(string $agent)Filter by agent key
forProvider(string $provider)Filter by provider
ofType(ExecutionType $type)Filter by execution type
producedAssets()Filter to executions that have related assets

ExecutionStep

Atlasphp\Atlas\Persistence\Models\ExecutionStep

A single round trip in the agent's tool call loop — one provider call and its response.

RelationshipTypeDescription
execution()BelongsTo → ExecutionParent execution
toolCalls()HasMany → ExecutionToolCallTool calls made during this step
MethodDescription
recordResponse(?string $content, ?string $reasoning, string $finishReason)Record the provider response (content, reasoning, finish reason)
markCompleted(?int $durationMs)Transition to completed
markFailed(string $error, ?int $durationMs)Transition to failed
hasToolCalls()Whether this step triggered tool calls (finish reason is tool_calls)
ScopeDescription
pending() / processing() / completed() / failed()Filter by ExecutionStatus (from HasExecutionStatus trait)

ExecutionToolCall

Atlasphp\Atlas\Persistence\Models\ExecutionToolCall

An individual tool invocation with arguments, result, and timing.

RelationshipTypeDescription
execution()BelongsTo → ExecutionParent execution
step()BelongsTo → ExecutionStepThe step that triggered this call
MethodDescription
markCompleted(string $result, int $durationMs)Record result and complete
markFailed(string $error, int $durationMs)Record error and fail
ScopeDescription
pending() / processing() / completed() / failed()Filter by ExecutionStatus (from HasExecutionStatus trait)
forTool(string $name)Filter by tool name

Asset

Atlasphp\Atlas\Persistence\Models\Asset

A stored file (image, audio, video, document) with optional vector embeddings for semantic search.

RelationshipTypeDescription
messageAssets()HasMany → ConversationMessageAssetMessages this asset is linked to
execution()BelongsTo → ExecutionThe execution that produced this asset
toolCall()BelongsTo → ExecutionToolCallThe tool call that generated this asset (null for direct calls and user uploads)
owner()MorphToWho generated this asset (from HasOwner trait)
MethodDescription
url(string $prefix)Generate a URL for this asset
extension()Get the file extension
isMedia()Whether this is an image, audio, or video
ScopeDescription
forExecution(int $executionId)Filter by execution

ConversationMessageAsset

Atlasphp\Atlas\Persistence\Models\ConversationMessageAsset

Join model linking a message to an asset. Carries metadata about which tool produced it.

RelationshipTypeDescription
message()BelongsTo → ConversationMessageThe message
asset()BelongsTo → AssetThe asset

VoiceCall

Atlasphp\Atlas\Persistence\Models\VoiceCall

A complete voice call session with its transcript stored as a JSON array. Voice transcripts are isolated from the messages table — they live here. Consumers listen for VoiceCallCompleted to post-process transcripts (create summaries, embed into memory, generate conversation messages).

RelationshipTypeDescription
conversation()BelongsTo → ConversationLinked conversation
execution()BelongsTo → ExecutionThe execution record for this voice call
owner()MorphToWho initiated this call (from HasOwner trait)
MethodDescription
saveTranscript(array $turns)Atomically replace transcript
markCompleted(array $turns)Complete with final transcript and duration
markFailed()Mark as failed
isActive()Whether this call is currently active
isCompleted()Whether this call has completed
ScopeDescription
forConversation(int $id)Filter by conversation
forSession(string $sessionId)Filter by session ID
active()Only active calls
completed()Only completed calls

Lifecycle

The framework handles lifecycle transitions automatically:

  • active — Created when asVoice() is called. Transcript is checkpointed as turns complete.
  • completed — Set when the browser sends a close request, or when the stale cleanup command runs. You don't need to call markCompleted() — the close endpoint does it.
  • failed — Available for consumer use. Call $voiceCall->markFailed() in your error handling if needed.

Querying

php
use Atlasphp\Atlas\Persistence\Models\VoiceCall;

// All calls for a conversation
VoiceCall::forConversation($conversationId)->get();

// By provider session ID
VoiceCall::forSession('rt_xai_abc123...')->first();

// Recent completed calls with their execution
VoiceCall::completed()->with('execution.toolCalls')->latest()->take(10)->get();

// Active calls (still in progress)
VoiceCall::active()->get();

Execution Relationship

The conversation_voice_calls table has an execution_id FK pointing to the executions table. This links the voice call to its execution record, which tracks tool calls made during the session:

php
$call = VoiceCall::forSession($sessionId)->first();

// Get tool calls from the voice session via the execution
$toolCalls = $call->execution?->toolCalls;

Model Overrides

Extend the base models with your own:

php
// config/atlas.php → persistence.models
'models' => [
    'conversation'              => \App\Models\AtlasConversation::class,
    'conversation_message'      => \App\Models\AtlasMessage::class,
    'asset'                     => \App\Models\AtlasAsset::class,
    'conversation_message_asset' => \App\Models\AtlasMessageAsset::class,
    'execution'                 => \App\Models\AtlasExecution::class,
    'execution_step'            => \App\Models\AtlasExecutionStep::class,
    'execution_tool_call'       => \App\Models\AtlasExecutionToolCall::class,
    'voice_call'                => \App\Models\AtlasVoiceCall::class,
],

Your custom models must extend the corresponding Atlas base model.

Configuration

php
// config/atlas.php → persistence
'persistence' => [
    'enabled' => env('ATLAS_PERSISTENCE_ENABLED', false),
    'table_prefix' => env('ATLAS_TABLE_PREFIX', 'atlas_'),
    'message_limit' => (int) env('ATLAS_MESSAGE_LIMIT', 50),
    'auto_store_assets' => env('ATLAS_AUTO_STORE_ASSETS', true),
],
OptionDefaultPurpose
enabledfalseEnable persistence globally
table_prefixatlas_Prefix for all persistence tables
message_limit50Default conversation history limit
auto_store_assetstrueAutomatically store generated files

Released under the MIT License.