Research Framework
The orchestrator, phases, schemas, adapters, and domain tools that turn a research request into a structured report. This is the core of the lab's research capability.
Status
Implemented. The orchestrator, the 8 phases, the 6 schemas, the 5 search adapters, and the 7 domain tools are operational. The framework is exercised by the Research Worker; the user typically interacts with it through natural-language requests forwarded by the Coordinator.
Purpose
The Research Framework answers multi-step research questions ("find hotels in Barcelona for next September under 200 EUR with good reviews") by:
- Parsing the request into a structured
Task. - Running a deterministic phase DAG that produces typed artifacts.
- Aggregating the artifacts into a final report.
- Persisting the run so it can be resumed, audited, or reused.
The framework is deterministic at the orchestration level (the DAG does not branch on the model's output) and probabilistic at the content level (adapters and synthesis can return different results for the same input). This split is the design principle that makes the framework auditable.
Architecture
The framework has four layers, each of which is testable in isolation:
| Layer | Responsibility | Files |
|---|---|---|
| Parser | Turn a raw user query into a Task and a ParsedQuery. | phases/f0_init.py |
| Orchestrator | Drive the phase DAG, persist state, retry, checkpoint. | orchestrator/ (5 files) |
| Phase | Run a single step: gather, normalize, score, format. | phases/ (8 files) |
| Adapter | Talk to an external search or media provider. | tools/adapters/ (6 files) |
| Schema | Pydantic models for every artifact and result. | schemas/ (8 files) |
| Artifact | Read/write typed artifacts to the run directory. | orchestrator/artifact_store.py |
| Report | Aggregate artifacts into a markdown report. | phases/f6_report.py |
| Notifier | Tell the user (via the Coordinator) the run is done. | tools/run_research.py |
Source layout
The framework lives in a single Python package with the following structure:
The directory names in {framework-root} are placeholders; the
real location is a path known to the agent. The framework
itself is portable.
Orchestrator
The orchestrator is in scout/orchestrator/. It has five
components:
dag.py — Phase DAG
Defines the dependency graph between phases. Each PhaseConfig
declares:
needs: artifacts the phase requires from previous phases.produces: artifacts the phase writes.parallel_group: optional integer for grouping parallel phases.retry: how many times a phase can be re-executed.allow_partial: whethersucceeded_partialis an acceptable terminal state.
PhaseDAG is the data structure; PhaseKind distinguishes
sequential and parallel phases.
runner.py — PhaseRunner
Executes a PhaseDAG for a mission:
- Reads the current
RunStatefromStateStore. - Resolves which phases are ready to run.
- Launches
sequentialphases one at a time,parallelones with aThreadPoolExecutor. - Captures each
PhaseResultand writes it back to state. - Re-throws
PhaseExecutionErrorwhen a phase isfailed_terminal.
state_store.py — StateStore + RunState
The checkpoint layer. The orchestrator persists RunState to
<run_dir>/phase.json after every phase. RunState is
loaded on startup so a crashed run can be resumed.
artifact_store.py — ArtifactStore
Read/write typed artifacts to disk with schema validation.
Uses a per-path RLock to allow safe concurrent writes from
parallel phases.
mission_runner.py — MissionRunner
The high-level entrypoint. run_mission(mission_id, query, runs_dir) is what the CLI calls. It:
- Runs
f0_initto create the run directory and writetask.json. - Loads the DAG and runs phases in order.
- Returns the path to the final report.
The module also exports helpers: get_mission_state,
get_mission_artifacts, and register_phase_handler (for
custom phases).
Phases
A phase is a single Python function that takes
(mission_id, runs_dir) and returns a PhaseResult. The
framework ships with 8 phases.
| Phase ID | File | Purpose | Produces |
|---|---|---|---|
F0 | phases/f0_init.py | Parse the raw query into a Task and ParsedQuery. Write task.json and the run directory. | task.json |
F1 | phases/f1_candidates.py | Run candidate gathering using the configured adapters. Deduplicate and normalize. | candidates.json |
F2 | phases/f2_reviews.py | Fetch reviews for each candidate from the configured sources. | reviews.json |
F4 | phases/f4_price.py | Pull pricing data and compute historical context. | prices.json |
P3 | phases/p3_perplexity_synthesis.py | Synthesize narrative summaries for the top candidates using the Perplexity adapter. | synthesis.json (optional) |
Y1 | phases/y1_youtube.py | Search YouTube for relevant content per candidate. | youtube_search.json |
Y2 | phases/y2_extract.py | Extract transcripts from the YouTube results. | transcripts.json |
F6 | phases/f6_report.py | Aggregate all artifacts into a final markdown report. | report.md |
The full DAG is in Mission DAG. The phase lifecycle (input, output, retry, failure) is in Mission Lifecycle.
Schemas
Schemas are Pydantic v2 models in scout/schemas/. They are
the contract between layers and the validation layer for
artifacts. Eight files:
| File | Models | Used by |
|---|---|---|
task.py | Task, ParsedQuery | F0 |
candidates.py | Candidate, CandidateList | F1 |
evidence.py | Evidence, EvidenceBundle | cross-phase evidence references |
sources.py | Source | every phase that produces URLs |
reviews.py | Review, ReviewSummary | F2 |
prices.py | PriceSnapshot, PriceSeries | F4 |
transcripts.py | Transcript, TranscriptChunk | Y2 |
phase_result.py | PhaseResult, PhaseStatus | every phase |
PhaseStatus is the only enum and has 9 values:
The full schema reference is in Data Dictionary.
Adapters
Adapters translate phase calls into provider-specific HTTP
requests. They are in scout/tools/adapters/.
| Adapter | Provider | Used by | Auth |
|---|---|---|---|
tavily_adapter.py | Tavily | F1, F2 | API key |
duckduckgo_adapter.py | DuckDuckGo | F1 (fallback) | none |
google_search_adapter.py | Google Custom Search | F1 (optional) | API key + cx |
youtube_transcript_api.py | YouTube Transcript API | Y2 | none |
yt_dlp_adapter.py | yt-dlp | Y2 (fallback) | none |
perplexity_adapter.py | Perplexity (in tools/) | P3 | token pool |
The Perplexity adapter lives one level up (scout/tools/) because
it is more than a search adapter — it is the synthesis engine for
P3. It has its own
configuration, token pool, and request journal documented in
External Providers.
All other adapters share a common contract:
Adapters return Source objects from schemas/sources.py so the
rest of the framework is provider-agnostic.
Domain tools
The framework includes 7 domain-specific tools that wrap
end-to-end workflows (e.g., "research a hotel", "research a
video"). Each is a self-contained package with its own
src/, tests/, skills/, and docs/.
| Tool | Purpose | Status |
|---|---|---|
tool-a-research-init | Initialize a research run, write task.json, manage checkpoint state. | Implemented |
tool-b-hotel-research | End-to-end hotel research: gather, normalize, rank, write. | Implemented |
tool-c-review-research | Pull and rank reviews for a list of candidates. | Implemented |
tool-d-photo-research | Download and organize photos for candidates. | Implemented |
tool-e-price-analysis | Compare prices across providers and discount historical data. | Implemented |
tool-f-video-scrape | Scrape and extract video metadata. | Implemented |
tool-g-email-sender | Send a templated email (report delivery, alerts). | Implemented |
tool-h-report-generator | Format the final report from artifacts. | Implemented |
All tools share the directory pattern from Tool Structure → Directory Layout.
Entry points
The framework has two CLI entry points:
research_cli.py — Orchestrator CLI
Returns the path to the final report.md.
run_research.py — Phase-by-phase runner
Runs phases one at a time with full logging per phase. Used for debugging and for the agent's step-by-step mode.
run_research.sh — Wrapper script
Bash wrapper around run_research.py that sets environment
variables (SCOUTE_RESEARCH_ROOT, PYTHONPATH) and runs
inside the framework's virtual environment.
Configuration
The framework is configured through environment variables and a per-tool config file.
| Variable | Purpose | Default |
|---|---|---|
SCOUTE_RESEARCH_ROOT | Where run directories are created. | /tmp/scoute/research |
SCOUTE_RUNS_DIR | Where missions write artifacts. | /tmp/scoute/runs |
SCOUTE_LOG_LEVEL | Logging level. | INFO |
SCOUTE_PARALLEL_WORKERS | Max concurrent phases in a parallel group. | 3 |
SCOUTE_DEFAULT_RETRY | Default retry count for phases that do not override. | 2 |
SCOUTE_TOKEN_POOL_PATH | Path to the Perplexity token pool config. | {framework-root}/token_pool.json |
Tool-specific configuration (e.g., Tavily key, Google cx) is
loaded by each adapter from a JSON file under
{framework-root}/config/.
Failure model
The framework distinguishes between retryable and
terminal failures. The classification is encoded in
PhaseStatus:
| Status | Meaning | Action |
|---|---|---|
pending | The phase has not started yet. | Wait for the orchestrator. |
running | The phase is currently executing. | Wait. |
succeeded | The phase completed and produced all expected outputs. | Mark downstream phases ready. |
succeeded_partial | The phase completed but some outputs are missing. | Mark downstream ready; flag in the report. |
failed_retryable | The phase failed but can be retried. | Orchestrator retries. |
failed_terminal | The phase failed and cannot be retried. | Orchestrator stops the mission; notify the user. |
skipped | The phase was intentionally not executed. | Mark downstream as blocked_missing_input. |
blocked_missing_input | The phase could not start because an input is missing. | Skip; mark downstream. |
stale | The phase's input was updated after the phase ran. | Re-run. |
The orchestrator's retry policy is configurable per phase
(PhaseConfig.retry). When retries are exhausted, the phase
moves to failed_terminal and the mission halts.
Auditability
Every run produces a complete audit trail:
task.json: the original query and the parsed structure.phase.json: the state of every phase at every checkpoint.- All artifacts (typed by schema).
report.md: the final aggregated output.- The framework's own log (one per mission).
The Coordinator can be asked to show the audit trail of any mission at any time. See Audit Model.
Performance
A typical research mission (1 phase F0, 1 F1 with 20 candidates, 3 parallel phases F2/F4/Y1, 1 Y2, 1 P3, 1 F6) completes in 2-5 minutes wall-clock, depending on adapter latency and parallel worker count. The bottleneck is usually the review and price phases; the YouTube phase can take longer when many candidates are involved.
Extensibility
The framework is designed to be extended in three ways:
- New adapter. Implement the
searchcontract inscout/tools/adapters/and register it intools/research_cli.py's adapter factory. - New phase. Implement a
run(mission_id, runs_dir)function inscout/phases/, export it fromphases/__init__.py, and add aPhaseConfigindag.py. - New domain tool. Create a new package under
docs/orchestrator/tools/with the standardsrc/,tests/,skills/,docs/structure.
See Tool Spec and Tool Structure for the templates.