@t2000/engine 0.46.5 → 0.46.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -922,6 +922,32 @@ declare class QueryEngine {
922
922
  reset(): void;
923
923
  getGuardEvents(): readonly GuardEvent[];
924
924
  loadMessages(messages: Message[]): void;
925
+ /**
926
+ * [v0.46.7] Run a read-only tool out-of-band, using the engine's tool
927
+ * registry and ToolContext. Used by hosts to deterministically pre-dispatch
928
+ * tools based on user-message intent (e.g. always call `balance_check` when
929
+ * the user says "what's my net worth?", regardless of whether the LLM would
930
+ * have otherwise re-called it).
931
+ *
932
+ * The host is responsible for:
933
+ * - Streaming the synthetic `tool_start` + `tool_result` events to the UI
934
+ * (so cards render as if the LLM had called the tool).
935
+ * - Appending matching `tool_use` + `tool_result` ContentBlocks to the
936
+ * engine's message history via `loadMessages([...getMessages(), ...synth])`
937
+ * BEFORE calling `submitMessage`, so the LLM sees the fresh data and
938
+ * doesn't re-call.
939
+ *
940
+ * Throws if the tool isn't registered, isn't read-only, or fails input
941
+ * validation. Tool execution errors are returned as `{ data, isError: true }`
942
+ * for the caller to handle (typically: skip the injection so the LLM falls
943
+ * back to its normal flow).
944
+ */
945
+ invokeReadTool(toolName: string, input: unknown, options?: {
946
+ signal?: AbortSignal;
947
+ }): Promise<{
948
+ data: unknown;
949
+ isError: boolean;
950
+ }>;
925
951
  setServerPositions(data: EngineConfig['serverPositions']): void;
926
952
  getUsage(): CostSnapshot;
927
953
  /**
@@ -1644,12 +1670,23 @@ declare const healthCheckTool: Tool<{}, {
1644
1670
  status: string;
1645
1671
  }>;
1646
1672
 
1647
- declare const ratesInfoTool: Tool<{}, _t2000_sdk.RatesResult>;
1673
+ type RateMap = Record<string, {
1674
+ saveApy: number;
1675
+ borrowApy: number;
1676
+ }>;
1677
+ declare const ratesInfoTool: Tool<{
1678
+ assets?: string[] | undefined;
1679
+ stableOnly?: boolean | undefined;
1680
+ topN?: number | undefined;
1681
+ }, RateMap>;
1648
1682
 
1649
1683
  declare const transactionHistoryTool: Tool<{
1650
1684
  action?: "send" | "swap" | "transaction" | "lending" | undefined;
1651
1685
  date?: string | undefined;
1686
+ direction?: "out" | "in" | undefined;
1652
1687
  limit?: number | undefined;
1688
+ minUsd?: number | undefined;
1689
+ assetSymbol?: string | undefined;
1653
1690
  }, Record<string, unknown>>;
1654
1691
 
1655
1692
  declare const saveDepositTool: Tool<{
@@ -1743,6 +1780,7 @@ declare const payApiTool: Tool<{
1743
1780
  declare const mppServicesTool: Tool<{
1744
1781
  query?: string | undefined;
1745
1782
  category?: string | undefined;
1783
+ mode?: "summary" | "full" | undefined;
1746
1784
  }, Record<string, unknown>>;
1747
1785
 
1748
1786
  declare const swapExecuteTool: Tool<{
@@ -1930,6 +1968,7 @@ declare const activitySummaryTool: Tool<{
1930
1968
  }, ActivitySummary>;
1931
1969
 
1932
1970
  declare const defillamaYieldPoolsTool: Tool<{
1971
+ stableOnly?: boolean | undefined;
1933
1972
  limit?: number | undefined;
1934
1973
  chain?: string | undefined;
1935
1974
  project?: string | undefined;
package/dist/index.js CHANGED
@@ -977,6 +977,36 @@ var healthCheckTool = buildTool({
977
977
  }
978
978
  });
979
979
  var YIELDS_API = "https://yields.llama.fi";
980
+ var STABLECOIN_SYMBOLS2 = /* @__PURE__ */ new Set([
981
+ "usdc",
982
+ "wusdc",
983
+ "usdt",
984
+ "wusdt",
985
+ "suiusdt",
986
+ "usdy",
987
+ "usdsui",
988
+ "usde",
989
+ "ausd",
990
+ "fdusd",
991
+ "buck"
992
+ ]);
993
+ function isStable(symbol) {
994
+ return STABLECOIN_SYMBOLS2.has(symbol.toLowerCase());
995
+ }
996
+ function applyFilters(rates, opts) {
997
+ let entries = Object.entries(rates);
998
+ if (opts.assets && opts.assets.length) {
999
+ const wanted = new Set(opts.assets.map((a) => a.toLowerCase()));
1000
+ entries = entries.filter(([sym]) => wanted.has(sym.toLowerCase()));
1001
+ } else if (opts.stableOnly) {
1002
+ entries = entries.filter(([sym]) => isStable(sym));
1003
+ }
1004
+ entries.sort(([, a], [, b]) => b.saveApy - a.saveApy);
1005
+ if (opts.topN && opts.topN > 0) {
1006
+ entries = entries.slice(0, opts.topN);
1007
+ }
1008
+ return Object.fromEntries(entries);
1009
+ }
980
1010
  function formatRatesSummary(rates) {
981
1011
  return Object.entries(rates).map(([asset, r]) => `${asset}: Save ${(r.saveApy * 100).toFixed(2)}% / Borrow ${(r.borrowApy * 100).toFixed(2)}%`).join(", ");
982
1012
  }
@@ -997,22 +1027,52 @@ async function fetchRatesFromDefiLlama() {
997
1027
  }
998
1028
  var ratesInfoTool = buildTool({
999
1029
  name: "rates_info",
1000
- description: "Get current NAVI Protocol lending/savings rates (APY) for supported assets on Sui. Returns save APY and borrow APY per asset.",
1001
- inputSchema: z.object({}),
1002
- jsonSchema: { type: "object", properties: {}, required: [] },
1030
+ description: 'NAVI Protocol lending markets ONLY (single-sided save/borrow, no impermanent-loss risk). Use this for stablecoin and bluechip lending yields. Renders a rich rates card. Filter args: `assets` (specific symbols like ["USDC"]), `stableOnly` (true to show only USD-pegged assets), `topN` (max rows in card, default 8, max 50). Do NOT call defillama_yield_pools in the same turn \u2014 that tool is for LP/farming pools with IL risk, not lending.',
1031
+ inputSchema: z.object({
1032
+ assets: z.array(z.string()).optional().describe('Filter to specific asset symbols (e.g. ["USDC"], ["USDC","USDT","USDSUI"]). Case-insensitive.'),
1033
+ stableOnly: z.boolean().optional().describe("When true, return only stablecoin markets (USDC, USDT, USDSUI, USDY, suiUSDT, etc.). Ignored when `assets` is supplied."),
1034
+ topN: z.number().int().min(1).max(50).optional().describe("Cap the number of rows in the card (default 8). Use 50 to render the full NAVI catalog.")
1035
+ }),
1036
+ jsonSchema: {
1037
+ type: "object",
1038
+ properties: {
1039
+ assets: {
1040
+ type: "array",
1041
+ items: { type: "string" },
1042
+ description: "Filter to specific asset symbols (case-insensitive)."
1043
+ },
1044
+ stableOnly: {
1045
+ type: "boolean",
1046
+ description: "When true, return only stablecoin markets. Ignored when `assets` is supplied."
1047
+ },
1048
+ topN: {
1049
+ type: "number",
1050
+ description: "Cap the number of rows in the card (default 8, max 50)."
1051
+ }
1052
+ },
1053
+ required: []
1054
+ },
1003
1055
  isReadOnly: true,
1004
- async call(_input, context) {
1056
+ async call(input, context) {
1057
+ const opts = {
1058
+ assets: input.assets,
1059
+ stableOnly: input.stableOnly,
1060
+ topN: input.topN ?? 8
1061
+ };
1005
1062
  if (hasNaviMcpGlobal(context)) {
1006
- const rates2 = await fetchRates(getMcpManager(context));
1007
- return { data: rates2, displayText: formatRatesSummary(rates2) };
1063
+ const all2 = await fetchRates(getMcpManager(context));
1064
+ const filtered2 = applyFilters(all2, opts);
1065
+ return { data: filtered2, displayText: formatRatesSummary(filtered2) };
1008
1066
  }
1009
1067
  if (hasAgent(context)) {
1010
1068
  const agent = requireAgent(context);
1011
- const rates2 = await agent.rates();
1012
- return { data: rates2, displayText: formatRatesSummary(rates2) };
1069
+ const all2 = await agent.rates();
1070
+ const filtered2 = applyFilters(all2, opts);
1071
+ return { data: filtered2, displayText: formatRatesSummary(filtered2) };
1013
1072
  }
1014
- const rates = await fetchRatesFromDefiLlama();
1015
- return { data: rates, displayText: formatRatesSummary(rates) };
1073
+ const all = await fetchRatesFromDefiLlama();
1074
+ const filtered = applyFilters(all, opts);
1075
+ return { data: filtered, displayText: formatRatesSummary(filtered) };
1016
1076
  }
1017
1077
  });
1018
1078
  function parseRpcTx(tx, address) {
@@ -1114,11 +1174,14 @@ var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
1114
1174
  var DEFAULT_LOOKBACK_DAYS = 30;
1115
1175
  var transactionHistoryTool = buildTool({
1116
1176
  name: "transaction_history",
1117
- description: "Retrieve recent transaction history (last 30 days by default): sends, saves, withdrawals, borrows, repayments, and rewards claims. Pass `date` (YYYY-MM-DD) for a specific day, `action` to filter by category (send/lending/swap), or both.",
1177
+ description: 'Retrieve recent transaction history (last 30 days by default): sends, saves, withdrawals, borrows, repayments, swaps, and rewards claims. Renders a rich transaction card. Filter args: `date` (YYYY-MM-DD), `action` (send/lending/swap), `minUsd` (minimum amount in USD \u2014 use this for "transactions over $X" instead of post-filtering), `assetSymbol` (e.g. "USDC", "SUI"), `direction` ("in" or "out"). The card itself respects all filters \u2014 never re-list the rows in narration.',
1118
1178
  inputSchema: z.object({
1119
1179
  limit: z.number().int().min(1).max(50).optional(),
1120
1180
  date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
1121
- action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction.")
1181
+ action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
1182
+ minUsd: z.number().min(0).optional().describe('Minimum transaction amount in USD. Use this for "transactions over $X" \u2014 the amount is converted to USD using the asset price snapshot.'),
1183
+ assetSymbol: z.string().optional().describe('Filter to a single asset symbol (case-insensitive, e.g. "USDC", "SUI", "LOFI"). Matches `tx.asset` exactly.'),
1184
+ direction: z.enum(["in", "out"]).optional().describe('Filter by user-side balance flow: "in" = received, "out" = spent.')
1122
1185
  }),
1123
1186
  jsonSchema: {
1124
1187
  type: "object",
@@ -1135,6 +1198,19 @@ var transactionHistoryTool = buildTool({
1135
1198
  type: "string",
1136
1199
  enum: [...HISTORY_ACTIONS],
1137
1200
  description: "Filter results by action category: send, lending, swap, or transaction."
1201
+ },
1202
+ minUsd: {
1203
+ type: "number",
1204
+ description: 'Minimum transaction amount in USD. Use this for "transactions over $X" queries.'
1205
+ },
1206
+ assetSymbol: {
1207
+ type: "string",
1208
+ description: 'Filter to a single asset symbol (case-insensitive, e.g. "USDC", "SUI").'
1209
+ },
1210
+ direction: {
1211
+ type: "string",
1212
+ enum: ["in", "out"],
1213
+ description: 'Filter by direction of user balance change: "in" = received, "out" = spent.'
1138
1214
  }
1139
1215
  }
1140
1216
  },
@@ -1183,17 +1259,48 @@ var transactionHistoryTool = buildTool({
1183
1259
  async call(input, context) {
1184
1260
  const limit = input.limit ?? 10;
1185
1261
  const action = input.action;
1262
+ const assetSymbol = input.assetSymbol?.toLowerCase();
1263
+ const direction = input.direction;
1264
+ const minUsd = input.minUsd;
1265
+ const prices = context.tokenPrices;
1266
+ const priceFor = (sym) => {
1267
+ if (!sym || !prices) return void 0;
1268
+ return prices[sym.toUpperCase()] ?? prices[sym.toLowerCase()] ?? prices[sym];
1269
+ };
1186
1270
  const finalize = (records2) => {
1187
1271
  let scoped = records2;
1188
1272
  if (action) scoped = scoped.filter((r) => r.action === action);
1273
+ if (assetSymbol) {
1274
+ scoped = scoped.filter((r) => r.asset?.toLowerCase() === assetSymbol);
1275
+ }
1276
+ if (direction) {
1277
+ scoped = scoped.filter((r) => r.direction === direction);
1278
+ }
1279
+ if (minUsd != null && minUsd > 0) {
1280
+ scoped = scoped.filter((r) => {
1281
+ if (r.amount == null) return false;
1282
+ const sym = r.asset?.toUpperCase() ?? "";
1283
+ const isStableLike = sym === "USDC" || sym === "USDT" || sym === "WUSDC" || sym === "WUSDT" || sym === "SUIUSDT" || sym === "USDY" || sym === "USDSUI" || sym === "USDE" || sym === "AUSD" || sym === "FDUSD" || sym === "BUCK";
1284
+ const usd = isStableLike ? r.amount : (priceFor(sym) ?? 0) * r.amount;
1285
+ if (!isStableLike && priceFor(sym) == null) return true;
1286
+ return usd >= minUsd;
1287
+ });
1288
+ }
1189
1289
  return scoped.slice(0, limit);
1190
1290
  };
1291
+ const filterMeta = {
1292
+ date: input.date ?? null,
1293
+ action: action ?? null,
1294
+ minUsd: minUsd ?? null,
1295
+ assetSymbol: input.assetSymbol ?? null,
1296
+ direction: direction ?? null
1297
+ };
1191
1298
  if (context.agent) {
1192
1299
  const agent = requireAgent(context);
1193
1300
  const records2 = await agent.history({ limit: input.date ? limit : Math.max(limit * 4, 50) });
1194
1301
  const filtered2 = finalize(records2);
1195
1302
  return {
1196
- data: { transactions: filtered2, count: filtered2.length, date: input.date ?? null, action: action ?? null },
1303
+ data: { transactions: filtered2, count: filtered2.length, ...filterMeta },
1197
1304
  displayText: `${filtered2.length} recent transaction(s)`
1198
1305
  };
1199
1306
  }
@@ -1210,7 +1317,7 @@ var transactionHistoryTool = buildTool({
1210
1317
  const filtered2 = finalize(records2);
1211
1318
  const dateLabel = new Date(input.date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
1212
1319
  return {
1213
- data: { transactions: filtered2, count: filtered2.length, date: input.date, action: action ?? null },
1320
+ data: { transactions: filtered2, count: filtered2.length, ...filterMeta },
1214
1321
  displayText: filtered2.length > 0 ? `${filtered2.length} transaction(s) on ${dateLabel}` : `No transactions found on ${dateLabel}`
1215
1322
  };
1216
1323
  }
@@ -1226,8 +1333,7 @@ var transactionHistoryTool = buildTool({
1226
1333
  data: {
1227
1334
  transactions: filtered,
1228
1335
  count: filtered.length,
1229
- date: null,
1230
- action: action ?? null,
1336
+ ...filterMeta,
1231
1337
  lookbackDays: DEFAULT_LOOKBACK_DAYS
1232
1338
  },
1233
1339
  displayText: `${filtered.length} transaction(s) in the last ${DEFAULT_LOOKBACK_DAYS} days`
@@ -1604,16 +1710,31 @@ async function fetchCatalog() {
1604
1710
  catalogCache = { data, ts: Date.now() };
1605
1711
  return data;
1606
1712
  }
1713
+ function renderServices(catalog) {
1714
+ return catalog.map((s) => ({
1715
+ id: s.id,
1716
+ name: s.name,
1717
+ description: s.description,
1718
+ categories: s.categories,
1719
+ endpoints: s.endpoints.map((e) => ({
1720
+ url: `${MPP_GATEWAY2}/${s.id}${e.path}`,
1721
+ method: e.method,
1722
+ description: e.description,
1723
+ price: `$${e.price}`
1724
+ }))
1725
+ }));
1726
+ }
1607
1727
  function matchesQuery(service, q) {
1608
1728
  const lower = q.toLowerCase();
1609
1729
  return service.id.toLowerCase().includes(lower) || service.name.toLowerCase().includes(lower) || service.description.toLowerCase().includes(lower) || service.categories.some((c) => c.toLowerCase().includes(lower)) || service.endpoints.some((e) => e.description.toLowerCase().includes(lower));
1610
1730
  }
1611
1731
  var mppServicesTool = buildTool({
1612
1732
  name: "mpp_services",
1613
- description: "Discover available MPP gateway services. Returns service names, descriptions, endpoints with required parameters, and pricing. Pass `query` for keyword search or `category` to filter by category. Calling with NO filters returns a category summary (not the full catalog) \u2014 narrow first, then fetch endpoints. Use this BEFORE calling pay_api.",
1733
+ description: 'Discover available MPP gateway services. Returns service names, descriptions, endpoints with required parameters, and pricing. Use BEFORE calling pay_api. With no args, returns the FULL catalog as a single card (default behavior \u2014 covers "show me available MPP services", "what services exist", "show me all MPP services"). Use `query` to keyword-search a specific need ("translate", "weather", "postcard"). Use `category` to filter to one category. Use `mode: "summary"` only if you explicitly want a category-counts overview without the full list.',
1614
1734
  inputSchema: z.object({
1615
- query: z.string().optional().describe('Filter by keyword (e.g. "postcard", "translate", "weather").'),
1616
- category: z.string().optional().describe('Filter by category exactly (e.g. "weather", "image"). See category summary returned when called without filters.')
1735
+ query: z.string().optional().describe('Filter by keyword (e.g. "postcard", "translate", "weather"). Returns matching services in one card.'),
1736
+ category: z.string().optional().describe('Filter by category exactly (e.g. "weather", "image"). Use mode:"summary" first if you need to see the category list.'),
1737
+ mode: z.enum(["summary", "full"]).optional().describe('"full" (default) returns the entire catalog in one card. "summary" returns category counts only \u2014 use this only when the user explicitly asks for a category overview.')
1617
1738
  }),
1618
1739
  jsonSchema: {
1619
1740
  type: "object",
@@ -1625,15 +1746,30 @@ var mppServicesTool = buildTool({
1625
1746
  category: {
1626
1747
  type: "string",
1627
1748
  description: 'Filter by category exactly (e.g. "weather", "image").'
1749
+ },
1750
+ mode: {
1751
+ type: "string",
1752
+ enum: ["summary", "full"],
1753
+ description: '"full" (default) returns the entire catalog in one card. "summary" returns category counts only.'
1628
1754
  }
1629
1755
  },
1630
1756
  required: []
1631
1757
  },
1632
1758
  isReadOnly: true,
1633
- maxResultSizeChars: 5e3,
1759
+ // [v0.46.6] Bumped to fit the full catalog (~40 services) in one
1760
+ // shot when `mode: 'full'` is used. The summarizeOnTruncate path
1761
+ // still applies if the catalog ever exceeds the budget.
1762
+ maxResultSizeChars: 12e3,
1634
1763
  async call(input) {
1635
1764
  const catalog = await fetchCatalog();
1636
- if (!input.query && !input.category) {
1765
+ if (input.mode !== "summary" && !input.query && !input.category) {
1766
+ const services2 = renderServices(catalog);
1767
+ return {
1768
+ data: { services: services2, total: services2.length, mode: "full" },
1769
+ displayText: `Full MPP catalog: ${services2.length} services.`
1770
+ };
1771
+ }
1772
+ if (input.mode === "summary" && !input.query && !input.category) {
1637
1773
  const counts = /* @__PURE__ */ new Map();
1638
1774
  for (const svc of catalog) {
1639
1775
  for (const cat of svc.categories) {
@@ -1644,13 +1780,14 @@ var mppServicesTool = buildTool({
1644
1780
  return {
1645
1781
  data: {
1646
1782
  _refine: {
1647
- reason: "MPP catalog has many services \u2014 pick a category or supply a query first.",
1648
- suggestedParams: { category: categories[0]?.category ?? "weather" }
1783
+ reason: 'Category summary (mode:"summary"). Re-call with a category or omit mode for the full catalog.',
1784
+ suggestedParams: { category: categories[0]?.category ?? "weather" },
1785
+ allModes: ["summary", "full"]
1649
1786
  },
1650
1787
  categories,
1651
1788
  totalServices: catalog.length
1652
1789
  },
1653
- displayText: `${catalog.length} services across ${categories.length} categories. Re-call with a category or query.`
1790
+ displayText: `${catalog.length} services across ${categories.length} categories.`
1654
1791
  };
1655
1792
  }
1656
1793
  let filtered = catalog;
@@ -1661,18 +1798,7 @@ var mppServicesTool = buildTool({
1661
1798
  if (input.query) {
1662
1799
  filtered = filtered.filter((s) => matchesQuery(s, input.query));
1663
1800
  }
1664
- const services = filtered.map((s) => ({
1665
- id: s.id,
1666
- name: s.name,
1667
- description: s.description,
1668
- categories: s.categories,
1669
- endpoints: s.endpoints.map((e) => ({
1670
- url: `${MPP_GATEWAY2}/${s.id}${e.path}`,
1671
- method: e.method,
1672
- description: e.description,
1673
- price: `$${e.price}`
1674
- }))
1675
- }));
1801
+ const services = renderServices(filtered);
1676
1802
  const filterDesc = [
1677
1803
  input.query ? `query "${input.query}"` : null,
1678
1804
  input.category ? `category "${input.category}"` : null
@@ -2933,22 +3059,44 @@ function fmtToolTvl(tvl) {
2933
3059
  if (tvl >= 1e3) return `$${(tvl / 1e3).toFixed(0)}K`;
2934
3060
  return `$${tvl}`;
2935
3061
  }
3062
+ var POOL_STABLE_LEGS = /* @__PURE__ */ new Set([
3063
+ "USDC",
3064
+ "WUSDC",
3065
+ "USDT",
3066
+ "WUSDT",
3067
+ "SUIUSDT",
3068
+ "USDY",
3069
+ "USDSUI",
3070
+ "USDE",
3071
+ "AUSD",
3072
+ "FDUSD",
3073
+ "BUCK",
3074
+ "DAI",
3075
+ "LUSD",
3076
+ "FRAX",
3077
+ "GUSD",
3078
+ "PYUSD",
3079
+ "USDS",
3080
+ "CRVUSD"
3081
+ ]);
2936
3082
  var defillamaYieldPoolsTool = buildTool({
2937
3083
  name: "defillama_yield_pools",
2938
- description: 'Get top DeFi yield pools across protocols. Filter by chain (e.g. "Sui"), project (e.g. "navi-lending"), and minimum TVL. For NAVI lending rates, use project "navi-lending".',
3084
+ description: 'Cross-protocol LP / vault yields with IMPERMANENT-LOSS RISK (Cetus, Bluefin, Full Sail, etc.). ONLY call when the user explicitly asks about LP pools, DeFi farming, or "higher yield with more risk". For safe single-sided lending yields (USDC save, NAVI, etc.) use rates_info instead \u2014 NEVER both in the same turn. Filter by chain (e.g. "Sui"), project, and minimum TVL.',
2939
3085
  inputSchema: z.object({
2940
3086
  chain: z.string().optional().describe('Filter by chain name (e.g. "Sui", "Ethereum")'),
2941
- project: z.string().optional().describe('Filter by protocol project name (e.g. "navi-lending", "cetus-clmm")'),
3087
+ project: z.string().optional().describe('Filter by protocol project name (e.g. "cetus-clmm", "bluefin-spot")'),
2942
3088
  limit: z.number().min(1).max(20).optional().describe("Max results (default 5)"),
2943
- minTvl: z.number().optional().describe("Minimum TVL in USD to filter out small/risky pools (default 100000)")
3089
+ minTvl: z.number().optional().describe("Minimum TVL in USD to filter out small/risky pools (default 100000)"),
3090
+ stableOnly: z.boolean().optional().describe('When true, only return pools where every leg is a stablecoin (USDC, USDT, USDSUI, etc.). Use this for "show stablecoin yield options" \u2014 keeps volatile-pair LPs (WAL-SUI, DEEP-SUI) out.')
2944
3091
  }),
2945
3092
  jsonSchema: {
2946
3093
  type: "object",
2947
3094
  properties: {
2948
3095
  chain: { type: "string", description: "Filter by chain name" },
2949
- project: { type: "string", description: 'Filter by protocol project name (e.g. "navi-lending")' },
3096
+ project: { type: "string", description: 'Filter by protocol project name (e.g. "cetus-clmm")' },
2950
3097
  limit: { type: "number", description: "Max results (default 5)" },
2951
- minTvl: { type: "number", description: "Minimum TVL in USD (default 100000)" }
3098
+ minTvl: { type: "number", description: "Minimum TVL in USD (default 100000)" },
3099
+ stableOnly: { type: "boolean", description: "When true, only return all-stablecoin pools." }
2952
3100
  },
2953
3101
  required: []
2954
3102
  },
@@ -2984,6 +3132,12 @@ var defillamaYieldPoolsTool = buildTool({
2984
3132
  }
2985
3133
  const minTvl = input.minTvl ?? 1e5;
2986
3134
  pools = pools.filter((p) => p.tvlUsd >= minTvl);
3135
+ if (input.stableOnly) {
3136
+ pools = pools.filter((p) => {
3137
+ const legs = p.symbol.split("-");
3138
+ return legs.every((leg) => POOL_STABLE_LEGS.has(leg.trim().toUpperCase()));
3139
+ });
3140
+ }
2987
3141
  pools.sort((a, b) => b.apy - a.apy);
2988
3142
  const limit = input.limit ?? 5;
2989
3143
  const top = pools.slice(0, limit);
@@ -4433,6 +4587,62 @@ var QueryEngine = class {
4433
4587
  loadMessages(messages) {
4434
4588
  this.messages = [...messages];
4435
4589
  }
4590
+ /**
4591
+ * [v0.46.7] Run a read-only tool out-of-band, using the engine's tool
4592
+ * registry and ToolContext. Used by hosts to deterministically pre-dispatch
4593
+ * tools based on user-message intent (e.g. always call `balance_check` when
4594
+ * the user says "what's my net worth?", regardless of whether the LLM would
4595
+ * have otherwise re-called it).
4596
+ *
4597
+ * The host is responsible for:
4598
+ * - Streaming the synthetic `tool_start` + `tool_result` events to the UI
4599
+ * (so cards render as if the LLM had called the tool).
4600
+ * - Appending matching `tool_use` + `tool_result` ContentBlocks to the
4601
+ * engine's message history via `loadMessages([...getMessages(), ...synth])`
4602
+ * BEFORE calling `submitMessage`, so the LLM sees the fresh data and
4603
+ * doesn't re-call.
4604
+ *
4605
+ * Throws if the tool isn't registered, isn't read-only, or fails input
4606
+ * validation. Tool execution errors are returned as `{ data, isError: true }`
4607
+ * for the caller to handle (typically: skip the injection so the LLM falls
4608
+ * back to its normal flow).
4609
+ */
4610
+ async invokeReadTool(toolName, input, options = {}) {
4611
+ const tool = findTool(this.tools, toolName);
4612
+ if (!tool) throw new Error(`invokeReadTool: tool not found: ${toolName}`);
4613
+ if (!tool.isReadOnly) {
4614
+ throw new Error(`invokeReadTool: tool is not read-only: ${toolName} (write tools must go through the permission gate)`);
4615
+ }
4616
+ const parsed = tool.inputSchema.safeParse(input);
4617
+ if (!parsed.success) {
4618
+ throw new Error(
4619
+ `invokeReadTool: invalid input for ${toolName}: ${parsed.error.issues.map((i) => i.message).join(", ")}`
4620
+ );
4621
+ }
4622
+ const signal = options.signal ?? new AbortController().signal;
4623
+ const context = {
4624
+ agent: this.agent,
4625
+ mcpManager: this.mcpManager,
4626
+ walletAddress: this.walletAddress,
4627
+ suiRpcUrl: this.suiRpcUrl,
4628
+ serverPositions: this.serverPositions,
4629
+ positionFetcher: this.positionFetcher,
4630
+ env: this.env,
4631
+ signal,
4632
+ priceCache: this.priceCache,
4633
+ permissionConfig: this.permissionConfig,
4634
+ sessionSpendUsd: this.sessionSpendUsd
4635
+ };
4636
+ try {
4637
+ const result = await tool.call(parsed.data, context);
4638
+ return { data: result.data, isError: false };
4639
+ } catch (err) {
4640
+ return {
4641
+ data: { error: err instanceof Error ? err.message : "Tool execution failed" },
4642
+ isError: true
4643
+ };
4644
+ }
4645
+ }
4436
4646
  setServerPositions(data) {
4437
4647
  this.serverPositions = data;
4438
4648
  }