Sessions and runs are the two main lifecycle containers in AgentSessionManager. A session groups related interactions with an agent, and a run represents a single execution within that session.
Session Lifecycle
Sessions follow this state machine:
pending ──> active ──> completed
──> failed
──> cancelled
paused ──> active (resumed)Creating a Session
alias AgentSessionManager.Core.Session
{:ok, session} = Session.new(%{
agent_id: "research-agent", # required -- identifies the agent type
context: %{system_prompt: "Be concise."}, # optional -- shared context for all runs
metadata: %{user_id: "u-123"}, # optional -- arbitrary metadata
tags: ["research", "v2"] # optional -- for filtering
})
session.status # => :pending
session.id # => "ses_a1b2c3d4..."The agent_id identifies what kind of agent this session is for. The context map carries data shared across all runs in the session (system prompts, configuration). The metadata map is for your application's own data.
Session State Transitions
# Activate a pending session
{:ok, active} = Session.update_status(session, :active)
# Pause an active session
{:ok, paused} = Session.update_status(active, :paused)
# Resume a paused session
{:ok, resumed} = Session.update_status(paused, :active)
# Complete a session
{:ok, completed} = Session.update_status(active, :completed)Invalid transitions return error tuples:
{:error, %Error{code: :invalid_status}} = Session.update_status(session, :not_a_status)Via SessionManager
When using SessionManager, session transitions also persist to the store and emit events:
# Creates session, saves to store, emits :session_created event
{:ok, session} = SessionManager.start_session(store, adapter, %{agent_id: "my-agent"})
# Updates status, saves, emits :session_started event
{:ok, session} = SessionManager.activate_session(store, session.id)
# Updates status, saves, emits :session_completed event
{:ok, session} = SessionManager.complete_session(store, session.id)
# Marks failed with error context
{:ok, session} = SessionManager.fail_session(store, session.id, error)Persisted lifecycle events receive durable sequence_number assignment from the store at append time, enabling cursor-based replay with SessionStore.get_events/3.
Hierarchical Sessions
Sessions can reference a parent session for hierarchical agent workflows:
{:ok, parent} = Session.new(%{agent_id: "orchestrator"})
{:ok, child} = Session.new(%{
agent_id: "worker",
parent_session_id: parent.id
})Run Lifecycle
Runs follow this state machine:
pending ──> running ──> completed
──> failed
──> cancelled
──> timeoutCreating a Run
alias AgentSessionManager.Core.Run
{:ok, run} = Run.new(%{
session_id: session.id, # required -- parent session
input: %{messages: [%{role: "user", content: "Hello"}]}, # optional -- input data
metadata: %{request_id: "req-789"} # optional -- arbitrary metadata
})
run.status # => :pending
run.turn_count # => 0
run.token_usage # => %{}Run State Transitions
# Start execution
{:ok, running} = Run.update_status(run, :running)
# Complete with output
{:ok, completed} = Run.set_output(running, %{content: "Hello!", stop_reason: "end_turn"})
completed.status # => :completed
completed.ended_at # => %DateTime{...}
# Or fail with error
{:ok, failed} = Run.set_error(running, %{code: :provider_timeout, message: "Timed out"})
failed.status # => :failedTerminal statuses (:completed, :failed, :cancelled, :timeout) automatically set the ended_at timestamp.
Token Usage Tracking
Token usage accumulates across updates:
{:ok, run} = Run.update_token_usage(run, %{input_tokens: 100, output_tokens: 50})
{:ok, run} = Run.update_token_usage(run, %{input_tokens: 20, output_tokens: 30})
run.token_usage
# => %{input_tokens: 120, output_tokens: 80}Turn Counting
{:ok, run} = Run.increment_turn(run)
{:ok, run} = Run.increment_turn(run)
run.turn_count # => 2Via SessionManager
# Creates run, saves, checks capabilities
{:ok, run} = SessionManager.start_run(store, adapter, session.id, input)
# Executes via adapter, persists events and result
{:ok, result} = SessionManager.execute_run(store, adapter, run.id)
# Cancels via adapter, updates status
{:ok, _} = SessionManager.cancel_run(store, adapter, run.id)Feature options are available through the same execute_run/4 API:
{:ok, result} = SessionManager.execute_run(store, adapter, run.id,
continuation: :auto,
continuation_opts: [max_messages: 100],
adapter_opts: [timeout: 120_000],
workspace: [
enabled: true,
path: File.cwd!(),
strategy: :auto,
capture_patch: true,
max_patch_bytes: 1_048_576,
rollback_on_failure: false
]
)Querying Sessions and Runs
The SessionStore port supports filtering:
alias AgentSessionManager.Ports.SessionStore
# List all sessions
{:ok, sessions} = SessionStore.list_sessions(store)
# Filter by status
{:ok, active} = SessionStore.list_sessions(store, status: :active)
# Filter by agent
{:ok, agent_sessions} = SessionStore.list_sessions(store, agent_id: "my-agent")
# List runs for a session
{:ok, runs} = SessionStore.list_runs(store, session.id)
{:ok, completed_runs} = SessionStore.list_runs(store, session.id, status: :completed)
# Get the currently running run
{:ok, active_run} = SessionStore.get_active_run(store, session.id)
# => {:ok, %Run{status: :running, ...}} or {:ok, nil}Serialization
Both sessions and runs can be serialized to maps (for JSON, database storage, etc.) and reconstructed:
# Serialize
map = Session.to_map(session)
# => %{"id" => "ses_...", "agent_id" => "my-agent", "status" => "pending", ...}
# Reconstruct
{:ok, restored} = Session.from_map(map)
# Same for runs
run_map = Run.to_map(run)
{:ok, restored_run} = Run.from_map(run_map)Provider Metadata
When runs execute through SessionManager, provider-specific metadata is merged
into run metadata and stored in session.metadata[:provider_sessions]:
{:ok, result} = SessionManager.execute_run(store, adapter, run.id)
{:ok, updated_run} = SessionStore.get_run(store, run.id)
updated_run.metadata
# => %{provider: "claude", provider_session_id: "sess_...", model: "claude-haiku-4-5-..."}