# Delegation

Declare specialists in the constructor, delegate isolated tasks, and understand permissions and recovery.

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.

```python
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](#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](../permissions/) 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.

```python
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](../retries-and-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](../sessions/).
