Sagents.AgentSupervisor (Sagents v0.16.2)
Copy MarkdownCustom supervisor for managing an Agent and its supporting infrastructure.
AgentSupervisor coordinates the lifecycle of:
- AgentServer - Agent execution and state management
- SubAgentsDynamicSupervisor - Dynamic supervisor for spawning sub-agents
Supervision Strategy
Uses :rest_for_one strategy to ensure crash resilience:
- If AgentServer crashes, SubAgentsDynamicSupervisor restarts
- SubAgentsDynamicSupervisor crashes only affect itself
Filesystem Integration
Filesystems are now managed independently via FileSystemSupervisor.
Agents reference filesystems through the filesystem_scope field in the Agent struct.
See Sagents.FileSystem for filesystem lifecycle management.
Configuration
Accepts a keyword list with:
:agent- The Agent struct (required unless:builderis given):initial_state- Initial State for AgentServer (optional):pubsub- PubSub configuration as{module(), atom()}tuple ornil(optional, default: nil):shutdown_delay- Delay in milliseconds to allow the supervisor to gracefully stop all children (optional, default: 5000):conversation_id- Optional conversation identifier for message persistence (optional, default: nil):agent_persistence- Module implementingSagents.AgentPersistence(optional, default: nil):display_message_persistence- Module implementingSagents.DisplayMessagePersistence(optional, default: nil)
Examples
# Minimal configuration
{:ok, agent} = Agent.new(
agent_id: "my-agent",
model: model,
base_system_prompt: "You are a helpful assistant."
)
{:ok, sup_pid} = AgentSupervisor.start_link(agent: agent)
# With initial state and PubSub
initial_state = State.new!(%{
messages: [Message.new_user!("Hello!")]
})
{:ok, sup_pid} = AgentSupervisor.start_link(
agent: agent,
initial_state: initial_state,
pubsub: {Phoenix.PubSub, :my_app_pubsub}
)
# With external filesystem
# Start filesystem separately
{:ok, config} = FileSystemConfig.new(%{
scope_key: {:user, 123},
base_directory: "Memories",
persistence_module: Sagents.FileSystem.Persistence.Disk,
storage_opts: [path: "/data/users/123"]
})
{:ok, _fs_pid} = FileSystem.ensure_filesystem({:user, 123}, [config])
# Create agent with filesystem reference
{:ok, agent} = Agent.new(%{
agent_id: "my-agent",
model: model,
filesystem_scope: {:user, 123}
})
{:ok, sup_pid} = AgentSupervisor.start_link(agent: agent)
Summary
Functions
Returns a specification to start this module under a supervisor.
Get the name of the AgentSupervisor process for a specific agent.
Get the PID of an AgentSupervisor by agent_id.
Start the AgentSupervisor.
Start the AgentSupervisor and wait for the AgentServer to be ready.
Stop the AgentSupervisor.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec get_name(String.t()) :: GenServer.name()
Get the name of the AgentSupervisor process for a specific agent.
Examples
name = AgentSupervisor.get_name("my-agent-1")
AgentSupervisor.stop(name)
Get the PID of an AgentSupervisor by agent_id.
{:error, :registry_unavailable} means this node's registry could not answer,
not that nothing is running. It is deliberately distinct from :not_found:
treating it as "not found" makes callers start a second AgentSupervisor for an
agent that already has one. See docs/deployment.md.
Examples
{:ok, pid} = AgentSupervisor.get_pid("my-agent-1")
{:error, :not_found} = AgentSupervisor.get_pid("non-existent")
@spec start_link(keyword()) :: Supervisor.on_start()
Start the AgentSupervisor.
Options
:agent- The Agent struct (required unless:builderis given):initial_state- Initial State for AgentServer (optional):pubsub- PubSub configuration as{module(), atom()}tuple ornil(optional, default: nil). Used only forPhoenix.Presencepresence_diffwiring; per-agent events are delivered directly to subscriber pids viaSagents.Publisher.:inactivity_timeout- Timeout in milliseconds for automatic shutdown (optional, default: 300_000 - 5 minutes) Set tonilor:infinityto disable automatic shutdown:name- Supervisor name registration (optional):shutdown_delay- Delay in milliseconds to allow the supervisor to gracefully stop all children (optional, default: 5000):conversation_id- Optional conversation identifier for message persistence (optional, default: nil):agent_persistence- Module implementingSagents.AgentPersistence(optional, default: nil):display_message_persistence- Module implementingSagents.DisplayMessagePersistence(optional, default: nil):builder-{module, function, args}that builds the start options on the node hosting the agent (optional). See "Building on the hosting node".
Building on the hosting node
Under the :horde distribution these options travel between nodes. Horde can
place a start on another member, and it restarts a departed node's agents on a
survivor from the options of the original start. During a rolling deploy those
nodes run different builds, so an %Agent{} built on one of them carries that
build's configuration: its system prompt, tools, and middleware config, and
anonymous functions that only that build can call.
A :builder lets the hosting node build its own. start_link/1 calls
apply(module, function, args) instead of using :agent when:
- there is no
:agent, - the start was requested by a different node, or by an earlier run of this
node (
Sagents.AgentsDynamicSupervisor.start_agent/1records which), or - the options hold an anonymous function this node cannot call.
The builder returns {:ok, opts} or {:error, reason}. The returned options
replace those of the original start, and options it does not return are kept,
so a builder returns everything it is responsible for (normally :agent,
:initial_state, and any configuration derived alongside them). An error is
returned from start_link/1, and on a handover the agent is not started.
Store only modules and plain data in args. They travel with the options, so
an anonymous function in args has the same problem the builder exists to
avoid. Keep them to what identifies the agent (an id, a scope, request
inputs) and let the builder derive the rest, since args were produced by
whatever build requested the start.
Sagents.Session supplies a builder that re-runs the configured router and
factory.
Functions the local node cannot call
Returns :ignore, starting nothing, when the options it would start with hold
an anonymous function this node cannot call. See Sagents.Closures. With a
:builder this only happens when the builder's own args, or options it does
not return, hold one.
An agent started with such a function would run normally and fail every tool call, so it is not started. Nothing is left registered for it, and the next start builds the agent from the code this node is running.
Examples
{:ok, sup_pid} = AgentSupervisor.start_link(
agent: agent,
pubsub: {Phoenix.PubSub, :my_app_pubsub}
)
{:ok, sup_pid} = AgentSupervisor.start_link(
agent: agent,
name: AgentSupervisor.get_name("agent-123")
)
# With custom inactivity timeout
{:ok, sup_pid} = AgentSupervisor.start_link(
agent: agent,
inactivity_timeout: 600_000 # 10 minutes
)
# Disable automatic shutdown
{:ok, sup_pid} = AgentSupervisor.start_link(
agent: agent,
inactivity_timeout: nil
)
Start the AgentSupervisor and wait for the AgentServer to be ready.
This is a synchronous version of start_link/1 that waits for the AgentServer
child process to be fully registered before returning. This prevents race
conditions where callers try to interact with the AgentServer (e.g., subscribe,
add_message) before it's ready.
Why This Is Needed
When start_link/1 returns successfully, the supervisor is running but its
children are still completing their initialization asynchronously. If you
immediately try to call AgentServer.get_pid/1 or AgentServer.subscribe/1,
the AgentServer might not be registered yet, causing failures.
This function solves that by:
- Starting the supervisor normally
- Polling for the AgentServer to be registered with exponential backoff
- Returning only when the AgentServer is confirmed ready
Use Cases
Use this function when you need immediate access to the AgentServer:
- Web request handlers that need to subscribe or send messages right away
- CLI tools that need synchronous agent startup
- Test setups that require the agent to be fully ready
Use regular start_link/1 when:
- Startup timing isn't critical
- You can handle eventual consistency
- You're starting many agents in parallel
Options
Same as start_link/1, plus:
:startup_timeout- Maximum time to wait for AgentServer readiness (default: 5000ms)
Examples
# Start and wait for agent to be ready
{:ok, sup_pid} = AgentSupervisor.start_link_sync(
agent: agent,
pubsub: {Phoenix.PubSub, :my_app_pubsub}
)
# Agent is guaranteed to be ready - safe to use immediately
:ok = AgentServer.subscribe(agent.agent_id)
# Custom startup timeout
{:ok, sup_pid} = AgentSupervisor.start_link_sync(
agent: agent,
startup_timeout: 10_000
)Returns
{:ok, supervisor_pid}- Supervisor started and AgentServer is ready{:error, {:agent_startup_timeout, agent_id}}- AgentServer failed to become ready{:error, :stale_closures}- The options hold an anonymous function this node cannot call, so nothing was started. Seestart_link/1.{:error, reason}- Supervisor failed to start
Stop the AgentSupervisor.
{:error, :registry_unavailable} means this node's registry could not answer,
so nothing was stopped and nothing is known about whether anything was
running. It is distinct from :not_found for the same reason it is in
get_pid/1.
Examples
AgentSupervisor.stop("my-agent-1")
AgentSupervisor.stop("my-agent-1", 10_000) # 10 second timeout