Architecture¶
Shape¶
A supervisor routes each turn to one specialist, or to a finalizer that produces the graded answer. Specialists are ReAct loops over a scoped toolset.
flowchart TD
START([question]) --> S{supervisor}
S -->|web_agent| W[web specialist]
S -->|code_agent| C[code specialist]
S -->|FINISH / budget exhausted| F[finalizer]
W --> S
C --> S
F --> END([exact-match answer])
W -.-> WT["search · scrape · wikipedia · files"]
C -.-> CT["python_repl · files"]
Layers¶
| Package | Responsibility | Depends on |
|---|---|---|
agent.config |
All environment reading, in one frozen Settings |
nothing |
agent.obs |
Logging, LangSmith tracing, metrics | config |
agent.tools |
Capability registry + tool implementations | config, obs |
agent.agents |
Specialist specs + the shared ReAct loop | tools, core.llm |
agent.core |
State, model factory, supervisor graph | agents, obs |
agent.eval |
Benchmark runner, scorers | core |
agent.cli / app.py |
Entry points | eval |
Dependencies point one way. Nothing below core imports the graph.
Why this shape¶
One loop, many specialists. Every specialist is the same reason → maybe call
tools → repeat cycle. That loop lives once in agents/base.py; a specialist is
declared as data (SpecialistSpec: prompt, tools, iteration cap). Adding one is
a spec() function and an entry in SPEC_BUILDERS — the router schema, the
graph nodes, and the supervisor's prompt roster all derive from that list.
Capabilities, not imports. Specialists request get_tools("search", "files")
rather than importing concrete tools. A new tool registers itself and becomes
available to every specialist holding that capability.
Budgets everywhere. Three nested bounds, each independently sufficient to guarantee termination:
| Bound | Default | What it stops |
|---|---|---|
max_supervisor_steps |
4 | Supervisor ping-ponging between specialists |
max_*_iterations |
3 | A specialist looping on a failing tool |
recursion_limit |
derived (18) | Anything the first two miss |
per_question_timeout_s |
180 | One task consuming the run |
total_budget_s |
2400 | The run outliving its host |
Degradation over failure. A missing credential produces a tool message ("web_search is unavailable: TAVILY_API_KEY is not configured; use scrape_webpage instead"), never an exception. The model reads it and adapts. Nothing is constructed at import time — see ADR 0003.
Data flow for one task¶
BenchmarkRunner.build_promptrenders the question and, if the task has an attachment, instructs the model to calldownload_task_file.Orchestrator.answerinvokes the graph with a trace config carryingtask_id.- The supervisor routes; specialists run; history is trimmed to a window before every model call so token spend stays bounded.
- The finalizer compresses the transcript into a bare answer.
run_onerecords aTaskMetric; the answer is cached to disk immediately.
Submission is a separate action reading that cache, so a dropped connection during a 40-minute run costs nothing.