Sagents.AgentSupervisor (Sagents v0.16.2)

Copy Markdown

Custom 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 :builder is given)
  • :initial_state - Initial State for AgentServer (optional)
  • :pubsub - PubSub configuration as {module(), atom()} tuple or nil (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 implementing Sagents.AgentPersistence (optional, default: nil)
  • :display_message_persistence - Module implementing Sagents.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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

get_name(agent_id)

@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_pid(agent_id)

@spec get_pid(String.t()) ::
  {:ok, pid()} | {:error, :not_found | :registry_unavailable}

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")

start_link(config)

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

Start the AgentSupervisor.

Options

  • :agent - The Agent struct (required unless :builder is given)

  • :initial_state - Initial State for AgentServer (optional)

  • :pubsub - PubSub configuration as {module(), atom()} tuple or nil (optional, default: nil). Used only for Phoenix.Presence presence_diff wiring; per-agent events are delivered directly to subscriber pids via Sagents.Publisher.

  • :inactivity_timeout - Timeout in milliseconds for automatic shutdown (optional, default: 300_000 - 5 minutes) Set to nil or :infinity to 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 implementing Sagents.AgentPersistence (optional, default: nil)

  • :display_message_persistence - Module implementing Sagents.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:

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
)

stop(agent_id, timeout \\ 5000)

@spec stop(String.t(), timeout()) ::
  :ok | {:error, :not_found | :registry_unavailable}

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