Declare specialists when creating an orchestrator. HarnessX handles creating, running, and closing the specialist for each task; you do not write or register delegation wrappers.

import asyncio
from harnessx import Agent, AgentConfig, Limits, SubAgent
from harnessx.builtin.web import fetch_url

async def main():
    async with Agent(
        config=AgentConfig(
            system_prompt="Delegate research when useful, then synthesize the findings.",
        ),
        subagents=[
            SubAgent(
                name="researcher",
                description="Read public URLs and summarize their evidence.",
                config=AgentConfig(
                    system_prompt="Read the supplied URLs and cite your sources.",
                    limits=Limits(max_iterations=5),
                ),
                tools=[fetch_url],
            ),
        ],
    ) as orchestrator:
        result = await orchestrator.run("Read https://example.com and summarize it.")
        result.raise_for_status()  # raises a RunError subclass unless the answer is whole
        print(result.output)

asyncio.run(main())

Definition and routing

SubAgent is a definition, not a running agent. Required fields:

  • name: 1–55 letters, digits, underscores, or hyphens, starting with a letter or underscore.
  • description: tells the orchestrator when this specialist is useful.
  • config: an explicit AgentConfig; its system prompt instructs the specialist.

Optional fields:

  • tools: the same functions, built-in names, definitions, or registry formats accepted by Agent.
  • skills: a list of skill paths, loaded separately for each task.
  • permission: permission to delegate; omitted means inherit the parent's manager.
  • timeout_seconds: whole-task timeout, default DEFAULT_TIMEOUT_SECONDS (300 seconds), including child model and tool calls.
  • concurrent: True by default. With False, calls to this specialist run one at a time. See Specialists in parallel.

The SDK exposes delegate_<name>(task: str) to the model, so no orchestrator class is needed. Do not register a tool with that name; collisions and duplicate specialist names fail during construction. The model decides whether to delegate, so write workflow code when you need a fixed sequence. See examples/05-control/delegating_to_subagents.py for a worked orchestrator.

Context, resources, and permissions

Shared with a delegated task Fresh or left behind
The parent's permission manager: defaults, name overrides, session grants, and approval callback Memory, configuration, and tool registry, one set per task
Python objects the handlers close over, such as files and databases The parent's model and injected provider
Parent tools, hooks, middleware, extensions, MCP connections, and sandboxes

The child sees only its own prompt and the task text, so put the code, question, URLs, or paths it needs there. Child-owned providers and memory close on completion, failure, and cancellation. Skills bring their own tools; do not also register a Skill tool.

Delegation permission is separate from the child's tool permissions, so approving delegation does not approve its tools. See Permissions & budgets for defaults. For a specialist that needs its own extensions, provider, or lifecycle, register an ordinary tool that creates, runs and closes its own Agent.

Results, streaming, and limits

The child returns its completed output to the orchestrator, which then synthesizes a response. Child failures and non-completed statuses become delegation errors. A child that completes without a whole answer says so:

Child result What the orchestrator gets
Cut off at its reply budget (truncated) Its output, with a note that it is incomplete
Stopped at its step limit (limited) Its output, with a note that this is its account of where it got to
Declined or filtered (refused) A delegation error
Completed with no text A note that the specialist finished without a reply

Partial output is never passed off as a finished result.

Delegation tools use manual replay policy. run and run_stream behave the same. Streaming reports ordinary tool_call_start, tool_call_complete, and tool_result events; child text never appears as parent answer text.

Each specialist has its own iteration and cost limits. Set them with AgentConfig(limits=Limits(...)). The parent pays for its children:

  • Known child usage is charged to the parent as each child response arrives, and included in the parent's RunResult.usage and lifetime totals. That includes responses before a child fails or is cancelled.
  • Child spend is priced at the child's configured rates.
  • The parent's max_cost_dollars is checked inside the child loop too, so a delegation cannot overrun the parent's budget.
  • An interrupted call whose usage is unknown sets usage_incomplete.

In a durable runtime, the delegation's outcome carries its usage and cost as deltas, so a recovered delegation is not charged twice.

A timeout cancels waiting and closes child resources. It cannot reverse external effects or stop a synchronous tool thread.

Specialists in parallel

If the model asks for several specialists in the same turn, they run at the same time, so three independent lookups take about as long as the slowest one. AgentConfig.max_parallel_subagents sets how many can run at once (4 by default). Any extra ones wait for a free slot, and setting it to 1 runs them one at a time.

from harnessx import AgentConfig, SubAgent

orchestrator_config = AgentConfig(max_parallel_subagents=2)

writer = SubAgent(
    name="writer",
    description="Write the final report to disk.",
    config=AgentConfig(system_prompt="Write the report you are given."),
    tools=["write_file"],
    concurrent=False,  # calls to this specialist run one at a time
)

A specialist with concurrent=False is treated like any other serial tool. Calls to it run one after another, in the order the model made them, and they take turns with the other serial calls in that turn. Concurrent specialists in the same turn still run alongside it.

A few things keep specialists that run together from stepping on each other:

  • Tools registered with concurrent=False, like write_file, ask_user and write_todos, never run at the same time, whether they're called by the parent or by any specialist. Delegating to a specialist doesn't count, so a serial specialist can't deadlock waiting on its own child.
  • If two specialists need approval at the same moment, the parent's approval callback is asked about one, then the other. If the first answer grants the tool for the session (permissions.grant_session(...)), the second specialist uses that grant and isn't asked again.
  • Results go back to the model in the order it asked for them, whichever specialist finishes first.

Each specialist that's running has its own model stream, so four of them means four requests to the provider at once. If that runs into a rate limit, each one retries on its own, as described in Retries & fallbacks.

Durable execution boundary

AgentRuntime persists the outer delegation as an ordinary manual-replay tool call. Committed results are reused on recovery. An interrupted delegation with an unknown outcome stops for manual recovery rather than starting a new specialist. Live model reruns are not deterministic.

The child itself runs non-durably. Its progress, internal approvals, artifacts, and conversation are not persisted by the parent runtime. A child ASK tool uses the approval callback and fails closed without one; it creates no resumable approval in the parent runtime. Treat output that refers to child-local artifacts accordingly, since this API does not transfer them durably.

Snapshots do not serialize Python handlers, so rebind subagents definitions exactly as you rebind tools. See Sessions & snapshots.