Sagents.Middleware.SubAgent (Sagents v0.8.0-rc.4)

Copy Markdown

Middleware 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 of SubAgent.Config or SubAgent.Compiled configurations 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 Tasks section (one bullet per configured sub-agent plus general-purpose) into the middleware's system prompt. Defaults to true.

    Set to false when the integrating application supplies the task menu another way. For example, a /commands flow 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. The task tool's task_name enum 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:

  1. SubAgent middleware is ALWAYS excluded - This prevents recursive subagent nesting which could lead to resource exhaustion. You cannot override this.

  2. Blocked middleware is excluded - Any modules listed in :block_middleware are 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 result

Architecture

Main Agent
  
   task("research task", "researcher")
     
      SubAgent (as SubAgentServer process)
          Fresh conversation
          Specialized tools
          LLM executes
          Returns final message only
  
   Receives result, continues

HITL 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(agent, state, resume_data, config, opts)

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.

start_subagent(instructions, task_name, args, context, config)

@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
end

Notes

  • 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