Sagents.SubAgent (Sagents v0.8.0-rc.5)
Copy MarkdownA runnable, pausable, and resumable agent execution context.
Core Philosophy
"The SubAgent struct HOLDS the LLMChain."
This is the key insight that makes pause/resume trivial:
- The chain persists in the SubAgent struct between pause and resume
- The chain remembers all messages, tool calls, and state
- Pause = stop executing, save the SubAgent struct
- Resume = continue the SAME chain with decisions
- No reconstruction needed
The SubAgent Struct
The SubAgent holds:
- The LLMChain - THE KEY FIELD - manages the entire conversation
- Status tracking (idle, running, interrupted, completed, error)
- Interrupt data when paused (which tools need approval)
- Error when failed
- Metadata (id, parent_agent_id, created_at)
How It Works
- Initialization: Create LLMChain with initial messages, tools, and model
- Execution: Run LLMChain in a loop until completion or interrupt
- Conversation: All messages live in the chain - it's all there
- Results: Extract from the final message in the chain
Key Design Principles
Chain Persistence = Simple Resume
- Chain is already paused at the right spot
- Resume just continues the chain with decisions
- The chain remembers everything
Direct Chain Management
- SubAgent.execute runs LLMChain.run directly
- No delegation to Agent
- Full control over the execution loop
HITL = Pause/Resume
- Interrupt → Chain pauses after LLM returns tool calls
- Save interrupt data (which tools, what arguments)
- Resume → Apply decisions, execute tools, continue chain
- Can have multiple pause/resume cycles naturally
SubAgent Execution Flow
Creating a SubAgent
# From configuration
subagent = SubAgent.new_from_config(
parent_agent_id: "main-agent",
instructions: "Research renewable energy",
agent_config: agent_from_registry,
parent_state: parent_state
)Executing
case SubAgent.execute(subagent) do
{:ok, completed_subagent} ->
# Extract result
result = SubAgent.extract_result(completed_subagent)
{:interrupt, interrupted_subagent} ->
# Needs human approval
# interrupted_subagent.interrupt_data contains action requests
{:error, error_subagent} ->
# Execution failed
# error_subagent.error contains the error
endResuming After Interrupt
case SubAgent.resume(interrupted_subagent, decisions) do
{:ok, completed_subagent} -> # Completed
{:interrupt, interrupted_subagent} -> # Another interrupt
{:error, error_subagent} -> # Failed
endMultiple Interrupts
The beauty of this design: multiple interrupts just repeat the pause/resume:
# First execution
{:interrupt, subagent1} = SubAgent.execute(subagent0)
# chain paused at: [user, assistant_with_tool_call_1]
# First resume
{:interrupt, subagent2} = SubAgent.resume(subagent1, [decision1])
# chain paused at: [user, assistant_1, tool_result_1, assistant_with_tool_call_2]
# Second resume
{:ok, subagent3} = SubAgent.resume(subagent2, [decision2])
# chain completed
Summary
Functions
Build an agent map of subagents from configurations.
Build a registry of subagents, raising on error.
Build descriptions map for subagents.
Check if SubAgent can be executed.
Check if SubAgent can be resumed.
Compose the child agent's base_system_prompt from a Config.
Execute the SubAgent.
Extract the module from any middleware format.
Extract result from completed SubAgent.
Check if a middleware spec refers to the SubAgent middleware.
Check if SubAgent is in a terminal state.
Create a SubAgent from compiled agent (pre-built).
Create a SubAgent from configuration (dynamic subagent).
Resume the SubAgent after HITL interrupt.
Build a middleware stack for subagents, filtering out blocked middleware.
Returns the universal framing prompt prepended to every task-style sub-agent's system prompt.
Types
@type t() :: %Sagents.SubAgent{ chain: LangChain.Chains.LLMChain.t() | nil, created_at: DateTime.t() | nil, error: term() | nil, id: String.t() | nil, interrupt_data: map() | nil, interrupt_on: map() | nil, max_runs: term(), parent_agent_id: String.t() | nil, status: :idle | :running | :interrupted | :completed | :error, until_tool: String.t() | [String.t()] | nil }
Functions
Build an agent map of subagents from configurations.
Build a registry of subagents, raising on error.
Build descriptions map for subagents.
Check if SubAgent can be executed.
Only SubAgents with status :idle can be executed.
Check if SubAgent can be resumed.
Only SubAgents with status :interrupted can be resumed.
Compose the child agent's base_system_prompt from a Config.
Composition rule:
- Header =
system_prompt_override(if set) else the built-in boilerplate. - Body =
instructions(if set) elsesystem_prompt(legacy) else "".
The header and body are joined with a blank line. Middleware-contributed
prompt fragments are appended later by the normal Sagents.Agent compile
path — this function does not handle those.
Execute the SubAgent.
Runs the LLMChain until:
- Natural completion (no more tool calls)
- HITL interrupt (tool needs approval)
- Error
Returns updated SubAgent struct with new status.
Options
:callbacks- Map of LLMChain callbacks (e.g.,%{on_message_processed: fn...})
Examples
case SubAgent.execute(subagent) do
{:ok, completed_subagent} ->
result = SubAgent.extract_result(completed_subagent)
{:interrupt, interrupted_subagent} ->
# interrupted_subagent.interrupt_data contains action_requests
{:error, error_subagent} ->
# error_subagent.error contains the error
end
# With callbacks for real-time message broadcasting
callbacks = %{
on_message_processed: fn _chain, message ->
broadcast_message(message)
end
}
SubAgent.execute(subagent, callbacks: callbacks)
Extract the module from any middleware format.
Returns the middleware module regardless of whether the input is:
- A raw module atom
- A {module, opts} tuple
- A MiddlewareEntry struct
Returns nil for unrecognized formats.
Extract result from completed SubAgent.
For completed SubAgents, extracts the final message content as a string. This is the default extraction - middleware or custom logic can provide different extraction.
Returns {:ok, string} on success or {:error, reason} on failure.
Examples
{:ok, completed_subagent} = SubAgent.execute(subagent)
{:ok, result} = SubAgent.extract_result(completed_subagent)
# => {:ok, "Research complete: Solar energy has shown..."}
Check if a middleware spec refers to the SubAgent middleware.
Handles all middleware formats: raw modules, tuples, and MiddlewareEntry structs.
Check if SubAgent is in a terminal state.
Terminal states are :completed and :error.
Create a SubAgent from compiled agent (pre-built).
A compiled subagent is pre-defined by the application with complete control over configuration.
Options
:parent_agent_id- Parent's agent ID (required):instructions- Task description (required):compiled_agent- Pre-built Agent struct (required):initial_messages- Optional initial message sequence (default: []):parent_tool_context- Parent's tool_context map to inherit (optional). Static, caller-supplied data (e.g.,user_id, feature flags) that tools access as flat top-level keys on context. Defaults to%{}.:parent_metadata- Parent's state metadata map to inherit (optional). Dynamic, middleware-managed data (e.g.,conversation_title) that tools access viacontext.state.metadata. Defaults to%{}.:parent_runtime- Parent'sstate.runtimemap to inherit (optional). Process-local middleware state such as capturedProcessContextsnapshots. Inherited so sub-agents continue to see the parent's propagated tenant / OTel / Sentry context. Defaults to%{}.:scope- Scope struct to inherit from the parent (optional). Propagated to the SubAgent'scustom_context.scopeso sub-agent tools and persistence callbacks see the same tenant context as the parent. Defaults tocompiled_agent.scope.
Examples
subagent = SubAgent.new_from_compiled(
parent_agent_id: "main-agent",
instructions: "Extract structured data",
compiled_agent: data_extractor_agent,
initial_messages: [prep_message],
parent_tool_context: %{user_id: 42},
parent_metadata: %{"conversation_title" => "Extraction"}
)
Create a SubAgent from configuration (dynamic subagent).
The main agent configures a subagent through the task tool by providing:
- Instructions (becomes user message)
- Agent configuration (Agent struct)
Options
:parent_agent_id- Parent's agent ID (required):instructions- Task description (required):agent_config- Agent struct with tools, model, middleware (required):parent_tool_context- Parent's tool_context map to inherit (optional). Static, caller-supplied data (e.g.,user_id, feature flags) that tools access as flat top-level keys on context. Defaults to%{}.:parent_metadata- Parent's state metadata map to inherit (optional). Dynamic, middleware-managed data (e.g.,conversation_title) that tools access viacontext.state.metadata. Defaults to%{}.:parent_runtime- Parent'sstate.runtimemap to inherit (optional). Process-local middleware state such as capturedProcessContextsnapshots. Inherited so sub-agents continue to see the parent's propagated tenant / OTel / Sentry context. Defaults to%{}.:scope- Scope struct to inherit from the parent (optional). Propagated to the SubAgent'scustom_context.scopeso sub-agent tools and persistence callbacks see the same tenant context as the parent. Defaults toagent_config.scope.
Examples
subagent = SubAgent.new_from_config(
parent_agent_id: "main-agent",
instructions: "Research renewable energy impacts",
agent_config: agent_struct,
parent_tool_context: %{user_id: 42, tenant: "acme"},
parent_metadata: %{"conversation_title" => "Research"},
scope: %MyApp.Accounts.Scope{user: user}
)
Resume the SubAgent after HITL interrupt.
Takes human decisions and continues execution from where it left off. This is the magic - the agent state is already paused at the right spot.
Parameters
subagent- SubAgent with status :interrupteddecisions- List of decision maps from human revieweropts- Optional keyword list with::callbacks- Map of LLMChain callbacks (e.g.,%{on_message_processed: fn...})
Returns
{:ok, completed_subagent}- Execution completed{:interrupt, interrupted_subagent}- Another interrupt (multiple HITL tools){:error, error_subagent}- Resume failed
Examples
decisions = [
%{type: :approve},
%{type: :edit, arguments: %{"path" => "safe.txt"}}
]
case SubAgent.resume(interrupted_subagent, decisions) do
{:ok, completed_subagent} ->
result = SubAgent.extract_result(completed_subagent)
{:interrupt, interrupted_again} ->
# Another interrupt - repeat the process
{:error, error_subagent} ->
# Handle error
end
Build a middleware stack for subagents, filtering out blocked middleware.
The SubAgent middleware itself is ALWAYS filtered out to prevent recursive subagent nesting, regardless of whether it appears in the block list.
This function handles middleware in all formats:
- Raw module atoms (e.g.,
Sagents.Middleware.SubAgent) - Raw tuples (e.g.,
{Sagents.Middleware.SubAgent, opts}) - Initialized MiddlewareEntry structs (from
agent.middleware)
Options
:block_middleware- List of middleware modules to exclude from inheritance. These modules will not be passed to general-purpose subagents. Defaults to[].
Examples
# Default behavior: only SubAgent middleware is filtered
subagent_middleware_stack(parent_middleware)
# Block additional middleware from being inherited
subagent_middleware_stack(parent_middleware, [],
block_middleware: [ConversationTitle, Summarization]
)
# Add additional middleware while blocking others
subagent_middleware_stack(parent_middleware, [CustomMiddleware],
block_middleware: [ConversationTitle]
)
Returns the universal framing prompt prepended to every task-style sub-agent's system prompt.
Encodes the "no user, complete-or-fail, no clarifying questions" contract so
that Sagents.SubAgent.Task authors can focus their instructions/0 on the
substantive procedure. Host compilers combine this with the task's
instructions/0 to form the child agent's system prompt; it can be replaced
per-sub-agent via Sagents.SubAgent.Config's system_prompt_override.