External Providers
How the research framework talks to external services — adapter pattern, token pool, request journal, retry and failover.
Status
Implemented. All adapters are operational. The token pool is in production use for the Perplexity integration. The request journal is enabled for all paid adapters.
Purpose
The research framework delegates all external communication to adapters. The goal is:
- Keep the framework's core (orchestrator, phases, schemas) provider-agnostic.
- Make the cost, the rate limits, and the failure modes explicit.
- Provide a complete audit trail of every external call.
This page is the canonical reference for the adapter layer and the cross-cutting policies (token pool, request journal, retry, failover). The framework as a whole is in Framework. The audit and security models are in Audit Model and Security Principles.
Adapter Contract
Every adapter implements (or wraps) a small, stable interface. The minimal contract:
For specialized adapters (YouTube, Perplexity), the contract is extended but always returns objects from scout/schemas/sources.py (or a typed equivalent).
The Source schema:
| Field | Type | Description |
|---|---|---|
id | string | Stable source ID. |
title | string | Source title. |
url | string | Canonical source URL. |
publisher | string | The publisher or host. |
phase | string | The phase that produced this source. |
retrieved_at | datetime (UTC) | When the source was retrieved. |
Returning typed Source objects is the contract that makes the rest of the framework provider-agnostic.
Adapter Inventory
| Adapter | Provider | Used by | Auth required | Cost model |
|---|---|---|---|---|
tavily_adapter.py | Tavily | F1, F2 | API key | Per request |
duckduckgo_adapter.py | DuckDuckGo | F1 (fallback) | none | Free |
google_search_adapter.py | Google Custom Search | F1 (optional) | API key + cx | Per query (100/day free) |
youtube_transcript_api.py | YouTube Transcript | Y2 | none | Free |
yt_dlp_adapter.py | yt-dlp | Y2 (fallback) | none | Free |
perplexity_adapter.py | Perplexity | P3 | Token pool | Per token |
The Perplexity adapter is documented in detail below. The search adapters share the same retry and rate-limit policies; the only thing that differs is the auth and the cost model.
Token Pool
The Perplexity integration is the only one that uses a token pool. The pool exists because the lab has multiple Perplexity accounts (for capacity and redundancy) and because the integration was originally built around a sk-cp-... token plan that has per-account rate limits.
Pool Configuration
The pool is configured by token_pool_config.json (or token_pool_config.example.json for the template):
| Field | Description |
|---|---|
name | A human-friendly label for the token. |
value | The actual API key. |
weight | The relative weight in the round-robin (higher = more often). |
The strategy can be round-robin, weighted-round-robin, least-used, or failover. The default is weighted-round-robin.
Pool Runtime
The token pool is held in memory. On every request:
- The pool selects a token based on the strategy.
- The adapter makes the request with the selected token.
- The pool records the request and the response.
- If the request fails with a 401 or 403, the pool marks the token as
quota_exhaustedand selects the next one. The request is retried with the new token. - If all tokens are
quota_exhausted, the pool returns an error and the phase is marked asfailed_retryable.
The pool also records:
- The total requests per token.
- The total errors per token.
- The last error per token.
- The last successful request per token.
The pool's state is exposed as a JSON file under {framework-root}/runtime/token_pool_state.json. The Coordinator can be asked to show the current state.
Pool Security
The pool file is the most sensitive file in the framework. It is:
- Stored with permissions
0600(owner read/write only). - Excluded from any backup that is uploaded off-machine.
- Re-loaded on every process restart (the file is the source of truth, the in-memory state is a cache).
- Audited via the request journal (every request records which token was used, by name, never by value).
The value of a token is never written to any log, journal, or artifact. The token's name is what the journal records.
Request Journal
Every adapter call is recorded in a single append-only journal. The journal is {framework-root}/runtime/request_journal.jsonl. Each line is a JSON object:
The journal is append-only. Entries are never updated or deleted. Rotation is by file: when the journal exceeds a configured size (SCOUTE_JOURNAL_MAX_BYTES, default 50 MB), it is renamed to request_journal.<timestamp>.jsonl and a new file is started.
The journal is the foundation of the audit model. The Coordinator can be asked to:
- Show the last N requests for a given mission.
- Show the requests for a given token.
- Show the requests that failed.
- Replay a request (debug aid).
The journal does not store request bodies that might contain personal data. If an adapter accepts a free-form input, the journal stores a redacted version (e.g., query keywords but not the full text).
Retry Policy
All adapters share a default retry policy. The policy is applied by a shared retry helper that wraps the adapter call.
| Parameter | Default | Description |
|---|---|---|
max_attempts | 3 | Total attempts (1 original + 2 retries). |
initial_backoff | 1.0 s | Initial wait between attempts. |
backoff_factor | 2.0 | Multiplier for the wait on each subsequent attempt. |
max_backoff | 30.0 s | Cap on the backoff wait. |
retry_on_status | 429, 5xx | HTTP status codes that trigger a retry. |
retry_on_timeout | true | Whether to retry on connection / read timeouts. |
retry_on_socket | true | Whether to retry on socket-level errors. |
The retry helper uses exponential backoff with full jitter. It does not retry on 4xx errors other than 429.
Failover
Failover is the act of switching from one provider to another when the primary is unavailable. The framework implements failover at two levels.
Adapter-Level Failover
Each phase declares an ordered list of adapters. The phase calls the first adapter; if it returns failed_retryable after exhausting its retries, the phase calls the next adapter.
Example for F1:
If Tavily returns a 5xx, F1 retries Tavily twice, then falls back to DuckDuckGo, then to Google. If all three fail, the phase is failed_retryable.
Provider-Level Failover (Perplexity Only)
The token pool implements provider-level failover. When a Perplexity request fails because of a token issue (401, 403, 429 from a quota perspective), the pool switches to the next token. The request is retried transparently.
This is in addition to the regular HTTP retry. The full flow for a Perplexity request is:
- Pick a token from the pool.
- Make the request.
- If the response is 200, return.
- If the response is 429 or 5xx, retry with the same token (exponential backoff).
- If the retries are exhausted, mark the token as
quota_exhausted(for 429) orerror(for 5xx) and pick the next token. - If all tokens are
quota_exhausted, fail the phase.
Rate Limits
Each adapter has a per-minute request cap. The cap is enforced by a per-adapter token bucket:
| Adapter | Default cap (req/min) |
|---|---|
tavily | 60 |
duckduckgo | 30 |
google_search | 100 |
youtube_transcript | 60 |
yt_dlp | 20 |
perplexity | 20 |
The cap is configurable per deployment. The cap is enforced before the request, not after, so the framework does not waste requests on rate-limited providers.
Cost Tracking
For paid adapters (Tavily, Perplexity), the framework records the cost of every request in the journal. The cost is the product of the response's reported usage and the provider's published rate.
The journal's cost field is populated when the adapter returns usage information. The framework exposes a /api/cost-summary endpoint that aggregates costs by mission, by adapter, and by token.