@t2000/engine 0.46.12 → 0.46.13

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
@@ -236,6 +236,17 @@ interface GuardConfig {
236
236
  * tool would silently ship USDC. Default on.
237
237
  */
238
238
  assetIntent?: boolean;
239
+ /**
240
+ * Root-cause fix for "LLM hallucinates a stale training-data price
241
+ * (e.g. '$3.50/SUI') and shows the user a wildly wrong estimate before
242
+ * the swap card renders". When enabled (default), `swap_execute` is
243
+ * blocked unless a matching `swap_quote(from, to, amount)` ran in the
244
+ * recent past (60s window, ±1% amount tolerance). The block forces the
245
+ * LLM to fetch a real on-chain quote and cite its actual numbers, not
246
+ * a guess. Set to `false` only if the host has its own pre-execution
247
+ * quote requirement.
248
+ */
249
+ swapPreview?: boolean;
239
250
  }
240
251
  declare const DEFAULT_GUARD_CONFIG: GuardConfig;
241
252
  declare class BalanceTracker {
@@ -255,11 +266,39 @@ declare class RetryTracker {
255
266
  previousResult?: unknown;
256
267
  };
257
268
  }
269
+ declare class SwapQuoteTracker {
270
+ /** Quotes recorded in the recent window. Trimmed lazily on every check. */
271
+ private quotes;
272
+ /** Match window: 60s is generous enough for slow LLM turns but tight enough
273
+ * to invalidate stale quotes from earlier in the session. */
274
+ private readonly windowMs;
275
+ /** Amount tolerance: ±1% (covers gas-padding, integer-rounding, and the
276
+ * rare case where the LLM rounds the input differently between quote and
277
+ * execute). Prices barely move in 60s so 1% is forgiving but meaningful. */
278
+ private readonly amountTolerance;
279
+ /**
280
+ * Normalize a token identifier so symbol vs. coinType vs. case don't
281
+ * cause spurious mismatches. Lowercase + trim is sufficient because the
282
+ * SDK's resolver itself is case-insensitive on symbols.
283
+ */
284
+ private normalize;
285
+ record(input: {
286
+ from: string;
287
+ to: string;
288
+ amount: number;
289
+ }): void;
290
+ hasMatchingQuote(input: {
291
+ from: string;
292
+ to: string;
293
+ amount: number;
294
+ }): boolean;
295
+ }
258
296
  declare function guardArtifactPreview(result: unknown): GuardInjection | null;
259
297
  declare function guardStaleData(toolFlags: ToolFlags): GuardInjection | null;
260
298
  interface GuardRunnerState {
261
299
  balanceTracker: BalanceTracker;
262
300
  retryTracker: RetryTracker;
301
+ swapQuoteTracker: SwapQuoteTracker;
263
302
  lastHealthFactor: number | null;
264
303
  }
265
304
  declare function createGuardRunnerState(): GuardRunnerState;
package/dist/index.js CHANGED
@@ -3628,7 +3628,8 @@ var DEFAULT_GUARD_CONFIG = {
3628
3628
  retryProtection: true,
3629
3629
  inputValidation: true,
3630
3630
  addressSource: true,
3631
- assetIntent: true
3631
+ assetIntent: true,
3632
+ swapPreview: true
3632
3633
  };
3633
3634
  var BalanceTracker = class {
3634
3635
  lastBalanceAt = 0;
@@ -3669,6 +3670,45 @@ var RetryTracker = class {
3669
3670
  return { blocked: true, previousResult: prev.result };
3670
3671
  }
3671
3672
  };
3673
+ var SwapQuoteTracker = class {
3674
+ /** Quotes recorded in the recent window. Trimmed lazily on every check. */
3675
+ quotes = [];
3676
+ /** Match window: 60s is generous enough for slow LLM turns but tight enough
3677
+ * to invalidate stale quotes from earlier in the session. */
3678
+ windowMs = 6e4;
3679
+ /** Amount tolerance: ±1% (covers gas-padding, integer-rounding, and the
3680
+ * rare case where the LLM rounds the input differently between quote and
3681
+ * execute). Prices barely move in 60s so 1% is forgiving but meaningful. */
3682
+ amountTolerance = 0.01;
3683
+ /**
3684
+ * Normalize a token identifier so symbol vs. coinType vs. case don't
3685
+ * cause spurious mismatches. Lowercase + trim is sufficient because the
3686
+ * SDK's resolver itself is case-insensitive on symbols.
3687
+ */
3688
+ normalize(token) {
3689
+ return token.trim().toLowerCase();
3690
+ }
3691
+ record(input) {
3692
+ const now = Date.now();
3693
+ this.quotes.push({
3694
+ from: this.normalize(input.from),
3695
+ to: this.normalize(input.to),
3696
+ amount: input.amount,
3697
+ ts: now
3698
+ });
3699
+ const cutoff = now - this.windowMs;
3700
+ this.quotes = this.quotes.filter((q) => q.ts > cutoff);
3701
+ }
3702
+ hasMatchingQuote(input) {
3703
+ const cutoff = Date.now() - this.windowMs;
3704
+ const fromN = this.normalize(input.from);
3705
+ const toN = this.normalize(input.to);
3706
+ const target = input.amount;
3707
+ return this.quotes.some(
3708
+ (q) => q.ts > cutoff && q.from === fromN && q.to === toN && target > 0 && Math.abs(q.amount - target) / target <= this.amountTolerance
3709
+ );
3710
+ }
3711
+ };
3672
3712
  function guardRetryProtection(tool, call, retryTracker) {
3673
3713
  const check = retryTracker.isBlocked(tool.name, call.input);
3674
3714
  if (check.blocked) {
@@ -3842,6 +3882,27 @@ function guardAssetIntent(tool, call, userText) {
3842
3882
  message: `Asset mismatch: the user's recent messages mention "${mentioned.symbol}" but send_transfer was called without an \`asset\` field (defaults to USDC). If the user asked you to send ${mentioned.symbol}, re-issue send_transfer with \`asset: "${mentioned.symbol}"\`. If the user really meant USDC, set \`asset: "USDC"\` explicitly to confirm intent. Never default to USDC when the user named a different token.`
3843
3883
  };
3844
3884
  }
3885
+ function guardSwapPreview(tool, call, swapQuoteTracker) {
3886
+ if (tool.name !== "swap_execute") {
3887
+ return { verdict: "pass", gate: "swap_preview", tier: "safety" };
3888
+ }
3889
+ const input = call.input;
3890
+ const from = typeof input.from === "string" ? input.from : "";
3891
+ const to = typeof input.to === "string" ? input.to : "";
3892
+ const amount = Number(input.amount ?? 0);
3893
+ if (!from || !to || !(amount > 0)) {
3894
+ return { verdict: "pass", gate: "swap_preview", tier: "safety" };
3895
+ }
3896
+ if (swapQuoteTracker.hasMatchingQuote({ from, to, amount })) {
3897
+ return { verdict: "pass", gate: "swap_preview", tier: "safety" };
3898
+ }
3899
+ return {
3900
+ verdict: "block",
3901
+ gate: "swap_preview",
3902
+ tier: "safety",
3903
+ message: `swap_execute requires a recent matching swap_quote so the user sees an accurate preview. Call swap_quote({ from: "${from}", to: "${to}", amount: ${amount} }) first, then re-issue swap_execute with the same params. swap_quote is read-only and returns the real on-chain output, route, and price impact \u2014 never estimate from memory.`
3904
+ };
3905
+ }
3845
3906
  function guardAddressSource(tool, call, userText, contacts, walletAddress) {
3846
3907
  if (tool.name !== "send_transfer") {
3847
3908
  return { verdict: "pass", gate: "address_source", tier: "safety" };
@@ -3897,6 +3958,7 @@ function createGuardRunnerState() {
3897
3958
  return {
3898
3959
  balanceTracker: new BalanceTracker(),
3899
3960
  retryTracker: new RetryTracker(),
3961
+ swapQuoteTracker: new SwapQuoteTracker(),
3900
3962
  lastHealthFactor: null
3901
3963
  };
3902
3964
  }
@@ -3955,6 +4017,9 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired,
3955
4017
  if (config.assetIntent !== false) {
3956
4018
  results.push(guardAssetIntent(tool, call, conversationContext.recentUserText));
3957
4019
  }
4020
+ if (config.swapPreview !== false) {
4021
+ results.push(guardSwapPreview(tool, call, state.swapQuoteTracker));
4022
+ }
3958
4023
  if (config.irreversibility !== false) {
3959
4024
  results.push(guardIrreversibility(tool, call, conversationContext.fullText));
3960
4025
  }
@@ -4021,6 +4086,15 @@ function updateGuardStateAfterToolResult(toolName, tool, input, result, isError,
4021
4086
  state.lastHealthFactor = hf;
4022
4087
  }
4023
4088
  }
4089
+ if (toolName === "swap_quote" && input && typeof input === "object") {
4090
+ const i = input;
4091
+ const from = typeof i.from === "string" ? i.from : "";
4092
+ const to = typeof i.to === "string" ? i.to : "";
4093
+ const amount = Number(i.amount ?? 0);
4094
+ if (from && to && amount > 0) {
4095
+ state.swapQuoteTracker.record({ from, to, amount });
4096
+ }
4097
+ }
4024
4098
  state.retryTracker.record(toolName, input, result);
4025
4099
  }
4026
4100
  function extractConversationText(messages) {