ExAgent.Session (ExAgent v0.3.0)

Copy Markdown View Source

A coordinated, multi-participant interaction with shared state.

ExAgent.Session is the coordination layer (Roadmap Phase 3): it owns a piece of application-defined shared_state and a set of participants (agents backed by ExAgent.Server, or humans driving via LiveView), and it decides — through a pluggable ExAgent.Session.TurnPolicy — whose turn it is.

It is deliberately agnostic: the shared_state can be a D&D world, a support ticket, a collaborative document, anything. ExAgent never assumes a shape.

The single-writer rule

The Session is the only process allowed to mutate shared_state. Participants never touch it directly. Instead, a participant whose turn it is passes a change function (state) -> {:ok, new_state} | {:error, reason}, and the Session applies it atomically, emits :shared_state_updated, and advances the turn. Tools running inside an agent reach the Session through an ExAgent.Session.SharedState handle placed in RunContext.deps.

Lifecycle

{:ok, session} =
  ExAgent.Session.start_link(
    shared_state: %{log: []},
    policy: :round_robin,
    participants: [
      ExAgent.Session.Participant.new(id: "dm", kind: :agent),
      ExAgent.Session.Participant.new(id: "player", kind: :human)
    ],
    pubsub: :local
  )

:ok = ExAgent.Session.start(session)
{:ok, state, next} = ExAgent.Session.take_turn(session, "dm", fn s -> {:ok, %{s | log: ["dm acted" | s.log]}} end)

Status flows :created → :running → (:paused ⇄ :running) → :closed. While not :running, take_turn/3 returns {:error, :paused} / {:error, {:not_running, _}}.

Events

Published on ExAgent.Event.session_topic(session_id) ("exagent:session:<id>"): :session_started · :participant_joined · :participant_left · :session_turn_changed · :shared_state_updated · :session_paused · :session_resumed · :session_closed. Events carry ids/status, never the bulk shared_state (read it with read_state/1).

Summary

Functions

Returns a specification to start this module under a supervisor.

Close the session. Further turns return {:error, {:not_running, :closed}}.

The participant id whose turn it currently is (or nil).

End the current participant's turn without changing state, advancing next.

Add a participant (allowed before or during a running session).

Remove a participant by id. Returns :ok or {:error, :not_found}.

All registered participants.

Pause a running session (take_turn/3 then returns {:error, :paused}).

Read the shared state (read-only snapshot; anyone may call this).

Resume a paused session.

Begin turns. Picks the first participant via the policy. :created → :running.

Start a supervised session.

Current session status (:created | :running | :paused | :closed | :done).

Take a turn: apply change_fn to the shared state atomically, then advance to the next participant. change_fn is (state) -> {:ok, new_state} | {:error, reason} (a bare value is treated as the new state).

Mutate the shared state without advancing the turn — for mid-turn changes (e.g. a tool inside an agent run proposes a change). Only the current participant may call this.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(session)

@spec close(GenServer.server()) :: :ok

Close the session. Further turns return {:error, {:not_running, :closed}}.

current(session)

@spec current(GenServer.server()) :: term() | nil

The participant id whose turn it currently is (or nil).

end_turn(session, participant_id)

@spec end_turn(GenServer.server(), term()) :: {:ok, term()} | {:error, term()}

End the current participant's turn without changing state, advancing next.

join(session, p)

Add a participant (allowed before or during a running session).

leave(session, id)

@spec leave(GenServer.server(), term()) :: :ok | {:error, :not_found}

Remove a participant by id. Returns :ok or {:error, :not_found}.

participants(session)

@spec participants(GenServer.server()) :: [ExAgent.Session.Participant.t()]

All registered participants.

pause(session)

@spec pause(GenServer.server()) :: :ok | {:error, term()}

Pause a running session (take_turn/3 then returns {:error, :paused}).

read_state(session)

@spec read_state(GenServer.server()) :: term()

Read the shared state (read-only snapshot; anyone may call this).

resume(session)

@spec resume(GenServer.server()) :: :ok | {:error, term()}

Resume a paused session.

start(session)

@spec start(GenServer.server()) :: {:ok, term()} | {:error, term()}

Begin turns. Picks the first participant via the policy. :created → :running.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Start a supervised session.

Options

  • :shared_state — the initial application-defined state (required-ish; defaults to nil).
  • :policy:round_robin / :initiative / {:initiative, order: [...]} / {module, opts} / module (default :round_robin).
  • :participants — initial list of ExAgent.Session.Participant.t().
  • :session_id — stable id for events; auto-generated if absent.
  • :pubsubnil/:none/:local/{module, config} (default nil).
  • :name — registered name for the GenServer.
  • :metadata — free-form map attached to every emitted event.

status(session)

@spec status(GenServer.server()) :: :created | :running | :paused | :closed | :done

Current session status (:created | :running | :paused | :closed | :done).

take_turn(session, participant_id, change_fn)

@spec take_turn(GenServer.server(), term(), (term() ->
                                         {:ok, term()}
                                         | {:error, term()}
                                         | term())) ::
  {:ok, term(), term()} | {:error, term()}

Take a turn: apply change_fn to the shared state atomically, then advance to the next participant. change_fn is (state) -> {:ok, new_state} | {:error, reason} (a bare value is treated as the new state).

Returns {:ok, new_state, next_participant_id} or {:error, reason}:not_your_turn, :paused, or {:not_running, status}.

update_state(session, participant_id, change_fn)

@spec update_state(GenServer.server(), term(), (term() ->
                                            {:ok, term()}
                                            | {:error, term()}
                                            | term())) ::
  {:ok, term()} | {:error, term()}

Mutate the shared state without advancing the turn — for mid-turn changes (e.g. a tool inside an agent run proposes a change). Only the current participant may call this.