Sagents.Middleware.SubAgent (Sagents v0.8.0-rc.5)
Copy MarkdownMiddleware for delegating tasks to specialized SubAgents.
Provides a task tool that allows the main agent to delegate complex,
multi-step work to specialized SubAgents. SubAgents run in isolated
contexts with their own conversation history, providing token efficiency
and clean separation of concerns.
Features
- Dynamic SubAgents: Create SubAgents from configuration at runtime
- Pre-compiled SubAgents: Use pre-built Agent instances
- HITL Propagation: SubAgent interrupts automatically propagate to parent
- Token Efficiency: Parent only sees final result, not SubAgent's internal work
- Process Isolation: SubAgents run as supervised processes
Configuration Options
The middleware accepts these options:
:subagents- List ofSubAgent.ConfigorSubAgent.Compiledconfigurations for pre-defined subagents. Defaults to[].:model- The chat model for dynamic subagents. Required.:middleware- Additional middleware to add to subagents. Defaults to[].:block_middleware- List of middleware modules to exclude from general-purpose subagent inheritance. Defaults to[]. See "Middleware Filtering" below.:include_task_list- Whether to render the## Available Taskssection (one bullet per configured sub-agent plusgeneral-purpose) into the middleware's system prompt. Defaults totrue.Set to
falsewhen the integrating application supplies the task menu another way. For example, a/commandsflow that injects only the relevant task entry on demand to keep the base context lean and reduce the chance of the model picking the wrong task. Thetasktool'stask_nameenum still constrains valid values regardless.
Configuration Example
middleware = [
{SubAgent, [
model: model,
subagents: [
SubAgent.Config.new!(%{
name: "researcher",
description: "Research topics using internet search",
system_prompt: "You are an expert researcher...",
tools: [internet_search_tool]
}),
SubAgent.Compiled.new!(%{
name: "coder",
description: "Write code for specific tasks",
agent: pre_built_coder_agent
})
],
block_middleware: [ConversationTitle, Summarization]
]}
]Middleware Filtering
When a general-purpose subagent is created, it inherits the parent agent's middleware stack with certain exclusions:
SubAgent middleware is ALWAYS excluded - This prevents recursive subagent nesting which could lead to resource exhaustion. You cannot override this.
Blocked middleware is excluded - Any modules listed in
:block_middlewareare filtered out before passing to the subagent.
Example: Blocking Unnecessary Middleware
Some middleware is inappropriate for short-lived subagents:
{SubAgent, [
model: model,
subagents: [],
# These middleware modules won't be inherited by general-purpose subagents
block_middleware: [
Sagents.Middleware.ConversationTitle, # Subagents don't need titles
Sagents.Middleware.Summarization # Short tasks don't need summarization
]
]}Pre-configured Subagents
The :block_middleware option only affects general-purpose subagents created
dynamically via the task tool. Pre-configured subagents (defined in :subagents)
use their own explicitly defined middleware and are NOT affected by this option.
{SubAgent, [
subagents: [
# This subagent defines its own middleware - block_middleware doesn't apply
SubAgent.Config.new!(%{
name: "researcher",
middleware: [ConversationTitle] # Explicitly included
})
],
block_middleware: [ConversationTitle] # Only affects general-purpose
]}Usage Example
# Main agent decides to delegate work
"I need to research renewable energy. I'll use the researcher SubAgent."
→ Calls: task("Research renewable energy impacts", "researcher")
# SubAgent executes independently
# If SubAgent hits HITL interrupt (e.g., internet_search needs approval):
# 1. SubAgent pauses
# 2. Interrupt propagates to parent
# 3. User sees: "SubAgent 'researcher' needs approval for 'internet_search'"
# 4. User approves
# 5. Parent resumes, which resumes SubAgent
# 6. SubAgent completes and returns resultArchitecture
Main Agent
│
├─ task("research task", "researcher")
│ │
│ └─ SubAgent (as SubAgentServer process)
│ ├─ Fresh conversation
│ ├─ Specialized tools
│ ├─ LLM executes
│ └─ Returns final message only
│
└─ Receives result, continuesHITL Interrupt Flow
1. SubAgent hits HITL interrupt
2. SubAgentServer.execute() returns {:interrupt, interrupt_data}
3. Task tool receives interrupt
4. Task tool returns {:interrupt, enhanced_data} to parent
5. Parent agent propagates to AgentServer
6. User approves
7. Parent agent resumes
8. Task tool calls SubAgentServer.resume(decisions)
9. SubAgent continues and completes
Summary
Functions
Handle resume for SubAgent interrupts.
Starts and executes a new SubAgent to delegate work.
Functions
Handle resume for SubAgent interrupts.
Claims interrupts where state.interrupt_data has type: :subagent_hitl.
Delegates to SubAgentServer.resume and handles completion, re-interrupt,
and error cases. Also handles type: :multiple_interrupts by processing
the first interrupt and queuing the rest.
@spec start_subagent(String.t(), String.t(), map(), map(), map()) :: {:ok, String.t()} | {:ok, String.t(), term()} | {:interrupt, map()} | {:error, String.t()}
Starts and executes a new SubAgent to delegate work.
This function allows custom tools and middleware to spawn SubAgents for
delegating complex, multi-step tasks, similar to how the built-in task tool
works. The SubAgent runs as an isolated, supervised process with its own
conversation context.
Parameters
instructions- Detailed instructions for what the SubAgent should accomplish. Be specific about the task, expected output, and any context needed.task_name- The name of the task to use. Must match a configured SubAgent name (from middleware init) or "general-purpose" for dynamic SubAgents.args- Full arguments map containing:"instructions"(required) - Same as instructions parameter"task_name"(required) - Same as task_name parameter"system_prompt"(optional) - Custom system prompt for general-purpose SubAgents
context- Tool execution context map containing::agent_id- Parent agent ID:state- Parent agent state:parent_middleware- Parent middleware list (for general-purpose SubAgents):resume_info- Resume information if continuing interrupted SubAgent
config- Middleware configuration map containing::agent_map- Map of task_name -> Agent struct:descriptions- Map of task_name -> description string:agent_id- Parent agent ID:model- Model configuration
Returns
{:ok, result}- SubAgent completed successfully, returns final message content{:interrupt, interrupt_data}- SubAgent hit HITL interrupt, needs approval{:error, reason}- Failed to start or execute SubAgent
Example
Using from a custom tool function:
def my_research_tool_function(args, context) do
# Build config from middleware state
subagent_config = %{
agent_map: context.subagent_map,
descriptions: context.subagent_descriptions,
agent_id: context.agent_id,
model: context.model
}
# Prepare arguments
task_args = %{
"instructions" => "Research quantum computing developments",
"task_name" => "research"
}
# Start SubAgent
case SubAgent.start_subagent(
"Research quantum computing developments",
"researcher",
task_args,
context,
subagent_config
) do
{:ok, result} ->
{:ok, "Research complete: " <> result}
{:interrupt, interrupt_data} ->
# Propagate interrupt to parent
{:interrupt, interrupt_data}
{:error, reason} ->
{:error, "Failed to research: " <> reason}
end
endNotes
- SubAgents run in isolated process contexts with their own conversation history
- Parent only sees final result, not intermediate reasoning (token efficient)
- HITL interrupts from SubAgents automatically propagate to parent
- For "general-purpose" type, tools and middleware are inherited from parent
- SubAgents are supervised and cleaned up automatically