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
                    > timeout

Creating 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  # => :failed

Terminal 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  # => 2

Via 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-..."}