How the multi-agent pipeline is wired: which skill runs in which phase, on which
model, with which tools and context. Everything below is verified against the
code — agents/orchestrator.py, agents/config.json, agents/runner.py, the
agents/skills/*.md files, and the route call sites.
The orchestrator runs agents in a strict phased pipeline; each phase's output becomes context for the next.
The Planner (Sonnet 5) runs first for free-form requests. A fast regex
pre-check and the planner can each reject a request ("mode": "rejected");
otherwise the planner emits a JSON plan — mode, targets, and a per-agent
{activate, task} map. The orchestrator itself hardcodes no activation matrix:
an agent runs only when its plan activate flag is set. The planner skill
(agents/skills/planner.md) is where the per-mode activation rules live.
The planner is skipped only for pre-baked requests: catalog / periodic
entries in requests.json ship a full plan (_PLAN_MAP in
orchestrator.py). See Pre-baked modes.
The mode splits the pipeline into two shapes that differ in their Phase-2 analysts and which backdrops are injected.
equity_deep_dive, swing_tradeThe request names specific stock(s), so targets is non-empty. A background
thread builds a fresh per-symbol snapshot for the first 2 targets (overlaps
Phase 1, joined before Phase 2). Company news rides inside that snapshot, so no
separate Market News block is built.
%%{init: {'flowchart': {'wrappingWidth': 420, 'nodeSpacing': 45, 'rankSpacing': 45}, 'themeVariables': {'fontSize': '12px'}}}%%
flowchart TD
U["<b>User Request</b><div style='text-align:left'><u>In:</u> —<br/><u>Does:</u> user types a ticker request<br/><u>Out:</u> raw text ("Analyse AAPL" · "swing trade NVDA")</div>"]
P["<b>Planner · Sonnet 5</b><div style='text-align:left'><u>In:</u> raw request<br/><u>Does:</u> classify the mode and extract the named tickers<br/><u>Out:</u> plan · mode = equity_deep_dive | swing_trade · targets = [AAPL, …] (non-empty)</div>"]
G["<b>Phase 1 — Gather (Haiku, parallel)</b><div style='text-align:left'><u>In:</u> planner task strings<br/><u>Does:</u> market_data · geopolitical_risk call external APIs (conditional — usually skipped here; see activation table)<br/><u>Out:</u> compressed data summaries</div>"]
S["<b>Symbol-snapshot builder (background thread)</b><div style='text-align:left'><u>In:</u> first 2 target tickers<br/><u>Does:</u> fetch quote · fundamentals + DCF + statements · technicals · analyst ratings · short/liquidity · whales · company news (overlaps Phase 1, joined before Phase 2)<br/><u>Out:</u> snapshots/dashboard_<sym>.md per symbol</div>"]
B["<b>Backdrops → every Phase 2/3/4 agent</b><div style='text-align:left'><u>In:</u> Market Snapshot (global macro) + Symbol Snapshots<br/><u>Does:</u> assemble context blocks (no Market News — company news is in the symbol snapshot)<br/><u>Out:</u> context injected into every downstream agent</div>"]
A2["<b>Phase 2 — Sequential Analysis</b><div style='text-align:left'><u>In:</u> all prior output + backdrops<br/><u>Does:</u> analysts run in order, each seeing prior output — equity_deep_dive: equity_analyst → swing_trading_analyst → scenario · swing_trade: swing_trading_analyst<br/><u>Out:</u> analysis summaries</div>"]
C["<b>Phase 3 — Review</b><div style='text-align:left'><u>In:</u> Phase-2 outputs + both snapshots<br/><u>Does:</u> critic (Opus 5) stress-tests the analysis<br/><u>Out:</u> critique for the writer</div>"]
W["<b>Phase 4 — Report</b><div style='text-align:left'><u>In:</u> all P1–P3 summaries + both snapshots<br/><u>Does:</u> report_writer synthesises into one narrative<br/><u>Out:</u> final markdown report</div>"]
R["<b>Output</b><div style='text-align:left'><u>In:</u> finished report<br/><u>Does:</u> persist to disk<br/><u>Out:</u> reports/{tenant_id}/*.md (+ .meta.json, .log)</div>"]
U --> P
P --> G
P -.-> S
G --> B
S -.-> B
B --> A2 --> C --> W --> R
market_brief, macro_outlook, news_digest, data_pullThe request is market-wide, so targets = [] and no per-symbol snapshots are
built. For market_brief / macro_outlook / news_digest a background thread
aggregates a market-wide Market News block (top-25 deduped headlines).
data_pull skips both the news build and the critic; its report is written
straight from the Phase-1 data table.
%%{init: {'flowchart': {'wrappingWidth': 420, 'nodeSpacing': 45, 'rankSpacing': 45}, 'themeVariables': {'fontSize': '12px'}}}%%
flowchart TD
U["<b>User Request</b><div style='text-align:left'><u>In:</u> —<br/><u>Does:</u> user asks a market-wide question (no ticker)<br/><u>Out:</u> raw text ("market brief" · "macro outlook" · "what's happening" · data pull)</div>"]
P["<b>Planner · Sonnet 5</b><div style='text-align:left'><u>In:</u> raw request<br/><u>Does:</u> pick a ticker-less mode<br/><u>Out:</u> plan · mode = market_brief | macro_outlook | news_digest | data_pull · targets = [] (empty)</div>"]
G["<b>Phase 1 — Gather (Haiku, parallel)</b><div style='text-align:left'><u>In:</u> planner task strings<br/><u>Does:</u> market_data (brief / data_pull; skipped in macro_outlook / news_digest) · geopolitical_risk (conditional)<br/><u>Out:</u> compressed data summaries</div>"]
N["<b>Market-News builder (background thread)</b><div style='text-align:left'><u>In:</u> — (SKIPPED for data_pull)<br/><u>Does:</u> _build_market_news() aggregates Benzinga live + Polygon + FMP + NewsAPI, deduped (overlaps Phase 1)<br/><u>Out:</u> top 25 market-wide headlines</div>"]
B["<b>Backdrops → every Phase 2/3/4 agent</b><div style='text-align:left'><u>In:</u> Market Snapshot (global macro) + Market News<br/><u>Does:</u> assemble context blocks (no Symbol Snapshots — no named targets)<br/><u>Out:</u> context injected into every downstream agent</div>"]
A2["<b>Phase 2 — Analysis</b><div style='text-align:left'><u>In:</u> backdrops<br/><u>Does:</u> equity_analyst / swing_trading_analyst / scenario — <b>ALL SKIPPED in these modes</b><br/><u>Out:</u> —</div>"]
C["<b>Phase 3 — Review</b><div style='text-align:left'><u>In:</u> Phase-1 output + Market Snapshot + Market News<br/><u>Does:</u> critic reviews (skipped in data_pull)<br/><u>Out:</u> critique for the writer</div>"]
W["<b>Phase 4 — Report</b><div style='text-align:left'><u>In:</u> everything + Market Snapshot + Market News<br/><u>Does:</u> report_writer composes the narrative<br/><u>Out:</u> final markdown report</div>"]
R["<b>Output</b><div style='text-align:left'><u>In:</u> finished report<br/><u>Does:</u> persist to disk<br/><u>Out:</u> reports/{tenant_id}/*.md (+ .meta.json, .log)</div>"]
U --> P
P --> G
P -.-> N
G --> B
N -.-> B
B --> A2 --> C --> W --> R
Both flows share Phase 3 (critic, except
data_pull) and Phase 4 (the singlereport_writer, run in every mode). An optional Phase 5 translation pass runs whenever a tenant has active translation languages, independent of mode.
Every skill in agents/skills/, the view it powers, its runtime LLM, and the
context fed into it. This spans the report pipeline plus the Trades, Dashboard,
and Today features (which call AgentRunner directly from their routes).
Screener / Order Cockpit are not AI-agent-driven.
| Skill | Phase / caller | View | LLM | Input provided |
|---|---|---|---|---|
planner |
Phase 0 | Reports | Sonnet 5.5 | user request only |
market_data |
Phase 1 | Reports | Haiku 5.5 | planner task string |
geopolitical_risk |
Phase 1 | Reports | Haiku 5.5 | planner task string |
equity_analyst |
Phase 2 | Reports | Sonnet 5.5 | Phase-1 summaries + Market Snapshot + Symbol Snapshot |
swing_trading_analyst |
Phase 2 / Trades propose | Reports + Trades | Sonnet 5 (reports) / Opus 5 (trades) | Two-sided momentum funnel (hard gates + 8-point scored checklist, score-derived probability). Reports: as equity_analyst. Trades: Market Snapshot + runtime prompt (allowed universe + held-ticker exclusion); per-symbol data on demand via get_symbol_snapshot |
scenario |
Phase 2 | Reports | Sonnet 5.5 | Phase-1 summaries + prior Phase-2 output + both snapshots |
critic |
Phase 3 | Reports | Opus 5.5 | Phase-2 outputs + both snapshots |
report_writer |
Phase 4 (all modes) | Reports | Sonnet 5.5 | all P1–P3 summaries + Market Snapshot + Market News + (equity/swing) both snapshots |
order_brief |
routes/trades.py condenser |
Trades | Haiku 5.5 | finalized swing_trading_analyst output |
news_summary |
routes/news.py + dashboard_interpret.py |
News / Dashboard | Haiku 5.5 | deduped multi-source headline list |
dashboard_brief |
routes/dashboard_interpret.py |
Dashboard | Haiku 5.5 | live section/card JSON |
dashboard_brief_symbol |
routes/dashboard_interpret.py |
Dashboard | Sonnet 5.5 | per-symbol snapshot JSON |
today_brief |
routes/today.py + scheduler |
Today | Haiku 5.5 | global dashboard JSON |
today_macro |
routes/today.py + scheduler |
Today | Haiku 5.5 | global dashboard JSON |
Conditionally active: the "Market Morning Briefing" pre-baked requests.json plan
activates report_writer with a fixed 7-section VIP task string (the planner never
picks a distinct VIP writer). swing_trading_analyst is the sole swing analyst skill,
used for both reports and the Trades propose flow regardless of a tenant's analyst
setting (that setting is retained only as proposition metadata).
For free-form requests the planner emits one mode and sets each agent's
activate. Rules are in agents/skills/planner.md (Mode Selection Rules +
Guidelines).
| Agent | equity_deep_dive | swing_trade | market_brief | macro_outlook | news_digest | data_pull |
|---|---|---|---|---|---|---|
| market_data | · | Yes | Yes | — | — | Yes |
| geopolitical_risk | · | · | · | · | — | — |
| equity_analyst | Yes | — | — | — | — | — |
| swing_trading_analyst | Yes | Yes | — | — | — | — |
| scenario | Yes | — | — | — | — | — |
| critic | Yes | Yes | Yes | Yes | Yes | — |
| report_writer | Yes | Yes | Yes | Yes | Yes | Yes |
· = conditional; the planner activates it only for a request-specific need the
backdrops can't cover:
equity_deep_dive — only when the request needs
cross-ticker discovery (screener / movers / 13F / peer universe); each named
ticker already gets a Symbol Snapshot.planner.md). Off for routine single-stock requests; explicitly skipped in
news_digest and data_pull (the snapshot's Geopolitics section covers the
baseline).Exactly one report writer runs per mode; critic runs in every mode except
data_pull. Always-injected backdrops are not agents — see Context backdrops.
The model: in a skill's frontmatter is informational. The authoritative
per-agent model is AGENT_MODEL in agents/config.json (loaded by
orchestrator.py); anything unlisted inherits the orchestrator default
(claude-sonnet-5-5).
agents/config.json no longer holds model IDs — it maps each agent onto a
role (LLM_REPORT_DATA_MODEL, LLM_REPORT_ANALYSIS_MODEL,
LLM_REPORT_CRITIC_MODEL, LLM_REPORT_GENERATION_MODEL). llm_config.py
turns a role into an ID: .env first, then llm_config.DEFAULTS, following
one more hop if the value is itself a variable name
(LLM_REPORT_CRITIC_MODEL=LLM_CLAUDE_OPUS_MODEL=claude-opus-5-5). Non-report
call sites ask for their own roles the same way — TRADE_ANALYZER,
TRADE_BRIEF, TRANSLATOR, NEWS, DASHBOARD_BRIEF, TODAY. The table
below is the shipped default; .env can move any row without a code change,
but a new ID also needs a price in subscriptions.json.
| Phase / agent | Model | Max tokens |
|---|---|---|
| Phase 0 — planner | Sonnet 5.5 (default) | 8192 |
| Phase 1 — market_data | Haiku 5.5 | 12288 |
| Phase 1 — geopolitical_risk | Haiku 5.5 | 8192 |
| Phase 2 — equity_analyst, swing_trading_analyst | Sonnet 5.5 | 8192 |
| Phase 2 — scenario | Sonnet 5.5 (default) | 8192 |
| Phase 3 — critic | Opus 5.5 | 8192 |
| Phase 4 — report_writer | Sonnet 5.5 | 16384 |
Model IDs: Haiku 5.5 = claude-haiku-5-5, Sonnet 5.5 =
claude-sonnet-5-5, Opus 5.5 = claude-opus-5-5.
market_data and geopolitical_risk:
their skill files say claude-sonnet-5-5, but AGENT_MODEL runs them on
Haiku 5.5.planner and scenario are absent from AGENT_MODEL
and inherit the default (Sonnet 5).Route-invoked callers set the model at the call site:
| Caller | Skill | Model | Max tokens |
|---|---|---|---|
routes/trades.py (propose) |
swing_trading_analyst |
Opus 5 | 16384 |
routes/trades.py (propose) |
critic |
Sonnet 5.5 | 16384 |
routes/trades.py (condenser) |
order_brief |
Haiku 5.5 | 512 |
routes/news.py + dashboard_interpret.py |
news_summary |
Haiku 5.5 | 512 |
routes/dashboard_interpret.py |
dashboard_brief |
Haiku 5.5 | 400–1024 |
routes/dashboard_interpret.py |
dashboard_brief_symbol |
Sonnet 5.5 | 1024 |
routes/today.py + scheduler |
today_brief |
Haiku 5.5 | 2048 |
routes/today.py + scheduler |
today_macro |
Haiku 5.5 | 2048 |
The Trades propose flow is a 4-agent pipeline (not a single analyst call) —
see Trade suggestion workflow. There is only one swing analyst skill file
(swing_trading_analyst.md); the _b / _c variants no longer exist. A
tenant's analyst setting (a / b / c) is retained only as proposition
metadata and does not select a different skill.
Injected into agents regardless of mode — data blocks, not agents. Assembled in
_build_context and prepended before per-agent outputs.
snapshots/snapshot_market.json by _load_market_snapshot() on the
snapshot-market-1.0 contract, served compact and capped at 30k chars, with the
eight classifier lines at interpretation.lines, injected as
## Market Snapshot (macro backdrop) into
every Phase 2/3/4 agent (and the Trades propose run). Carries rates
(incl. the 6-bucket rate-conditions stance, per-yield session moves, and the
2Y·10Y yield regime with its one-day overlay),
credit, the composite liquidity regime, margin debt, sentiment, sectors,
forex, geopolitics, econ calendar.equity_deep_dive / swing_trade with targets, a
fresh snapshots/dashboard_<sym>.md is built for the first 2 targets
(MAX_SYMBOL_SNAPSHOTS = 2) via _build_symbol_snapshots, read back by
_load_symbol_snapshot (each JSON block capped at 1k, whole block at 18k),
injected as # Symbol Snapshots. Covers quote, fundamentals + DCF +
statements, technicals, analyst ratings, short/liquidity, whales/insiders,
multi-source company news.data_pull, _build_market_news()
calls _aggregate_symbol_news("", limit=25) (empty symbol = market-wide) for
the top-25 deduped headlines (Benzinga live + Polygon + FMP + NewsAPI),
injected as # Market News (multi-source, last 24h). Symbol modes carry news
inside the Symbol Snapshot, so no separate block is built.The authoritative mapping for the report pipeline is agent_tools in
agents/config.json (exposed as AGENT_TOOLS). An entry is either module
(all of that module's tools) or module:tool1,tool2 (a named subset).
| Agent | Tool modules (subset after :) |
|---|---|
| market_data | fmp, polygon, finra_short, sec, openfigi, finviz:screener,latest_filings,quote_history,group_performance, 13f, uw, alpaca_mktdata:get_market_movers,get_most_active,get_stock_snapshots |
| geopolitical_risk | newsapi_ai, newsdata, benzinga:benzinga_news, gdelt:search_gdelt_news,get_gdelt_geo_news, web_search |
| equity_analyst | fmp:get_batch_quotes,get_key_metrics,get_financial_statements |
| swing_trading_analyst | fmp:get_technical_indicator, uw:ext_get_oi_changes,ext_get_options_screener |
| planner, scenario, critic, report_writer | none (pure reasoning / writing) |
Route-invoked agents are not in config.json — tools are passed at the call
site. swing_trading_analyst gets a larger toolset in the Trades propose flow
than in its Phase-2 report run:
| Agent | Caller | Tool modules |
|---|---|---|
swing_trading_analyst (+ _finalize) |
routes/trades.py (propose) |
fmp, fred, finra_short, newsapi_ai, symbol_snapshot, newsdata:search_newsdata, uw:ext_get_options_flow,ext_get_oi_changes,ext_get_options_screener |
critic |
routes/trades.py (propose) |
none |
order_brief |
routes/trades.py |
none |
news_summary |
routes/news.py, routes/dashboard_interpret.py |
none |
dashboard_brief, dashboard_brief_symbol |
routes/dashboard_interpret.py |
none |
today_brief, today_macro |
routes/today.py |
none |
Short names resolve to modules via TOOL_MODULES in agents/runner.py:
fmp fred imf newsapi newsapi_ai gdelt newsdata sec openfigi
macro_liquidity finviz 13f uw economic_calendar iborrowdesk polygon
finra_short finra_margin eodhd sec_ftd benzinga alpaca_mktdata
symbol_snapshot
tools.json enables/disables modules without code changes. newsapi (legacy
NewsAPI.org) is superseded by newsapi_ai; gdelt + newsdata serve as news
fallbacks.uw → tools/data_ingestion_external_data.py — the live Unusual Whales
API (phx.unusualwhales.com, session-token auth via tools/uw_session.py):
options flow, OI changes, dark pool, stock/options screeners. Returns empty
gracefully when unavailable.alpaca_mktdata → Alpaca Market Data REST: get_market_movers,
get_most_active, get_stock_snapshots, get_alpaca_news. Env-var keys (not
per-tenant), so usable directly by agents.macro_liquidity and finra_margin are consumed by the Dashboard snapshot
builder (routes/dashboard_global.py), not bound to any report agent.symbol_snapshot exposes get_symbol_snapshot for on-demand per-ticker data
(used by the Trades propose flow).web_search is an Anthropic server-side tool (SERVER_TOOLS
in runner.py, max 3 uses), not a local module — wired into
geopolitical_risk.Phase 4 runs the single report_writer in every mode. It receives the plan's
mode and targets in its task. Fallback: if report_writer is not activated,
or the writer call raises, the orchestrator concatenates the raw Phase-1/2/3
outputs (\n\n---\n\n) as the report.
The Trades view proposes 1–3 day swing trades executed on Alpaca. Unlike the
report pipeline (driven by the orchestrator + planner), the propose flow lives in
routes/trades.py::generate_proposition and wires the agents by hand — but it
reuses the report pipeline's skills, models, tools, and context helpers.
generate_proposition(tenant_id) runs discovery-first (no planner, no named
target) and ends in a structured proposition rather than a saved report. Each
phase sees the prior phases as compressed context, plus the Market Snapshot
backdrop throughout and the discovered ticker's Symbol Snapshot from Phase 2 on.
Trimmed on 2026-08-19 for cost, not correctness: equity_analyst and scenario
ran as phases 2 and 3 (~$4.4/day) for output the finalize pass barely used, and
the critic ran on Opus (~$3/day). A 1–3 day entry needs a level, a stop, a target
and a catalyst — reports keep both agents and keep Opus for the critic.
| # | Agent | Model | Role |
|---|---|---|---|
| 1 | swing_trading_analyst |
Opus 5 | Discovery — scans the allowed universe, picks ONE ticker, emits an initial Final Trade Summary. Calls get_symbol_snapshot per candidate. |
| 2 | critic |
Sonnet 5.5 | Stress-tests the trade — attacks the load-bearing claim, checks entry/stop/target and the ≥2:1 reward:risk. |
| 3 | swing_trading_analyst (_finalize) |
Sonnet 5.5 | Confirms / adjusts / withdraws the SAME ticker per the critique. Outputs the final plain + JSON Final Trade Summary. |
| 4 | order_brief |
Haiku 5.5 | Condenses the finalized trade into 5 plain-English thesis bullets. |
The result is saved to the propositions table (.data/trades.db) and spend is
recorded via record_feature_usage(..., "trade", ...).
Guards (the analyst can disobey the prompt, so each is enforced in code):
- Held-ticker exclusion — trade_db.get_active_symbols(tenant_id, mode) feeds
both a prompt exclusion clause and hard drops at Phase 1 and Phase 3.
- Watchlist universe — if the tenant set watchlist_filter, the candidate
universe is restricted to those symbols (minus held); empty → "no offer" note.
- Ticker-switch backstop — if the finalize pass switches away from the
reviewed ticker, the proposition is discarded (only the vetted ticker ships).
- Critic gate — a finalized trade with no ticker/entry is treated as withdrawn.
- Cancellation token — _generating[tenant_id] holds a per-generation token;
the cancel endpoint or a newer run overwrites it. A stale token means the result
is discarded before save/auto-submit (the thread can't be killed, but its side
effects are withheld).
Two independent tenant settings gate the two halves — generate and submit:
| Setting | Controls | Off (manual) | On (automatic) |
|---|---|---|---|
auto_pilot |
proposition generation | user clicks Suggest Order to generate on demand | the scheduler loop is the only generator; the manual endpoint returns 409 |
auto_order |
order submission | user clicks Buy/Sell to place the bracket order | a freshly-generated proposition is auto-submitted to Alpaca |
analyst (a/b/c) and budget / per-account order_limit are also tenant
settings; analyst is metadata only (single skill).
Manual generation — POST /api/trades/propose (trades_propose, VIP-gated)
spawns generate_proposition in a background thread regardless of proposition
age; returns 202 {generating:true}. Manual mode only: with auto_pilot on it
returns 409 — the backend owns the rhythm and a manual run on top of it is an extra
Sonnet pass plus a proposition racing the loop's. The frontend disables the button in
that state anyway (btnDisabled = generating || _tradesAuto). POST /api/trades/propose/cancel pops the
tenant from _generating so the UI stops spinning and the in-flight result is
discarded on completion.
Automatic generation — exactly one trigger, auto-pilot-only:
Scheduler Thread 4 (scheduler._trade_propose_loop) — every 20 min, aligned to
:00/:20/:40 UTC, only during NYSE regular hours (Mon–Fri 09:30–16:00 ET, via Alpaca's
clock). For each VIP tenant with auto_pilot on and below the position cap, if the
latest proposition is missing / submitted / expired / older than the interval, it
generates and then notifies (SMS + email via _notify_proposition).
Loading the trades page starts nothing. It once mirrored the same check as a catch-up
for a loop that slept boot-relative and could drift; the loop now wakes on the wall
clock with _last_propose_at starting at 0, so a restart catches up by itself.
Submission — submit_proposition(tenant_id, proposition, budget, mode) places
a bracket limit order on Alpaca (entry + stop + target), sizes quantity from
the account's order_limit/budget, enforces the per-account position cap
(_position_cap: paper 20 / live 10), and creates the positions row. It's
called from the manual Buy/Sell endpoint (source="manual") and, when
auto_order is on, from _auto_submit_if_enabled after every generation path
(the scheduler loop and manual Suggest Order) — so auto-submit never depends on
which path created the proposition. Auto-submit only runs during regular trading
hours, requires the mode's Alpaca keys to be configured, and skips tickers already
held (skip_if_held=True).
Source of truth: Alpaca is authoritative for open positions; the DB adds context (stop/target/proposition/opened_at). Positions Alpaca holds with no DB record are auto-registered on the next trades load (
get_bracket_contextreconstructs stop/target/opened_at from Alpaca orders). See CLAUDE.md → Trades.
Catalog / periodic requests in requests.json (≈22 entries) ship a full plan
(planner skipped). They all use the generic planner modes — market_brief,
macro_outlook, swing_trade, data_pull — so the orchestrator's mode
branches behave exactly as for a planner-produced plan. Many performance /
screen entries run as data_pull (no critic).
Where a request needs a bespoke layout, that layout lives in the request's
report_writer task string, not in a custom mode: report_writer.md gives an
explicit task structure precedence over the mode's template. The "Market
Morning Briefing" (7-section VIP format, trade ideas with anchors) works this
way under market_brief.
Each agent is a markdown skill (agents/skills/*.md) loaded as the system prompt
by AgentRunner. All pipeline agents end their output with a <summary> block
(1000–2000 chars) passed as compressed context downstream; the writers and the
route-only XML skills do not. Below, LLM = runtime model (see Runtime models).
Orchestration brain. No tools. Validates the request (rejects off-topic /
inappropriate / PII / prompt-injection with "mode": "rejected") then emits a
single JSON plan: mode, targets, and a per-agent {activate, task} map. No
prose output. Vague requests default to equity_deep_dive (tickers),
market_brief (sectors), or macro_outlook (economy).
Data-fetch specialist, minimal interpretation. Tools: FMP (quotes, prices,
company, analyst, insider, filings, technicals, market-wide, research bundle),
SEC EDGAR, OpenFIGI, FINRA short, SEC FTD, plus the config subsets above. Budget
≤ 15 tool calls; prefers batch tools. Output: structured tables (Quote Summary,
Financial Summary, Key Metrics, Analyst Consensus, Technical Levels, Sector
Landscape) + <summary>.
Geopolitical risk analyst. Tools: NewsAPI.ai, NewsData, GDELT, Benzinga,
web_search (≤ 3 uses); budget ≤ 8 tool calls. Output: Geopolitical Risk
Register table (probability × impact × timeframe × affected assets), per-risk
detail, sentiment, central-bank watch + <summary>.
Senior equity research analyst. Leans on the injected Symbol Snapshot and fetches
only missing data (config subset: batch quotes, key metrics, statements). Output:
Business Quality, Competitive Position, Financial Analysis, Earnings, Comparable
Company table, Valuation (DCF + relative → blended fair value), Investment Thesis
(recommendation, fair-value range, conviction), Catalysts, Risks + <summary>.
Institutional swing analyst for 1–3 day trades in single US equities/ETFs
(executed on Alpaca). Reads per-ticker data from the Symbol Snapshot; fetches
SMA-20 + OI changes. 9-step top-down process (macro → sector → industry → stock →
execution). Output: a plain-text trade card plus a machine-readable JSON
block (the "Final Trade Summary") parsed by the order system, then <summary>.
Single skill file — the former _b / _c variants are gone; the analyst
setting (a/b/c) survives only as proposition metadata. Reused twice in the
Trades propose pipeline (discovery pass + _finalize pass) — see Trade
suggestion workflow.
Scenario analyst. No tools. Output: Scenario Summary table (bull/base/bear —
probability, target, return, driver), per-case assumptions/triggers/signposts,
Risk/Reward, Investment Thesis Scorecard, Catalyst Calendar + <summary>.
Probabilities sum to 100%; base case most probable (40–60%).
Adversarial reviewer — the last check before a user risks money. No tools;
consumes prior outputs + both snapshots. Output: Verdict (lead), Assumption
Challenges table, Data Quality Concerns, Analytical Gaps, Counter-Arguments, Bias
Check, Confidence Assessment table, Recommendations + <summary>.
The single writer for every mode. No tools. ADHD-friendly, plain-English style
with a jargon-ban table, emoji trend/signal indicators, and per-mode templates
and word targets. Ends with the required HTML disclaimer/copyright footer. No
<summary>. Also serves pre-baked plans whose structure comes entirely from the
request's task string (e.g. "Market Morning Briefing", 7-section VIP layout) —
an explicit task structure overrides the mode's template and word target.
<bullets>). No tools.news_summary in tool_cache.<bullets> (3 items, last is **Bottom line:**) + a 10-sentence <paragraph>.
No frontmatter model: (set by the route). No tools.<bias> +
<thesis> + bullet/detail pairs citing concrete numbers. No frontmatter at
all (name/model come from the route). No tools.<items>). No model:
frontmatter. No tools.<items> contract and the same
parser as today_brief, but a fixed 8 items, one per macro driver (rates, Fed,
inflation, growth, jobs, credit, dollar/commodities, event risk), each ending in
a supportive/neutral/cautionary read for today's trades. No tools.macro (old Phase 1), macro_liquidity_analyst (old
Phase 2), and news no longer exist as skills or run — the Market Snapshot
backdrop supplies macro/liquidity, and news is injected via the Symbol Snapshot
(symbol modes) or Market News block (tickerless modes).routes/module{2,3,4,6,7,8}.py) are deterministic
(no-LLM-in-the-scoring-path) macro/headline engines implementing the
documentation/Module*.md specs.
The one NLP step in each is quarantined into a single upstream extraction call
driven by agents/skills/module{N}_extract.md (Haiku); everything downstream is
pure arithmetic. These are not part of the report or trades pipelines. (The old
MASTER_MACRO_ENVIRONMENT_ENGINE_PART_ONE.md orphan spec is gone.)