Normandy.Agents.Turn.Session
(normandy v2.2.0)
View Source
A handle on one durable session. new/1 checks the options once; run/2 routes a turn
to the session's Turn.Server, approve/2 delivers approval decisions to it and
delete/1 stops it and deletes it from the store.
session =
Turn.Session.new(
session_id: "user-42",
config: config,
store: {Normandy.Behaviours.SessionStore.Postgres, MyApp.Repo},
registry: {Normandy.Behaviours.SessionRegistry.Native, MyApp.SessionRegistry},
supervisor: MyApp.TurnSupervisor
)
{:ok, output} = Turn.Session.run(session, "hello")run/2 resolves session_id to a live pid via the SessionRegistry; on a miss, it
starts a Turn.Server under the supervisor with the caller-supplied config (the
store never holds config/credentials). The server loads its turn state and
conversation memory from the SessionStore on every start, so a supervisor restart
resumes from the store, not from the state captured when the session first started.
A %Turn.Session{} is plain data: building one starts nothing, and the same handle can
be used from any process.
Options
:session_id(String.t/0) - Required. The session's id: its key in the registry and in the store. A non-empty string without{or}(the Redis stores put it inside a cluster hash tag).:registry- Required. TheNormandy.Behaviours.SessionRegistry, as{module, handle}.:supervisor- Required. The supervisor that starts the session'sTurn.Server: a pid or a name.:supervisor_mod(module/0) - ANormandy.Agents.Turn.ServerSupervisor: the module whosestart_server/2starts the server and whosestop_server/2stops it (Normandy.Agents.Turn.Supervisor.Hordein a cluster). The default value isNormandy.Agents.Turn.Supervisor.:store- Required. TheNormandy.Behaviours.SessionStore, as{module, handle}.:config(a%Normandy.Agents.BaseAgentConfig{}) - The%Normandy.Agents.BaseAgentConfig{}a server starts with. With:template_providertoo, it is saved as the session's template on first start.:template_provider- ANormandy.Behaviours.AgentTemplate, as{module, handle}: the server rebuilds its config from the stored template and this node's supplement.:template_id(String.t/0) - The supplement's id in the template provider. Default: the config's:name, else"default".:resume_policy-:eagerresumes an unfinished turn when the server restarts, and a Tier-2 start saves it soResumeReaperrestarts the session after a node loss. The default value is:lazy.:subscriber(function of arity 2) -fn event_name, meta -> any end, passed toTurn.Server.:handlers(aNormandy.Agents.Turn.Driver.Handlersornil) - ATurn.Driver.Handlersset, passed toTurn.Server;nilis its default.:approval_timeout_ms(timeout/0) - Passed toTurn.Server, in ms or:infinity; saved with a Tier-2 template.:on_approval_timeout- Passed toTurn.Server; saved with a Tier-2 template.:idle_timeout_ms(timeout/0) - Passed toTurn.Server, in ms or:infinity; saved with a Tier-2 template.
Tier-2 thin start (when :template_provider is set)
With a :config and a :template_provider, the first start persists a secret-free
ConfigTemplate derived from the config, then launches Turn.Server with thin
options (no :config). The server reconstructs the full config node-locally from the
stored template + supplement + credential token. Later starts (after passivation)
find the persisted template in the store and reconstruct without any caller config.
A session may also have only a :template_provider (no :config), for example on a
surviving node after the session's node died: the server starts from the template
already saved in the store. With no saved template, run/2 returns
{:error, %Normandy.TurnError{type: :session_unavailable, reason: {:no_config_template, session_id}}}.
Resume policy (:resume_policy, :lazy or :eager)
:lazy (default) restarts a session only when a caller routes to it. With
:eager, a restarted server resumes a non-terminal turn on its own, and a
Tier-2 start saves the policy in the template, so ResumeReaper restarts the
session when its node goes down. A session started from a saved template keeps
the policy it was saved with.
Start-time race
Two callers that both find no server may both start one; one start wins and the
other gets {:error, {:already_started, pid}}, which run/2 treats as {:ok, pid},
so all callers converge on the single winner. For via-based registries (Horde,
Redis) the supervisor supplies a :name computed by child_name/2, so the OTP
supervisor registers atomically at start. For :self_register (Native) registries
the losing server finds the session taken in init/1 and stops before loading
anything, with the same {:already_started, pid}.
Summary
Functions
Delivers approval decisions (tool_call_id => :approve | :reject) to the session's
parked server and returns :ok; the turn's result goes to the run/2 caller waiting
on it. Never starts a server: with none live it returns {:error, %Normandy.TurnError{type: :session_unavailable, reason: :no_session}}, and a registry
that cannot answer returns that TurnError with the registry's reason.
Deletes the session: stops its live server, if any, through :supervisor_mod's
stop_server/2, then deletes it from the store (SessionStore.delete_session/2: turn
state, template, in-flight index membership, and the history no fork still uses).
Returns :ok, also for a session that does not exist.
Builds a session handle. Starts nothing.
Returns session with config as the config a later server start uses (a first
start, a restart after passivation, a Tier-2 template save). A live server keeps the
config it started with.
Routes a turn to the session's server, starting it if needed, and returns what
Turn.Server.run/2 returns: {:ok, output} or {:error, exception}. A session that
cannot be started (a registry or store fault, no saved template) returns
{:error, %Normandy.TurnError{type: :session_unavailable, reason: reason}}. A server
option Turn.Server rejects (only reachable by editing the struct, since new/1 checks
them too) raises ArgumentError in the caller, from Turn.Supervisor.start_server/2
(Ruling 7).
Types
@type t() :: %Normandy.Agents.Turn.Session{ config: Normandy.Agents.BaseAgentConfig.t() | nil, handlers: Normandy.Agents.Turn.Driver.Handlers.t() | nil, registry: {module(), term()}, resume_policy: :lazy | :eager, server_options: keyword(), session_id: String.t(), store: {module(), term()}, subscriber: (atom(), map() -> any()) | nil, supervisor: GenServer.server(), supervisor_mod: module(), template_id: String.t() | nil, template_provider: {module(), term()} | nil }
Functions
@spec approve(t(), %{optional(String.t()) => :approve | :reject}) :: :ok | {:error, Normandy.TurnError.t()}
Delivers approval decisions (tool_call_id => :approve | :reject) to the session's
parked server and returns :ok; the turn's result goes to the run/2 caller waiting
on it. Never starts a server: with none live it returns {:error, %Normandy.TurnError{type: :session_unavailable, reason: :no_session}}, and a registry
that cannot answer returns that TurnError with the registry's reason.
@spec delete(t()) :: :ok | {:error, Normandy.TurnError.t()}
Deletes the session: stops its live server, if any, through :supervisor_mod's
stop_server/2, then deletes it from the store (SessionStore.delete_session/2: turn
state, template, in-flight index membership, and the history no fork still uses).
Returns :ok, also for a session that does not exist.
A server is stopped by its supervisor's shutdown, mid-turn included: a run/2 waiting
on it exits, and a turn in flight is not resumed. Errors:
{:error, %Normandy.TurnError{type: :session_unavailable, reason: reason}}- the registry could not answer (reasonis its reason), or the live server is not a child of:supervisor(reason: {:not_stopped, :not_found}; it may have just exited, so calling again is safe). Nothing was deleted.{:error, %Normandy.TurnError{type: :persist_failed, reason: reason}}- the store refused the delete (reasonis its reason reduced to its tags, as forrun/2's store failures), or its call exited (reason: {:store_exit, tag}, the exit reason reduced the same way, e.g.{:store_exit, {:timeout, {GenServer, :call, "a list"}}}). The server is already stopped; calling again finishes the job. A store that raises raises.
new/1 checks that the handle's :supervisor_mod exports stop_server/2
(Normandy.Agents.Turn.ServerSupervisor); a handle whose struct was edited to another
module raises ArgumentError naming it.
Nothing stops a new turn from starting the session again: a run/2 that starts after
the server stopped (or a ResumeReaper restart between the stop and the delete) gets
a fresh server. One that loads after the delete starts empty. One that loaded just
before it holds the old history in memory and appends onto entries the delete removed,
so its writes partly bring the session back. Stop routing turns to a session before
deleting it, and call delete/1 again once no server runs for it.
Builds a session handle. Starts nothing.
Raises ArgumentError naming the key for an unknown key, a missing required key or a
value of the wrong type, and when neither :config nor :template_provider is given.
@spec put_config(t(), Normandy.Agents.BaseAgentConfig.t()) :: t()
Returns session with config as the config a later server start uses (a first
start, a restart after passivation, a Tier-2 template save). A live server keeps the
config it started with.
@spec run(t(), term()) :: {:ok, term()} | {:error, Exception.t()}
Routes a turn to the session's server, starting it if needed, and returns what
Turn.Server.run/2 returns: {:ok, output} or {:error, exception}. A session that
cannot be started (a registry or store fault, no saved template) returns
{:error, %Normandy.TurnError{type: :session_unavailable, reason: reason}}. A server
option Turn.Server rejects (only reachable by editing the struct, since new/1 checks
them too) raises ArgumentError in the caller, from Turn.Supervisor.start_server/2
(Ruling 7).
A server that passivates while the call waits for it never runs the turn; run/2
routes the turn again, to a fresh server, up to three servers in all, then returns
{:error, %Normandy.TurnError{type: :session_unavailable, reason: :passivated}}. Only
a server that stopped with {:shutdown, :passivated} (Turn.Server's passivation) or
was already gone is retried; any other exit of the server, :normal included,
propagates to the caller.