Automatic delegation

The agent reaches for sub-agents on its own — the tool is guaranteed to exist, the task is sized by a model, and the directive lands where a model acts on it.

5 min read
11 sections
Edit this page

A sub-agent is the difference between an agent that reads twenty files into one context and an agent that reads twenty files in twenty contexts and keeps only the summaries.

Left to a plain prompt, a model does this rarely: the tool exists, the guidance is a paragraph in the system prompt, and the straight path is right there. delegation=True makes it a policy.

python
agent = Agent(llm=llm, tools=[read_file, glob_files], delegation=True)

agent.run("Summarize each of the twelve incident reports in reports/.")

Nothing in that prompt says delegate. Note also what is not in tools:

python
>>> [t.name for t in agent._effective_tools("…")]
['read_file', 'glob_files', 'sub_agent']

What it looks like

Agent(delegation=True) Live
Summarize each of the twelve incident reports in reports/.
Delegated 3 tasksSummarize reports/01.md · reports/02.md · reports/03.md3 tools · 4.2s
sub-agent · read reports/01.md✓ read_file · 2ms — 84 lines
sub-agent · read reports/02.md✓ read_file · 3ms — 121 lines
Three came back: two outages and one degraded read replica.
the parent's context holds three summaries, not three files

The prompt never says delegate, and sub_agent was never in the tools list.


Three parts, each measured

1. The tool is guaranteed to exist

If the agent has no sub_agent, one is built from the agent's own LLM and its read-only tools — a child that can write is a side effect nobody reviewed. It is built once per agent, because a SubAgentTool owns a thread pool.

It is present even for a narrow task: the directive is conditional, the tool is not, because a model that decides mid-run to delegate must find it there.

2. The task is sized by a model, not a word list

A keyword table is a guess about one language's phrasing, and it goes stale the moment someone writes "handle these tickets" instead of "for each ticket".

ModelAssessor (the default) asks the LLM one cheap question — does this split into independent pieces, and how many? — and caches the answer per task. StructuralAssessor is the fallback and the floor: it counts structure only — enumerated lists, distinct concrete targets, stated quantities. If the task names eight files, a model that says "one piece" is overruled.

python
from shipit_agent.delegation import DelegationPolicy, StructuralAssessor

policy = DelegationPolicy(assessor=StructuralAssessor())
policy.assess("What is in guests.csv?")
# DelegationAdvice(delegate=False, items=1)

policy.assess("Summarize a.md, b.md and c.md.")
# DelegationAdvice(delegate=True, items=3,
#                  reasons=['3 concrete targets are named'])

3. The directive lands on the task

Not the system prompt. Measured against Gemma 4 on Bedrock with three reports to summarize:

Where the directive wentDelegations
System prompt0
Appended to the task6

A small model reads the system prompt as background and the task as instructions. That is the whole difference between the feature working and not.


What it will not do

It does not delegate behind the model's back. The runtime cannot know which parts of a task are independent, and guessing would spawn children working on halves of an indivisible problem. The model still decides — with the tool present and the case put in the right place.


Two runs, side by side

Same agent, same flag, live against Gemma 4:

python
agent.run("Summarize emea.csv, amer.csv and apac.csv — total and top rep each.")
text
parent calls : ['sub_agent', 'sub_agent', 'sub_agent', …]
delegations  : 6
child calls  : [('sub-agent', 'read_file'), ('sub-agent', 'read_file'), …]

The children opened the files; the parent's context holds three one-line answers.

python
agent.run("What is the total amount in emea.csv?")
text
parent calls : ['glob_files', 'read_file']
delegations  : 0

A policy that delegates everything is only a slower agent. The second run matters as much as the first.


Tuning

python
from shipit_agent.delegation import DelegationPolicy

Agent(
    llm=llm,
    delegation=DelegationPolicy(
        min_items=3,             # below this, delegating costs more than it saves
        max_iterations=6,        # loop budget for each child
        max_workers=4,           # children in flight at once
        read_only_children=True, # default
        child_tools=[...],       # or hand them an explicit toolset
        attach_tool=True,        # False if you supply your own sub_agent
        assessor=my_assessor,    # anything with .assess(prompt, llm=…)
    ),
)

delegation=True is shorthand for the defaults; delegation={"min_items": 5} works too.


Watching it happen

Every tool call a child makes is streamed up to the parent as a sub_agent_event, so nothing a child does is invisible:

python
for event in agent.stream(prompt):
    if event.type == "sub_agent_event" and event.payload["inner_type"] == "tool_called":
        inner = event.payload["inner"]
        print(f"  [{event.payload['agent']}] {inner['tool']}")

In the tree and the live panel, delegated work gets its own attributed row — a nested read_file is never mistaken for the parent reading a file.


Cost

Each delegation is a real agent loop with real LLM calls. That is why delegation is off by default, and why it pays for itself only when the children's material is large: three one-line summaries in your context instead of three files.


See also