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:
- Parsea la request en un
Taskestructurado. - Corre un phase DAG determinista que produce typed artifacts.
- Agrega los artifacts en un report final.
- 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:
| Layer | Responsibility | Files |
|---|---|---|
| Parser | Convierte una raw user query en un Task y un ParsedQuery. | phases/f0_init.py |
| Orchestrator | Drive el phase DAG, persiste state, retry, checkpoint. | orchestrator/ (5 files) |
| Phase | Corre un único step: gather, normalize, score, format. | phases/ (8 files) |
| Adapter | Habla con un external search o media provider. | tools/adapters/ (6 files) |
| Schema | Modelos Pydantic para cada artifact y result. | schemas/ (8 files) |
| Artifact | Lee/escribe typed artifacts al run directory. | orchestrator/artifact_store.py |
| Report | Agrega los artifacts en un markdown report. | phases/f6_report.py |
| Notifier | Le 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:
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: sisucceeded_partiales 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
RunStatedelStateStore. - Resuelve qué phases están ready para correr.
- Lanza las
sequentialphases una a una, lasparallelcon unThreadPoolExecutor. - Captura cada
PhaseResulty lo escribe de vuelta al state. - Re-lanza
PhaseExecutionErrorcuando 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_initpara crear el run directory y escribirtask.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 ID | File | Purpose | Produces |
|---|---|---|---|
F0 | phases/f0_init.py | Parsea la raw query en un Task y ParsedQuery. Escribe task.json y el run directory. | task.json |
F1 | phases/f1_candidates.py | Corre candidate gathering con los configured adapters. Deduplica y normaliza. | candidates.json |
F2 | phases/f2_reviews.py | Trae reviews para cada candidate desde las configured sources. | reviews.json |
F4 | phases/f4_price.py | Trae pricing data y calcula historical context. | prices.json |
P3 | phases/p3_perplexity_synthesis.py | Sintetiza narrative summaries para los top candidates usando el Perplexity adapter. | synthesis.json (optional) |
Y1 | phases/y1_youtube.py | Busca YouTube para content relevante per candidate. | youtube_search.json |
Y2 | phases/y2_extract.py | Extrae transcripts de los YouTube results. | transcripts.json |
F6 | phases/f6_report.py | Agrega 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:
| 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 | cada phase que produce URLs |
reviews.py | Review, ReviewSummary | F2 |
prices.py | PriceSnapshot, PriceSeries | F4 |
transcripts.py | Transcript, TranscriptChunk | Y2 |
phase_result.py | PhaseResult, PhaseStatus | cada phase |
PhaseStatus es el único enum y tiene 9 values:
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/.
| 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 (en tools/) | P3 | token 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:
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/.
| Tool | Purpose | Status |
|---|---|---|
tool-a-research-init | Inicializa una research run, escribe task.json, maneja el checkpoint state. | Implemented |
tool-b-hotel-research | Hotel research end-to-end: gather, normalize, rank, write. | Implemented |
tool-c-review-research | Trae y rankea reviews para una lista de candidates. | Implemented |
tool-d-photo-research | Descarga y organiza fotos para los candidates. | Implemented |
tool-e-price-analysis | Compara prices entre providers y descuento datos históricos. | Implemented |
tool-f-video-scrape | Scrapea y extrae video metadata. | Implemented |
tool-g-email-sender | Envía un email templated (report delivery, alerts). | Implemented |
tool-h-report-generator | Formatea 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
Devuelve el path al final report.md.
run_research.py — Phase-by-phase runner
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.
| Variable | Purpose | Default |
|---|---|---|
SCOUTE_RESEARCH_ROOT | Dónde se crean los run directories. | /tmp/scoute/research |
SCOUTE_RUNS_DIR | Dónde las missions escriben artifacts. | /tmp/scoute/runs |
SCOUTE_LOG_LEVEL | Logging level. | INFO |
SCOUTE_PARALLEL_WORKERS | Max concurrent phases en un parallel group. | 3 |
SCOUTE_DEFAULT_RETRY | Default retry count para phases que no hacen override. | 2 |
SCOUTE_TOKEN_POOL_PATH | Path 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:
| Status | Meaning | Action |
|---|---|---|
pending | La phase no ha empezado aún. | Wait for the orchestrator. |
running | La phase se está ejecutando. | Wait. |
succeeded | La phase terminó y produjo todos los expected outputs. | Mark downstream phases ready. |
succeeded_partial | La phase terminó pero faltan algunos outputs. | Mark downstream ready; flag in the report. |
failed_retryable | La phase falló pero se puede reintentar. | Orchestrator retries. |
failed_terminal | La phase falló y no se puede reintentar. | Orchestrator stops the mission; notify the user. |
skipped | La phase fue intencionalmente no ejecutada. | Mark downstream as blocked_missing_input. |
blocked_missing_input | La phase no pudo empezar porque falta un input. | Skip; mark downstream. |
stale | El 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:
- New adapter. Implementar el contract
searchenscout/tools/adapters/y registrarlo en la adapter factory detools/research_cli.py. - New phase. Implementar una función
run(mission_id, runs_dir)enscout/phases/, exportarla dephases/__init__.py, y añadir unPhaseConfigendag.py. - New domain tool. Crear un nuevo package bajo
docs/orchestrator/tools/con la standard structuresrc/,tests/,skills/,docs/.
Ver Tool Spec y Tool Structure para los templates.