@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 +39 -0
- package/dist/index.js +75 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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) {
|