@t2000/engine 0.41.0 → 0.42.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 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;
@@ -699,6 +709,43 @@ interface EngineConfig {
699
709
  * verdict→action mapping. Errors thrown by the host are caught.
700
710
  */
701
711
  onGuardFired?: (guard: GuardMetric) => void;
712
+ /**
713
+ * [v1.5] Map of write tool name → list of read tool names whose state
714
+ * the write invalidates. After a successful write resumes via
715
+ * `resumeWithToolResult`, the engine auto-runs each configured read
716
+ * tool with empty input, pushes synthetic `tool_use` + `tool_result`
717
+ * messages into the conversation, and yields `tool_result` events
718
+ * with `wasPostWriteRefresh: true` BEFORE handing control back to the
719
+ * LLM for narration.
720
+ *
721
+ * Why: writes change on-chain state. Without a fresh read, the LLM
722
+ * narrates from the pre-write snapshot and frequently invents balance
723
+ * totals. Auto-injecting fresh reads makes the hallucination class
724
+ * physically impossible — the model has authoritative ground truth in
725
+ * its context before generating the post-write sentence.
726
+ *
727
+ * Constraints:
728
+ * - Refresh tools MUST be `isReadOnly` and `isConcurrencySafe`.
729
+ * - Refresh runs only when the write succeeded (executionResult is
730
+ * not `{ success: false }`); failed writes leave state unchanged
731
+ * and refreshing would be misleading.
732
+ * - Tools are invoked with empty input; refresh tools should accept
733
+ * an empty object schema (e.g. `balance_check`, `savings_info`).
734
+ * - Errors during refresh are non-fatal — a tool_result with
735
+ * `isError: true` is still pushed so the LLM knows refresh failed.
736
+ *
737
+ * Example:
738
+ * ```
739
+ * {
740
+ * save_deposit: ['balance_check', 'savings_info'],
741
+ * send_transfer: ['balance_check'],
742
+ * borrow: ['balance_check', 'savings_info', 'health_check'],
743
+ * }
744
+ * ```
745
+ *
746
+ * Omit (undefined / empty map) to disable post-write refresh entirely.
747
+ */
748
+ postWriteRefresh?: Record<string, string[]>;
702
749
  }
703
750
  interface LLMProvider {
704
751
  chat(params: ChatParams): AsyncGenerator<ProviderEvent>;
@@ -824,6 +871,7 @@ declare class QueryEngine {
824
871
  private readonly sessionSpendUsd;
825
872
  private readonly onAutoExecuted;
826
873
  private readonly onGuardFired;
874
+ private readonly postWriteRefresh;
827
875
  private matchedRecipe;
828
876
  private messages;
829
877
  private abortController;
@@ -845,6 +893,16 @@ declare class QueryEngine {
845
893
  * This is a separate HTTP request — no persistent connection from submitMessage.
846
894
  */
847
895
  resumeWithToolResult(action: PendingAction, response: PermissionResponse): AsyncGenerator<EngineEvent>;
896
+ /**
897
+ * [v1.5] Auto-run configured read tools after a successful write,
898
+ * push their results into the conversation, and yield `tool_result`
899
+ * events so hosts/UI render them in the timeline. See
900
+ * `EngineConfig.postWriteRefresh`.
901
+ *
902
+ * Pure injection — no LLM call here. The next `agentLoop` turn sees
903
+ * the fresh tool results and narrates from them.
904
+ */
905
+ private runPostWriteRefresh;
848
906
  interrupt(): void;
849
907
  getMessages(): readonly Message[];
850
908
  getMatchedRecipe(): Recipe | null;
@@ -916,6 +974,9 @@ type SSEEvent = {
916
974
  toolUseId: string;
917
975
  result: unknown;
918
976
  isError: boolean;
977
+ wasEarlyDispatched?: boolean;
978
+ resultDeduped?: boolean;
979
+ wasPostWriteRefresh?: boolean;
919
980
  } | {
920
981
  type: 'pending_action';
921
982
  action: PendingAction;
package/dist/index.js CHANGED
@@ -4177,6 +4177,9 @@ var QueryEngine = class {
4177
4177
  sessionSpendUsd;
4178
4178
  onAutoExecuted;
4179
4179
  onGuardFired;
4180
+ // [v1.5] See `EngineConfig.postWriteRefresh` — drives the post-write
4181
+ // synthetic read injection in `resumeWithToolResult`.
4182
+ postWriteRefresh;
4180
4183
  matchedRecipe = null;
4181
4184
  messages = [];
4182
4185
  abortController = null;
@@ -4209,6 +4212,7 @@ var QueryEngine = class {
4209
4212
  this.sessionSpendUsd = config.sessionSpendUsd;
4210
4213
  this.onAutoExecuted = config.onAutoExecuted;
4211
4214
  this.onGuardFired = config.onGuardFired;
4215
+ this.postWriteRefresh = config.postWriteRefresh;
4212
4216
  this.tools = config.tools ?? (config.agent ? getDefaultTools() : []);
4213
4217
  }
4214
4218
  /**
@@ -4277,8 +4281,96 @@ var QueryEngine = class {
4277
4281
  yield { type: "turn_complete", stopReason: "end_turn" };
4278
4282
  return;
4279
4283
  }
4284
+ yield* this.runPostWriteRefresh(action, response, signal);
4280
4285
  yield* this.agentLoop(null, signal, false);
4281
4286
  }
4287
+ /**
4288
+ * [v1.5] Auto-run configured read tools after a successful write,
4289
+ * push their results into the conversation, and yield `tool_result`
4290
+ * events so hosts/UI render them in the timeline. See
4291
+ * `EngineConfig.postWriteRefresh`.
4292
+ *
4293
+ * Pure injection — no LLM call here. The next `agentLoop` turn sees
4294
+ * the fresh tool results and narrates from them.
4295
+ */
4296
+ async *runPostWriteRefresh(action, response, signal) {
4297
+ const refreshList = this.postWriteRefresh?.[action.toolName];
4298
+ if (!refreshList || refreshList.length === 0) return;
4299
+ const exec = response.executionResult;
4300
+ const writeFailed = exec != null && typeof exec === "object" && "success" in exec && exec.success === false;
4301
+ if (writeFailed) return;
4302
+ const refreshTools = refreshList.map((name) => findTool(this.tools, name)).filter(
4303
+ (t) => t !== void 0 && t.isReadOnly && t.isConcurrencySafe
4304
+ );
4305
+ if (refreshTools.length === 0) return;
4306
+ const context = {
4307
+ agent: this.agent,
4308
+ mcpManager: this.mcpManager,
4309
+ walletAddress: this.walletAddress,
4310
+ suiRpcUrl: this.suiRpcUrl,
4311
+ serverPositions: this.serverPositions,
4312
+ positionFetcher: this.positionFetcher,
4313
+ env: this.env,
4314
+ signal,
4315
+ priceCache: this.priceCache,
4316
+ permissionConfig: this.permissionConfig,
4317
+ sessionSpendUsd: this.sessionSpendUsd
4318
+ };
4319
+ const idStem = `pwr_${action.toolUseId.slice(-6)}`;
4320
+ const refreshes = await Promise.all(
4321
+ refreshTools.map(async (tool, idx) => {
4322
+ const id = `${idStem}_${idx}_${tool.name}`;
4323
+ try {
4324
+ const parsed = tool.inputSchema.safeParse({});
4325
+ if (!parsed.success) {
4326
+ return {
4327
+ tool,
4328
+ id,
4329
+ isError: true,
4330
+ data: {
4331
+ error: `Post-write refresh: invalid input for ${tool.name}`
4332
+ }
4333
+ };
4334
+ }
4335
+ const result = await tool.call(parsed.data, context);
4336
+ return { tool, id, isError: false, data: result.data };
4337
+ } catch (err) {
4338
+ return {
4339
+ tool,
4340
+ id,
4341
+ isError: true,
4342
+ data: {
4343
+ error: err instanceof Error ? err.message : "Post-write refresh failed"
4344
+ }
4345
+ };
4346
+ }
4347
+ })
4348
+ );
4349
+ const refreshUses = refreshes.map((r) => ({
4350
+ type: "tool_use",
4351
+ id: r.id,
4352
+ name: r.tool.name,
4353
+ input: {}
4354
+ }));
4355
+ this.messages.push({ role: "assistant", content: refreshUses });
4356
+ const refreshResults = refreshes.map((r) => ({
4357
+ type: "tool_result",
4358
+ toolUseId: r.id,
4359
+ content: typeof r.data === "string" ? r.data : JSON.stringify(r.data),
4360
+ isError: r.isError
4361
+ }));
4362
+ this.messages.push({ role: "user", content: refreshResults });
4363
+ for (const r of refreshes) {
4364
+ yield {
4365
+ type: "tool_result",
4366
+ toolName: r.tool.name,
4367
+ toolUseId: r.id,
4368
+ result: r.data,
4369
+ isError: r.isError,
4370
+ wasPostWriteRefresh: true
4371
+ };
4372
+ }
4373
+ }
4282
4374
  interrupt() {
4283
4375
  this.abortController?.abort();
4284
4376
  }