Sagents.SubAgentServer (Sagents v0.8.0-rc.6)
Copy MarkdownGenServer wrapper for SubAgent providing blocking API.
Simple Design
SubAgentServer is a simple wrapper that:
- Holds a SubAgent struct in its state
- Provides a blocking API for execute/resume
- Uses Registry for named access (via sub-agent ID)
- NO PubSub (simpler than AgentServer)
- NO auto-shutdown - lifecycle managed by parent agent
The GenServer state is SIMPLE - just the SubAgent struct! Everything is delegated to SubAgent functions.
New Architecture
The SubAgent struct HOLDS the execution state including the LLMChain. This makes the server's job trivial:
- Store the SubAgent struct
- Delegate execute/resume to SubAgent module
- Return results
Lifecycle
- Spawned - Created by task tool under SubAgentsDynamicSupervisor
- Execute - Tool calls
execute/1, blocking until completion or interrupt - Interrupt (optional) - Returns
{:interrupt, interrupt_data}if HITL needed - Resume (optional) - Tool calls
resume/2with decisions, blocks again - Complete - Returns
{:ok, final_result}when done - Shutdown - Cleaned up when parent agent terminates
Usage
# Create a SubAgent struct
subagent = SubAgent.new_from_config(
parent_agent_id: "main-agent",
instructions: "Research renewable energy",
agent_config: agent,
parent_state: parent_state
)
# Start the server
{:ok, _pid} = SubAgentServer.start_link(subagent: subagent)
# Execute synchronously (blocks until completion or interrupt)
case SubAgentServer.execute(subagent.id) do
{:ok, final_result} ->
{:ok, final_result}
{:interrupt, interrupt_data} ->
# SubAgent needs HITL approval
# Propagate interrupt to parent
{:interrupt, %{
type: :subagent_hitl,
sub_agent_id: subagent.id,
interrupt_data: interrupt_data
}}
{:error, reason} ->
{:error, reason}
end
# Resume after user provides decisions
case SubAgentServer.resume(subagent.id, decisions) do
{:ok, final_result} -> {:ok, final_result}
{:interrupt, interrupt_data} -> # Another interrupt
{:error, reason} -> {:error, reason}
endSupervision
SubAgentServers are supervised by SubAgentsDynamicSupervisor with
:temporary restart strategy. If a SubAgent crashes:
- The supervisor logs the crash
- The blocking call receives an exit signal
- The parent agent can handle the error
- No automatic restart (SubAgents are ephemeral)
Summary
Functions
Cancel a running SubAgentServer process.
Returns a specification to start this module under a supervisor.
Execute the SubAgent synchronously.
Get the registry name for a SubAgent.
Get the current status of the SubAgent.
Get the current SubAgent struct.
Resume the SubAgent after an HITL interrupt.
Start a SubAgentServer.
Stop a SubAgentServer process.
Find the PID of a SubAgent by ID.
Functions
@spec cancel(String.t()) :: :ok
Cancel a running SubAgentServer process.
Called when the parent AgentServer is cancelled — the sub-agent's work is being abandoned because there is no longer anyone to return results to.
Broadcasts {:subagent_status_changed, :cancelled} and
{:subagent_cancelled, %{final_messages, turn_count}} on the parent's debug
topic BEFORE terminating, so observers (debugger) see the terminal event
instead of the sub-agent silently vanishing.
If the sub-agent is blocked in an LLM call and cannot respond to the pre-cancel broadcast within a short window, it is terminated regardless — the parent cancel must not be delayed.
Idempotent: returns :ok if the sub-agent is already gone.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec execute(String.t()) :: {:ok, String.t()} | {:ok, String.t(), term()} | {:interrupt, map()} | {:error, term()}
Execute the SubAgent synchronously.
This function blocks until the SubAgent either:
- Completes successfully:
{:ok, final_result} - Encounters an HITL interrupt:
{:interrupt, interrupt_data} - Fails with an error:
{:error, reason}
Important: This uses :infinity timeout because SubAgents may perform
multiple LLM calls and tool executions. The blocking is intentional - the
caller waits for the SubAgent to finish.
Examples
case SubAgentServer.execute("main-agent-sub-1") do
{:ok, final_result} -> handle_completion(final_result)
{:interrupt, interrupt_data} -> propagate_interrupt(interrupt_data)
{:error, reason} -> handle_error(reason)
end
@spec get_name(String.t()) :: GenServer.name()
Get the registry name for a SubAgent.
Examples
name = SubAgentServer.get_name("main-agent-sub-1")
Get the current status of the SubAgent.
Returns one of: :idle, :running, :interrupted, :completed, :error
Examples
status = SubAgentServer.get_status("main-agent-sub-1")
@spec get_subagent(String.t()) :: Sagents.SubAgent.t()
Get the current SubAgent struct.
Note: This is primarily for debugging. In normal operation, the SubAgent should stay encapsulated in the process.
Examples
subagent = SubAgentServer.get_subagent("main-agent-sub-1")
@spec resume(String.t(), [map()]) :: {:ok, String.t()} | {:ok, String.t(), term()} | {:interrupt, map()} | {:error, term()}
Resume the SubAgent after an HITL interrupt.
This function blocks until the SubAgent either:
- Completes successfully:
{:ok, final_result} - Encounters another HITL interrupt:
{:interrupt, interrupt_data} - Fails with an error:
{:error, reason}
Parameters
sub_agent_id- The SubAgent identifierdecisions- List of decision maps from human reviewer (seeLangChain.Agent.resume/3)
Examples
decisions = [
%{type: :approve},
%{type: :edit, arguments: %{"path" => "safe.txt"}}
]
case SubAgentServer.resume("main-agent-sub-1", decisions) do
{:ok, final_result} -> handle_completion(final_result)
{:interrupt, interrupt_data} -> propagate_interrupt(interrupt_data)
{:error, reason} -> handle_error(reason)
end
Start a SubAgentServer.
Options
:subagent- The SubAgent struct (required)
Examples
subagent = SubAgent.new_from_config(
parent_agent_id: "main-agent",
instructions: "Research renewable energy",
agent_config: agent,
parent_state: parent_state
)
{:ok, pid} = SubAgentServer.start_link(subagent: subagent)
@spec stop(String.t()) :: :ok
Stop a SubAgentServer process.
Called after the sub-agent has completed or errored to free the process. This is a synchronous call that waits for the process to terminate.
Returns :ok if stopped successfully, or :ok if the process was already
gone (idempotent).
Find the PID of a SubAgent by ID.
Returns nil if the SubAgent doesn't exist.
Examples
pid = SubAgentServer.whereis("main-agent-sub-1")