Introduction
Agents are stateless by default — anAgentLoop takes an AgentState, runs to completion, and returns the updated state. There is no built-in persistence between requests. The SessionRuntime layer adds that persistence, turning an agent into a long-lived conversation that survives across HTTP requests, CLI invocations, or background jobs.
A session wraps an AgentDefinition (what the agent is) and an AgentState (what the agent has done) together with lifecycle metadata like status, version, and timestamps. The runtime manages loading, executing actions, and saving sessions through a transactional pipeline with optimistic locking and event emission.
This is the foundation for building multi-turn chat applications, resumable workflows, and any scenario where agent state must persist beyond a single process.
Core Types
The session system is built around a small set of types, each with a focused responsibility:| Type | Purpose |
|---|---|
AgentSession | Combines session info, agent definition, and agent state into one persistent unit |
AgentSessionInfo | Header data: session ID, agent name, status, version, timestamps, parent session ID |
SessionId | Value object wrapping a UUID string. Use SessionId::generate() to create new IDs. |
SessionStatus | Enum: Active, Suspended, Completed, Failed, Deleted |
SessionRepository | Thin wrapper over a CanStoreSessions implementation |
SessionRuntime | Preferred create/read/write boundary: creates sessions, executes actions, applies hooks, and emits events |
SessionFactory | Lower-level helper that builds fresh AgentSession instances from an AgentDefinition |
The Runtime Contract
TheCanManageAgentSessions interface defines the public API that SessionRuntime implements:
create() for brand-new root sessions. The read methods (listSessions, getSessionInfo, getSession) load data but do not persist any changes. The execute() method updates an existing persisted session by loading it, running an action, saving the result, and returning the updated session.
Quick Start
The following example creates a session, sends a message, and retrieves the result:The Create and Execute Pipelines
The Create Pipeline
When you call$runtime->create($definition, $seed), the following pipeline runs:
- Instantiate session — A fresh
AgentSessionis created from theAgentDefinitionand optional seed state. - BeforeCreate hook — The session controller’s
onStage(BeforeCreate, ...)is called. Use this for create-only logic such as setting defaults or validating creation-time policy. - BeforeSave hook — The session controller’s
onStage(BeforeSave, ...)is called. This fires on both create and execute, so use it for logic that should run before every persist. - Create — The session is persisted through the repository. Stores require a fresh session with version
0and persist it as version1. - AfterSave hook — Post-persistence processing runs on the persisted session returned by the store. Fires on both create and execute.
- AfterCreate hook — The session controller’s
onStage(AfterCreate, ...)is called. Use this for create-only post-persist logic such as sending notifications. - SessionSaved event — Emitted to confirm successful persistence.
SessionSaveFailed is emitted and the original exception is rethrown.
Use this path for new root sessions. Reach for SessionFactory + repository create() only when you already have a concrete AgentSession instance to persist, such as a forked branch.
The Execute Pipeline
When you call$runtime->execute($sessionId, $action), the following pipeline runs:
- Load — The session is loaded from the repository. If not found,
SessionNotFoundExceptionis thrown. - AfterLoad hook — The session controller’s
onStage(AfterLoad, ...)is called, allowing pre-processing. - SessionLoaded event — Emitted for observability.
- Action execution —
$action->executeOn($session)runs the action and returns the next session state. - AfterAction hook — The session controller processes the post-action state.
- BeforeSave hook — Last chance to modify the session before persistence (e.g., auto-suspend).
- SessionActionExecuted event — Emitted with before/after status and version.
- Save — The session is saved with optimistic version checking.
- AfterSave hook — Post-persistence processing.
- SessionSaved event — Emitted to confirm successful persistence.
SessionLoadFailed is emitted. If saving fails (e.g., version conflict), SessionSaveFailed is emitted. In both cases, the original exception is rethrown after the event.
Built-in Actions
Actions implement theCanExecuteSessionAction interface, which defines a single method:
SendMessage
The primary action for agent interaction. Appends a user message to the session’s state, instantiates anAgentLoop from the session’s definition, runs the loop to completion, and stores the resulting state.
message parameter accepts a string, \Stringable, or Message object. Stringable values are cast to string at the boundary. The loopFactory must implement CanInstantiateAgentLoop — typically a DefinitionLoopFactory.
SuspendSession and ResumeSession
Pause and resume a session. Suspended sessions are preserved but not actively processing.SuspendSession sets the status to Suspended. ResumeSession sets it back to Active.
ClearSession
Resets the session’s agent state while preserving the session identity and definition. The state is prepared for the next execution viaforNextExecution().
ForkSession
Creates a new session that inherits the state and definition of the source session. The forked session gets a freshSessionId and its parent is set to the source session’s ID.
ForkSession is typically used outside the runtime’s execute() pipeline because it creates a new session rather than modifying the existing one. This is the main case where persisting via repository create() is still appropriate: you already have a fully constructed AgentSession, so you persist that branch directly instead of calling SessionRuntime::create().
ChangeSystemPrompt
Updates the system prompt in the session’s agent state. Acceptsstring|\Stringable — Stringable values are cast to string at the boundary.
ChangeModel
Swaps the LLM configuration for future executions within the session.WriteMetadata
Stores a key-value pair in the session’s metadata. Useful for tracking external references, workflow state, or custom tags.UpdateTask
Updates the task description associated with the session.Versioning and Optimistic Locking
Sessions use optimistic locking to prevent concurrent modifications from silently overwriting each other. Every session has a monotonically increasing version number.Version Lifecycle
- Create — The session must have version
0. It is persisted as version1. - Save — The incoming session’s version must match the stored version. The persisted session is returned with version incremented by
1. - Read — Loading a session returns it with the stored version, which must be used for the next write.
Conflict Handling
If two processes load the same session (both see version5), the first to save succeeds and advances the version to 6. The second process’s save fails because it still has version 5, which no longer matches the stored version 6. This triggers a SessionConflictException.
Exception Types
| Exception | Condition |
|---|---|
SessionNotFoundException | The session ID does not exist in the store |
SessionConflictException | Version mismatch during save, or attempting to create an existing session |
InvalidSessionFileException | File-based store encountered a corrupt or unreadable file |
Persistence Stores
The session system ships with twoCanStoreSessions implementations.
InMemorySessionStore
Stores sessions in a PHP array. Useful for testing, prototyping, and single-process applications.FileSessionStore
Stores each session as a JSON file on disk. Supports concurrent access through file locking (flock).
{session_id}.json with atomic writes (write to .tmp, then rename). Lock files ({session_id}.lock) are used for mutual exclusion during create and save operations.
Custom Stores
Implement theCanStoreSessions interface to integrate with any persistence backend:
create() requires version 0, and save() must match the stored version. Use AgentSession::reconstitute() to set the next version and timestamp before persisting.
Session Lifecycle vs Execution Lifecycle
The session system has two distinct lifecycle models that operate independently.Session Lifecycle
The session lifecycle tracks the overall status of the agent conversation across multiple requests. Status transitions are explicit — they only happen when an action explicitly changes the status.AgentSession::withState() method updates the agent state without changing the session status. This is intentional: the session status represents a cross-run concern (is this conversation still active?), while the execution status represents a per-run concern (did this particular run succeed?).
Execution Lifecycle
Each call toSendMessage creates a new execution within the session. The AgentState tracks execution status (Pending, InProgress, Completed, Stopped, Failed) independently of the session status. Between executions, the state is reset via forNextExecution().
A session can be Active while its last execution was Failed — the session is still open for new messages, even though the most recent run encountered an error.
Session Controllers
Session controllers intercept the runtime pipeline at four stages, allowing you to modify the session at each point. This is how you implement cross-cutting session concerns like auto-suspend, validation, or audit logging.The CanControlAgentSession Interface
AgentSessionStage enum defines the four interception points:
| Stage | When | Typical Use |
|---|---|---|
AfterLoad | After loading from the store | Validation, enrichment |
AfterAction | After the action has executed | Post-processing, derived state |
BeforeSave | Before persisting to the store | Auto-suspend, status derivation |
AfterSave | After successful persistence | Notifications, audit logging |
Using SessionHookStack
TheSessionHookStack composes multiple controllers into a priority-ordered pipeline:
SessionHookStack itself implements CanControlAgentSession, so you can also pass a single controller directly to the runtime constructor.
If no controller is provided, the runtime uses PassThroughSessionController, which returns the session unchanged at every stage.
Events
TheSessionRuntime emits events at key points in the pipeline. All events are dispatched through the CanHandleEvents instance passed to the runtime constructor.
| Event | When | Key Data |
|---|---|---|
SessionLoaded | After successfully loading a session | sessionId, version, status |
SessionActionExecuted | After an action completes (before save) | sessionId, action class name, before/after version and status |
SessionSaved | After successful persistence | sessionId, version, status |
SessionLoadFailed | When loading throws an exception | sessionId, error, errorType |
SessionSaveFailed | When saving throws an exception | sessionId, error, errorType |
Writing Custom Actions
To create a custom action, implement theCanExecuteSessionAction interface: