import { type Address, type Hex } from "viem"; import type { LucidlyClient } from "./client.js"; /** * ResearchToolsModule — Lucidly-native read-only knowledge tools the research agent calls. * * This file is the W2-1 starting point per docs/WORKFLOWS-DESIGN.md. Each public method on the * module is a "tool" the LLM can invoke by name; the JSON descriptor for each tool is exported as * `LUCIDLY_RESEARCH_TOOLS` so the agent loop can advertise the toolset to the model. * * Design constraints: * • Pure read-only. No tool here can broadcast a tx — that's W4's job and is gated by user consent. * • Stateless. Tools take a vault address (or other primary key) as input; no hidden context. * • Composable. Tools return plain JSON-friendly objects so the agent can pass them between calls. * • Cheap. Each tool is one indexer query or one on-chain read batch — token-cost discipline. * • Defensive. When the indexer is unreachable or a vault is unknown, tools return empty/null * rather than throwing, so the agent can degrade gracefully. */ /** Holder of a vault's share token, with current balance derived from the share-transfer ledger. */ export interface HolderRow { address: Address; /** Share balance in vault decimals (bigint serialized as string for JSON compatibility). */ balance: string; } /** A deposit event row, as seen by the indexer's Teller-deposit handler. */ export interface DepositorRow { receiver: Address; depositAsset: Address; /** Base-asset amount deposited (atomic units, string-encoded). */ depositAmount: string; /** Share amount minted (atomic units, string-encoded). */ shareAmount: string; at: string; txHash: Hex; } /** A single NAV/share rate update from the Accountant. */ export interface NavPoint { at: string; oldRate: string; newRate: string; txHash: Hex; } /** A policy that the vault's PolicyManager has enabled (currently in its Merkle root). */ export interface EnabledPolicy { leaf: Hex; } /** Status of one protocol the vault has touched (or could touch). */ export interface ProtocolPolicyStatus { protocolId: Hex; /** Components currently cloned for this vault — decoder/tvlAdapter/oracle. */ decoder: Address; tvlAdapter: Address; oracle: Address; exists: boolean; } /** A non-whitelisted policy request waiting in the owner-review queue. */ export interface PolicyRequestRow { chainId: number; protocolId: Hex; target: Address; selector: Hex; note: string; requestedBy: Address; requestedAt: string; txHash: Hex; } /** Morpho Blue MarketParams — the 5-tuple that hashes to a market's id. */ export interface MorphoMarketParams { loanToken: Address; collateralToken: Address; oracle: Address; irm: Address; /** Liquidation LTV, on the 1e18 scale (0.86e18 → 86%). */ lltv: string; } /** Morpho Blue market state. */ export interface MorphoMarketState { marketId: Hex; params: MorphoMarketParams; totalSupplyAssets: string; totalSupplyShares: string; totalBorrowAssets: string; totalBorrowShares: string; lastUpdate: string; fee: string; /** Derived: total supply - total borrow, in loan-token atomic units. */ availableLiquidity: string; /** Derived: utilization (borrowAssets / supplyAssets), on the 1e18 scale. 0 if supply is zero. */ utilization: string; } /** Morpho Blue per-account position. */ export interface MorphoPosition { marketId: Hex; account: Address; supplyShares: string; borrowShares: string; /** Collateral in collateral-token atomic units. */ collateral: string; } /** Aave V3 account-level summary (Pool.getUserAccountData). */ export interface AavePosition { account: Address; /** All four base-denominated values are in Aave's "base currency" — USD on mainnet/L2s, 8 decimals. */ totalCollateralBase: string; totalDebtBase: string; availableBorrowsBase: string; currentLiquidationThresholdBps: number; ltvBps: number; /** Health factor on the 1e18 scale. >1.0 = healthy; <1.0 = liquidatable. uint(-1) when no debt. */ healthFactor: string; } /** Aave V3 reserve state (the useful subset of Pool.getReserveData). */ export interface AaveReserve { asset: Address; aToken: Address; variableDebtToken: Address; /** Supply APY in ray (1e27 scale). Divide by 1e27 for fractional, ×100 for %. */ liquidityRate: string; /** Variable-rate borrow APY in ray. */ variableBorrowRate: string; /** Per-second indexes (also in ray) — accrue between updates. */ liquidityIndex: string; variableBorrowIndex: string; lastUpdateTimestamp: number; } /** ERC20 token metadata read live from chain. (Distinct from `TokenInfo` in chains.ts, which is * static metadata bundled with the SDK for tokens the SDK already knows about.) */ export interface Erc20TokenInfo { asset: Address; name: string; symbol: string; decimals: number; } export interface ToolDescriptor { name: string; description: string; input_schema: Record; } export declare const LUCIDLY_RESEARCH_TOOLS: ToolDescriptor[]; /** Morpho Blue — chain-invariant via CREATE2. Deployed on Ethereum + Base. Op/Arb: not deployed. */ export declare const MORPHO_BLUE_PER_CHAIN: Record; /** Compute a Morpho Blue market id from MarketParams: keccak256(abi.encode(loanToken, collateralToken, oracle, irm, lltv)). */ export declare function morphoMarketId(p: MorphoMarketParams): Hex; export declare class ResearchToolsModule { private readonly client; constructor(client: LucidlyClient); /** * Per-protocol policy status for one vault. * * Strategy: * 1. Indexer tells us which vault row this address belongs to (we trust the indexer's * denormalized cache to know its policyManager + accountant + teller addresses). * 2. PolicyManager.usedProtocolsList() gives the set of protocols this vault has clones for. * 3. For each protocol, read clonesOf(protocolId) to get the per-vault component addresses. * 4. PolicyManager.enabledLeavesList() gives the currently enabled Merkle leaves (un-attributed * to protocol — leaves don't carry their protocolId, so we return them as a flat list). */ policies(opts: { vault: Address; }): Promise<{ vault: Address; policyManager: Address | null; perProtocol: ProtocolPolicyStatus[]; enabledLeaves: EnabledPolicy[]; }>; /** * Current share-token holders for a vault, balance descending. * * Implementation: pull the full share-transfer ledger (capped by indexer pagination) and * aggregate. For very large vaults this gets slow; if it becomes a hot path we'll add a * Postgres aggregation route to the indexer. For now it's bounded by the indexer's 200-row * `shareTransfers` default — we explicitly request 200, which is plenty for early-stage vaults. */ holders(opts: { vault: Address; limit?: number; }): Promise; /** Recent Teller deposits for a vault, newest first. Direct indexer pass-through. */ depositors(opts: { vault: Address; limit?: number; }): Promise; /** NAV history for a vault, oldest first. Direct indexer pass-through. */ navHistory(opts: { vault: Address; limit?: number; }): Promise; /** * Open policy requests across all vaults. Optionally filtered to one vault's PolicyManager — * the indexer keys these by policyManager address, so the filter is exact-match. We pull from * the indexer's global feed and filter in TS because the indexer's GraphQL `where` clause on * policyRequests doesn't yet support `policyManager` (could be added later). */ policyRequests(opts?: { policyManager?: Address; limit?: number; }): Promise; /** * Read a Morpho Blue market's current state. The MarketParams 5-tuple identifies the market — * Morpho Blue can have many markets per (loan, collateral) pair, differentiated by * oracle/irm/lltv. The market id is keccak256(abi.encode(params)). * * Throws if Morpho Blue isn't deployed on the client's active chain (currently Op/Arb). */ morphoMarket(params: MorphoMarketParams): Promise; /** Read a specific account's Morpho Blue position in one market. */ morphoPosition(params: MorphoMarketParams, account: Address): Promise; /** Read an account's Aave V3 position summary. Uses the active chain's Aave Pool address from chains.ts. */ aavePosition(account: Address): Promise; /** Read an Aave V3 reserve's state for one asset. Returns the useful subset of getReserveData. */ aaveMarket(asset: Address): Promise; /** Read an ERC20's name/symbol/decimals. Defensive: returns defaults rather than throwing. */ tokenInfo(asset: Address): Promise; } /** * Execute one tool call by name. The agent loop receives `{ name, input }` tool-use blocks from * the model and calls this dispatcher; the result is sent back as a `tool_result` block. * * Returns a JSON-serializable object that the model can read. Throws on unknown tools so the * agent loop can surface a clear error to the model (which can then retry or give up). */ export declare function dispatchLucidlyTool(tools: ResearchToolsModule, name: string, input: Record): Promise;