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 explicitAgentConfig; its system prompt instructs the specialist.
Optional fields:
tools: the same functions, built-in names, definitions, or registry formats accepted byAgent.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, defaultDEFAULT_TIMEOUT_SECONDS(300 seconds), including child model and tool calls.concurrent:Trueby default. WithFalse, 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.usageand 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_dollarsis 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, likewrite_file,ask_userandwrite_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.