/** * Proactive prefetch for live-world questions. * * Why this exists: * When a user asks "what is CRCL trading at?", the agent has TradingMarket * in CORE and the system prompt demands it be used. The evaluator catches * refusals. The auto-retry loop feeds findings back. All four layers run * every turn. It still isn't enough — Sonnet 4.6 (the strongest model we * route to) confidently answers "Circle is a private company" from 2022 * training data, refusing the tool across retries. * * The lesson: every mechanism above depends on the model *agreeing* to call * a tool. When the model is confident-but-wrong about current-world state, * it doesn't reach for the tool at all. No prompt tweak will fix this — * fine-tuning priors beat prompt priors. * * Harness-level fix: prefetch the data *before* the model decides. When * the user's message contains a ticker or a current-events ask, Franklin's * harness spends the $0.001 unprompted, injects the result into context, * and then the model answers a question it already has evidence for — * not a question its training data has a prior about. * * This is the pattern Anthropic's harness-design writeup calls out: * "Remove components that encode a stale assumption (the model will * reach for tools on its own), replace with components that handle the * coordination gap (harness fetches, model synthesizes)." */ import type { ModelClient } from './llm.js'; import type { Dialogue } from './types.js'; import type { MarketCode } from '../trading/providers/standard-models.js'; export interface TickerIntent { kind: 'ticker'; /** Raw symbol as the user wrote it; may be company name or ticker. */ symbol: string; /** Resolved market if the classifier was confident; `us` default when `assetClass === 'stock'`. */ market?: MarketCode; /** Asset class — stock prefers paid Gateway path; crypto stays free on CoinGecko. */ assetClass: 'stock' | 'crypto'; /** Does the user also want the news / "why did it move"? */ wantNews: boolean; } export type Intent = TickerIntent | null; export interface PrefetchResult { /** Markdown snippet that gets prepended to the user's message for the LLM. */ contextBlock: string; /** User-visible status line ("*Prefetched CRCL ...*"). */ statusLine: string; /** Spend incurred by prefetch. For telemetry + Markets panel display. */ costUsd: number; /** Did any prefetch call actually succeed? If all failed, the caller may * decide to skip injection entirely and let the model try its own way. */ anyOk: boolean; } /** Run the prefetch for an intent. Concurrent fan-out for price + news. */ export declare function prefetchForIntent(intent: Intent, client: ModelClient): Promise; /** * Read an ExaAnswer response through BOTH wire shapes — the live BlockRun * gateway returns `{ answer, costDollars }` at the TOP level, while older or * proxied deployments nest them under `data`. This mirrors the `res.data ?? res` * read in src/tools/exa.ts; this prefetch path is the twin of the ExaAnswer * tool and historically drifted out of sync, paying the USDC then dropping the * answer. Exported for regression tests. `paid` records whether a real x402 * charge settled, so a paid-but-empty answer still counts against spend. */ export declare function readExaAnswer(raw: unknown, paid: boolean): { text: string | null; costUsd: number; }; /** * Augment a user message with the prefetch context block prepended. The * final model sees the data as part of the "incoming" user turn — no * synthetic tool_use fabrication needed, history stays clean. */ export declare function augmentUserMessage(originalInput: string, prefetch: PrefetchResult): Dialogue;