The claim to test as you read: everything on this page is the machinery to
re-enter the loop you already read, not a parallel
implementation of it
(ADR-0006,
ADR-0054).
If you find a second turn implementation anywhere here, that is a bug worth
reporting.
flowchart TD
m["the model calls task, maybe twice in a row"] --> ex["tools/execute.py
consecutive task calls group"]
ex --> f1["subagent A"]
ex --> f2["subagent B"]
f1 --> loop["the same run_turn,
a fresh session, a narrowed policy,
the parent's remaining budget"]
f2 --> loop
loop --> res["one text result each,
in call order"]
res --> back["back to the parent's context
as ordinary tool results"]
Stage 1. Re-entering the loop about 15 minutes
1What an agent is
tools/builtin/task.py agents/definition.py agents/discovery.py
Look for: TaskTool subject AgentDefinition Found scan _agent
task.py is short on purpose — about fifty lines. It does four
things: check the call is valid, look the agent up, hand the work to
spawn, and return one string. subject is the short label
the status bar shows, so a human watching four rows scroll past can tell them
apart.
An agent is nothing more than a markdown file with front matter:
- a name and a description
- which tools it is allowed to use
- optionally, a different model to run on
Nothing executable, nothing in Python — that is the rule for every extension
(ADR-0018).
discovery.py searches the usual layered way: project files win over
user files, and the first definition of a name wins. Found remembers
where each one came from, so edgar doctor and error messages can say
exactly which file they mean.
One thing a subagent does not get: the parent's transcript. It starts
from a prompt and a fresh session. That is the whole point of using one — the
parent's context stays small — and it is also why a subagent can never ask the
parent a question.
2Narrowing, not widening
agents/spawn.py
Look for: spawn SpawnLimits HARD_DEPTH subagents_config narrow_mode _cap _subset
This file carries edgar's second standing rule: humans widen, machines tighten
(ADR-0021).
Read narrow_mode and _subset together and check there is
no way a child ends up with a looser mode or a tool its parent did not have. If an
agent file asks for a tool the parent lacks, it quietly gets only the tools both
have — never more.
HARD_DEPTH — a constant, not a setting. A subagent may spawn one
of its own, but the nesting stops at this depth no matter what the config says.
_cap — splits the parent's remaining budget rather than
handing out a fresh one, so ten subagents cannot cost ten times the cap.
SpawnLimits — one dataclass holding concurrency, depth and
per-child ceilings, so all the rules sit in one readable place.
Take with you: every limit here is checked at the
moment of spawning, where a clear error is possible, rather than mid-turn where it
would only produce a confusing failure.
Stage 2. Several at once about 10 minutes
3Fan-out, and watching it happen
tools/execute.py cli/statusbar.py
Look for: execute execute_many _fan_out_group FRAMES Status StderrLine rows terminal_ready
Tool calls from one response normally run one after another — that is Core's
rule, and it has not changed. execute_many adds exactly one exception:
a consecutive run of task calls runs at the same time
[TOOL-12]. Find _fan_out_group and check the two things that matter:
for each tool call in the response, in order:
if this call is "task" and the next ones are too:
group the run of "task" calls together
run the whole group at the same time
keep the results in the original call order
else:
run this one call by itself, then move on
- An ordinary tool call between two
task calls breaks the group —
no surprise concurrency slips in.
- Results always come back in call order, whichever subagent actually finishes
first.
- A subagent that fails becomes an error result for just its own call. It never
cancels its siblings.
statusbar.py earns its size on the terminal handling, not the
logic. It subscribes to the event bus like every other output component
(ADR-0011)
and draws one row per running agent [SUB-9].
terminal_ready — refuses to draw unless stderr is a real
terminal, so piped and scheduled runs stay clean.
StderrLine — exists so that anything else printing to stderr
doesn't land in the middle of a half-drawn frame.
Stage 3. Choosing a model about 10 minutes
4Three mechanisms, kept apart
providers/routing.py providers/fallback.py
Look for: Role RoutingContext Route Selection select_model check_capabilities routes_from_config Candidate NoFallback choose next_provider chain_from_config
Routing, escalation and fallback get mixed up together almost everywhere else.
Here they are three separate files, and one of them is not built yet
(ADR-0013).
- Routing decides up front.
select_model is a
pure function: a RoutingContext plus a list of Route
rules in, a Selection out. That is why it is property-tested rather
than mocked.
- Fallback goes sideways when a provider is unreachable, and
never lowers capabilities —
choose skips any candidate that cannot
do what the selected model could.
- Escalation goes upward, on a capability failure. It belongs
to v3 — there is no file for it yet.
check_capabilities is where a model with no tool support gets
refused, and where matters: at selection, with a message naming the model and the
rule that picked it — not as a provider error halfway through a turn [ROUTE-6].
NoFallback is the honest ending: when the chain runs out, the run
stops and says so, instead of quietly reaching for something you never configured
(ADR-0023).
flowchart LR
ctx["role, prompt size,
agent, config"] --> sel["routing
select_model, pure"]
sel --> cap{"can it use tools,
if this turn needs them?"}
cap -->|no| refuse["refused here, by name"]
cap -->|yes| run["run the turn"]
run -->|"provider unreachable"| fb["fallback
sideways, same capabilities"]
fb -->|"chain exhausted"| nf["NoFallback: stop and say so"]
run -->|"capability failure"| esc["escalation
upward, capped, announced — v3"]
Sizes
Lines of code: blank lines and comments do not count, docstrings do.
just loc prints the current totals.
| File | Lines of code | What it is |
tools/builtin/task.py | ~54 | The tool: validate, look up, spawn, return one string |
agents/definition.py | ~27 | An agent is a markdown file with front matter |
agents/discovery.py | ~82 | Layered search, and where each one came from |
agents/spawn.py | ~135 | Narrowed policy, split budget, capped depth |
tools/execute.py | ~150 | The pipeline, and the one concurrent exception |
cli/statusbar.py | ~153 | One row per running agent, on stderr |
providers/routing.py | ~99 | Declarative rules, resolved by a pure function |
providers/fallback.py | ~72 | Sideways on unreachability, never downward |
Source: just loc, which counts with
tests/support/budget.py. Rounded; the tour's test fails if a figure drifts
more than 25 lines of code from the code.