/** * StrategyDecomposer — the heart of Lucidly Pro's "any strategy, any user" promise. * * Takes a free-text strategy idea ("I want delta-neutral USDC with a Pendle long + HL short") * and produces a structured `DecompositionResult` with: * • the nearest archetype from the curated corpus, * • a multi-chain leaf plan with explicit bridge edges, * • per-leaf verification status (verified | missing | needs_research), * • a `simulationReady` flag that is TRUE only when every leaf is verified. * * Architecture: * 1. Wrap the existing LLM provider stack (Anthropic/Groq/DeepSeek/OpenAI) — same provider * surface as ResearchAgent. * 2. Advertise the DECOMPOSER_TOOLS catalog (research + corpus + onchain + simulator). * 3. Run a strict system prompt that: * - forbids inventing leaves (must call verify_policy_id before emitting), * - enforces the homeChain vs positionChains topology distinction, * - mandates a structured JSON answer at the end, * - cites every claim (which corpus entry, which tool call). * 4. Parse the final JSON answer via zod, recompute simulationReady + confidence from the * verified leaves (the model can't lie), lint for structural issues, surface warnings. * * The agent is allowed to be "other" archetype — the corpus is a learning resource, not the * universe of valid strategies. The agent uses corpus entries as nearest-neighbour examples * but is free to compose novel plans when the user's idea doesn't fit a known archetype. */ import type { Address } from "viem"; import type { LLMProvider } from "./llm.js"; import { type DecompositionResult } from "./decompositionTypes.js"; import type { ResearchToolsModule } from "./researchTools.js"; import type { SimulatorClient } from "./simulatorClient.js"; export interface StrategyDecomposerConfig { /** LLM provider — pick the right tier for the user's needs (Sonnet/Haiku/Groq/DeepSeek). */ provider: LLMProvider; /** Research module — required, provides catalog/policies/verify_policy_id. */ research: ResearchToolsModule; /** Simulator client — optional but STRONGLY recommended. When set, the agent can dry-run * candidate plans on an anvil fork before finalising. */ simulator?: SimulatorClient; /** Optional onchain reader — when set, the agent can do raw eth_call against any chain. */ rpcCall?: (chainId: number, target: Address, calldata: `0x${string}`, blockTag?: string) => Promise<`0x${string}`>; /** Optional native-balance reader. */ rpcBalance?: (chainId: number, address: Address, blockTag?: string) => Promise; /** Optional web search adapter — when set, the agent can lucidly.web_search for novel protocols * not in the corpus. Inject Brave / Tavily / DuckDuckGo / etc. with shape * (query, maxResults?) → [{title, url, snippet}]. */ webSearch?: (query: string, maxResults?: number) => Promise>; /** Optional Etherscan v2 unified API key. When set, lucidly.read_protocol_abi works across * every chain on the same key. Get one at https://etherscan.io/apis (free tier). */ etherscanApiKey?: string; /** Optional target vault address. When set, the agent will call verify_policy_id against this * specific vault to mark leaves as verified. When NOT set, the decomposer operates in DESIGN * MODE — every leaf is marked needs_research with a remediation step "deploy a vault first * and re-run with vaultAddress set". */ vaultAddress?: Address; /** Token budget — total input+output across the whole decomposition. Default 200k (corpus * entries are large; the budget needs headroom for re-included tool results). */ sessionTokenCap?: number; /** Iteration cap — max LLM round-trips. Default 12. */ maxIterations?: number; /** Max output tokens per individual model call. Default 8192 (final JSON answer is ~3-5kB * and we need headroom for the agent's reasoning + the answer). */ maxTokensPerCall?: number; /** Extra system-prompt text appended to the default. Useful for vault-specific context. */ extraSystemPrompt?: string; } export interface DecomposerToolCallLog { iteration: number; name: string; input: Record; resultJson: string; isError: boolean; } export interface DecomposerResult { /** Parsed structured decomposition. Null if the agent couldn't produce a valid JSON answer. */ decomposition: DecompositionResult | null; /** Raw text the agent emitted (last message). Useful for debugging when decomposition === null. */ rawText: string; /** Every tool call the agent made, in order. */ toolCalls: DecomposerToolCallLog[]; tokensUsed: { input: number; output: number; }; iterations: number; /** Why the agent stopped — "end_turn" | "max_iterations" | "session_cap" | "user_stop" | "parse_failed". */ stopReason: string; /** Lint warnings the post-parse check found (separate from agent-declared warnings). */ lintWarnings: string[]; providerInfo: { name: string; model: string; }; } export interface DecomposerStreamCallbacks { onIterationStart?: (iteration: number) => void; onTextDelta?: (chunk: string) => void; onToolCall?: (call: DecomposerToolCallLog) => void; onTokens?: (tokens: { input: number; output: number; }) => void; onRateLimit?: (info: { waitSec: number; attempt: number; maxRetries: number; }) => void; abortSignal?: AbortSignal; getStopped?: () => boolean; } export declare class StrategyDecomposer { private readonly provider; private readonly ctx; private readonly maxTokensPerCall; private readonly sessionTokenCap; private readonly maxIterations; private readonly systemPrompt; constructor(cfg: StrategyDecomposerConfig); get providerInfo(): { name: string; model: string; }; /** Run a decomposition end-to-end with no UI feedback. */ decompose(idea: string): Promise; /** Run a decomposition with streaming callbacks for live UI feedback. */ decomposeStream(idea: string, callbacks: DecomposerStreamCallbacks): Promise; }