Normandy.Coordination.AgentProcess
(normandy v2.2.0)
View Source
GenServer wrapper for running BaseAgent instances as supervised processes.
AgentProcess enables agents to run as long-lived processes that can:
- Maintain state across multiple invocations
- Be supervised and restarted on failure
- Receive messages asynchronously
- Integrate with process registries
Example
# Start an agent process
{:ok, pid} = AgentProcess.start_link(
agent: my_agent,
name: :research_agent
)
# Execute synchronously
{:ok, result} = AgentProcess.run(pid, "Analyze this data")
# Execute asynchronously
:ok = AgentProcess.cast(pid, "Process in background", reply_to: self())
# Get current agent state
agent = AgentProcess.get_agent(pid)Inline turns (the default)
With turn_engine: :inline, runs, casts and update_agent/2 calls go through one FIFO
queue and execute one at a time, each turn in a worker process started from the config
the previous one left, so a cast's turn is kept in memory like a run's. The process
itself never waits on a turn: get_agent/1 (the config as of the last finished turn),
get_id/1, get_stats/1 and approve/2 answer at once while a turn runs. A run whose
caller died while it waited in the queue is skipped. The worker dies with the process
(stop/1 kills a turn in flight); a worker killed from outside replies a :task_down
Normandy.TurnError.
Pending runs
Runs and casts that were admitted and have not finished (queued or in flight, in
either mode) are bounded by :max_pending_runs (default 100). One more is refused at
once: run/3 returns, and cast/3 sends to :reply_to,
{:error, %Normandy.TurnError{type: :queue_full, reason: limit}}. get_stats/1's
run_count counts runs and casts that finished, failed ones included.
Durable turn engine (:server mode)
Pass turn_engine: :server to route turns through the durable
Normandy.Agents.Turn.Session/Turn.Server engine (approval parking,
passivation, persistence) instead of the default :inline path, which runs
BaseAgent.run/3 in a worker process, one turn at a time. :inline is the default and is unchanged.
{:ok, pid} = AgentProcess.start_link(agent: config, turn_engine: :server)In :server mode:
run/3/cast/3route throughTurn.Session;run/3is non-blocking internally (the GenServer stays responsive while a turn is parked);approve/2delivers human-approval decisions to a parked turn.- The
SessionStoreowns conversation memory:get_agent/1reconstructs it from the store, andupdate_agent/2updates only the config template (model/temperature/behaviours/tools) — memory mutations are ignored. - Session infra (
:store,:registry,:supervisor) may be supplied viastart_link; if omitted, the process starts and owns an in-memory store and aTurn.Supervisorthat terminate with it, and registers its sessions under a scope of its own in a registry Normandy starts (no atom per process).start_linkbuilds oneTurn.Sessionfrom them and from:supervisor_mod,:template_provider,:template_id,:resume_policy,:subscriber,:handlers,:approval_timeout_ms,:on_approval_timeoutand:idle_timeout_ms(seeTurn.Session.new/1;start_link/1checks them with its types);update_agent/2gives it the new config for the next server start.
Summary
Functions
Delivers approval decisions to a turn parked awaiting human approval
(:server mode only). decisions maps tool_call_id to :approve | :reject;
any parked id absent or :reject is treated as rejected (fail-closed).
Executes the agent asynchronously.
Returns a specification to start this module under a supervisor.
Returns the current agent state.
Returns the agent ID.
Returns agent statistics and metadata.
Executes the agent synchronously.
Starts an AgentProcess GenServer.
Stops the agent process gracefully.
Updates the agent config: update_fn gets the current
%Normandy.Agents.BaseAgentConfig{} and returns the new one.
Types
Functions
@spec approve(GenServer.server(), %{optional(String.t()) => :approve | :reject}) :: :ok | {:error, Normandy.TurnError.t()} | {:error, :inline_mode}
Delivers approval decisions to a turn parked awaiting human approval
(:server mode only). decisions maps tool_call_id to :approve | :reject;
any parked id absent or :reject is treated as rejected (fail-closed).
Returns :ok; with no live parked session, {:error, %Normandy.TurnError{type: :session_unavailable, reason: :no_session}} (see Normandy.Agents.Turn.Session.approve/2);
{:error, :inline_mode} when the process runs in :inline mode.
@spec cast(GenServer.server(), term(), keyword()) :: :ok
Executes the agent asynchronously.
The agent runs in the background. If :reply_to is provided,
sends {:agent_result, agent_id, result} when complete.
Options
:reply_to- Where to send{:agent_result, agent_id, result}when the run ends.
Example
:ok = AgentProcess.cast(pid, input, reply_to: self())
receive do
{:agent_result, agent_id, result} ->
IO.inspect(result)
end
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec get_agent(GenServer.server()) :: struct()
Returns the current agent state.
Example
agent = AgentProcess.get_agent(pid)
@spec get_id(GenServer.server()) :: agent_id()
Returns the agent ID.
Example
agent_id = AgentProcess.get_id(pid)
#=> "agent_1"
@spec get_stats(GenServer.server()) :: map()
Returns agent statistics and metadata.
Example
stats = AgentProcess.get_stats(pid)
#=> %{
agent_id: "agent_1",
run_count: 42,
last_run: ~U[2024-01-15 10:30:00Z],
total_runtime_ms: 15420
}
@spec run(GenServer.server(), term(), run_opts()) :: {:ok, term()} | {:error, Exception.t()}
Executes the agent synchronously.
Runs the agent with the given input and returns the result.
Options
:timeout(timeout/0) - How long the caller waits for the reply, in ms. A run is bounded by its retries, tool timeouts andmax_tool_iterations, so the default waits for it, asTurn.Server.run/2does. The default value is:infinity.
Example
{:ok, result} = AgentProcess.run(pid, "What is AI?")
@spec start_link(keyword()) :: GenServer.on_start()
Starts an AgentProcess GenServer.
Options
:agent(a%Normandy.Agents.BaseAgentConfig{}) - Required. The agent config the process runs.:name(term/0) - AGenServer.name/0for the process.:agent_id(String.t/0) - The agent's id (default: a new UUID). In:servermode it is also the session id.:context_pid- ANormandy.Coordination.StatefulContextprocess, kept in the agent's state.:turn_engine-:inlinerunsBaseAgent.run/3in a worker process, one turn at a time;:serverroutes turns throughTurn.Session/Turn.Server(approval parking, passivation, persistence). The default value is:inline.:store({module, handle}) -:servermode: theSessionStoreas{module, handle}. Without it the process starts and owns an in-memory store.:registry({module, handle}) -:servermode: theSessionRegistryas{module, handle}. Without it the process uses a scope of its own in a registry Normandy starts.:supervisor(a pid or a supervisor name) -:servermode: the session supervisor. Without it the process starts and owns aTurn.Supervisor.:supervisor_mod-:servermode: the module whosestart_server/2starts sessions (defaultNormandy.Agents.Turn.Supervisor).:template_provider({module, handle}) -:servermode: theAgentTemplateas{module, handle}.:resume_policy-:servermode: the session's resume policy (default:lazy).:template_id(String.t/0) -:servermode: the template id (default: the config's name).:subscriber(function of arity 2) -:servermode: passed toTurn.Server.start_link/1.:handlers(aNormandy.Agents.Turn.Driver.Handlersornil) -:servermode: passed toTurn.Server.start_link/1.:approval_timeout_ms(timeout/0) -:servermode: passed toTurn.Server.start_link/1.:on_approval_timeout-:servermode: passed toTurn.Server.start_link/1.:idle_timeout_ms(timeout/0) -:servermode: passed toTurn.Server.start_link/1.:max_pending_runs(a positive integer or:infinity) - Runs and casts admitted and not finished (queued or in flight). One more is refused with a:queue_fullNormandy.TurnError. The default value is100.
Example
{:ok, pid} = AgentProcess.start_link(
agent: my_agent,
name: :my_agent,
agent_id: "agent_1"
)
@spec stop(GenServer.server()) :: :ok
Stops the agent process gracefully.
Example
:ok = AgentProcess.stop(pid)
@spec update_agent( GenServer.server(), (Normandy.Agents.BaseAgentConfig.t() -> Normandy.Agents.BaseAgentConfig.t()), [{:timeout, timeout()}] ) :: :ok
Updates the agent config: update_fn gets the current
%Normandy.Agents.BaseAgentConfig{} and returns the new one.
Raises ArgumentError when update_fn returns anything else, and in :server mode
when the session refuses the new config (Normandy.Agents.Turn.Session.put_config/2:
a session with a :template_provider needs a credential provider); the process keeps
its config.
In :inline mode the update is queued behind the turn in flight and any runs before it
(see "Inline turns"), so it sees their memory; the call waits for it.
Options
:timeout(timeout/0) - How long the caller waits for the reply, in ms. In:inlinemode the update waits behind the turn in flight, so the default waits for it, asrun/3does. The bound is on the wait: an update the caller stopped waiting for still applies. The default value is:infinity.
Example
:ok = AgentProcess.update_agent(pid, fn agent ->
%{agent | temperature: 0.2}
end)