MINARA

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:

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 under tool_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:

BranchTriggerHeadlineConfidence
Some tools succeededtoolsTried.some(t => t.status === "ok")"<ticker> (<role>): raw tool summary (model synthesis unavailable)"0.3
All tools failed / no tools calledtoolsTried.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:

  1. Check the SQLite cache (canonical_asset_cache table).
  2. 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.
  3. Outcome routing:
    • resolved (unique chain+contract): enrich meta.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:

OutcomeDefault TTLWhy
resolved30 daysStable chain+contract; rarely changes
multi14 daysUser disambiguation may pin a chain
ambiguous7 daysProvider data may converge
none1 dayProviders update daily; retry soon
user_supplied365 daysOperator 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:

  1. Live resolution: the next /institution <ticker> call triggers preflight, caches the result, and from that point on all roles see the canonical identity.
  2. 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>

On this page