@t2000/engine 0.41.0 → 0.43.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 +91 -1
- package/dist/index.js +123 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -432,6 +432,16 @@ type EngineEvent = {
|
|
|
432
432
|
* actually re-running the tool.
|
|
433
433
|
*/
|
|
434
434
|
resultDeduped?: boolean;
|
|
435
|
+
/**
|
|
436
|
+
* [v1.5] True when this result was produced by the engine's
|
|
437
|
+
* post-write refresh mechanism (see `EngineConfig.postWriteRefresh`).
|
|
438
|
+
* The engine auto-runs configured read tools immediately after a
|
|
439
|
+
* successful write so the LLM narrates from fresh on-chain state
|
|
440
|
+
* instead of inferring from a stale snapshot. Hosts should render
|
|
441
|
+
* these like any other tool result; the flag is for analytics and
|
|
442
|
+
* UI affordances (e.g. a subtle "auto-refreshed" badge).
|
|
443
|
+
*/
|
|
444
|
+
wasPostWriteRefresh?: boolean;
|
|
435
445
|
} | {
|
|
436
446
|
type: 'pending_action';
|
|
437
447
|
action: PendingAction;
|
|
@@ -615,6 +625,18 @@ interface Tool<TInput = unknown, TOutput = unknown> {
|
|
|
615
625
|
maxResultSizeChars?: number;
|
|
616
626
|
/** Custom truncation strategy. Falls back to generic slice + hint when omitted. */
|
|
617
627
|
summarizeOnTruncate?: (result: string, maxChars: number) => string;
|
|
628
|
+
/**
|
|
629
|
+
* [v1.5.1] Whether `microcompact` may dedupe this tool's results across
|
|
630
|
+
* multiple calls with identical input. Default `true` — most tools are
|
|
631
|
+
* effectively pure within a session (price lookups, protocol info,
|
|
632
|
+
* yield pools). Set to `false` for tools whose result depends on
|
|
633
|
+
* mutable on-chain state and therefore changes after writes
|
|
634
|
+
* (`balance_check`, `savings_info`, `health_check`,
|
|
635
|
+
* `transaction_history`). Non-cacheable tools are excluded from the
|
|
636
|
+
* `seen` map entirely, so neither this call nor any later call with
|
|
637
|
+
* the same input gets replaced with a "[Same result …]" back-reference.
|
|
638
|
+
*/
|
|
639
|
+
cacheable?: boolean;
|
|
618
640
|
}
|
|
619
641
|
type ThinkingEffort = 'low' | 'medium' | 'high' | 'max';
|
|
620
642
|
type ThinkingConfig = {
|
|
@@ -699,6 +721,43 @@ interface EngineConfig {
|
|
|
699
721
|
* verdict→action mapping. Errors thrown by the host are caught.
|
|
700
722
|
*/
|
|
701
723
|
onGuardFired?: (guard: GuardMetric) => void;
|
|
724
|
+
/**
|
|
725
|
+
* [v1.5] Map of write tool name → list of read tool names whose state
|
|
726
|
+
* the write invalidates. After a successful write resumes via
|
|
727
|
+
* `resumeWithToolResult`, the engine auto-runs each configured read
|
|
728
|
+
* tool with empty input, pushes synthetic `tool_use` + `tool_result`
|
|
729
|
+
* messages into the conversation, and yields `tool_result` events
|
|
730
|
+
* with `wasPostWriteRefresh: true` BEFORE handing control back to the
|
|
731
|
+
* LLM for narration.
|
|
732
|
+
*
|
|
733
|
+
* Why: writes change on-chain state. Without a fresh read, the LLM
|
|
734
|
+
* narrates from the pre-write snapshot and frequently invents balance
|
|
735
|
+
* totals. Auto-injecting fresh reads makes the hallucination class
|
|
736
|
+
* physically impossible — the model has authoritative ground truth in
|
|
737
|
+
* its context before generating the post-write sentence.
|
|
738
|
+
*
|
|
739
|
+
* Constraints:
|
|
740
|
+
* - Refresh tools MUST be `isReadOnly` and `isConcurrencySafe`.
|
|
741
|
+
* - Refresh runs only when the write succeeded (executionResult is
|
|
742
|
+
* not `{ success: false }`); failed writes leave state unchanged
|
|
743
|
+
* and refreshing would be misleading.
|
|
744
|
+
* - Tools are invoked with empty input; refresh tools should accept
|
|
745
|
+
* an empty object schema (e.g. `balance_check`, `savings_info`).
|
|
746
|
+
* - Errors during refresh are non-fatal — a tool_result with
|
|
747
|
+
* `isError: true` is still pushed so the LLM knows refresh failed.
|
|
748
|
+
*
|
|
749
|
+
* Example:
|
|
750
|
+
* ```
|
|
751
|
+
* {
|
|
752
|
+
* save_deposit: ['balance_check', 'savings_info'],
|
|
753
|
+
* send_transfer: ['balance_check'],
|
|
754
|
+
* borrow: ['balance_check', 'savings_info', 'health_check'],
|
|
755
|
+
* }
|
|
756
|
+
* ```
|
|
757
|
+
*
|
|
758
|
+
* Omit (undefined / empty map) to disable post-write refresh entirely.
|
|
759
|
+
*/
|
|
760
|
+
postWriteRefresh?: Record<string, string[]>;
|
|
702
761
|
}
|
|
703
762
|
interface LLMProvider {
|
|
704
763
|
chat(params: ChatParams): AsyncGenerator<ProviderEvent>;
|
|
@@ -824,6 +883,7 @@ declare class QueryEngine {
|
|
|
824
883
|
private readonly sessionSpendUsd;
|
|
825
884
|
private readonly onAutoExecuted;
|
|
826
885
|
private readonly onGuardFired;
|
|
886
|
+
private readonly postWriteRefresh;
|
|
827
887
|
private matchedRecipe;
|
|
828
888
|
private messages;
|
|
829
889
|
private abortController;
|
|
@@ -845,6 +905,16 @@ declare class QueryEngine {
|
|
|
845
905
|
* This is a separate HTTP request — no persistent connection from submitMessage.
|
|
846
906
|
*/
|
|
847
907
|
resumeWithToolResult(action: PendingAction, response: PermissionResponse): AsyncGenerator<EngineEvent>;
|
|
908
|
+
/**
|
|
909
|
+
* [v1.5] Auto-run configured read tools after a successful write,
|
|
910
|
+
* push their results into the conversation, and yield `tool_result`
|
|
911
|
+
* events so hosts/UI render them in the timeline. See
|
|
912
|
+
* `EngineConfig.postWriteRefresh`.
|
|
913
|
+
*
|
|
914
|
+
* Pure injection — no LLM call here. The next `agentLoop` turn sees
|
|
915
|
+
* the fresh tool results and narrates from them.
|
|
916
|
+
*/
|
|
917
|
+
private runPostWriteRefresh;
|
|
848
918
|
interrupt(): void;
|
|
849
919
|
getMessages(): readonly Message[];
|
|
850
920
|
getMatchedRecipe(): Recipe | null;
|
|
@@ -887,6 +957,11 @@ interface BuildToolOptions<TInput, TOutput> {
|
|
|
887
957
|
preflight?: (input: TInput) => PreflightResult;
|
|
888
958
|
maxResultSizeChars?: number;
|
|
889
959
|
summarizeOnTruncate?: (result: string, maxChars: number) => string;
|
|
960
|
+
/**
|
|
961
|
+
* [v1.5.1] See `Tool.cacheable`. Default `true`. Set `false` for
|
|
962
|
+
* tools whose results depend on mutable on-chain state.
|
|
963
|
+
*/
|
|
964
|
+
cacheable?: boolean;
|
|
890
965
|
}
|
|
891
966
|
declare function buildTool<TInput, TOutput>(opts: BuildToolOptions<TInput, TOutput>): Tool<TInput, TOutput>;
|
|
892
967
|
declare function toolsToDefinitions(tools: Tool[]): {
|
|
@@ -916,6 +991,9 @@ type SSEEvent = {
|
|
|
916
991
|
toolUseId: string;
|
|
917
992
|
result: unknown;
|
|
918
993
|
isError: boolean;
|
|
994
|
+
wasEarlyDispatched?: boolean;
|
|
995
|
+
resultDeduped?: boolean;
|
|
996
|
+
wasPostWriteRefresh?: boolean;
|
|
919
997
|
} | {
|
|
920
998
|
type: 'pending_action';
|
|
921
999
|
action: PendingAction;
|
|
@@ -1119,11 +1197,23 @@ interface MicrocompactResult extends Array<Message> {
|
|
|
1119
1197
|
* the full prior result with a compact back-reference. Runs before any
|
|
1120
1198
|
* LLM-based compaction and costs nothing.
|
|
1121
1199
|
*
|
|
1200
|
+
* [v1.5.1] Tools may opt out of dedupe by setting `cacheable: false` on
|
|
1201
|
+
* their `Tool` definition. Non-cacheable tools (e.g. `balance_check`,
|
|
1202
|
+
* `savings_info`, `health_check`, `transaction_history`) are excluded
|
|
1203
|
+
* from the `seen` map entirely, so neither the current call nor any
|
|
1204
|
+
* later call with identical inputs gets replaced — necessary because
|
|
1205
|
+
* their results depend on mutable on-chain state that writes invalidate.
|
|
1206
|
+
*
|
|
1122
1207
|
* Returns a new array — does not mutate the input. The returned array
|
|
1123
1208
|
* carries a `dedupedToolUseIds` property listing every tool-use ID whose
|
|
1124
1209
|
* tool_result block was replaced with a back-reference this pass.
|
|
1210
|
+
*
|
|
1211
|
+
* @param messages — conversation ledger to compact.
|
|
1212
|
+
* @param tools — optional tool registry consulted to resolve the
|
|
1213
|
+
* per-tool `cacheable` flag. Omit to dedupe every
|
|
1214
|
+
* tool (legacy behavior — back-compat).
|
|
1125
1215
|
*/
|
|
1126
|
-
declare function microcompact(messages: readonly Message[]): MicrocompactResult;
|
|
1216
|
+
declare function microcompact(messages: readonly Message[], tools?: readonly Tool[]): MicrocompactResult;
|
|
1127
1217
|
|
|
1128
1218
|
/**
|
|
1129
1219
|
* EarlyToolDispatcher — dispatches read-only tools mid-stream.
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,8 @@ function buildTool(opts) {
|
|
|
23
23
|
flags: opts.flags ?? {},
|
|
24
24
|
preflight: opts.preflight,
|
|
25
25
|
maxResultSizeChars: opts.maxResultSizeChars,
|
|
26
|
-
summarizeOnTruncate: opts.summarizeOnTruncate
|
|
26
|
+
summarizeOnTruncate: opts.summarizeOnTruncate,
|
|
27
|
+
cacheable: opts.cacheable
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
function toolsToDefinitions(tools) {
|
|
@@ -557,6 +558,10 @@ var balanceCheckTool = buildTool({
|
|
|
557
558
|
inputSchema: z.object({}),
|
|
558
559
|
jsonSchema: { type: "object", properties: {}, required: [] },
|
|
559
560
|
isReadOnly: true,
|
|
561
|
+
// [v1.5.1] Wallet contents change after every send/swap/save/etc.
|
|
562
|
+
// Microcompact must NEVER dedupe these calls — each one reflects a
|
|
563
|
+
// different on-chain state.
|
|
564
|
+
cacheable: false,
|
|
560
565
|
async call(_input, context) {
|
|
561
566
|
if (hasNaviMcp(context)) {
|
|
562
567
|
const address = getWalletAddress(context);
|
|
@@ -850,6 +855,9 @@ var savingsInfoTool = buildTool({
|
|
|
850
855
|
inputSchema: z.object({}),
|
|
851
856
|
jsonSchema: { type: "object", properties: {}, required: [] },
|
|
852
857
|
isReadOnly: true,
|
|
858
|
+
// [v1.5.1] NAVI deposits change on save_deposit / withdraw / claim.
|
|
859
|
+
// Each call reflects a fresh on-chain snapshot — never dedupe.
|
|
860
|
+
cacheable: false,
|
|
853
861
|
async call(_input, context) {
|
|
854
862
|
if (context.positionFetcher && context.walletAddress) {
|
|
855
863
|
const sp = await context.positionFetcher(context.walletAddress);
|
|
@@ -910,6 +918,9 @@ var healthCheckTool = buildTool({
|
|
|
910
918
|
inputSchema: z.object({}),
|
|
911
919
|
jsonSchema: { type: "object", properties: {}, required: [] },
|
|
912
920
|
isReadOnly: true,
|
|
921
|
+
// [v1.5.1] Health factor changes on every borrow / repay / collateral
|
|
922
|
+
// movement and even passively as oracle prices update. Never dedupe.
|
|
923
|
+
cacheable: false,
|
|
913
924
|
async call(_input, context) {
|
|
914
925
|
if (context.positionFetcher && context.walletAddress) {
|
|
915
926
|
const sp = await context.positionFetcher(context.walletAddress);
|
|
@@ -1151,6 +1162,10 @@ var transactionHistoryTool = buildTool({
|
|
|
1151
1162
|
},
|
|
1152
1163
|
isReadOnly: true,
|
|
1153
1164
|
maxResultSizeChars: 8e3,
|
|
1165
|
+
// [v1.5.1] New transactions land continuously. Even with an explicit
|
|
1166
|
+
// `date` filter the dedupe is wrong post-write because the just-
|
|
1167
|
+
// executed write may now be in history. Never dedupe.
|
|
1168
|
+
cacheable: false,
|
|
1154
1169
|
async call(input, context) {
|
|
1155
1170
|
const limit = input.limit ?? 10;
|
|
1156
1171
|
const action = input.action;
|
|
@@ -3724,15 +3739,23 @@ function extractConversationText(messages) {
|
|
|
3724
3739
|
}
|
|
3725
3740
|
|
|
3726
3741
|
// src/compact/microcompact.ts
|
|
3727
|
-
function microcompact(messages) {
|
|
3742
|
+
function microcompact(messages, tools) {
|
|
3728
3743
|
const seen = /* @__PURE__ */ new Map();
|
|
3729
3744
|
let toolUseIndex = 0;
|
|
3730
3745
|
const toolUseInputs = /* @__PURE__ */ new Map();
|
|
3746
|
+
const cacheableByName = /* @__PURE__ */ new Map();
|
|
3747
|
+
if (tools) {
|
|
3748
|
+
for (const t of tools) {
|
|
3749
|
+
cacheableByName.set(t.name, t.cacheable ?? true);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3731
3752
|
const dedupedToolUseIds = /* @__PURE__ */ new Set();
|
|
3753
|
+
const toolNameById = /* @__PURE__ */ new Map();
|
|
3732
3754
|
for (const msg of messages) {
|
|
3733
3755
|
for (const block of msg.content) {
|
|
3734
3756
|
if (block.type === "tool_use") {
|
|
3735
3757
|
toolUseInputs.set(block.id, `${block.name}:${stableStringify(block.input)}`);
|
|
3758
|
+
toolNameById.set(block.id, block.name);
|
|
3736
3759
|
}
|
|
3737
3760
|
}
|
|
3738
3761
|
}
|
|
@@ -3744,6 +3767,11 @@ function microcompact(messages) {
|
|
|
3744
3767
|
if (block.type !== "tool_result") return block;
|
|
3745
3768
|
const key = toolUseInputs.get(block.toolUseId);
|
|
3746
3769
|
if (!key) return block;
|
|
3770
|
+
const toolName = toolNameById.get(block.toolUseId);
|
|
3771
|
+
if (toolName && cacheableByName.get(toolName) === false) {
|
|
3772
|
+
toolUseIndex++;
|
|
3773
|
+
return block;
|
|
3774
|
+
}
|
|
3747
3775
|
toolUseIndex++;
|
|
3748
3776
|
const prior = seen.get(key);
|
|
3749
3777
|
if (prior && !block.isError) {
|
|
@@ -4177,6 +4205,9 @@ var QueryEngine = class {
|
|
|
4177
4205
|
sessionSpendUsd;
|
|
4178
4206
|
onAutoExecuted;
|
|
4179
4207
|
onGuardFired;
|
|
4208
|
+
// [v1.5] See `EngineConfig.postWriteRefresh` — drives the post-write
|
|
4209
|
+
// synthetic read injection in `resumeWithToolResult`.
|
|
4210
|
+
postWriteRefresh;
|
|
4180
4211
|
matchedRecipe = null;
|
|
4181
4212
|
messages = [];
|
|
4182
4213
|
abortController = null;
|
|
@@ -4209,6 +4240,7 @@ var QueryEngine = class {
|
|
|
4209
4240
|
this.sessionSpendUsd = config.sessionSpendUsd;
|
|
4210
4241
|
this.onAutoExecuted = config.onAutoExecuted;
|
|
4211
4242
|
this.onGuardFired = config.onGuardFired;
|
|
4243
|
+
this.postWriteRefresh = config.postWriteRefresh;
|
|
4212
4244
|
this.tools = config.tools ?? (config.agent ? getDefaultTools() : []);
|
|
4213
4245
|
}
|
|
4214
4246
|
/**
|
|
@@ -4277,8 +4309,96 @@ var QueryEngine = class {
|
|
|
4277
4309
|
yield { type: "turn_complete", stopReason: "end_turn" };
|
|
4278
4310
|
return;
|
|
4279
4311
|
}
|
|
4312
|
+
yield* this.runPostWriteRefresh(action, response, signal);
|
|
4280
4313
|
yield* this.agentLoop(null, signal, false);
|
|
4281
4314
|
}
|
|
4315
|
+
/**
|
|
4316
|
+
* [v1.5] Auto-run configured read tools after a successful write,
|
|
4317
|
+
* push their results into the conversation, and yield `tool_result`
|
|
4318
|
+
* events so hosts/UI render them in the timeline. See
|
|
4319
|
+
* `EngineConfig.postWriteRefresh`.
|
|
4320
|
+
*
|
|
4321
|
+
* Pure injection — no LLM call here. The next `agentLoop` turn sees
|
|
4322
|
+
* the fresh tool results and narrates from them.
|
|
4323
|
+
*/
|
|
4324
|
+
async *runPostWriteRefresh(action, response, signal) {
|
|
4325
|
+
const refreshList = this.postWriteRefresh?.[action.toolName];
|
|
4326
|
+
if (!refreshList || refreshList.length === 0) return;
|
|
4327
|
+
const exec = response.executionResult;
|
|
4328
|
+
const writeFailed = exec != null && typeof exec === "object" && "success" in exec && exec.success === false;
|
|
4329
|
+
if (writeFailed) return;
|
|
4330
|
+
const refreshTools = refreshList.map((name) => findTool(this.tools, name)).filter(
|
|
4331
|
+
(t) => t !== void 0 && t.isReadOnly && t.isConcurrencySafe
|
|
4332
|
+
);
|
|
4333
|
+
if (refreshTools.length === 0) return;
|
|
4334
|
+
const context = {
|
|
4335
|
+
agent: this.agent,
|
|
4336
|
+
mcpManager: this.mcpManager,
|
|
4337
|
+
walletAddress: this.walletAddress,
|
|
4338
|
+
suiRpcUrl: this.suiRpcUrl,
|
|
4339
|
+
serverPositions: this.serverPositions,
|
|
4340
|
+
positionFetcher: this.positionFetcher,
|
|
4341
|
+
env: this.env,
|
|
4342
|
+
signal,
|
|
4343
|
+
priceCache: this.priceCache,
|
|
4344
|
+
permissionConfig: this.permissionConfig,
|
|
4345
|
+
sessionSpendUsd: this.sessionSpendUsd
|
|
4346
|
+
};
|
|
4347
|
+
const idStem = `pwr_${action.toolUseId.slice(-6)}`;
|
|
4348
|
+
const refreshes = await Promise.all(
|
|
4349
|
+
refreshTools.map(async (tool, idx) => {
|
|
4350
|
+
const id = `${idStem}_${idx}_${tool.name}`;
|
|
4351
|
+
try {
|
|
4352
|
+
const parsed = tool.inputSchema.safeParse({});
|
|
4353
|
+
if (!parsed.success) {
|
|
4354
|
+
return {
|
|
4355
|
+
tool,
|
|
4356
|
+
id,
|
|
4357
|
+
isError: true,
|
|
4358
|
+
data: {
|
|
4359
|
+
error: `Post-write refresh: invalid input for ${tool.name}`
|
|
4360
|
+
}
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
const result = await tool.call(parsed.data, context);
|
|
4364
|
+
return { tool, id, isError: false, data: result.data };
|
|
4365
|
+
} catch (err) {
|
|
4366
|
+
return {
|
|
4367
|
+
tool,
|
|
4368
|
+
id,
|
|
4369
|
+
isError: true,
|
|
4370
|
+
data: {
|
|
4371
|
+
error: err instanceof Error ? err.message : "Post-write refresh failed"
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
})
|
|
4376
|
+
);
|
|
4377
|
+
const refreshUses = refreshes.map((r) => ({
|
|
4378
|
+
type: "tool_use",
|
|
4379
|
+
id: r.id,
|
|
4380
|
+
name: r.tool.name,
|
|
4381
|
+
input: {}
|
|
4382
|
+
}));
|
|
4383
|
+
this.messages.push({ role: "assistant", content: refreshUses });
|
|
4384
|
+
const refreshResults = refreshes.map((r) => ({
|
|
4385
|
+
type: "tool_result",
|
|
4386
|
+
toolUseId: r.id,
|
|
4387
|
+
content: typeof r.data === "string" ? r.data : JSON.stringify(r.data),
|
|
4388
|
+
isError: r.isError
|
|
4389
|
+
}));
|
|
4390
|
+
this.messages.push({ role: "user", content: refreshResults });
|
|
4391
|
+
for (const r of refreshes) {
|
|
4392
|
+
yield {
|
|
4393
|
+
type: "tool_result",
|
|
4394
|
+
toolName: r.tool.name,
|
|
4395
|
+
toolUseId: r.id,
|
|
4396
|
+
result: r.data,
|
|
4397
|
+
isError: r.isError,
|
|
4398
|
+
wasPostWriteRefresh: true
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4282
4402
|
interrupt() {
|
|
4283
4403
|
this.abortController?.abort();
|
|
4284
4404
|
}
|
|
@@ -4350,7 +4470,7 @@ var QueryEngine = class {
|
|
|
4350
4470
|
};
|
|
4351
4471
|
const dispatcher = new EarlyToolDispatcher(this.tools, context);
|
|
4352
4472
|
try {
|
|
4353
|
-
const microcompacted = microcompact(this.messages);
|
|
4473
|
+
const microcompacted = microcompact(this.messages, this.tools);
|
|
4354
4474
|
this.messages = microcompacted;
|
|
4355
4475
|
for (const dedupedId of microcompacted.dedupedToolUseIds) {
|
|
4356
4476
|
yield {
|