Deep Research Agent¶
A flagship template for long-horizon research, native to fastaiagent-sdk. Implements the now-canonical pattern (popularized by Open Deep Research and Anthropic's research agents): Scope → parallel Research → Write.
The full source lives at examples/deep-research-agent/.
Architecture¶
ScopeAgent ──→ ResearchBrief
│
▼
Researcher × N (parallel via asyncio.gather)
│
▼
ResearchFindings × N
│
▼
WriteAgent ──→ Markdown report
Why this shape. Empirically, multi-agent synthesis fails at writing — outputs become disjoint sections that don't compose. The pattern that works is to parallelize information gathering and serialize writing. Each sub-researcher works in its own context window on a single subtopic; a single one-shot writer composes the final report from their pruned findings.
Three phases¶
Phase 1 — Scope¶
A single ScopeAgent with output_type=ResearchBrief (Pydantic) reads the user's topic and produces a structured brief: a refined topic statement, a 2–3 sentence summary, and 2–5 independent subtopics.
class Subtopic(BaseModel):
title: str
rationale: str
class ResearchBrief(BaseModel):
topic: str
summary: str
subtopics: list[Subtopic]
The brief is persisted on a deep_research.scope span as JSON in the fastaiagent.research.brief attribute.
Phase 2 — parallel Research¶
For each subtopic, the template constructs a fresh Researcher agent (its own context window, its own tool budget) and dispatches them all via asyncio.gather:
tasks = [
_run_one_researcher(st.title, st.rationale, ctx)
for st in brief.subtopics
]
all_findings = await asyncio.gather(*tasks)
Each researcher has two tools: web_search (Tavily / Brave / Serper / mock fallback) and web_fetch (httpx + stdlib HTML stripper). It returns a typed ResearchFindings (subtopic, summary, citations).
Why no Supervisor here? The SDK's executor runs tool calls within a single agent turn sequentially today, so going through
Supervisor'sdelegate_to_<role>mechanism wouldn't actually parallelize the branches. Cross-agent parallelism viaasyncio.gatherover independentAgent.aruncalls is the right pattern for this template.
Each branch emits its own deep_research.research span carrying the subtopic and structured findings.
Phase 3 — Write¶
A single WriteAgent receives the brief plus all ResearchFindings and composes the final Markdown report in one LLM call. No iteration, no revision loop — the eval suite checks citation coverage post-hoc.
The deep_research.write span carries lightweight report metadata (length, citation count).
Trace shape¶
deep_research.session ← template.kind="deep-research", topic, plan
├── deep_research.scope ← ResearchBrief (JSON)
├── deep_research.research ← ResearchFindings (parallel × N)
└── deep_research.write ← report metadata
Identifying a deep-research trace¶
The session span carries fastaiagent.template.kind = "deep-research". This is the canonical marker — the UI can render a per-template badge and filter trace lists without parsing span names. Any other flagship template can do the same via set_template_kind(span, "<kind>") from fastaiagent.trace.span.
Inspect via the local UI or directly:
# Find every deep-research run by template marker
sqlite3 .fastaiagent/local.db \
"SELECT trace_id, json_extract(attributes, '\$.fastaiagent.research.topic')
FROM spans
WHERE json_extract(attributes, '\$.fastaiagent.template.kind') = 'deep-research';"
# Same listing via the REST API
curl -s 'http://127.0.0.1:7843/api/traces?last_hours=24' \
| jq '.rows[] | select(.name == "deep_research.session")'
# Drill into one trace's structured payloads
curl -s 'http://127.0.0.1:7843/api/traces/<trace_id>' \
| jq '.spans[] | select(.name | startswith("deep_research")) | {name, attrs: .attributes}'
Running¶
cd examples/deep-research-agent
pip install -r requirements.txt
export TAVILY_API_KEY=... # optional — falls back to mock
python agent.py --topic "Current state of MCP adoption in 2026"
The companion scripts:
- streaming_demo.py — per-phase progress trace with timings
- replay_demo.py — re-render the most recent run from local.db
- eval_suite.py — golden questions with two non-LLM scorers
Configuration¶
| Env var | Default | Controls |
|---|---|---|
LLM_MODEL_SCOPE |
gpt-4o |
Scope agent model |
LLM_MODEL_RESEARCHER |
gpt-4o-mini |
Researcher model (cheap, volume) |
LLM_MODEL_WRITER |
gpt-4o |
Writer model |
SEARCH_BACKEND |
auto |
tavily / brave / serper / mock |
SEARCH_TOP_K |
5 |
Results per search call |
RESEARCH_TOOL_BUDGET |
6 |
Max tool calls per branch |
FETCH_MAX_CHARS |
8000 |
Truncate web_fetch output |
What's coming next¶
memory_setup.py is a placeholder for the trace learning loop — an offline fastaiagent learn CLI that reads completed traces from local.db, extracts durable facts, and re-injects them into future runs via PersistentFactBlock. Together: a self-improving research agent running entirely on your local machine.