Lab Notes
Research

Research Framework

El orchestrator, las phases, los schemas, los adapters y las domain tools que convierten una research request en un report estructurado. Es el core de la capability de research del lab.

Status

Implemented. El orchestrator, las 8 phases, los 6 schemas, los 5 search adapters y las 7 domain tools están operational. El framework lo ejercita el Research Worker; el usuario típicamente interactúa con él a través de natural-language requests que el Coordinator forward.

Propósito

El Research Framework responde multi-step research questions ("find hotels in Barcelona for next September under 200 EUR with good reviews") haciendo:

  1. Parsea la request en un Task estructurado.
  2. Corre un phase DAG determinista que produce typed artifacts.
  3. Agrega los artifacts en un report final.
  4. Persiste la run para que se pueda resumir, auditar o reusar.

El framework es deterministic at the orchestration level (el DAG no branchea sobre el output del model) y probabilistic at the content level (los adapters y la synthesis pueden devolver resultados distintos para el mismo input). Este split es el design principle que hace el framework auditable.

Architecture

El framework tiene cuatro layers, cada una testeable de forma aislada:

Loading diagram…
LayerResponsibilityFiles
ParserConvierte una raw user query en un Task y un ParsedQuery.phases/f0_init.py
OrchestratorDrive el phase DAG, persiste state, retry, checkpoint.orchestrator/ (5 files)
PhaseCorre un único step: gather, normalize, score, format.phases/ (8 files)
AdapterHabla con un external search o media provider.tools/adapters/ (6 files)
SchemaModelos Pydantic para cada artifact y result.schemas/ (8 files)
ArtifactLee/escribe typed artifacts al run directory.orchestrator/artifact_store.py
ReportAgrega los artifacts en un markdown report.phases/f6_report.py
NotifierLe dice al usuario (vía el Coordinator) que la run terminó.tools/run_research.py

Source layout

El framework vive en un único Python package con la siguiente estructura:

{framework-root}/
├── pyproject.toml
├── uv.lock
├── .agents/
│   └── skills/
│       └── scout-orchestrate/
│           └── SKILL.md
├── scout/
│   ├── __init__.py
│   ├── orchestrator/
│   │   ├── __init__.py
│   │   ├── dag.py
│   │   ├── runner.py
│   │   ├── state_store.py
│   │   ├── artifact_store.py
│   │   └── mission_runner.py
│   ├── phases/
│   │   ├── __init__.py
│   │   ├── f0_init.py
│   │   ├── f1_candidates.py
│   │   ├── f2_reviews.py
│   │   ├── f4_price.py
│   │   ├── p3_perplexity_synthesis.py
│   │   ├── y1_youtube.py
│   │   ├── y2_extract.py
│   │   └── f6_report.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── task.py
│   │   ├── candidates.py
│   │   ├── evidence.py
│   │   ├── sources.py
│   │   ├── reviews.py
│   │   ├── prices.py
│   │   ├── transcripts.py
│   │   └── phase_result.py
│   └── tools/
│       ├── __init__.py
│       ├── research_cli.py
│       ├── run_research.py
│       ├── run_research.sh
│       ├── perplexity_adapter.py
│       └── adapters/
│           ├── __init__.py
│           ├── tavily_adapter.py
│           ├── duckduckgo_adapter.py
│           ├── google_search_adapter.py
│           ├── youtube_transcript_api.py
│           └── yt_dlp_adapter.py
└── docs/
    ├── README.md
    ├── code-reference.md
    ├── architecture.md
    └── orchestrator/
        └── tools/
            ├── tool-a-research-init/
            ├── tool-b-hotel-research/
            ├── tool-c-review-research/
            ├── tool-d-photo-research/
            ├── tool-e-price-analysis/
            ├── tool-f-video-scrape/
            ├── tool-g-email-sender/
            └── tool-h-report-generator/

Los directory names en {framework-root} son placeholders; la real location es un path conocido por el agent. El framework en sí es portable.

Orchestrator

El orchestrator está en scout/orchestrator/. Tiene cinco componentes:

dag.py — Phase DAG

Define el dependency graph entre phases. Cada PhaseConfig declara:

  • needs: artifacts que la phase requiere de phases anteriores.
  • produces: artifacts que la phase escribe.
  • parallel_group: integer opcional para agrupar parallel phases.
  • retry: cuántas veces se puede re-ejecutar una phase.
  • allow_partial: si succeeded_partial es un terminal state aceptable.

PhaseDAG es la data structure; PhaseKind distingue sequential y parallel phases.

runner.py — PhaseRunner

Ejecuta un PhaseDAG para una mission:

  • Lee el current RunState del StateStore.
  • Resuelve qué phases están ready para correr.
  • Lanza las sequential phases una a una, las parallel con un ThreadPoolExecutor.
  • Captura cada PhaseResult y lo escribe de vuelta al state.
  • Re-lanza PhaseExecutionError cuando una phase está failed_terminal.

state_store.py — StateStore + RunState

La checkpoint layer. El orchestrator persiste RunState a <run_dir>/phase.json después de cada phase. RunState se carga al startup para que una run con crash se pueda resumir.

artifact_store.py — ArtifactStore

Lee/escribe typed artifacts a disco con schema validation. Usa un RLock per-path para permitir concurrent writes seguros desde parallel phases.

mission_runner.py — MissionRunner

El entrypoint de high-level. run_mission(mission_id, query, runs_dir) es lo que el CLI llama. Hace:

  • Corre f0_init para crear el run directory y escribir task.json.
  • Carga el DAG y corre las phases en orden.
  • Devuelve el path al report final.

El module también exporta helpers: get_mission_state, get_mission_artifacts y register_phase_handler (para custom phases).

Phases

Una phase es una única función Python que toma (mission_id, runs_dir) y devuelve un PhaseResult. El framework viene con 8 phases.

Phase IDFilePurposeProduces
F0phases/f0_init.pyParsea la raw query en un Task y ParsedQuery. Escribe task.json y el run directory.task.json
F1phases/f1_candidates.pyCorre candidate gathering con los configured adapters. Deduplica y normaliza.candidates.json
F2phases/f2_reviews.pyTrae reviews para cada candidate desde las configured sources.reviews.json
F4phases/f4_price.pyTrae pricing data y calcula historical context.prices.json
P3phases/p3_perplexity_synthesis.pySintetiza narrative summaries para los top candidates usando el Perplexity adapter.synthesis.json (optional)
Y1phases/y1_youtube.pyBusca YouTube para content relevante per candidate.youtube_search.json
Y2phases/y2_extract.pyExtrae transcripts de los YouTube results.transcripts.json
F6phases/f6_report.pyAgrega todos los artifacts en un final markdown report.report.md

El DAG completo está en Mission DAG. El phase lifecycle (input, output, retry, failure) está en Mission Lifecycle.

Schemas

Los schemas son modelos Pydantic v2 en scout/schemas/. Son el contract entre layers y la validation layer para artifacts. Ocho files:

FileModelsUsed by
task.pyTask, ParsedQueryF0
candidates.pyCandidate, CandidateListF1
evidence.pyEvidence, EvidenceBundlecross-phase evidence references
sources.pySourcecada phase que produce URLs
reviews.pyReview, ReviewSummaryF2
prices.pyPriceSnapshot, PriceSeriesF4
transcripts.pyTranscript, TranscriptChunkY2
phase_result.pyPhaseResult, PhaseStatuscada phase

PhaseStatus es el único enum y tiene 9 values:

class PhaseStatus(str, Enum):
    pending              = "pending"
    running              = "running"
    succeeded            = "succeeded"
    succeeded_partial    = "succeeded_partial"
    failed_retryable     = "failed_retryable"
    failed_terminal      = "failed_terminal"
    skipped              = "skipped"
    blocked_missing_input = "blocked_missing_input"
    stale                = "stale"

La schema reference completa está en Data Dictionary.

Adapters

Los adapters traducen las llamadas de phase en provider-specific HTTP requests. Están en scout/tools/adapters/.

AdapterProviderUsed byAuth
tavily_adapter.pyTavilyF1, F2API key
duckduckgo_adapter.pyDuckDuckGoF1 (fallback)none
google_search_adapter.pyGoogle Custom SearchF1 (optional)API key + cx
youtube_transcript_api.pyYouTube Transcript APIY2none
yt_dlp_adapter.pyyt-dlpY2 (fallback)none
perplexity_adapter.pyPerplexity (en tools/)P3token pool

El Perplexity adapter vive un nivel arriba (scout/tools/) porque es más que un search adapter — es el synthesis engine para P3. Tiene su propia configuration, token pool y request journal documentados en External Providers.

Todos los demás adapters comparten un common contract:

def search(query: str, *, limit: int = 10, **opts) -> list[Source]:
    """Return a list of Source objects matching the query."""

Los adapters devuelven objects Source de schemas/sources.py para que el resto del framework sea provider-agnostic.

Domain tools

El framework incluye 7 domain-specific tools que envuelven end-to-end workflows (p. ej., "research a hotel", "research a video"). Cada una es un package autocontenido con su propio src/, tests/, skills/ y docs/.

ToolPurposeStatus
tool-a-research-initInicializa una research run, escribe task.json, maneja el checkpoint state.Implemented
tool-b-hotel-researchHotel research end-to-end: gather, normalize, rank, write.Implemented
tool-c-review-researchTrae y rankea reviews para una lista de candidates.Implemented
tool-d-photo-researchDescarga y organiza fotos para los candidates.Implemented
tool-e-price-analysisCompara prices entre providers y descuento datos históricos.Implemented
tool-f-video-scrapeScrapea y extrae video metadata.Implemented
tool-g-email-senderEnvía un email templated (report delivery, alerts).Implemented
tool-h-report-generatorFormatea el report final desde los artifacts.Implemented

Todas las tools comparten el directory pattern de Tool Structure → Directory Layout.

Entry points

El framework tiene dos CLI entry points:

research_cli.py — Orchestrator CLI

python -m scout.tools.research_cli run \
  --mission-id hotel-barcelona-2026-09 \
  --query "find hotels in Barcelona for September 2026 under 200 EUR with good reviews" \
  --runs-dir /tmp/scoute/runs

Devuelve el path al final report.md.

run_research.py — Phase-by-phase runner

python -m scout.tools.run_research \
  hotel-barcelona-2026-09 \
  "find hotels in Barcelona for September 2026 under 200 EUR" \
  --runs-dir /tmp/scoute/runs

Corre las phases una a una con full logging per phase. Usado para debugging y para el step-by-step mode del agent.

run_research.sh — Wrapper script

Bash wrapper sobre run_research.py que setea environment variables (SCOUTE_RESEARCH_ROOT, PYTHONPATH) y corre dentro del virtual environment del framework.

Configuration

El framework se configura a través de environment variables y un per-tool config file.

VariablePurposeDefault
SCOUTE_RESEARCH_ROOTDónde se crean los run directories./tmp/scoute/research
SCOUTE_RUNS_DIRDónde las missions escriben artifacts./tmp/scoute/runs
SCOUTE_LOG_LEVELLogging level.INFO
SCOUTE_PARALLEL_WORKERSMax concurrent phases en un parallel group.3
SCOUTE_DEFAULT_RETRYDefault retry count para phases que no hacen override.2
SCOUTE_TOKEN_POOL_PATHPath al Perplexity token pool config.{framework-root}/token_pool.json

Tool-specific configuration (p. ej., Tavily key, Google cx) la carga cada adapter desde un JSON file bajo {framework-root}/config/.

Failure model

El framework distingue entre failures retryable y terminal. La classification está encoded en PhaseStatus:

StatusMeaningAction
pendingLa phase no ha empezado aún.Wait for the orchestrator.
runningLa phase se está ejecutando.Wait.
succeededLa phase terminó y produjo todos los expected outputs.Mark downstream phases ready.
succeeded_partialLa phase terminó pero faltan algunos outputs.Mark downstream ready; flag in the report.
failed_retryableLa phase falló pero se puede reintentar.Orchestrator retries.
failed_terminalLa phase falló y no se puede reintentar.Orchestrator stops the mission; notify the user.
skippedLa phase fue intencionalmente no ejecutada.Mark downstream as blocked_missing_input.
blocked_missing_inputLa phase no pudo empezar porque falta un input.Skip; mark downstream.
staleEl input de la phase se actualizó después de que corrió.Re-run.

La retry policy del orchestrator es configurable per phase (PhaseConfig.retry). Cuando se agotan los retries, la phase pasa a failed_terminal y la mission se detiene.

Auditability

Cada run produce un audit trail completo:

  • task.json: la query original y la parsed structure.
  • phase.json: el state de cada phase en cada checkpoint.
  • Todos los artifacts (typed por schema).
  • report.md: el final aggregated output.
  • El log propio del framework (uno por mission).

Se le puede pedir al Coordinator que muestre el audit trail de cualquier mission en cualquier momento. Ver Audit Model.

Performance

Una research mission típica (1 phase F0, 1 F1 con 20 candidates, 3 parallel phases F2/F4/Y1, 1 Y2, 1 P3, 1 F6) tarda 2-5 minutos wall-clock, dependiendo de adapter latency y parallel worker count. El bottleneck suele ser las review y price phases; la YouTube phase puede tardar más cuando participan muchos candidates.

Extensibility

El framework está diseñado para extenderse de tres maneras:

  1. New adapter. Implementar el contract search en scout/tools/adapters/ y registrarlo en la adapter factory de tools/research_cli.py.
  2. New phase. Implementar una función run(mission_id, runs_dir) en scout/phases/, exportarla de phases/__init__.py, y añadir un PhaseConfig en dag.py.
  3. New domain tool. Crear un nuevo package bajo docs/orchestrator/tools/ con la standard structure src/, tests/, skills/, docs/.

Ver Tool Spec y Tool Structure para los templates.

See also