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.
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.
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:
>>> [t.name for t in agent._effective_tools("…")]
['read_file', 'glob_files', 'sub_agent']What it looks like
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.
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 went | Delegations |
|---|---|
| System prompt | 0 |
| Appended to the task | 6 |
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:
agent.run("Summarize emea.csv, amer.csv and apac.csv — total and top rep each.")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.
agent.run("What is the total amount in emea.csv?")parent calls : ['glob_files', 'read_file']
delegations : 0A policy that delegates everything is only a slower agent. The second run matters as much as the first.
Tuning
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:
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
- Sub-agents — the tool itself, used explicitly
- Progress narration
- Apps — the other way to keep work out of your context