@t2000/engine 0.31.3 → 0.33.0
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 +454 -34
- package/dist/index.js +1146 -128
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { resolveSymbol, getDecimalsForCoinType, assertAllowedAsset, getSwapQuote, SUI_TYPE } from '@t2000/sdk';
|
|
3
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
3
6
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
7
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
8
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
@@ -7,15 +10,18 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
7
10
|
|
|
8
11
|
// src/tool.ts
|
|
9
12
|
function buildTool(opts) {
|
|
13
|
+
const isReadOnly = opts.isReadOnly ?? true;
|
|
10
14
|
return {
|
|
11
15
|
name: opts.name,
|
|
12
16
|
description: opts.description,
|
|
13
17
|
inputSchema: opts.inputSchema,
|
|
14
18
|
jsonSchema: opts.jsonSchema,
|
|
15
19
|
call: opts.call,
|
|
16
|
-
isReadOnly
|
|
17
|
-
isConcurrencySafe:
|
|
18
|
-
permissionLevel: opts.permissionLevel ?? (
|
|
20
|
+
isReadOnly,
|
|
21
|
+
isConcurrencySafe: isReadOnly,
|
|
22
|
+
permissionLevel: opts.permissionLevel ?? (isReadOnly ? "auto" : "confirm"),
|
|
23
|
+
flags: opts.flags ?? {},
|
|
24
|
+
preflight: opts.preflight
|
|
19
25
|
};
|
|
20
26
|
}
|
|
21
27
|
function toolsToDefinitions(tools) {
|
|
@@ -152,6 +158,45 @@ async function executeSingleTool(tool, call, context) {
|
|
|
152
158
|
const result = await tool.call(parsed.data, context);
|
|
153
159
|
return { data: result.data, isError: false };
|
|
154
160
|
}
|
|
161
|
+
|
|
162
|
+
// src/tool-flags.ts
|
|
163
|
+
var TOOL_FLAGS = {
|
|
164
|
+
// Write tools — financial
|
|
165
|
+
save_deposit: { mutating: true, requiresBalance: true },
|
|
166
|
+
withdraw: { mutating: true, affectsHealth: true },
|
|
167
|
+
send_transfer: { mutating: true, requiresBalance: true, irreversible: true },
|
|
168
|
+
swap_execute: { mutating: true, requiresBalance: true },
|
|
169
|
+
borrow: { mutating: true, affectsHealth: true },
|
|
170
|
+
repay_debt: { mutating: true, requiresBalance: true },
|
|
171
|
+
claim_rewards: { mutating: true },
|
|
172
|
+
volo_stake: { mutating: true, requiresBalance: true },
|
|
173
|
+
volo_unstake: { mutating: true },
|
|
174
|
+
// Write tools — pay / services
|
|
175
|
+
pay_api: { mutating: true, requiresBalance: true, costAware: true, producesArtifact: true, maxRetries: 1 },
|
|
176
|
+
// Write tools — lightweight (no financial guards)
|
|
177
|
+
save_contact: {},
|
|
178
|
+
create_schedule: { mutating: true },
|
|
179
|
+
cancel_schedule: { mutating: true },
|
|
180
|
+
// Allowance tools — API mutations disguised as reads
|
|
181
|
+
toggle_allowance: { mutating: true },
|
|
182
|
+
update_daily_limit: { mutating: true },
|
|
183
|
+
update_permissions: { mutating: true },
|
|
184
|
+
// Receive tools — create/cancel mutate server state
|
|
185
|
+
create_payment_link: { mutating: true },
|
|
186
|
+
cancel_payment_link: { mutating: true },
|
|
187
|
+
create_invoice: { mutating: true },
|
|
188
|
+
cancel_invoice: { mutating: true }
|
|
189
|
+
};
|
|
190
|
+
function applyToolFlags(tools) {
|
|
191
|
+
return tools.map((tool) => {
|
|
192
|
+
const flags = TOOL_FLAGS[tool.name];
|
|
193
|
+
if (!flags) return tool;
|
|
194
|
+
return { ...tool, flags: { ...tool.flags, ...flags } };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function getToolFlags(name) {
|
|
198
|
+
return TOOL_FLAGS[name] ?? {};
|
|
199
|
+
}
|
|
155
200
|
var SUI_MAINNET_URL = "https://fullnode.mainnet.sui.io:443";
|
|
156
201
|
var EXTRA_COINS = {
|
|
157
202
|
"0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN": { symbol: "USDT", decimals: 6 }
|
|
@@ -1119,6 +1164,13 @@ var saveDepositTool = buildTool({
|
|
|
1119
1164
|
},
|
|
1120
1165
|
isReadOnly: false,
|
|
1121
1166
|
permissionLevel: "confirm",
|
|
1167
|
+
flags: { mutating: true, requiresBalance: true },
|
|
1168
|
+
preflight: (input) => {
|
|
1169
|
+
if (input.asset && input.asset.toUpperCase() !== "USDC") {
|
|
1170
|
+
return { valid: false, error: `Only USDC deposits are supported. Got: "${input.asset}"` };
|
|
1171
|
+
}
|
|
1172
|
+
return { valid: true };
|
|
1173
|
+
},
|
|
1122
1174
|
async call(input, context) {
|
|
1123
1175
|
assertAllowedAsset("save", input.asset);
|
|
1124
1176
|
const agent = requireAgent(context);
|
|
@@ -1160,6 +1212,7 @@ var withdrawTool = buildTool({
|
|
|
1160
1212
|
},
|
|
1161
1213
|
isReadOnly: false,
|
|
1162
1214
|
permissionLevel: "confirm",
|
|
1215
|
+
flags: { mutating: true, affectsHealth: true },
|
|
1163
1216
|
async call(input, context) {
|
|
1164
1217
|
const agent = requireAgent(context);
|
|
1165
1218
|
const result = await agent.withdraw({
|
|
@@ -1207,6 +1260,16 @@ var sendTransferTool = buildTool({
|
|
|
1207
1260
|
},
|
|
1208
1261
|
isReadOnly: false,
|
|
1209
1262
|
permissionLevel: "confirm",
|
|
1263
|
+
flags: { mutating: true, requiresBalance: true, irreversible: true },
|
|
1264
|
+
preflight: (input) => {
|
|
1265
|
+
if (input.to.startsWith("0x") && !/^0x[a-fA-F0-9]{64}$/.test(input.to)) {
|
|
1266
|
+
return { valid: false, error: `Invalid Sui address format: "${input.to}". Must be 0x followed by 64 hex characters.` };
|
|
1267
|
+
}
|
|
1268
|
+
if (input.amount <= 0) {
|
|
1269
|
+
return { valid: false, error: "Amount must be positive." };
|
|
1270
|
+
}
|
|
1271
|
+
return { valid: true };
|
|
1272
|
+
},
|
|
1210
1273
|
async call(input, context) {
|
|
1211
1274
|
const agent = requireAgent(context);
|
|
1212
1275
|
const result = await agent.send({ to: input.to, amount: input.amount });
|
|
@@ -1249,6 +1312,13 @@ var borrowTool = buildTool({
|
|
|
1249
1312
|
},
|
|
1250
1313
|
isReadOnly: false,
|
|
1251
1314
|
permissionLevel: "confirm",
|
|
1315
|
+
flags: { mutating: true, affectsHealth: true },
|
|
1316
|
+
preflight: (input) => {
|
|
1317
|
+
if (input.asset && input.asset.toUpperCase() !== "USDC") {
|
|
1318
|
+
return { valid: false, error: `Only USDC borrows are supported. Got: "${input.asset}"` };
|
|
1319
|
+
}
|
|
1320
|
+
return { valid: true };
|
|
1321
|
+
},
|
|
1252
1322
|
async call(input, context) {
|
|
1253
1323
|
assertAllowedAsset("borrow", input.asset);
|
|
1254
1324
|
const agent = requireAgent(context);
|
|
@@ -1283,6 +1353,7 @@ var repayDebtTool = buildTool({
|
|
|
1283
1353
|
},
|
|
1284
1354
|
isReadOnly: false,
|
|
1285
1355
|
permissionLevel: "confirm",
|
|
1356
|
+
flags: { mutating: true, requiresBalance: true },
|
|
1286
1357
|
async call(input, context) {
|
|
1287
1358
|
const agent = requireAgent(context);
|
|
1288
1359
|
const result = await agent.repay({ amount: input.amount });
|
|
@@ -1305,6 +1376,7 @@ var claimRewardsTool = buildTool({
|
|
|
1305
1376
|
jsonSchema: { type: "object", properties: {}, required: [] },
|
|
1306
1377
|
isReadOnly: false,
|
|
1307
1378
|
permissionLevel: "confirm",
|
|
1379
|
+
flags: { mutating: true },
|
|
1308
1380
|
async call(_input, context) {
|
|
1309
1381
|
const agent = requireAgent(context);
|
|
1310
1382
|
const result = await agent.claimRewards();
|
|
@@ -1379,6 +1451,28 @@ Always use ISO-3166 country codes (GB not UK, US not USA). A return address ("fr
|
|
|
1379
1451
|
},
|
|
1380
1452
|
isReadOnly: false,
|
|
1381
1453
|
permissionLevel: "confirm",
|
|
1454
|
+
flags: { mutating: true, requiresBalance: true, costAware: true, producesArtifact: true, maxRetries: 1 },
|
|
1455
|
+
preflight: (input) => {
|
|
1456
|
+
if (!input.url.startsWith(MPP_GATEWAY)) {
|
|
1457
|
+
return { valid: false, error: `URL must start with ${MPP_GATEWAY}. Got: "${input.url}"` };
|
|
1458
|
+
}
|
|
1459
|
+
if (input.body) {
|
|
1460
|
+
try {
|
|
1461
|
+
JSON.parse(input.body);
|
|
1462
|
+
} catch {
|
|
1463
|
+
return { valid: false, error: "body must be valid JSON." };
|
|
1464
|
+
}
|
|
1465
|
+
if (input.url.includes("lob/")) {
|
|
1466
|
+
const body = JSON.parse(input.body);
|
|
1467
|
+
const to = body.to;
|
|
1468
|
+
const country = to?.address_country;
|
|
1469
|
+
if (typeof country === "string" && country.length !== 2) {
|
|
1470
|
+
return { valid: false, error: `Country must be ISO-3166 2-letter code (got "${country}")` };
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return { valid: true };
|
|
1475
|
+
},
|
|
1382
1476
|
async call(input, context) {
|
|
1383
1477
|
const agent = requireAgent(context);
|
|
1384
1478
|
const result = await agent.pay({
|
|
@@ -1480,6 +1574,13 @@ var swapExecuteTool = buildTool({
|
|
|
1480
1574
|
},
|
|
1481
1575
|
isReadOnly: false,
|
|
1482
1576
|
permissionLevel: "confirm",
|
|
1577
|
+
flags: { mutating: true, requiresBalance: true },
|
|
1578
|
+
preflight: (input) => {
|
|
1579
|
+
if (input.from.toLowerCase() === input.to.toLowerCase()) {
|
|
1580
|
+
return { valid: false, error: `Cannot swap ${input.from} to itself.` };
|
|
1581
|
+
}
|
|
1582
|
+
return { valid: true };
|
|
1583
|
+
},
|
|
1483
1584
|
async call(input, context) {
|
|
1484
1585
|
const agent = requireAgent(context);
|
|
1485
1586
|
const result = await agent.swap({
|
|
@@ -1554,6 +1655,7 @@ var voloStakeTool = buildTool({
|
|
|
1554
1655
|
},
|
|
1555
1656
|
isReadOnly: false,
|
|
1556
1657
|
permissionLevel: "confirm",
|
|
1658
|
+
flags: { mutating: true, requiresBalance: true },
|
|
1557
1659
|
async call(input, context) {
|
|
1558
1660
|
const agent = requireAgent(context);
|
|
1559
1661
|
const result = await agent.stakeVSui({ amount: input.amount });
|
|
@@ -1584,6 +1686,7 @@ var voloUnstakeTool = buildTool({
|
|
|
1584
1686
|
},
|
|
1585
1687
|
isReadOnly: false,
|
|
1586
1688
|
permissionLevel: "confirm",
|
|
1689
|
+
flags: { mutating: true },
|
|
1587
1690
|
async call(input, context) {
|
|
1588
1691
|
const agent = requireAgent(context);
|
|
1589
1692
|
const result = await agent.unstakeVSui({ amount: input.amount });
|
|
@@ -3416,7 +3519,7 @@ var WRITE_TOOLS = [
|
|
|
3416
3519
|
cancelScheduleTool
|
|
3417
3520
|
];
|
|
3418
3521
|
function getDefaultTools() {
|
|
3419
|
-
return [...READ_TOOLS, ...WRITE_TOOLS];
|
|
3522
|
+
return applyToolFlags([...READ_TOOLS, ...WRITE_TOOLS]);
|
|
3420
3523
|
}
|
|
3421
3524
|
|
|
3422
3525
|
// src/prompt.ts
|
|
@@ -3517,6 +3620,511 @@ var CostTracker = class {
|
|
|
3517
3620
|
}
|
|
3518
3621
|
};
|
|
3519
3622
|
|
|
3623
|
+
// src/guards.ts
|
|
3624
|
+
var DEFAULT_GUARD_CONFIG = {
|
|
3625
|
+
balanceValidation: true,
|
|
3626
|
+
healthFactor: { warnBelow: 2, blockBelow: 1.5 },
|
|
3627
|
+
largeTransfer: { warnAbove: 50, strongWarnAbove: 500 },
|
|
3628
|
+
slippage: true,
|
|
3629
|
+
staleData: true,
|
|
3630
|
+
irreversibility: true,
|
|
3631
|
+
artifactPreview: true,
|
|
3632
|
+
costWarning: true,
|
|
3633
|
+
retryProtection: true,
|
|
3634
|
+
inputValidation: true
|
|
3635
|
+
};
|
|
3636
|
+
var BalanceTracker = class {
|
|
3637
|
+
lastBalanceAt = 0;
|
|
3638
|
+
lastWriteAt = 0;
|
|
3639
|
+
recordRead() {
|
|
3640
|
+
this.lastBalanceAt = Date.now();
|
|
3641
|
+
}
|
|
3642
|
+
recordWrite() {
|
|
3643
|
+
this.lastWriteAt = Date.now();
|
|
3644
|
+
}
|
|
3645
|
+
isStale() {
|
|
3646
|
+
return this.lastWriteAt > this.lastBalanceAt;
|
|
3647
|
+
}
|
|
3648
|
+
hasEverRead() {
|
|
3649
|
+
return this.lastBalanceAt > 0;
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
var BALANCE_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
3653
|
+
"balance_check",
|
|
3654
|
+
"savings_info",
|
|
3655
|
+
"health_check"
|
|
3656
|
+
]);
|
|
3657
|
+
var RetryTracker = class {
|
|
3658
|
+
executed = /* @__PURE__ */ new Map();
|
|
3659
|
+
key(toolName, input) {
|
|
3660
|
+
const url = input?.url ?? "";
|
|
3661
|
+
return `${toolName}:${url}`;
|
|
3662
|
+
}
|
|
3663
|
+
record(toolName, input, result) {
|
|
3664
|
+
const r = result;
|
|
3665
|
+
if (r?.paymentConfirmed || r?.doNotRetry) {
|
|
3666
|
+
this.executed.set(this.key(toolName, input), { result, paidAt: Date.now() });
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
isBlocked(toolName, input) {
|
|
3670
|
+
const prev = this.executed.get(this.key(toolName, input));
|
|
3671
|
+
if (!prev) return { blocked: false };
|
|
3672
|
+
return { blocked: true, previousResult: prev.result };
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
function guardRetryProtection(tool, call, retryTracker) {
|
|
3676
|
+
const check = retryTracker.isBlocked(tool.name, call.input);
|
|
3677
|
+
if (check.blocked) {
|
|
3678
|
+
return {
|
|
3679
|
+
verdict: "block",
|
|
3680
|
+
gate: "retry_blocked",
|
|
3681
|
+
tier: "safety",
|
|
3682
|
+
message: `Blocked: ${tool.name} was already called and payment was confirmed. Do not retry.`
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
return { verdict: "pass", gate: "retry_blocked", tier: "safety" };
|
|
3686
|
+
}
|
|
3687
|
+
function guardIrreversibility(tool, _call, conversationText) {
|
|
3688
|
+
if (!tool.flags.irreversible) {
|
|
3689
|
+
return { verdict: "pass", gate: "irreversibility", tier: "safety" };
|
|
3690
|
+
}
|
|
3691
|
+
const hasPreview = /preview|here.s what|confirm.*send|looks? good/i.test(conversationText);
|
|
3692
|
+
if (hasPreview) {
|
|
3693
|
+
return { verdict: "pass", gate: "irreversibility", tier: "safety" };
|
|
3694
|
+
}
|
|
3695
|
+
return {
|
|
3696
|
+
verdict: "hint",
|
|
3697
|
+
gate: "irreversibility",
|
|
3698
|
+
tier: "safety",
|
|
3699
|
+
message: "This action is irreversible. Show a preview and ask the user to confirm before proceeding."
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
function guardBalanceValidation(tool, _call, balanceTracker) {
|
|
3703
|
+
if (!tool.flags.requiresBalance) {
|
|
3704
|
+
return { verdict: "pass", gate: "balance_required", tier: "financial" };
|
|
3705
|
+
}
|
|
3706
|
+
if (!balanceTracker.hasEverRead()) {
|
|
3707
|
+
return {
|
|
3708
|
+
verdict: "hint",
|
|
3709
|
+
gate: "balance_required",
|
|
3710
|
+
tier: "financial",
|
|
3711
|
+
message: "Balance has not been checked this session. Call balance_check first to verify sufficient funds."
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
if (balanceTracker.isStale()) {
|
|
3715
|
+
return {
|
|
3716
|
+
verdict: "hint",
|
|
3717
|
+
gate: "balance_required",
|
|
3718
|
+
tier: "financial",
|
|
3719
|
+
message: "Balance data is stale (a write action occurred since last check). Call balance_check first to verify sufficient funds."
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
return { verdict: "pass", gate: "balance_required", tier: "financial" };
|
|
3723
|
+
}
|
|
3724
|
+
function guardHealthFactor(tool, _call, lastHealthFactor, config) {
|
|
3725
|
+
if (!tool.flags.affectsHealth) {
|
|
3726
|
+
return { verdict: "pass", gate: "health_factor", tier: "financial" };
|
|
3727
|
+
}
|
|
3728
|
+
if (lastHealthFactor === null) {
|
|
3729
|
+
return {
|
|
3730
|
+
verdict: "hint",
|
|
3731
|
+
gate: "health_factor",
|
|
3732
|
+
tier: "financial",
|
|
3733
|
+
message: "Health factor has not been checked this session. Call health_check before this action."
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
if (lastHealthFactor < config.blockBelow) {
|
|
3737
|
+
return {
|
|
3738
|
+
verdict: "block",
|
|
3739
|
+
gate: "health_factor",
|
|
3740
|
+
tier: "financial",
|
|
3741
|
+
message: `Health factor is ${lastHealthFactor.toFixed(2)} \u2014 this action risks liquidation. Refusing.`
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
if (lastHealthFactor < config.warnBelow) {
|
|
3745
|
+
return {
|
|
3746
|
+
verdict: "warn",
|
|
3747
|
+
gate: "health_factor",
|
|
3748
|
+
tier: "financial",
|
|
3749
|
+
message: `Health factor is ${lastHealthFactor.toFixed(2)} \u2014 this action may reduce it further.`
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
return { verdict: "pass", gate: "health_factor", tier: "financial" };
|
|
3753
|
+
}
|
|
3754
|
+
function guardLargeTransfer(tool, call, config) {
|
|
3755
|
+
if (tool.name !== "send_transfer") {
|
|
3756
|
+
return { verdict: "pass", gate: "large_transfer", tier: "financial" };
|
|
3757
|
+
}
|
|
3758
|
+
const input = call.input;
|
|
3759
|
+
const amount = Number(input.amount ?? 0);
|
|
3760
|
+
if (!amount || amount <= 0) {
|
|
3761
|
+
return { verdict: "pass", gate: "large_transfer", tier: "financial" };
|
|
3762
|
+
}
|
|
3763
|
+
const recipient = String(input.recipient ?? input.to ?? "");
|
|
3764
|
+
const shortAddr = recipient.length > 10 ? `${recipient.slice(0, 6)}...${recipient.slice(-4)}` : recipient;
|
|
3765
|
+
if (amount > config.strongWarnAbove) {
|
|
3766
|
+
return {
|
|
3767
|
+
verdict: "warn",
|
|
3768
|
+
gate: "large_transfer",
|
|
3769
|
+
tier: "financial",
|
|
3770
|
+
message: `High-value transfer ($${amount}). Double-check the address: ${shortAddr}`
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
if (amount > config.warnAbove) {
|
|
3774
|
+
return {
|
|
3775
|
+
verdict: "hint",
|
|
3776
|
+
gate: "large_transfer",
|
|
3777
|
+
tier: "financial",
|
|
3778
|
+
message: `This is a large transfer ($${amount}). Verify the recipient address.`
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
return { verdict: "pass", gate: "large_transfer", tier: "financial" };
|
|
3782
|
+
}
|
|
3783
|
+
function guardSlippage(tool, _call, lastAssistantText) {
|
|
3784
|
+
if (tool.name !== "swap_execute") {
|
|
3785
|
+
return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
|
|
3786
|
+
}
|
|
3787
|
+
const hasEstimate = /~?\$?[\d,]+\.?\d*\s*(SUI|USDC|USDT|WETH)/i.test(lastAssistantText) || /approximately|≈|about|expect|receive/i.test(lastAssistantText);
|
|
3788
|
+
if (hasEstimate) {
|
|
3789
|
+
return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
|
|
3790
|
+
}
|
|
3791
|
+
return {
|
|
3792
|
+
verdict: "hint",
|
|
3793
|
+
gate: "slippage_warning",
|
|
3794
|
+
tier: "financial",
|
|
3795
|
+
message: "State the expected output amount to the user before executing the swap."
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
function guardCostWarning(tool, _call, conversationText) {
|
|
3799
|
+
if (!tool.flags.costAware) {
|
|
3800
|
+
return { verdict: "pass", gate: "cost_warning", tier: "ux" };
|
|
3801
|
+
}
|
|
3802
|
+
const hasCostMention = /\$\d+\.?\d*|cost|fee|charge|price|pay/i.test(conversationText);
|
|
3803
|
+
if (hasCostMention) {
|
|
3804
|
+
return { verdict: "pass", gate: "cost_warning", tier: "ux" };
|
|
3805
|
+
}
|
|
3806
|
+
return {
|
|
3807
|
+
verdict: "hint",
|
|
3808
|
+
gate: "cost_warning",
|
|
3809
|
+
tier: "ux",
|
|
3810
|
+
message: "This action has a monetary cost. Confirm the user is aware before proceeding."
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
3813
|
+
function guardArtifactPreview(result) {
|
|
3814
|
+
if (!result || typeof result !== "object") return null;
|
|
3815
|
+
const r = result;
|
|
3816
|
+
const hasImage = typeof r.url === "string" && /\.(png|jpg|jpeg|webp|gif|svg)(\?|$)/i.test(r.url) || Array.isArray(r.images) && r.images.length > 0 || typeof r.image_url === "string";
|
|
3817
|
+
const hasPdf = typeof r.url === "string" && /\.pdf(\?|$)/i.test(r.url);
|
|
3818
|
+
if (hasImage || hasPdf) {
|
|
3819
|
+
return {
|
|
3820
|
+
_gate: "artifact_preview",
|
|
3821
|
+
_hint: "Show this to the user before proceeding. Output as ."
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
return null;
|
|
3825
|
+
}
|
|
3826
|
+
function guardStaleData(toolFlags) {
|
|
3827
|
+
if (!toolFlags.mutating) return null;
|
|
3828
|
+
return {
|
|
3829
|
+
_gate: "stale_data",
|
|
3830
|
+
_hint: "A write action just completed. The balance snapshot is outdated. Do NOT calculate new balances from old data \u2014 call balance_check for fresh numbers, or use only the data returned by the write tool."
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
function createGuardRunnerState() {
|
|
3834
|
+
return {
|
|
3835
|
+
balanceTracker: new BalanceTracker(),
|
|
3836
|
+
retryTracker: new RetryTracker(),
|
|
3837
|
+
lastHealthFactor: null
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
function runGuards(tool, call, state, config, conversationContext) {
|
|
3841
|
+
const results = [];
|
|
3842
|
+
const now = Date.now();
|
|
3843
|
+
if (config.inputValidation !== false && tool.preflight) {
|
|
3844
|
+
const check = tool.preflight(call.input);
|
|
3845
|
+
if (!check.valid) {
|
|
3846
|
+
const event = {
|
|
3847
|
+
timestamp: now,
|
|
3848
|
+
toolName: tool.name,
|
|
3849
|
+
toolUseId: call.id,
|
|
3850
|
+
gate: "input_validation",
|
|
3851
|
+
verdict: "block",
|
|
3852
|
+
tier: "safety",
|
|
3853
|
+
message: check.error
|
|
3854
|
+
};
|
|
3855
|
+
return {
|
|
3856
|
+
blocked: true,
|
|
3857
|
+
blockReason: check.error,
|
|
3858
|
+
blockGate: "input_validation",
|
|
3859
|
+
injections: [],
|
|
3860
|
+
events: [event]
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
if (config.retryProtection !== false) {
|
|
3865
|
+
results.push(guardRetryProtection(tool, call, state.retryTracker));
|
|
3866
|
+
}
|
|
3867
|
+
if (config.irreversibility !== false) {
|
|
3868
|
+
results.push(guardIrreversibility(tool, call, conversationContext.fullText));
|
|
3869
|
+
}
|
|
3870
|
+
if (config.balanceValidation !== false) {
|
|
3871
|
+
results.push(guardBalanceValidation(tool, call, state.balanceTracker));
|
|
3872
|
+
}
|
|
3873
|
+
if (config.healthFactor) {
|
|
3874
|
+
results.push(guardHealthFactor(tool, call, state.lastHealthFactor, config.healthFactor));
|
|
3875
|
+
}
|
|
3876
|
+
if (config.largeTransfer) {
|
|
3877
|
+
results.push(guardLargeTransfer(tool, call, config.largeTransfer));
|
|
3878
|
+
}
|
|
3879
|
+
if (config.slippage !== false) {
|
|
3880
|
+
results.push(guardSlippage(tool, call, conversationContext.lastAssistantText));
|
|
3881
|
+
}
|
|
3882
|
+
if (config.costWarning !== false) {
|
|
3883
|
+
results.push(guardCostWarning(tool, call, conversationContext.fullText));
|
|
3884
|
+
}
|
|
3885
|
+
const events = results.filter((r) => r.verdict !== "pass").map((r) => ({
|
|
3886
|
+
timestamp: now,
|
|
3887
|
+
toolName: tool.name,
|
|
3888
|
+
toolUseId: call.id,
|
|
3889
|
+
gate: r.gate,
|
|
3890
|
+
verdict: r.verdict,
|
|
3891
|
+
tier: r.tier,
|
|
3892
|
+
message: r.message
|
|
3893
|
+
}));
|
|
3894
|
+
const block = results.find((r) => r.verdict === "block");
|
|
3895
|
+
if (block) {
|
|
3896
|
+
return {
|
|
3897
|
+
blocked: true,
|
|
3898
|
+
blockReason: block.message ?? `Blocked by ${block.gate}`,
|
|
3899
|
+
blockGate: block.gate,
|
|
3900
|
+
injections: [],
|
|
3901
|
+
events
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
const injections = results.filter((r) => r.verdict === "hint" || r.verdict === "warn").map((r) => ({
|
|
3905
|
+
_gate: r.gate,
|
|
3906
|
+
...r.verdict === "hint" ? { _hint: r.message } : { _warning: r.message }
|
|
3907
|
+
}));
|
|
3908
|
+
return { blocked: false, injections, events };
|
|
3909
|
+
}
|
|
3910
|
+
function updateGuardStateAfterToolResult(toolName, tool, input, result, isError, state) {
|
|
3911
|
+
if (isError) return;
|
|
3912
|
+
if (BALANCE_READ_TOOLS.has(toolName)) {
|
|
3913
|
+
state.balanceTracker.recordRead();
|
|
3914
|
+
}
|
|
3915
|
+
if (tool?.flags.mutating) {
|
|
3916
|
+
state.balanceTracker.recordWrite();
|
|
3917
|
+
}
|
|
3918
|
+
if (toolName === "health_check" && result && typeof result === "object") {
|
|
3919
|
+
const r = result;
|
|
3920
|
+
const hf = Number(r.healthFactor ?? r.health_factor ?? r.hf);
|
|
3921
|
+
if (!isNaN(hf) && hf > 0) {
|
|
3922
|
+
state.lastHealthFactor = hf;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
state.retryTracker.record(toolName, input, result);
|
|
3926
|
+
}
|
|
3927
|
+
function extractConversationText(messages) {
|
|
3928
|
+
const textParts = [];
|
|
3929
|
+
let lastAssistantText = "";
|
|
3930
|
+
for (const msg of messages) {
|
|
3931
|
+
if (!Array.isArray(msg.content)) continue;
|
|
3932
|
+
for (const block of msg.content) {
|
|
3933
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
3934
|
+
textParts.push(block.text);
|
|
3935
|
+
if (msg.role === "assistant") {
|
|
3936
|
+
lastAssistantText = block.text;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return {
|
|
3942
|
+
fullText: textParts.join("\n"),
|
|
3943
|
+
lastAssistantText
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// src/context.ts
|
|
3948
|
+
var CHARS_PER_TOKEN = 4;
|
|
3949
|
+
var DEFAULT_CONTEXT_LIMIT = 2e5;
|
|
3950
|
+
function estimateTokens(messages) {
|
|
3951
|
+
let chars = 0;
|
|
3952
|
+
for (const msg of messages) {
|
|
3953
|
+
for (const block of msg.content) {
|
|
3954
|
+
chars += blockCharCount(block);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
3958
|
+
}
|
|
3959
|
+
function blockCharCount(block) {
|
|
3960
|
+
switch (block.type) {
|
|
3961
|
+
case "text":
|
|
3962
|
+
return block.text.length;
|
|
3963
|
+
case "thinking":
|
|
3964
|
+
return block.thinking.length;
|
|
3965
|
+
case "redacted_thinking":
|
|
3966
|
+
return block.data.length;
|
|
3967
|
+
case "tool_use":
|
|
3968
|
+
return block.name.length + JSON.stringify(block.input).length;
|
|
3969
|
+
case "tool_result":
|
|
3970
|
+
return block.content.length;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
var ContextBudget = class {
|
|
3974
|
+
estimatedTokens = 0;
|
|
3975
|
+
contextLimit;
|
|
3976
|
+
compactThreshold;
|
|
3977
|
+
warnThreshold;
|
|
3978
|
+
constructor(config = {}) {
|
|
3979
|
+
this.contextLimit = config.contextLimit ?? DEFAULT_CONTEXT_LIMIT;
|
|
3980
|
+
this.compactThreshold = config.compactThreshold ?? 0.85;
|
|
3981
|
+
this.warnThreshold = config.warnThreshold ?? 0.7;
|
|
3982
|
+
}
|
|
3983
|
+
/** Update with actual input_tokens from the API usage event. */
|
|
3984
|
+
update(inputTokens) {
|
|
3985
|
+
this.estimatedTokens = inputTokens;
|
|
3986
|
+
}
|
|
3987
|
+
/** True when the session should be compacted (at 85% of context limit). */
|
|
3988
|
+
shouldCompact() {
|
|
3989
|
+
return this.estimatedTokens >= this.contextLimit * this.compactThreshold;
|
|
3990
|
+
}
|
|
3991
|
+
/** True when nearing the limit (at 70% of context limit). */
|
|
3992
|
+
shouldWarn() {
|
|
3993
|
+
return this.estimatedTokens >= this.contextLimit * this.warnThreshold;
|
|
3994
|
+
}
|
|
3995
|
+
/** Current token count. */
|
|
3996
|
+
get tokens() {
|
|
3997
|
+
return this.estimatedTokens;
|
|
3998
|
+
}
|
|
3999
|
+
/** Remaining tokens before compaction triggers. */
|
|
4000
|
+
get remaining() {
|
|
4001
|
+
return Math.max(0, Math.floor(this.contextLimit * this.compactThreshold) - this.estimatedTokens);
|
|
4002
|
+
}
|
|
4003
|
+
/** Usage ratio (0..1). */
|
|
4004
|
+
get usage() {
|
|
4005
|
+
return this.estimatedTokens / this.contextLimit;
|
|
4006
|
+
}
|
|
4007
|
+
reset() {
|
|
4008
|
+
this.estimatedTokens = 0;
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
4011
|
+
async function compactMessages(messages, opts = {}) {
|
|
4012
|
+
const maxTokens = opts.maxTokens ?? 1e5;
|
|
4013
|
+
const keepRecent = opts.keepRecentCount ?? 8;
|
|
4014
|
+
const systemTokens = opts.systemPromptTokens ?? 500;
|
|
4015
|
+
const budget = maxTokens - systemTokens;
|
|
4016
|
+
if (messages.length === 0) return [];
|
|
4017
|
+
const mutable = messages.map((m) => ({
|
|
4018
|
+
role: m.role,
|
|
4019
|
+
content: m.content.map((b) => ({ ...b }))
|
|
4020
|
+
}));
|
|
4021
|
+
if (estimateTokens(mutable) <= budget) return mutable;
|
|
4022
|
+
const splitIdx = Math.max(0, mutable.length - keepRecent);
|
|
4023
|
+
const oldMessages = mutable.slice(0, splitIdx);
|
|
4024
|
+
const recent = mutable.slice(splitIdx);
|
|
4025
|
+
if (opts.summarizer && oldMessages.length > 0) {
|
|
4026
|
+
const strippedOld = stripThinkingBlocks(oldMessages);
|
|
4027
|
+
try {
|
|
4028
|
+
const summary = await opts.summarizer(strippedOld);
|
|
4029
|
+
const summaryMessages = [
|
|
4030
|
+
{ role: "user", content: [{ type: "text", text: `[Session summary: ${summary}]` }] },
|
|
4031
|
+
{ role: "assistant", content: [{ type: "text", text: "Understood. I have the context from our earlier conversation." }] }
|
|
4032
|
+
];
|
|
4033
|
+
const withSummary = [...summaryMessages, ...recent];
|
|
4034
|
+
if (estimateTokens(withSummary) <= budget) return sanitizeMessages(withSummary);
|
|
4035
|
+
} catch {
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
for (let i = 0; i < splitIdx; i++) {
|
|
4039
|
+
mutable[i].content = mutable[i].content.map((block) => {
|
|
4040
|
+
if (block.type === "tool_result" && block.content.length > 200) {
|
|
4041
|
+
return {
|
|
4042
|
+
...block,
|
|
4043
|
+
content: truncateToolResult(block.content)
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
return block;
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
for (let i = 0; i < splitIdx; i++) {
|
|
4050
|
+
mutable[i].content = mutable[i].content.filter(
|
|
4051
|
+
(b) => b.type !== "thinking" && b.type !== "redacted_thinking"
|
|
4052
|
+
);
|
|
4053
|
+
}
|
|
4054
|
+
if (estimateTokens(mutable) <= budget) return mutable;
|
|
4055
|
+
const first = mutable[0];
|
|
4056
|
+
const recentFromMutable = mutable.slice(splitIdx);
|
|
4057
|
+
const oldSection = mutable.slice(1, splitIdx);
|
|
4058
|
+
while (oldSection.length > 0 && estimateTokens([first, ...oldSection, ...recentFromMutable]) > budget) {
|
|
4059
|
+
oldSection.shift();
|
|
4060
|
+
}
|
|
4061
|
+
const compacted = [first, ...oldSection, ...recentFromMutable];
|
|
4062
|
+
if (estimateTokens(compacted) > budget) {
|
|
4063
|
+
for (const msg of compacted) {
|
|
4064
|
+
msg.content = msg.content.map((block) => {
|
|
4065
|
+
if (block.type === "tool_result" && block.content.length > 100) {
|
|
4066
|
+
return { ...block, content: truncateToolResult(block.content) };
|
|
4067
|
+
}
|
|
4068
|
+
return block;
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
return sanitizeMessages(compacted);
|
|
4073
|
+
}
|
|
4074
|
+
function stripThinkingBlocks(messages) {
|
|
4075
|
+
return messages.map((m) => ({
|
|
4076
|
+
...m,
|
|
4077
|
+
content: m.content.filter(
|
|
4078
|
+
(b) => b.type !== "thinking" && b.type !== "redacted_thinking"
|
|
4079
|
+
)
|
|
4080
|
+
})).filter((m) => m.content.length > 0);
|
|
4081
|
+
}
|
|
4082
|
+
function sanitizeMessages(messages) {
|
|
4083
|
+
const toolUseIds = /* @__PURE__ */ new Set();
|
|
4084
|
+
const toolResultIds = /* @__PURE__ */ new Set();
|
|
4085
|
+
for (const msg of messages) {
|
|
4086
|
+
for (const block of msg.content) {
|
|
4087
|
+
if (block.type === "tool_use") toolUseIds.add(block.id);
|
|
4088
|
+
if (block.type === "tool_result") toolResultIds.add(block.toolUseId);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
return messages.map((msg) => {
|
|
4092
|
+
const filtered = msg.content.filter((block) => {
|
|
4093
|
+
if (block.type === "tool_result") return toolUseIds.has(block.toolUseId);
|
|
4094
|
+
if (block.type === "tool_use") return toolResultIds.has(block.id);
|
|
4095
|
+
return true;
|
|
4096
|
+
});
|
|
4097
|
+
if (filtered.length === 0) return null;
|
|
4098
|
+
return { ...msg, content: filtered };
|
|
4099
|
+
}).filter((m) => m !== null);
|
|
4100
|
+
}
|
|
4101
|
+
function truncateToolResult(content) {
|
|
4102
|
+
try {
|
|
4103
|
+
const parsed = JSON.parse(content);
|
|
4104
|
+
if (parsed.error) {
|
|
4105
|
+
return JSON.stringify({ error: parsed.error });
|
|
4106
|
+
}
|
|
4107
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
4108
|
+
const summary = {};
|
|
4109
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
4110
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
4111
|
+
summary[key] = value;
|
|
4112
|
+
} else if (typeof value === "string") {
|
|
4113
|
+
summary[key] = value.length > 50 ? value.slice(0, 50) + "\u2026" : value;
|
|
4114
|
+
} else if (Array.isArray(value)) {
|
|
4115
|
+
summary[key] = `[${value.length} items]`;
|
|
4116
|
+
} else {
|
|
4117
|
+
summary[key] = "{\u2026}";
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
return JSON.stringify(summary);
|
|
4121
|
+
}
|
|
4122
|
+
return content.slice(0, 100);
|
|
4123
|
+
} catch {
|
|
4124
|
+
return content.slice(0, 100);
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
|
|
3520
4128
|
// src/engine.ts
|
|
3521
4129
|
var DEFAULT_MAX_TURNS = 10;
|
|
3522
4130
|
var DEFAULT_MAX_TOKENS = 4096;
|
|
@@ -3529,6 +4137,8 @@ var QueryEngine = class {
|
|
|
3529
4137
|
maxTokens;
|
|
3530
4138
|
temperature;
|
|
3531
4139
|
toolChoice;
|
|
4140
|
+
thinking;
|
|
4141
|
+
outputConfig;
|
|
3532
4142
|
agent;
|
|
3533
4143
|
mcpManager;
|
|
3534
4144
|
walletAddress;
|
|
@@ -3538,8 +4148,15 @@ var QueryEngine = class {
|
|
|
3538
4148
|
env;
|
|
3539
4149
|
txMutex = new TxMutex();
|
|
3540
4150
|
costTracker;
|
|
4151
|
+
guardConfig;
|
|
4152
|
+
guardState;
|
|
4153
|
+
recipes;
|
|
4154
|
+
contextBudget;
|
|
4155
|
+
contextSummarizer;
|
|
4156
|
+
matchedRecipe = null;
|
|
3541
4157
|
messages = [];
|
|
3542
4158
|
abortController = null;
|
|
4159
|
+
guardEvents = [];
|
|
3543
4160
|
constructor(config) {
|
|
3544
4161
|
this.provider = config.provider;
|
|
3545
4162
|
this.agent = config.agent;
|
|
@@ -3554,8 +4171,15 @@ var QueryEngine = class {
|
|
|
3554
4171
|
this.maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
3555
4172
|
this.temperature = config.temperature;
|
|
3556
4173
|
this.toolChoice = config.toolChoice;
|
|
4174
|
+
this.thinking = config.thinking;
|
|
4175
|
+
this.outputConfig = config.outputConfig;
|
|
3557
4176
|
this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
3558
4177
|
this.costTracker = new CostTracker(config.costTracker);
|
|
4178
|
+
this.guardConfig = config.guards;
|
|
4179
|
+
this.guardState = createGuardRunnerState();
|
|
4180
|
+
this.recipes = config.recipes;
|
|
4181
|
+
this.contextBudget = new ContextBudget(config.contextBudget);
|
|
4182
|
+
this.contextSummarizer = config.contextSummarizer;
|
|
3559
4183
|
this.tools = config.tools ?? (config.agent ? getDefaultTools() : []);
|
|
3560
4184
|
}
|
|
3561
4185
|
/**
|
|
@@ -3573,6 +4197,7 @@ var QueryEngine = class {
|
|
|
3573
4197
|
}
|
|
3574
4198
|
this.abortController = new AbortController();
|
|
3575
4199
|
const signal = this.abortController.signal;
|
|
4200
|
+
this.matchedRecipe = this.recipes?.match(prompt) ?? null;
|
|
3576
4201
|
this.messages.push({
|
|
3577
4202
|
role: "user",
|
|
3578
4203
|
content: [{ type: "text", text: prompt }]
|
|
@@ -3631,9 +4256,21 @@ var QueryEngine = class {
|
|
|
3631
4256
|
getMessages() {
|
|
3632
4257
|
return this.messages;
|
|
3633
4258
|
}
|
|
4259
|
+
getMatchedRecipe() {
|
|
4260
|
+
return this.matchedRecipe;
|
|
4261
|
+
}
|
|
4262
|
+
getContextBudget() {
|
|
4263
|
+
return this.contextBudget;
|
|
4264
|
+
}
|
|
3634
4265
|
reset() {
|
|
3635
4266
|
this.messages = [];
|
|
3636
4267
|
this.costTracker.reset();
|
|
4268
|
+
this.contextBudget.reset();
|
|
4269
|
+
this.guardEvents = [];
|
|
4270
|
+
this.matchedRecipe = null;
|
|
4271
|
+
}
|
|
4272
|
+
getGuardEvents() {
|
|
4273
|
+
return this.guardEvents;
|
|
3637
4274
|
}
|
|
3638
4275
|
loadMessages(messages) {
|
|
3639
4276
|
this.messages = [...messages];
|
|
@@ -3680,11 +4317,20 @@ var QueryEngine = class {
|
|
|
3680
4317
|
pendingToolCalls: []
|
|
3681
4318
|
};
|
|
3682
4319
|
try {
|
|
4320
|
+
if (this.contextBudget.shouldCompact()) {
|
|
4321
|
+
this.messages = await compactMessages(this.messages, {
|
|
4322
|
+
maxTokens: 1e5,
|
|
4323
|
+
keepRecentCount: 8,
|
|
4324
|
+
summarizer: this.contextSummarizer
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
3683
4327
|
this.messages = validateHistory(this.messages);
|
|
3684
4328
|
if (process.env.NODE_ENV !== "test") {
|
|
3685
4329
|
const summary = this.messages.map((m, idx) => {
|
|
3686
4330
|
const blocks = m.content.map((b) => {
|
|
3687
4331
|
if (b.type === "text") return `text(${b.text.slice(0, 40)}\u2026)`;
|
|
4332
|
+
if (b.type === "thinking") return `thinking(${b.thinking.length}ch)`;
|
|
4333
|
+
if (b.type === "redacted_thinking") return `redacted_thinking`;
|
|
3688
4334
|
if (b.type === "tool_use") return `tool_use:${b.id.slice(-8)}/${b.name}`;
|
|
3689
4335
|
return `tool_result:${b.toolUseId.slice(-8)}`;
|
|
3690
4336
|
});
|
|
@@ -3693,14 +4339,32 @@ var QueryEngine = class {
|
|
|
3693
4339
|
console.log(`[engine] provider.chat turn=${turns} msgs=${this.messages.length}
|
|
3694
4340
|
${summary.join("\n")}`);
|
|
3695
4341
|
}
|
|
4342
|
+
const thinkingEnabled = this.thinking && this.thinking.type !== "disabled";
|
|
4343
|
+
const effectiveToolChoice = thinkingEnabled ? applyToolChoice && turns === 1 ? "auto" : void 0 : applyToolChoice && turns === 1 ? this.toolChoice : void 0;
|
|
4344
|
+
let effectivePrompt = this.systemPrompt;
|
|
4345
|
+
if (this.matchedRecipe && this.recipes) {
|
|
4346
|
+
const recipeCtx = this.recipes.toPromptContext(this.matchedRecipe);
|
|
4347
|
+
if (typeof effectivePrompt === "string") {
|
|
4348
|
+
effectivePrompt = `${effectivePrompt}
|
|
4349
|
+
|
|
4350
|
+
${recipeCtx}`;
|
|
4351
|
+
} else if (Array.isArray(effectivePrompt)) {
|
|
4352
|
+
effectivePrompt = [
|
|
4353
|
+
...effectivePrompt,
|
|
4354
|
+
{ type: "text", text: recipeCtx }
|
|
4355
|
+
];
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
3696
4358
|
const stream = this.provider.chat({
|
|
3697
4359
|
messages: this.messages,
|
|
3698
|
-
systemPrompt:
|
|
4360
|
+
systemPrompt: effectivePrompt,
|
|
3699
4361
|
tools: toolDefs,
|
|
3700
4362
|
model: this.model,
|
|
3701
4363
|
maxTokens: this.maxTokens,
|
|
3702
4364
|
temperature: this.temperature,
|
|
3703
|
-
toolChoice:
|
|
4365
|
+
toolChoice: effectiveToolChoice,
|
|
4366
|
+
thinking: this.thinking,
|
|
4367
|
+
outputConfig: this.outputConfig,
|
|
3704
4368
|
signal
|
|
3705
4369
|
});
|
|
3706
4370
|
for await (const event of stream) {
|
|
@@ -3746,7 +4410,42 @@ ${summary.join("\n")}`);
|
|
|
3746
4410
|
pendingWrite = { call, tool };
|
|
3747
4411
|
break;
|
|
3748
4412
|
}
|
|
3749
|
-
|
|
4413
|
+
const guardedApproved = [];
|
|
4414
|
+
if (this.guardConfig) {
|
|
4415
|
+
const convCtx = extractConversationText(this.messages);
|
|
4416
|
+
for (const call of approved) {
|
|
4417
|
+
const tool = findTool(this.tools, call.name);
|
|
4418
|
+
if (!tool) {
|
|
4419
|
+
guardedApproved.push(call);
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
const check = runGuards(tool, call, this.guardState, this.guardConfig, convCtx);
|
|
4423
|
+
this.guardEvents.push(...check.events);
|
|
4424
|
+
if (check.blocked) {
|
|
4425
|
+
yield {
|
|
4426
|
+
type: "tool_result",
|
|
4427
|
+
toolName: call.name,
|
|
4428
|
+
toolUseId: call.id,
|
|
4429
|
+
result: { error: check.blockReason, _gate: check.blockGate },
|
|
4430
|
+
isError: true
|
|
4431
|
+
};
|
|
4432
|
+
toolResultBlocks.push({
|
|
4433
|
+
type: "tool_result",
|
|
4434
|
+
toolUseId: call.id,
|
|
4435
|
+
content: JSON.stringify({ error: check.blockReason, _gate: check.blockGate }),
|
|
4436
|
+
isError: true
|
|
4437
|
+
});
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
if (check.injections.length > 0) {
|
|
4441
|
+
call._guardInjections = check.injections;
|
|
4442
|
+
}
|
|
4443
|
+
guardedApproved.push(call);
|
|
4444
|
+
}
|
|
4445
|
+
} else {
|
|
4446
|
+
guardedApproved.push(...approved);
|
|
4447
|
+
}
|
|
4448
|
+
for await (const toolEvent of runTools(guardedApproved, this.tools, context, this.txMutex)) {
|
|
3750
4449
|
if (toolEvent.type === "tool_result" && !toolEvent.isError) {
|
|
3751
4450
|
const warning = flagSuspiciousResult(toolEvent.toolName, toolEvent.result);
|
|
3752
4451
|
if (warning) {
|
|
@@ -3764,29 +4463,89 @@ ${summary.join("\n")}`);
|
|
|
3764
4463
|
continue;
|
|
3765
4464
|
}
|
|
3766
4465
|
}
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
const
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
4466
|
+
if (toolEvent.type === "tool_result") {
|
|
4467
|
+
const tool = findTool(this.tools, toolEvent.toolName);
|
|
4468
|
+
const originalCall = guardedApproved.find((c) => c.id === toolEvent.toolUseId);
|
|
4469
|
+
updateGuardStateAfterToolResult(
|
|
4470
|
+
toolEvent.toolName,
|
|
4471
|
+
tool,
|
|
4472
|
+
originalCall?.input ?? null,
|
|
4473
|
+
toolEvent.result,
|
|
4474
|
+
toolEvent.isError,
|
|
4475
|
+
this.guardState
|
|
4476
|
+
);
|
|
4477
|
+
let enrichedResult = toolEvent.result;
|
|
4478
|
+
if (this.guardConfig && !toolEvent.isError && tool) {
|
|
4479
|
+
const artifactInj = this.guardConfig.artifactPreview !== false ? guardArtifactPreview(toolEvent.result) : null;
|
|
4480
|
+
const staleInj = this.guardConfig.staleData !== false ? guardStaleData(tool.flags) : null;
|
|
4481
|
+
const preInjections = guardedApproved.find((c) => c.id === toolEvent.toolUseId)?._guardInjections ?? [];
|
|
4482
|
+
const allInjections = [
|
|
4483
|
+
...preInjections,
|
|
4484
|
+
...artifactInj ? [artifactInj] : [],
|
|
4485
|
+
...staleInj ? [staleInj] : []
|
|
4486
|
+
];
|
|
4487
|
+
if (allInjections.length > 0 && typeof enrichedResult === "object" && enrichedResult) {
|
|
4488
|
+
enrichedResult = { ...enrichedResult, _guards: allInjections };
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
const finalEvent = enrichedResult !== toolEvent.result ? { ...toolEvent, result: enrichedResult } : toolEvent;
|
|
4492
|
+
yield finalEvent;
|
|
4493
|
+
if (finalEvent.type === "tool_result" && !finalEvent.isError) {
|
|
4494
|
+
const r = finalEvent.result;
|
|
4495
|
+
if (r && r.__canvas === true) {
|
|
4496
|
+
yield {
|
|
4497
|
+
type: "canvas",
|
|
4498
|
+
template: String(r.template ?? ""),
|
|
4499
|
+
title: String(r.title ?? ""),
|
|
4500
|
+
data: r.templateData ?? null,
|
|
4501
|
+
toolUseId: finalEvent.toolUseId
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
3778
4504
|
}
|
|
4505
|
+
toolResultBlocks.push({
|
|
4506
|
+
type: "tool_result",
|
|
4507
|
+
toolUseId: finalEvent.toolUseId,
|
|
4508
|
+
content: JSON.stringify(finalEvent.result),
|
|
4509
|
+
isError: finalEvent.isError
|
|
4510
|
+
});
|
|
4511
|
+
continue;
|
|
3779
4512
|
}
|
|
3780
|
-
|
|
4513
|
+
yield toolEvent;
|
|
4514
|
+
}
|
|
4515
|
+
if (pendingWrite && this.guardConfig) {
|
|
4516
|
+
const convCtx = extractConversationText(this.messages);
|
|
4517
|
+
const check = runGuards(
|
|
4518
|
+
pendingWrite.tool,
|
|
4519
|
+
pendingWrite.call,
|
|
4520
|
+
this.guardState,
|
|
4521
|
+
this.guardConfig,
|
|
4522
|
+
convCtx
|
|
4523
|
+
);
|
|
4524
|
+
this.guardEvents.push(...check.events);
|
|
4525
|
+
if (check.blocked) {
|
|
4526
|
+
yield {
|
|
4527
|
+
type: "tool_result",
|
|
4528
|
+
toolName: pendingWrite.call.name,
|
|
4529
|
+
toolUseId: pendingWrite.call.id,
|
|
4530
|
+
result: { error: check.blockReason, _gate: check.blockGate },
|
|
4531
|
+
isError: true
|
|
4532
|
+
};
|
|
3781
4533
|
toolResultBlocks.push({
|
|
3782
4534
|
type: "tool_result",
|
|
3783
|
-
toolUseId:
|
|
3784
|
-
content: JSON.stringify(
|
|
3785
|
-
isError:
|
|
4535
|
+
toolUseId: pendingWrite.call.id,
|
|
4536
|
+
content: JSON.stringify({ error: check.blockReason, _gate: check.blockGate }),
|
|
4537
|
+
isError: true
|
|
3786
4538
|
});
|
|
4539
|
+
this.messages.push({ role: "assistant", content: acc.assistantBlocks });
|
|
4540
|
+
this.messages.push({ role: "user", content: toolResultBlocks });
|
|
4541
|
+
continue;
|
|
4542
|
+
}
|
|
4543
|
+
if (check.injections.length > 0) {
|
|
4544
|
+
pendingWrite.call._guardInjections = check.injections;
|
|
3787
4545
|
}
|
|
3788
4546
|
}
|
|
3789
4547
|
if (pendingWrite) {
|
|
4548
|
+
const writeGuardInjections = pendingWrite.call._guardInjections;
|
|
3790
4549
|
yield {
|
|
3791
4550
|
type: "pending_action",
|
|
3792
4551
|
action: {
|
|
@@ -3799,7 +4558,8 @@ ${summary.join("\n")}`);
|
|
|
3799
4558
|
toolUseId: b.toolUseId,
|
|
3800
4559
|
content: b.content,
|
|
3801
4560
|
isError: b.isError ?? false
|
|
3802
|
-
}))
|
|
4561
|
+
})),
|
|
4562
|
+
...writeGuardInjections?.length ? { guardInjections: writeGuardInjections } : {}
|
|
3803
4563
|
}
|
|
3804
4564
|
};
|
|
3805
4565
|
return;
|
|
@@ -3829,6 +4589,26 @@ ${summary.join("\n")}`);
|
|
|
3829
4589
|
}
|
|
3830
4590
|
*handleProviderEvent(event, acc) {
|
|
3831
4591
|
switch (event.type) {
|
|
4592
|
+
case "thinking_delta": {
|
|
4593
|
+
yield { type: "thinking_delta", text: event.text };
|
|
4594
|
+
break;
|
|
4595
|
+
}
|
|
4596
|
+
case "thinking_done": {
|
|
4597
|
+
acc.assistantBlocks.push({
|
|
4598
|
+
type: "thinking",
|
|
4599
|
+
thinking: event.thinking,
|
|
4600
|
+
signature: event.signature
|
|
4601
|
+
});
|
|
4602
|
+
yield { type: "thinking_done", signature: event.signature };
|
|
4603
|
+
break;
|
|
4604
|
+
}
|
|
4605
|
+
case "redacted_thinking": {
|
|
4606
|
+
acc.assistantBlocks.push({
|
|
4607
|
+
type: "redacted_thinking",
|
|
4608
|
+
data: event.data
|
|
4609
|
+
});
|
|
4610
|
+
break;
|
|
4611
|
+
}
|
|
3832
4612
|
case "text_delta": {
|
|
3833
4613
|
acc.text += event.text;
|
|
3834
4614
|
yield { type: "text_delta", text: event.text };
|
|
@@ -3859,6 +4639,7 @@ ${summary.join("\n")}`);
|
|
|
3859
4639
|
event.cacheReadTokens,
|
|
3860
4640
|
event.cacheWriteTokens
|
|
3861
4641
|
);
|
|
4642
|
+
this.contextBudget.update(event.inputTokens);
|
|
3862
4643
|
yield {
|
|
3863
4644
|
type: "usage",
|
|
3864
4645
|
inputTokens: event.inputTokens,
|
|
@@ -4075,114 +4856,305 @@ var MemorySessionStore = class {
|
|
|
4075
4856
|
}
|
|
4076
4857
|
}
|
|
4077
4858
|
};
|
|
4859
|
+
var StepRequirementSchema = z.object({
|
|
4860
|
+
step: z.string().optional(),
|
|
4861
|
+
field: z.string().optional(),
|
|
4862
|
+
confirmation: z.boolean().optional()
|
|
4863
|
+
});
|
|
4864
|
+
var OnErrorSchema = z.object({
|
|
4865
|
+
action: z.enum(["abort", "refuse", "report", "retry"]),
|
|
4866
|
+
message: z.string(),
|
|
4867
|
+
suggest: z.string().optional()
|
|
4868
|
+
});
|
|
4869
|
+
var StepSchema = z.object({
|
|
4870
|
+
name: z.string().min(1),
|
|
4871
|
+
tool: z.string().optional(),
|
|
4872
|
+
service: z.string().optional(),
|
|
4873
|
+
purpose: z.string().min(1),
|
|
4874
|
+
cost: z.string().optional(),
|
|
4875
|
+
output: z.object({ type: z.string(), key: z.string() }).optional(),
|
|
4876
|
+
gate: z.enum(["none", "preview", "review", "estimate"]).optional(),
|
|
4877
|
+
gate_prompt: z.string().optional(),
|
|
4878
|
+
requires: z.array(StepRequirementSchema).optional(),
|
|
4879
|
+
rules: z.array(z.string()).optional(),
|
|
4880
|
+
condition: z.string().optional(),
|
|
4881
|
+
notes: z.string().optional(),
|
|
4882
|
+
flags: z.record(z.unknown()).optional(),
|
|
4883
|
+
on_error: OnErrorSchema.optional(),
|
|
4884
|
+
input_template: z.record(z.string()).optional(),
|
|
4885
|
+
cost_per_unit: z.string().optional()
|
|
4886
|
+
});
|
|
4887
|
+
var RecipeSchema = z.object({
|
|
4888
|
+
name: z.string().min(1),
|
|
4889
|
+
description: z.string().min(1),
|
|
4890
|
+
triggers: z.array(z.string().min(1)).min(1),
|
|
4891
|
+
services: z.array(z.string()).optional(),
|
|
4892
|
+
prerequisites: z.array(z.object({ field: z.string(), prompt: z.string() })).optional(),
|
|
4893
|
+
steps: z.array(StepSchema).min(1)
|
|
4894
|
+
}).refine(
|
|
4895
|
+
(r) => {
|
|
4896
|
+
const names = r.steps.map((s) => s.name);
|
|
4897
|
+
return new Set(names).size === names.length;
|
|
4898
|
+
},
|
|
4899
|
+
{ message: "Step names must be unique within a recipe" }
|
|
4900
|
+
);
|
|
4901
|
+
function loadRecipes(yamlDir) {
|
|
4902
|
+
const files = readdirSync(yamlDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
4903
|
+
const recipes = [];
|
|
4904
|
+
for (const file of files) {
|
|
4905
|
+
const content = readFileSync(join(yamlDir, file), "utf-8");
|
|
4906
|
+
const raw = yaml.load(content);
|
|
4907
|
+
const parsed = RecipeSchema.parse(raw);
|
|
4908
|
+
recipes.push(parsed);
|
|
4909
|
+
}
|
|
4910
|
+
return recipes;
|
|
4911
|
+
}
|
|
4912
|
+
function parseRecipe(yamlContent) {
|
|
4913
|
+
const raw = yaml.load(yamlContent);
|
|
4914
|
+
return RecipeSchema.parse(raw);
|
|
4915
|
+
}
|
|
4078
4916
|
|
|
4079
|
-
// src/
|
|
4080
|
-
var
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
chars += blockCharCount(block);
|
|
4086
|
-
}
|
|
4917
|
+
// src/recipes/registry.ts
|
|
4918
|
+
var RecipeRegistry = class {
|
|
4919
|
+
recipes = [];
|
|
4920
|
+
/** Load all recipes from a directory of YAML files. */
|
|
4921
|
+
loadDir(yamlDir) {
|
|
4922
|
+
this.recipes.push(...loadRecipes(yamlDir));
|
|
4087
4923
|
}
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
switch (block.type) {
|
|
4092
|
-
case "text":
|
|
4093
|
-
return block.text.length;
|
|
4094
|
-
case "tool_use":
|
|
4095
|
-
return block.name.length + JSON.stringify(block.input).length;
|
|
4096
|
-
case "tool_result":
|
|
4097
|
-
return block.content.length;
|
|
4924
|
+
/** Register a single recipe from a YAML string. */
|
|
4925
|
+
loadYaml(yamlContent) {
|
|
4926
|
+
this.recipes.push(parseRecipe(yamlContent));
|
|
4098
4927
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
const keepRecent = opts.keepRecentCount ?? 6;
|
|
4103
|
-
const systemTokens = opts.systemPromptTokens ?? 500;
|
|
4104
|
-
const budget = maxTokens - systemTokens;
|
|
4105
|
-
if (messages.length === 0) return [];
|
|
4106
|
-
const mutable = messages.map((m) => ({
|
|
4107
|
-
role: m.role,
|
|
4108
|
-
content: m.content.map((b) => ({ ...b }))
|
|
4109
|
-
}));
|
|
4110
|
-
if (estimateTokens(mutable) <= budget) return mutable;
|
|
4111
|
-
const splitIdx = Math.max(0, mutable.length - keepRecent);
|
|
4112
|
-
for (let i = 0; i < splitIdx; i++) {
|
|
4113
|
-
mutable[i].content = mutable[i].content.map((block) => {
|
|
4114
|
-
if (block.type === "tool_result" && block.content.length > 200) {
|
|
4115
|
-
return {
|
|
4116
|
-
...block,
|
|
4117
|
-
content: truncateToolResult(block.content)
|
|
4118
|
-
};
|
|
4119
|
-
}
|
|
4120
|
-
return block;
|
|
4121
|
-
});
|
|
4928
|
+
/** Register a pre-parsed Recipe object. */
|
|
4929
|
+
register(recipe) {
|
|
4930
|
+
this.recipes.push(recipe);
|
|
4122
4931
|
}
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const oldSection = mutable.slice(1, splitIdx);
|
|
4127
|
-
while (oldSection.length > 0 && estimateTokens([first, ...oldSection, ...recent]) > budget) {
|
|
4128
|
-
oldSection.shift();
|
|
4932
|
+
/** All loaded recipes. */
|
|
4933
|
+
all() {
|
|
4934
|
+
return this.recipes;
|
|
4129
4935
|
}
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4936
|
+
/**
|
|
4937
|
+
* Match a user message to the most specific recipe.
|
|
4938
|
+
* Longest trigger phrase match wins. Returns null if no match.
|
|
4939
|
+
*/
|
|
4940
|
+
match(userMessage) {
|
|
4941
|
+
const normalized = userMessage.toLowerCase().trim();
|
|
4942
|
+
let best = null;
|
|
4943
|
+
let bestLength = 0;
|
|
4944
|
+
for (const recipe of this.recipes) {
|
|
4945
|
+
for (const trigger of recipe.triggers) {
|
|
4946
|
+
const triggerLower = trigger.toLowerCase();
|
|
4947
|
+
if (normalized.includes(triggerLower) && triggerLower.length > bestLength) {
|
|
4948
|
+
best = recipe;
|
|
4949
|
+
bestLength = triggerLower.length;
|
|
4136
4950
|
}
|
|
4137
|
-
|
|
4138
|
-
});
|
|
4951
|
+
}
|
|
4139
4952
|
}
|
|
4953
|
+
return best;
|
|
4140
4954
|
}
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4955
|
+
/**
|
|
4956
|
+
* Format a matched recipe as a compact context block for the system prompt.
|
|
4957
|
+
* Injected dynamically — only when the recipe matches.
|
|
4958
|
+
*/
|
|
4959
|
+
toPromptContext(recipe) {
|
|
4960
|
+
const lines = [
|
|
4961
|
+
`## Active Recipe: ${recipe.name}`,
|
|
4962
|
+
recipe.description,
|
|
4963
|
+
"Follow these steps:"
|
|
4964
|
+
];
|
|
4965
|
+
for (let i = 0; i < recipe.steps.length; i++) {
|
|
4966
|
+
const step = recipe.steps[i];
|
|
4967
|
+
const num = i + 1;
|
|
4968
|
+
const toolNote = step.tool ? ` \u2192 ${step.tool}` : "";
|
|
4969
|
+
const serviceNote = step.service ? ` (${step.service})` : "";
|
|
4970
|
+
const costNote = step.cost ? ` \u2014 ${step.cost}` : "";
|
|
4971
|
+
const gateNote = step.gate && step.gate !== "none" ? ` [GATE: ${step.gate}]` : "";
|
|
4972
|
+
let line = `${num}. ${step.name}${toolNote}${serviceNote}${costNote}${gateNote}`;
|
|
4973
|
+
if (step.gate_prompt) {
|
|
4974
|
+
line += ` \u2014 "${step.gate_prompt}"`;
|
|
4975
|
+
}
|
|
4976
|
+
lines.push(line);
|
|
4977
|
+
if (step.rules?.length) {
|
|
4978
|
+
for (const rule of step.rules) {
|
|
4979
|
+
lines.push(` - ${rule}`);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
if (step.notes) {
|
|
4983
|
+
lines.push(` Note: ${step.notes}`);
|
|
4984
|
+
}
|
|
4985
|
+
if (step.on_error) {
|
|
4986
|
+
lines.push(` On error: ${step.on_error.action} \u2014 ${step.on_error.message}`);
|
|
4987
|
+
}
|
|
4988
|
+
if (step.condition) {
|
|
4989
|
+
lines.push(` Condition: ${step.condition}`);
|
|
4990
|
+
}
|
|
4150
4991
|
}
|
|
4992
|
+
if (recipe.prerequisites?.length) {
|
|
4993
|
+
lines.push("Prerequisites (ask before starting):");
|
|
4994
|
+
for (const pre of recipe.prerequisites) {
|
|
4995
|
+
lines.push(`- ${pre.field}: "${pre.prompt}"`);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
return lines.join("\n");
|
|
4151
4999
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
5000
|
+
};
|
|
5001
|
+
|
|
5002
|
+
// src/classify-effort.ts
|
|
5003
|
+
function classifyEffort(model, userMessage, matchedRecipe, sessionWriteCount) {
|
|
5004
|
+
const supportsMax = model.includes("opus-4-6");
|
|
5005
|
+
const msg = userMessage.toLowerCase();
|
|
5006
|
+
if (supportsMax) {
|
|
5007
|
+
if (matchedRecipe?.name === "portfolio_rebalance") return "max";
|
|
5008
|
+
if (matchedRecipe?.name === "emergency_withdraw") return "max";
|
|
5009
|
+
if (/rebalance|reallocate|dca setup|close.*position/i.test(msg)) return "max";
|
|
5010
|
+
}
|
|
5011
|
+
if (matchedRecipe && matchedRecipe.steps.length >= 3) return "high";
|
|
5012
|
+
if (matchedRecipe?.name === "safe_borrow" || matchedRecipe?.name === "bulk_mail") return "high";
|
|
5013
|
+
if (sessionWriteCount > 0 && /borrow|withdraw|send|swap/i.test(msg)) return "high";
|
|
5014
|
+
if (/balance|rate|how much|what is|check|history|show|price/i.test(msg)) return "low";
|
|
5015
|
+
if (!matchedRecipe && !/deposit|send|swap|borrow|withdraw|save|pay/i.test(msg)) return "low";
|
|
5016
|
+
return "medium";
|
|
4161
5017
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
5018
|
+
|
|
5019
|
+
// src/prompt-cache.ts
|
|
5020
|
+
function buildCachedSystemPrompt(staticParts, dynamicPart) {
|
|
5021
|
+
const blocks = staticParts.map((text, i) => ({
|
|
5022
|
+
type: "text",
|
|
5023
|
+
text,
|
|
5024
|
+
...i === staticParts.length - 1 && { cache_control: { type: "ephemeral" } }
|
|
5025
|
+
}));
|
|
5026
|
+
if (dynamicPart) {
|
|
5027
|
+
blocks.push({ type: "text", text: dynamicPart });
|
|
5028
|
+
}
|
|
5029
|
+
return blocks;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
// src/intelligence.ts
|
|
5033
|
+
function buildProfileContext(profile) {
|
|
5034
|
+
if (!profile || profile.riskConfidence < 0.3) return "";
|
|
5035
|
+
const lines = ["User financial profile (inferred from conversation history):"];
|
|
5036
|
+
if (profile.riskConfidence >= 0.5) {
|
|
5037
|
+
lines.push(`- Risk appetite: ${profile.riskAppetite}`);
|
|
5038
|
+
}
|
|
5039
|
+
if (profile.literacyConfidence >= 0.5) {
|
|
5040
|
+
lines.push(`- Financial literacy: ${profile.financialLiteracy}`);
|
|
5041
|
+
if (profile.financialLiteracy === "advanced") {
|
|
5042
|
+
lines.push(" \u2192 Skip basic DeFi explanations (health factor, APY, etc). User knows these.");
|
|
4167
5043
|
}
|
|
4168
|
-
if (
|
|
4169
|
-
|
|
4170
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
4171
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
4172
|
-
summary[key] = value;
|
|
4173
|
-
} else if (typeof value === "string") {
|
|
4174
|
-
summary[key] = value.length > 50 ? value.slice(0, 50) + "\u2026" : value;
|
|
4175
|
-
} else if (Array.isArray(value)) {
|
|
4176
|
-
summary[key] = `[${value.length} items]`;
|
|
4177
|
-
} else {
|
|
4178
|
-
summary[key] = "{\u2026}";
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
return JSON.stringify(summary);
|
|
5044
|
+
if (profile.financialLiteracy === "novice") {
|
|
5045
|
+
lines.push(" \u2192 Always explain DeFi concepts in plain language.");
|
|
4182
5046
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
5047
|
+
}
|
|
5048
|
+
if (profile.currencyFraming === "fiat") {
|
|
5049
|
+
lines.push('- Frame amounts as dollars (e.g. "$50" not "50 USDC")');
|
|
5050
|
+
}
|
|
5051
|
+
if (profile.prefersBriefResponses) {
|
|
5052
|
+
lines.push("- Prefers brief responses \u2014 be concise");
|
|
5053
|
+
}
|
|
5054
|
+
if (profile.primaryGoals.length > 0) {
|
|
5055
|
+
lines.push(`- Stated goals: ${profile.primaryGoals.join(", ")}`);
|
|
5056
|
+
}
|
|
5057
|
+
if (profile.knownPatterns.length > 0) {
|
|
5058
|
+
lines.push(`- Behavioural patterns: ${profile.knownPatterns.join(", ")}`);
|
|
5059
|
+
}
|
|
5060
|
+
return lines.join("\n");
|
|
5061
|
+
}
|
|
5062
|
+
function buildProactivenessInstructions(profile) {
|
|
5063
|
+
const brevityGuidance = profile?.prefersBriefResponses ? "This user prefers brevity \u2014 only surface context if urgent or directly actionable." : "Surface relevant context when criteria are met.";
|
|
5064
|
+
const styleGuidance = profile?.financialLiteracy === "novice" ? "Frame observations in plain English, no DeFi jargon." : "Technical framing is fine.";
|
|
5065
|
+
return `Proactive awareness:
|
|
5066
|
+
After completing the user's request, consider whether ONE additional piece of financial
|
|
5067
|
+
context is worth mentioning. ${brevityGuidance}
|
|
5068
|
+
|
|
5069
|
+
\u2713 Mention if:
|
|
5070
|
+
- Their savings goal is materially off-track (>20% behind pace)
|
|
5071
|
+
- Yield rate changed significantly since last session (>0.5%)
|
|
5072
|
+
- They have idle USDC >$50 sitting for >48h
|
|
5073
|
+
- An action they just took interacts with an active goal or debt position
|
|
5074
|
+
- A pattern would materially benefit from their attention
|
|
5075
|
+
|
|
5076
|
+
\u2717 Do NOT mention if:
|
|
5077
|
+
- Tangentially related but not actionable
|
|
5078
|
+
- Already surfaced this session
|
|
5079
|
+
- Requires more explanation than the original answer
|
|
5080
|
+
- Would seem pushy or sales-y
|
|
5081
|
+
|
|
5082
|
+
${styleGuidance}
|
|
5083
|
+
Format: One sentence maximum, after main response, separated by a line break.
|
|
5084
|
+
Frame as observation, not advice: "Your Tokyo goal is $80 behind pace." \u2014 not "You should deposit more."`;
|
|
5085
|
+
}
|
|
5086
|
+
function buildSelfEvaluationInstruction() {
|
|
5087
|
+
return `Self-evaluation (apply silently before composing your response):
|
|
5088
|
+
|
|
5089
|
+
1. ACCURACY \u2014 Quote exact values from tool results, not estimates or rounded figures.
|
|
5090
|
+
Never combine post-action tool results with pre-action snapshot numbers.
|
|
5091
|
+
If the tool returned an error, label it as an error \u2014 do not paraphrase it as success.
|
|
5092
|
+
|
|
5093
|
+
2. STATE CONSISTENCY \u2014 Describe the actual outcome of all steps.
|
|
5094
|
+
Partial success (swap ok, deposit failed): describe both clearly.
|
|
5095
|
+
Never describe a failed action as if it succeeded.
|
|
5096
|
+
|
|
5097
|
+
3. COMPLETENESS \u2014 If the user asked multiple things, answer all of them.
|
|
5098
|
+
If you couldn't complete something, explain why and what the current state is.
|
|
5099
|
+
|
|
5100
|
+
4. TONE \u2014 Match tone to outcome.
|
|
5101
|
+
Success: confirming and forward-looking.
|
|
5102
|
+
Failure: clear about what failed, unchanged, and what to do next.
|
|
5103
|
+
Warning: specific risk, not generic caution.
|
|
5104
|
+
|
|
5105
|
+
If any check fails, rewrite before outputting.`;
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
// src/state/conversation-state.ts
|
|
5109
|
+
function buildStateContext(state) {
|
|
5110
|
+
switch (state.type) {
|
|
5111
|
+
case "idle":
|
|
5112
|
+
return "";
|
|
5113
|
+
case "mid_recipe": {
|
|
5114
|
+
const elapsed = Math.round((Date.now() - state.startedAt) / 6e4);
|
|
5115
|
+
const outputs = JSON.stringify(state.completedStepOutputs);
|
|
5116
|
+
return [
|
|
5117
|
+
`Conversation state: MID-RECIPE`,
|
|
5118
|
+
`Active recipe: ${state.recipeName} (step ${state.currentStep + 1} of ${state.totalSteps})`,
|
|
5119
|
+
`Started: ${elapsed} minutes ago`,
|
|
5120
|
+
`Completed step key outputs: ${outputs}`,
|
|
5121
|
+
`If the user asks an unrelated question: answer briefly, then offer to continue the ${state.recipeName} flow.`,
|
|
5122
|
+
`If the user says "cancel" or "stop": confirm you have abandoned the recipe and return to idle.`
|
|
5123
|
+
].join("\n");
|
|
5124
|
+
}
|
|
5125
|
+
case "awaiting_confirmation": {
|
|
5126
|
+
const expiryMins = Math.max(0, Math.round((state.expiresAt - Date.now()) / 6e4));
|
|
5127
|
+
const expired = state.expiresAt < Date.now();
|
|
5128
|
+
return [
|
|
5129
|
+
`Conversation state: AWAITING CONFIRMATION`,
|
|
5130
|
+
`Proposed action: ${state.action}${state.amount ? ` for $${state.amount}` : ""}${state.recipient ? ` to ${state.recipient}` : ""}`,
|
|
5131
|
+
expired ? `Status: EXPIRED \u2014 ask if user still wants to proceed` : `Expires in: ${expiryMins} minutes`,
|
|
5132
|
+
`"yes/confirm/do it" \u2192 execute. "no/cancel/wait" \u2192 abort, reset to idle.`
|
|
5133
|
+
].join("\n");
|
|
5134
|
+
}
|
|
5135
|
+
case "post_error":
|
|
5136
|
+
return [
|
|
5137
|
+
`Conversation state: POST-ERROR`,
|
|
5138
|
+
`Failed action: ${state.failedAction}`,
|
|
5139
|
+
`Error: ${state.errorMessage}`,
|
|
5140
|
+
state.partialState ? `Partial state: ${state.partialState}` : "",
|
|
5141
|
+
`Acknowledge failure clearly. Offer a specific recovery path if one exists.`,
|
|
5142
|
+
`This state clears automatically on the next successful action.`
|
|
5143
|
+
].filter(Boolean).join("\n");
|
|
5144
|
+
case "post_liquidation_warning":
|
|
5145
|
+
return [
|
|
5146
|
+
`Conversation state: LIQUIDATION WARNING ACTIVE`,
|
|
5147
|
+
`Health factor: ${state.healthFactor.toFixed(2)} \u2014 below safe threshold`,
|
|
5148
|
+
`Prioritise debt repayment or collateral deposit.`,
|
|
5149
|
+
`Do not proceed with any action that would further reduce health factor.`
|
|
5150
|
+
].join("\n");
|
|
5151
|
+
case "onboarding":
|
|
5152
|
+
return [
|
|
5153
|
+
`Conversation state: ONBOARDING (session ${state.sessionNumber})`,
|
|
5154
|
+
state.sessionNumber === 1 ? "First session \u2014 introduce capabilities through context, not a feature list." : `Returning user \u2014 ${state.hasSavedBefore ? "has saved before" : "has not saved yet"}.`
|
|
5155
|
+
].join("\n");
|
|
5156
|
+
default:
|
|
5157
|
+
return "";
|
|
4186
5158
|
}
|
|
4187
5159
|
}
|
|
4188
5160
|
|
|
@@ -4419,6 +5391,7 @@ function adaptMcpTool(mcpTool, config) {
|
|
|
4419
5391
|
isReadOnly,
|
|
4420
5392
|
isConcurrencySafe: isReadOnly,
|
|
4421
5393
|
permissionLevel,
|
|
5394
|
+
flags: {},
|
|
4422
5395
|
async call(input, _context) {
|
|
4423
5396
|
const result = await config.manager.callTool(
|
|
4424
5397
|
config.serverName,
|
|
@@ -4484,17 +5457,26 @@ var AnthropicProvider = class {
|
|
|
4484
5457
|
toolChoice = { type: "tool", name: params.toolChoice.name };
|
|
4485
5458
|
}
|
|
4486
5459
|
}
|
|
4487
|
-
const
|
|
5460
|
+
const thinkingParam = toAnthropicThinking(params.thinking);
|
|
5461
|
+
const systemParam = toAnthropicSystem(params.systemPrompt);
|
|
5462
|
+
const baseParams = {
|
|
4488
5463
|
model: params.model ?? this.defaultModel,
|
|
4489
5464
|
max_tokens: params.maxTokens ?? this.defaultMaxTokens,
|
|
4490
|
-
system:
|
|
5465
|
+
system: systemParam,
|
|
4491
5466
|
messages,
|
|
5467
|
+
stream: true,
|
|
4492
5468
|
tools: tools.length > 0 ? tools : void 0,
|
|
4493
|
-
|
|
5469
|
+
...!thinkingParam && params.temperature !== void 0 && { temperature: params.temperature },
|
|
4494
5470
|
...toolChoice && { tool_choice: toolChoice }
|
|
4495
5471
|
};
|
|
5472
|
+
const streamParams = {
|
|
5473
|
+
...baseParams,
|
|
5474
|
+
...thinkingParam && { thinking: thinkingParam },
|
|
5475
|
+
...params.outputConfig?.effort && { output_config: { effort: params.outputConfig.effort } }
|
|
5476
|
+
};
|
|
4496
5477
|
const stream = params.signal ? this.client.messages.stream(streamParams, { signal: params.signal }) : this.client.messages.stream(streamParams);
|
|
4497
5478
|
const toolInputBuffers = /* @__PURE__ */ new Map();
|
|
5479
|
+
const thinkingBuffers = /* @__PURE__ */ new Map();
|
|
4498
5480
|
let outputTokensFromStart = 0;
|
|
4499
5481
|
try {
|
|
4500
5482
|
for await (const event of stream) {
|
|
@@ -4532,6 +5514,10 @@ var AnthropicProvider = class {
|
|
|
4532
5514
|
id: block.id,
|
|
4533
5515
|
name: block.name
|
|
4534
5516
|
};
|
|
5517
|
+
} else if (block.type === "thinking") {
|
|
5518
|
+
thinkingBuffers.set(event.index, { type: "thinking", text: "", signature: "" });
|
|
5519
|
+
} else if (block.type === "redacted_thinking") {
|
|
5520
|
+
thinkingBuffers.set(event.index, { type: "redacted_thinking", data: block.data ?? "" });
|
|
4535
5521
|
}
|
|
4536
5522
|
break;
|
|
4537
5523
|
}
|
|
@@ -4549,26 +5535,41 @@ var AnthropicProvider = class {
|
|
|
4549
5535
|
partialJson: delta.partial_json
|
|
4550
5536
|
};
|
|
4551
5537
|
}
|
|
5538
|
+
} else if (delta.type === "thinking_delta") {
|
|
5539
|
+
const buf = thinkingBuffers.get(event.index);
|
|
5540
|
+
if (buf?.type === "thinking") buf.text += delta.thinking ?? "";
|
|
5541
|
+
yield { type: "thinking_delta", text: delta.thinking ?? "" };
|
|
5542
|
+
} else if (delta.type === "signature_delta") {
|
|
5543
|
+
const buf = thinkingBuffers.get(event.index);
|
|
5544
|
+
if (buf?.type === "thinking") buf.signature = delta.signature ?? "";
|
|
4552
5545
|
}
|
|
4553
5546
|
break;
|
|
4554
5547
|
}
|
|
4555
5548
|
case "content_block_stop": {
|
|
4556
|
-
const
|
|
4557
|
-
if (
|
|
5549
|
+
const toolBuf = toolInputBuffers.get(event.index);
|
|
5550
|
+
if (toolBuf) {
|
|
4558
5551
|
let input = {};
|
|
4559
5552
|
try {
|
|
4560
|
-
input = JSON.parse(
|
|
5553
|
+
input = JSON.parse(toolBuf.json || "{}");
|
|
4561
5554
|
} catch {
|
|
4562
5555
|
input = {};
|
|
4563
5556
|
}
|
|
4564
5557
|
yield {
|
|
4565
5558
|
type: "tool_use_done",
|
|
4566
|
-
id:
|
|
4567
|
-
name:
|
|
5559
|
+
id: toolBuf.id,
|
|
5560
|
+
name: toolBuf.name,
|
|
4568
5561
|
input
|
|
4569
5562
|
};
|
|
4570
5563
|
toolInputBuffers.delete(event.index);
|
|
4571
5564
|
}
|
|
5565
|
+
const thinkBuf = thinkingBuffers.get(event.index);
|
|
5566
|
+
if (thinkBuf?.type === "thinking") {
|
|
5567
|
+
yield { type: "thinking_done", thinking: thinkBuf.text, signature: thinkBuf.signature };
|
|
5568
|
+
thinkingBuffers.delete(event.index);
|
|
5569
|
+
} else if (thinkBuf?.type === "redacted_thinking") {
|
|
5570
|
+
yield { type: "redacted_thinking", data: thinkBuf.data };
|
|
5571
|
+
thinkingBuffers.delete(event.index);
|
|
5572
|
+
}
|
|
4572
5573
|
break;
|
|
4573
5574
|
}
|
|
4574
5575
|
case "message_delta": {
|
|
@@ -4598,11 +5599,28 @@ var AnthropicProvider = class {
|
|
|
4598
5599
|
}
|
|
4599
5600
|
}
|
|
4600
5601
|
};
|
|
5602
|
+
function toAnthropicSystem(prompt) {
|
|
5603
|
+
if (typeof prompt === "string") return prompt;
|
|
5604
|
+
return prompt.map((block) => ({
|
|
5605
|
+
type: "text",
|
|
5606
|
+
text: block.text,
|
|
5607
|
+
...block.cache_control && { cache_control: block.cache_control }
|
|
5608
|
+
}));
|
|
5609
|
+
}
|
|
5610
|
+
function toAnthropicThinking(config) {
|
|
5611
|
+
if (!config || config.type === "disabled") return void 0;
|
|
5612
|
+
if (config.type === "adaptive") return { type: "adaptive" };
|
|
5613
|
+
return { type: "enabled", budget_tokens: config.budgetTokens };
|
|
5614
|
+
}
|
|
4601
5615
|
function toAnthropicMessage(msg) {
|
|
4602
5616
|
const content = msg.content.map((block) => {
|
|
4603
5617
|
switch (block.type) {
|
|
4604
5618
|
case "text":
|
|
4605
5619
|
return { type: "text", text: block.text };
|
|
5620
|
+
case "thinking":
|
|
5621
|
+
return { type: "thinking", thinking: block.thinking, signature: block.signature };
|
|
5622
|
+
case "redacted_thinking":
|
|
5623
|
+
return { type: "redacted_thinking", data: block.data };
|
|
4606
5624
|
case "tool_use":
|
|
4607
5625
|
return {
|
|
4608
5626
|
type: "tool_use",
|
|
@@ -4618,7 +5636,7 @@ function toAnthropicMessage(msg) {
|
|
|
4618
5636
|
is_error: block.isError
|
|
4619
5637
|
};
|
|
4620
5638
|
}
|
|
4621
|
-
});
|
|
5639
|
+
}).filter((b) => b !== null);
|
|
4622
5640
|
return { role: msg.role, content };
|
|
4623
5641
|
}
|
|
4624
5642
|
function toAnthropicTool(def) {
|
|
@@ -4707,6 +5725,6 @@ function sanitizeAnthropicMessages(messages) {
|
|
|
4707
5725
|
return merged;
|
|
4708
5726
|
}
|
|
4709
5727
|
|
|
4710
|
-
export { AnthropicProvider, CANVAS_TEMPLATES, CostTracker, DEFAULT_SYSTEM_PROMPT, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_SERVER_NAME, NaviTools, QueryEngine, READ_TOOLS, TxMutex, WRITE_TOOLS, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, balanceCheckTool, borrowTool, buildMcpTools, buildTool, claimRewardsTool, clearPriceCache, compactMessages, defillamaChainTvlTool, defillamaPriceChangeTool, defillamaProtocolFeesTool, defillamaProtocolInfoTool, defillamaSuiProtocolsTool, defillamaTokenPricesTool, defillamaYieldPoolsTool, engineToSSE, estimateTokens, explainTxTool, extractMcpText, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getDefaultTools, getMcpManager, getWalletAddress, hasNaviMcp, healthCheckTool, mppServicesTool, parseMcpJson, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
|
|
5728
|
+
export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_SYSTEM_PROMPT, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_SERVER_NAME, NaviTools, QueryEngine, READ_TOOLS, RecipeRegistry, RetryTracker, TOOL_FLAGS, TxMutex, WRITE_TOOLS, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, applyToolFlags, balanceCheckTool, borrowTool, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, claimRewardsTool, classifyEffort, clearPriceCache, compactMessages, createGuardRunnerState, defillamaChainTvlTool, defillamaPriceChangeTool, defillamaProtocolFeesTool, defillamaProtocolInfoTool, defillamaSuiProtocolsTool, defillamaTokenPricesTool, defillamaYieldPoolsTool, engineToSSE, estimateTokens, explainTxTool, extractConversationText, extractMcpText, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getDefaultTools, getMcpManager, getToolFlags, getWalletAddress, guardArtifactPreview, guardStaleData, hasNaviMcp, healthCheckTool, loadRecipes, mppServicesTool, parseMcpJson, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
|
|
4711
5729
|
//# sourceMappingURL=index.js.map
|
|
4712
5730
|
//# sourceMappingURL=index.js.map
|