@t2000/engine 0.46.12 → 0.46.14
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 +46 -0
- package/dist/index.js +86 -2
- 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;
|
|
@@ -1421,6 +1460,13 @@ declare class EarlyToolDispatcher {
|
|
|
1421
1460
|
hasPending(): boolean;
|
|
1422
1461
|
/** List of call IDs that were early-dispatched. */
|
|
1423
1462
|
dispatchedIds(): Set<string>;
|
|
1463
|
+
/**
|
|
1464
|
+
* Look up the original tool input by `tool_use_id`. Used by the engine to
|
|
1465
|
+
* feed `updateGuardStateAfterToolResult` (which needs the call input to
|
|
1466
|
+
* record swap_quote → swap_execute pairing, etc.) for tools that were
|
|
1467
|
+
* dispatched here instead of going through the normal post-stream loop.
|
|
1468
|
+
*/
|
|
1469
|
+
getInputById(toolUseId: string): unknown | undefined;
|
|
1424
1470
|
/**
|
|
1425
1471
|
* Collect all results in original dispatch order.
|
|
1426
1472
|
* Yields `tool_result` events as each promise resolves.
|
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) {
|
|
@@ -4534,6 +4608,15 @@ var EarlyToolDispatcher = class {
|
|
|
4534
4608
|
dispatchedIds() {
|
|
4535
4609
|
return new Set(this.entries.map((e) => e.call.id));
|
|
4536
4610
|
}
|
|
4611
|
+
/**
|
|
4612
|
+
* Look up the original tool input by `tool_use_id`. Used by the engine to
|
|
4613
|
+
* feed `updateGuardStateAfterToolResult` (which needs the call input to
|
|
4614
|
+
* record swap_quote → swap_execute pairing, etc.) for tools that were
|
|
4615
|
+
* dispatched here instead of going through the normal post-stream loop.
|
|
4616
|
+
*/
|
|
4617
|
+
getInputById(toolUseId) {
|
|
4618
|
+
return this.entries.find((e) => e.call.id === toolUseId)?.call.input;
|
|
4619
|
+
}
|
|
4537
4620
|
/**
|
|
4538
4621
|
* Collect all results in original dispatch order.
|
|
4539
4622
|
* Yields `tool_result` events as each promise resolves.
|
|
@@ -5089,10 +5172,11 @@ ${recipeCtx}`;
|
|
|
5089
5172
|
}
|
|
5090
5173
|
}
|
|
5091
5174
|
const tool = findTool(this.tools, earlyEvent.toolName);
|
|
5175
|
+
const earlyInput = dispatcher.getInputById(earlyEvent.toolUseId) ?? null;
|
|
5092
5176
|
updateGuardStateAfterToolResult(
|
|
5093
5177
|
earlyEvent.toolName,
|
|
5094
5178
|
tool,
|
|
5095
|
-
|
|
5179
|
+
earlyInput,
|
|
5096
5180
|
earlyEvent.result,
|
|
5097
5181
|
earlyEvent.isError,
|
|
5098
5182
|
this.guardState
|