Institution Mode: operator reference
Analyst recovery contract and canonical-asset preflight for the multi-agent investment committee
minara_institution_analyze runs either Minara's default committee
workflow or the session-owned Roundtable configured in the Web UI.
This page documents lower-level recovery and asset-resolution behavior
used by the default analyst path. For phase composition, Agent settings,
templates, and immutable run snapshots, see
Institution Mode.
The default workflow contains parallel analysts, bull/bear debate, research synthesis, trade planning, risk debate, and a final portfolio decision. A custom Roundtable can replace that arrangement while keeping the same read-only safety boundary.
For pipeline timeouts and benchmark configuration see Environment variables: Institution Mode.
Roundtable state and snapshots
The Builder stores an active pipeline per Institution session. Saving uses an expected revision, so a second browser window receives a conflict rather than silently overwriting newer work. When a session has no saved pipeline, the gateway uses its latest run snapshot and then the built-in default.
Agent, phase, and Roundtable templates are stored separately. Built-in templates are immutable. Starting a run copies the complete pipeline into an immutable snapshot with phase results, Agent usage, final status, and report files. Relevant endpoints include:
- Get a session pipeline
- Save a session pipeline
- List session runs
- Get a run snapshot
- List Agent templates
- List Roundtable templates
- List eligible Institution skills
Analyst recovery: synthesis-and-parse
Each Phase-1 analyst slot runs the model in a free tool-calling loop
(submit_analyst_report is always exposed alongside the role's data
tools). When the main loop submits a real, non-stub report, the slot
attaches the captured tool_outputs[] and returns it.
When the main loop doesn't submit (turn cap hit, model wrote prose instead) or submits empty / placeholder args, the orchestrator runs one synthesis turn:
-
No tools, no
tool_choice. Thinking is enabled: this is the call that previously failed undertool_choice: { type: "tool" }because Anthropic rejects that combination, leaving no reasoning space for the model to populate the args. -
The user message asks for a strict three-section prose format:
HEADLINE: <one-sentence conclusion> KEY FINDINGS: - <finding 1, citing tool name + concrete number> - <finding 2, ...> CONFIDENCE: <number 0.0 to 1.0>
The orchestrator parses this prose directly into an AnalystReport
via parseSynthesisProseToReport. The parser is tolerant of
mixed bullet styles, markdown emphasis around section markers, and
out-of-range confidence values (clamps to [0, 1]).
When the synthesis prose is empty, malformed, or every bullet is
placeholder-shaped ("tried X: ok"), the orchestrator falls back to
buildSubagentSummaryReport: the universal "always emit something
usable" builder. Two branches:
| Branch | Trigger | Headline | Confidence |
|---|---|---|---|
| Some tools succeeded | toolsTried.some(t => t.status === "ok") | "<ticker> (<role>): raw tool summary (model synthesis unavailable)" | 0.3 |
| All tools failed / no tools called | toolsTried.every(t => t.status !== "ok") or empty | "<ticker> (<role>): no data gathered this session" or "... no tools available this session" | 0.1 |
When some tools succeeded, the report cites the captured tool data
(one bullet per tool, derived from its preview) so downstream
phases see what came back. When everything failed, the report
enumerates what was tried and ends with the role's
domain-default reasoning string (one sentence per role, defined in
roles.ts adjacent to the role definitions).
Either way the slot's ok flag is true: the legacy "data-gap"
concept has been retired. Every Phase 1 slot now contributes a
usable summary to the main agent, so Phases 2-6 always run.
Tool outputs: captured returns surface
Each AnalystReport carries an optional tool_outputs[] populated
by the orchestrator from the slot's conversation. One entry per tool
call:
{
tool: string; // tool name
ok: boolean; // true when the tool returned successfully
preview: string; // success: truncated JSON / text (~1500 chars)
// failure: error message string
args_summary?: string; // one-line summary of the call input
error_code?: string; // structured failure category, when present
}Successful calls carry a pretty-printed JSON preview; failed calls
carry the failure reason inline so the operator sees WHY a call
didn't return data, not just that it didn't. The
PersonaOutputRenderer in the web-ui renders this as a "Tool
outputs" section appended to the structured-output popup with
click-to-expand rows.
Canonical-asset preflight
Before Phase 1 dispatches, if classifyAsset(ticker) === "unknown"
(or INSTITUTION_FORCE_RESOLVER_PREFLIGHT=true), the orchestrator
runs a server-side preflight:
- Check the SQLite cache (
canonical_asset_cachetable). - On cache miss or expiry: parallel CoinGecko / CMC / DexScreener
lookups, normalised to a chain+contract identity (CAIP-19-ish
discriminated union of
evm/native/solana/cosmos/polkadot/equity). Persist with per-outcome TTL. - Outcome routing:
resolved(unique chain+contract): enrichmeta.resolved_ticker, analysts see the canonical identity in their preamble.multi(multi-chain deployment): same, but the array of candidates surfaces in the disambiguation context.ambiguous(provider disagreement): centralized disambiguation event, NOT four parallel "which token did you mean?" prompts.none: abort before Phase 1, no LLM calls. This is the only abort kind the orchestrator still emits.
The canonical identity is chain+contract, not the CoinGecko id.
CoinGecko's internal id is a centralized index dependent on their
curation; using it as the bootstrap output would lock the agent
into one provider's view of the world. Downstream tools that
accept on-chain queries take CAIP-19 directly; provider-specific
tools (CoinGecko price chart) read their id from sources only
after the canonical identity is established.
Per-outcome TTL
The cache uses per-outcome TTLs because outcomes age differently:
| Outcome | Default TTL | Why |
|---|---|---|
resolved | 30 days | Stable chain+contract; rarely changes |
multi | 14 days | User disambiguation may pin a chain |
ambiguous | 7 days | Provider data may converge |
none | 1 day | Providers update daily; retry soon |
user_supplied | 365 days | Operator override; trust the human |
Set per-class TTL via CANONICAL_ASSET_CACHE_TTL_*_DAYS env vars
(see env-vars reference).
When the live aggregator fails (providers down, rate-limited),
CANONICAL_ASSET_CACHE_FALLBACK_TO_EXPIRED=true (default) returns
the stale cache entry tagged from_expired_fallback: true. Set
false if your deployment cannot tolerate any drift.
Adding a missing ticker
The agent learns ticker → canonical-identity mappings the first time it encounters a new ticker. No code deploy needed for new tokens. Two pathways:
- Live resolution: the next
/institution <ticker>call triggers preflight, caches the result, and from that point on all roles see the canonical identity. - Operator override: when CoinGecko / CMC / DexScreener don't
have the token (e.g. brand-new launch), an operator can pin the
canonical identity directly. UI: the abort banner has a
contract-address form. CLI:
minara assets pin <ticker> --chain <chain> --contract <0x...>(when the assets CLI is enabled in your deployment).
The methodology-store TICKER_TO_CLASS table still exists, but
serves a different purpose now (asset-class lookup for risk /
portfolio code). The canonical resolver writes through to it when
its asset_class field gives us new info, but TICKER_TO_CLASS
is no longer the source of truth for canonical identity.
Inspecting recovery in practice
The institution_role_outputs.data_gap column persists on the
schema for legacy rows produced before the data-gap concept was
retired. New writes always set it to 0. The metrics aggregator
InstitutionStore.getAnalystStubMetrics still reads it for
historical reporting, but the column is no longer load-bearing in
the live recovery path.
Per-run audit:
# All analyst rows for a run, including legacy retry_count + data_gap.
minara learning methodology cases --run-id <run_id>