A sub-agent is a specialized agent that a parent agent can delegate work to. The parent model picks a registered role, sends it a task, and receives the child agent's final answer as a tool result. Each sub-agent is a full Condukt.Session with its own model, system prompt, tools, and conversation history.

Use a sub-agent when work needs several reasoning steps, but should stay out of the parent agent's conversation history. Use a normal tool when the work is a single function call.

Declaring sub-agents

Agents declare sub-agents with subagents/0. The callback mirrors tools/0:

defmodule MyApp.LeadAgent do
  use Condukt

  @impl true
  def tools, do: Condukt.Tools.read_only_tools()

  @impl true
  def subagents do
    [
      researcher: MyApp.ResearchAgent,
      coder:
        {MyApp.CoderAgent,
         model: "anthropic:claude-sonnet-4-20250514",
         input: %{
           type: "object",
           properties: %{
             files: %{type: "array", items: %{type: "string"}},
             focus: %{type: "string"}
           },
           required: ["files"]
         },
         output: %{
           type: "object",
           properties: %{
             summary: %{type: "string"},
             changed_files: %{type: "array", items: %{type: "string"}}
           },
           required: ["summary", "changed_files"]
         }},
      summarizer: [
        model: "anthropic:claude-haiku-4-5",
        system_prompt: "Summarize delegated context into concise notes."
      ]
    ]
  end
end

Each entry is role: AgentModule, role: {AgentModule, opts}, or role: opts for an anonymous child agent. The role atom is the identifier the parent model uses. Most registration opts are passed to the child session startup call. :input, :input_schema, :output, and :output_schema are reserved for the sub-agent contract.

Anonymous child agents use the internal anonymous agent module, so you can configure the child inline with session options such as :model, :system_prompt, :tools, :sandbox, and structured contract options. They default :load_project_instructions to false; set it to true in the role opts if the child should load project instructions.

You can also override registrations when starting a session:

{:ok, agent} =
  MyApp.LeadAgent.start_link(
    subagents: [
      reviewer: {MyApp.ReviewerAgent, model: "openai:gpt-5.2"},
      summarizer: [model: "anthropic:claude-haiku-4-5"]
    ]
  )

The subagent tool

When subagents/0 returns at least one role, Condukt injects one built-in tool into the parent agent:

{
  "name": "subagent",
  "parameters": {
    "type": "object",
    "oneOf": [
      {
        "type": "object",
        "properties": {
          "role": {"type": "string", "enum": ["researcher"]},
          "task": {"type": "string"}
        },
        "required": ["role", "task"]
      },
      {
        "type": "object",
        "properties": {
          "role": {"type": "string", "enum": ["coder"]},
          "task": {"type": "string"},
          "input": {
            "type": "object",
            "properties": {
              "files": {"type": "array", "items": {"type": "string"}},
              "focus": {"type": "string"}
            },
            "required": ["files"]
          }
        },
        "required": ["role", "task", "input"]
      }
    ]
  }
}

The model sees a role-specific schema. Fields listed in the JSON Schema required list are required. Properties omitted from required stay optional. When the model calls the tool, Condukt validates the optional structured input, starts a child session, runs the task, returns the final result as the tool result, and then terminates the child session.

Structured input and output

Sub-agent input and output schemas are optional:

  • :input or :input_schema validates the input argument on the subagent tool call before the child starts.
  • :output or :output_schema adds a submit_result tool to the child session and validates the submitted value before returning it to the parent.
  • If no output schema is declared, the child returns free-form text.

Example:

def subagents do
  [
    reviewer:
      {MyApp.ReviewerAgent,
       input: %{
         type: "object",
         properties: %{
           path: %{type: "string"},
           severity: %{type: "string", enum: ["low", "medium", "high"]}
         },
         required: ["path"]
       },
       output: %{
         type: "object",
         properties: %{
           findings: %{type: "array", items: %{type: "object"}},
           summary: %{type: "string"}
         },
         required: ["findings", "summary"]
       }}
  ]
end

In this example path is required and severity is optional. The parent receives a validated map with findings and summary instead of parsing text.

Inheritance

By default a child sub-agent inherits these parent session values:

  • :sandbox
  • :cwd
  • :api_key
  • :base_url
  • :secrets

Registration opts override inherited values:

def subagents do
  [
    researcher: {MyApp.ResearchAgent, sandbox: Condukt.Sandbox.Local}
  ]
end

The default shared sandbox keeps file operations consistent. A sub-agent that reads lib/foo.ex sees the same filesystem view as the parent unless the registration overrides :sandbox or :cwd.

Supervision

A parent session with sub-agents starts a linked DynamicSupervisor. Sub-agent sessions are started on demand under that supervisor with restart: :temporary.

Properties:

  • Stopping the parent session stops the sub-agent supervisor and its children.
  • A child that fails to start or crashes returns an error to the parent tool call. The parent session keeps running.
  • Child sessions are one-shot in this version. They are started for one task and terminated after Condukt.run/2 returns.
  • When a model emits multiple tool calls in one turn, Condukt executes them concurrently and preserves result order in the conversation history.

Events

Condukt emits [:condukt, :subagent, :start] and [:condukt, :subagent, :stop] telemetry events around each delegation. The metadata identifies the parent agent, role, child agent, whether structured input and output contracts are configured, and the final :status.

The telemetry never includes task text, structured input values, or structured output values.

For now, child stream events are not forwarded to the parent stream. The parent stream observes the subagent tool call and the matching tool result. Forwarding child events as tagged parent events can be added later without changing the declaration API.

Errors

Unknown roles return:

{:error, "no sub-agent registered as writer"}

Child start failures and child crashes return {:error, reason} from the tool call. The model receives that error as the tool result and can recover in the next turn.