@t2000/engine 1.15.0 → 1.16.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
@@ -1032,26 +1032,42 @@ interface PendingAction {
1032
1032
  steps?: PendingActionStep[];
1033
1033
  /**
1034
1034
  * [SPEC 7 v0.3 Quote-Refresh] Milliseconds since the upstream read tools
1035
- * that fed this bundle's composition completed. Engine stamps at emit
1035
+ * that fed this action's composition completed. Engine stamps at emit
1036
1036
  * time using `Date.now() - min(tool_result.timestamp)` across the listed
1037
1037
  * `regenerateInput.toolUseIds`. Host renders as a "QUOTE Ns OLD" badge in
1038
- * the PermissionCard header. Only set on bundled `pending_action`s.
1038
+ * the PermissionCard header.
1039
+ *
1040
+ * **[SPEC 15 v0.7 follow-up — 2026-05-04]** Now also stamped on
1041
+ * single-write confirm-tier `pending_action`s when same-turn
1042
+ * regeneratable reads contributed (e.g. a confirm-tier
1043
+ * `swap_execute` whose Cetus quote is referenced via the prior
1044
+ * `swap_quote` read). Pre-v0.7 this was bundle-only.
1039
1045
  */
1040
1046
  quoteAge?: number;
1041
1047
  /**
1042
- * [SPEC 7 v0.3 Quote-Refresh] True when the bundle was composed from
1048
+ * [SPEC 7 v0.3 Quote-Refresh] True when the action was composed from
1043
1049
  * re-runnable read tools (`swap_quote`, `rates_info`, `balance_check`,
1044
1050
  * `portfolio_analysis`). False when amounts came from user-provided
1045
- * inputs that don't depend on upstream quotes. Single-write
1046
- * `pending_action`s set this to `false` (regenerate is N≥2 only).
1051
+ * inputs that don't depend on upstream quotes.
1052
+ *
1053
+ * **[SPEC 15 v0.7 follow-up — 2026-05-04]** Now populated for
1054
+ * single-write confirm-tier actions too — pre-v0.7 single-writes
1055
+ * always emitted `false` because the regenerate path was N≥2 only.
1056
+ * Closing that gap surfaced the Refresh-quote affordance for the
1057
+ * single-write confirm-tier scenario (e.g. $50 swap_execute).
1047
1058
  */
1048
1059
  canRegenerate?: boolean;
1049
1060
  /**
1050
1061
  * [SPEC 7 v0.3 Quote-Refresh] Engine-internal payload listing which
1051
1062
  * upstream read `tool_use` ids to re-fire when the user taps REGENERATE.
1052
1063
  * Host echoes this back via `POST /api/engine/regenerate`; engine re-runs
1053
- * each tool with the same input (no LLM call), rebuilds the bundle, and
1054
- * emits a fresh `pending_action` with new per-step `attemptId`s.
1064
+ * each tool with the same input (no LLM call), rebuilds the action, and
1065
+ * emits a fresh `pending_action` with a fresh `attemptId`.
1066
+ *
1067
+ * **[SPEC 15 v0.7 follow-up — 2026-05-04]** Populated for both
1068
+ * bundle (N≥2) and single-write (N=1) shapes; the engine's
1069
+ * `regenerateBundle()` rebuild branches on `action.steps?.length`
1070
+ * to pick the right composition path.
1055
1071
  */
1056
1072
  regenerateInput?: {
1057
1073
  toolUseIds: string[];
@@ -1906,15 +1922,17 @@ type RegenerateResult = RegenerateSuccess | RegenerateFailure;
1906
1922
  * store after a successful regenerate.
1907
1923
  *
1908
1924
  * **Failure modes.**
1909
- * - `pending_action_not_found` — the action isn't a bundle (no
1910
- * `steps`, or fewer than 2 steps). Single-write actions can't be
1911
- * regenerated by design.
1925
+ * - `pending_action_not_found` — reserved for host-side mismatches
1926
+ * (session expired, attemptId mismatch). The engine itself never
1927
+ * returns this from `regenerateBundle`; the route surfaces it
1928
+ * pre-call. (Pre-v0.7 it also fired here for single-write
1929
+ * actions; lifted as part of single-write regenerate support.)
1912
1930
  * - `cannot_regenerate` — the action's `canRegenerate` flag is
1913
1931
  * false, OR no contributing read tool_use_ids could be located in
1914
1932
  * session history.
1915
- * - `engine_error` — a tool re-execution threw, OR bundle
1916
- * composition rejected (defensive — should not happen if the
1917
- * original bundle was valid).
1933
+ * - `engine_error` — a tool re-execution threw, OR action
1934
+ * rebuild rejected (defensive — should not happen if the
1935
+ * original action was valid).
1918
1936
  *
1919
1937
  * Errors in tool re-execution short-circuit the whole regenerate
1920
1938
  * (bundle composition would inherit a broken read result). The host
package/dist/index.js CHANGED
@@ -3647,26 +3647,35 @@ var swapExecuteTool = buildTool({
3647
3647
  },
3648
3648
  async call(input, context) {
3649
3649
  const agent = requireAgent(context);
3650
- const result = await agent.swap({
3651
- from: input.from,
3652
- to: input.to,
3653
- amount: input.amount,
3654
- byAmountIn: input.byAmountIn,
3655
- slippage: input.slippage
3656
- });
3657
- return {
3658
- data: {
3659
- tx: result.tx,
3660
- fromToken: result.fromToken,
3661
- toToken: result.toToken,
3662
- fromAmount: result.fromAmount,
3663
- toAmount: result.toAmount,
3664
- priceImpact: result.priceImpact,
3665
- route: result.route,
3666
- gasCost: result.gasCost
3667
- },
3668
- displayText: `Swapped ${result.fromAmount} ${result.fromToken} for ${result.toAmount.toFixed(4)} ${result.toToken} (tx: ${result.tx.slice(0, 8)}...)`
3669
- };
3650
+ const sink = getTelemetrySink();
3651
+ const start = Date.now();
3652
+ try {
3653
+ const result = await agent.swap({
3654
+ from: input.from,
3655
+ to: input.to,
3656
+ amount: input.amount,
3657
+ byAmountIn: input.byAmountIn,
3658
+ slippage: input.slippage
3659
+ });
3660
+ sink.histogram("cetus.swap_execute_total_ms", Date.now() - start);
3661
+ sink.counter("cetus.swap_execute_count", { outcome: "success" });
3662
+ return {
3663
+ data: {
3664
+ tx: result.tx,
3665
+ fromToken: result.fromToken,
3666
+ toToken: result.toToken,
3667
+ fromAmount: result.fromAmount,
3668
+ toAmount: result.toAmount,
3669
+ priceImpact: result.priceImpact,
3670
+ route: result.route,
3671
+ gasCost: result.gasCost
3672
+ },
3673
+ displayText: `Swapped ${result.fromAmount} ${result.fromToken} for ${result.toAmount.toFixed(4)} ${result.toToken} (tx: ${result.tx.slice(0, 8)}...)`
3674
+ };
3675
+ } catch (err) {
3676
+ sink.counter("cetus.swap_execute_count", { outcome: "error" });
3677
+ throw err;
3678
+ }
3670
3679
  }
3671
3680
  });
3672
3681
  var swapQuoteTool = buildTool({
@@ -3691,17 +3700,26 @@ var swapQuoteTool = buildTool({
3691
3700
  isReadOnly: true,
3692
3701
  async call(input, context) {
3693
3702
  const walletAddress = context.agent ? context.agent.address() : getWalletAddress(context);
3694
- const result = await getSwapQuote({
3695
- walletAddress,
3696
- from: input.from,
3697
- to: input.to,
3698
- amount: input.amount,
3699
- byAmountIn: input.byAmountIn
3700
- });
3701
- return {
3702
- data: result,
3703
- displayText: `${result.fromAmount} ${result.fromToken} \u2192 ${result.toAmount.toFixed(4)} ${result.toToken} (impact: ${(result.priceImpact * 100).toFixed(2)}%, via ${result.route})`
3704
- };
3703
+ const sink = getTelemetrySink();
3704
+ const start = Date.now();
3705
+ try {
3706
+ const result = await getSwapQuote({
3707
+ walletAddress,
3708
+ from: input.from,
3709
+ to: input.to,
3710
+ amount: input.amount,
3711
+ byAmountIn: input.byAmountIn
3712
+ });
3713
+ sink.histogram("cetus.find_route_ms", Date.now() - start);
3714
+ sink.counter("cetus.find_route_count", { outcome: "success" });
3715
+ return {
3716
+ data: result,
3717
+ displayText: `${result.fromAmount} ${result.fromToken} \u2192 ${result.toAmount.toFixed(4)} ${result.toToken} (impact: ${(result.priceImpact * 100).toFixed(2)}%, via ${result.route})`
3718
+ };
3719
+ } catch (err) {
3720
+ sink.counter("cetus.find_route_count", { outcome: "error" });
3721
+ throw err;
3722
+ }
3705
3723
  }
3706
3724
  });
3707
3725
  var voloStakeTool = buildTool({
@@ -6781,6 +6799,18 @@ function shouldChainCoin(producer, consumer) {
6781
6799
  if (!out || !inA) return false;
6782
6800
  return out === inA;
6783
6801
  }
6802
+ function computeRegenerateFields(readResults) {
6803
+ const regenerateToolUseIds = readResults.filter((r) => REGENERATABLE_READ_TOOLS.has(r.toolName)).map((r) => r.toolUseId);
6804
+ const canRegenerate = regenerateToolUseIds.length > 0;
6805
+ let quoteAge;
6806
+ if (canRegenerate) {
6807
+ const stalest = Math.min(
6808
+ ...readResults.filter((r) => REGENERATABLE_READ_TOOLS.has(r.toolName)).map((r) => r.timestamp)
6809
+ );
6810
+ quoteAge = Math.max(0, Date.now() - stalest);
6811
+ }
6812
+ return { canRegenerate, regenerateToolUseIds, quoteAge };
6813
+ }
6784
6814
  function composeBundleFromToolResults(input) {
6785
6815
  if (input.pendingWrites.length < 2) {
6786
6816
  throw new Error(
@@ -6819,15 +6849,7 @@ function composeBundleFromToolResults(input) {
6819
6849
  });
6820
6850
  }
6821
6851
  }
6822
- const regenerateToolUseIds = input.readResults.filter((r) => REGENERATABLE_READ_TOOLS.has(r.toolName)).map((r) => r.toolUseId);
6823
- const canRegenerate = regenerateToolUseIds.length > 0;
6824
- let quoteAge;
6825
- if (regenerateToolUseIds.length > 0) {
6826
- const stalest = Math.min(
6827
- ...input.readResults.filter((r) => REGENERATABLE_READ_TOOLS.has(r.toolName)).map((r) => r.timestamp)
6828
- );
6829
- quoteAge = Math.max(0, Date.now() - stalest);
6830
- }
6852
+ const { canRegenerate, regenerateToolUseIds, quoteAge } = computeRegenerateFields(input.readResults);
6831
6853
  const allGuardInjections = [];
6832
6854
  if (input.guardInjectionsByCallId) {
6833
6855
  for (const call of input.pendingWrites) {
@@ -7960,6 +7982,7 @@ ${recipeCtx}`;
7960
7982
  const writeGuardInjections = guardInjectionsByCallId[pendingWrite.call.id];
7961
7983
  const modifiableFields = getModifiableFields(pendingWrite.call.name);
7962
7984
  const attemptId = randomUUID();
7985
+ const singleWriteRegen = computeRegenerateFields(turnReadToolResults);
7963
7986
  yield {
7964
7987
  type: "pending_action",
7965
7988
  action: {
@@ -7976,7 +7999,12 @@ ${recipeCtx}`;
7976
7999
  ...writeGuardInjections?.length ? { guardInjections: writeGuardInjections } : {},
7977
8000
  ...modifiableFields?.length ? { modifiableFields } : {},
7978
8001
  turnIndex,
7979
- attemptId
8002
+ attemptId,
8003
+ ...singleWriteRegen.canRegenerate ? {
8004
+ canRegenerate: true,
8005
+ regenerateInput: { toolUseIds: singleWriteRegen.regenerateToolUseIds },
8006
+ ...singleWriteRegen.quoteAge !== void 0 ? { quoteAge: singleWriteRegen.quoteAge } : {}
8007
+ } : {}
7980
8008
  }
7981
8009
  };
7982
8010
  recordTurnOutcome("pending_action_single");
@@ -8204,13 +8232,6 @@ function harnessShapeForEffort(effort) {
8204
8232
  }
8205
8233
  }
8206
8234
  async function regenerateBundle(engine, action) {
8207
- if (!action.steps || action.steps.length < 2) {
8208
- return {
8209
- success: false,
8210
- reason: "pending_action_not_found",
8211
- message: "Action is not a multi-step bundle"
8212
- };
8213
- }
8214
8235
  if (action.canRegenerate !== true) {
8215
8236
  return {
8216
8237
  success: false,
@@ -8324,11 +8345,6 @@ async function regenerateBundle(engine, action) {
8324
8345
  { role: "user", content: synthUserBlocks }
8325
8346
  ];
8326
8347
  engine.loadMessages([...messages, ...synthMessages]);
8327
- const pendingWrites = action.steps.map((step) => ({
8328
- id: step.toolUseId,
8329
- name: step.toolName,
8330
- input: step.input
8331
- }));
8332
8348
  const readResults = newReads.map((r) => ({
8333
8349
  toolUseId: r.toolUseId,
8334
8350
  toolName: r.toolName,
@@ -8337,20 +8353,61 @@ async function regenerateBundle(engine, action) {
8337
8353
  const assistantContent = action.assistantContent ?? [];
8338
8354
  const completedResults = action.completedResults ?? [];
8339
8355
  let newPendingAction;
8340
- try {
8341
- newPendingAction = composeBundleFromToolResults({
8342
- pendingWrites,
8343
- tools,
8344
- readResults,
8356
+ if (action.steps && action.steps.length >= 2) {
8357
+ const pendingWrites = action.steps.map((step) => ({
8358
+ id: step.toolUseId,
8359
+ name: step.toolName,
8360
+ input: step.input
8361
+ }));
8362
+ try {
8363
+ newPendingAction = composeBundleFromToolResults({
8364
+ pendingWrites,
8365
+ tools,
8366
+ readResults,
8367
+ assistantContent,
8368
+ completedResults,
8369
+ turnIndex: action.turnIndex
8370
+ });
8371
+ } catch (err) {
8372
+ return {
8373
+ success: false,
8374
+ reason: "engine_error",
8375
+ message: err instanceof Error ? err.message : "Bundle rebuild failed"
8376
+ };
8377
+ }
8378
+ } else {
8379
+ const tool = findTool(tools, action.toolName);
8380
+ if (!tool) {
8381
+ return {
8382
+ success: false,
8383
+ reason: "engine_error",
8384
+ message: `Unknown tool '${action.toolName}' in single-write rebuild`
8385
+ };
8386
+ }
8387
+ const call = {
8388
+ id: action.toolUseId,
8389
+ name: action.toolName,
8390
+ input: action.input
8391
+ };
8392
+ const description = describeAction(tool, call);
8393
+ const modifiableFields = getModifiableFields(action.toolName);
8394
+ const { canRegenerate, regenerateToolUseIds, quoteAge } = computeRegenerateFields(readResults);
8395
+ newPendingAction = {
8396
+ toolName: action.toolName,
8397
+ toolUseId: action.toolUseId,
8398
+ input: action.input,
8399
+ description,
8345
8400
  assistantContent,
8346
8401
  completedResults,
8347
- turnIndex: action.turnIndex
8348
- });
8349
- } catch (err) {
8350
- return {
8351
- success: false,
8352
- reason: "engine_error",
8353
- message: err instanceof Error ? err.message : "Bundle rebuild failed"
8402
+ ...action.guardInjections?.length ? { guardInjections: action.guardInjections } : {},
8403
+ ...modifiableFields?.length ? { modifiableFields } : {},
8404
+ turnIndex: action.turnIndex,
8405
+ attemptId: randomUUID(),
8406
+ ...canRegenerate ? {
8407
+ canRegenerate: true,
8408
+ regenerateInput: { toolUseIds: regenerateToolUseIds },
8409
+ ...quoteAge !== void 0 ? { quoteAge } : {}
8410
+ } : {}
8354
8411
  };
8355
8412
  }
8356
8413
  return { success: true, newPendingAction, timelineEvents };