la-machina-engine 0.19.3 → 0.19.7

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.cjs CHANGED
@@ -2098,6 +2098,7 @@ var AnthropicClient = class {
2098
2098
  if (betas.length > 0) {
2099
2099
  requestOptions.headers = { "anthropic-beta": betas.join(",") };
2100
2100
  }
2101
+ if (request.abortSignal !== void 0) requestOptions.signal = request.abortSignal;
2101
2102
  let stream;
2102
2103
  try {
2103
2104
  stream = this.sdk.messages.stream(params, requestOptions);
@@ -2284,6 +2285,7 @@ var AISdkAdapter = class {
2284
2285
  tools,
2285
2286
  ...request.maxTokens !== void 0 ? { maxOutputTokens: request.maxTokens } : {},
2286
2287
  ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
2288
+ ...request.abortSignal !== void 0 ? { abortSignal: request.abortSignal } : {},
2287
2289
  // Plan 025 — pass through `'required'` so the AI SDK forwards it
2288
2290
  // to the provider as that provider's "force tool call" flag.
2289
2291
  ...request.toolChoice === "required" ? { toolChoice: "required" } : {},
@@ -3305,6 +3307,9 @@ async function emitInspectTurn(args) {
3305
3307
  ...cacheRead !== void 0 ? { cacheReadInput: cacheRead } : {}
3306
3308
  });
3307
3309
  }
3310
+ function isAbortSignalAborted(signal) {
3311
+ return signal?.aborted === true;
3312
+ }
3308
3313
  var DEFAULT_COMPACTION = {
3309
3314
  strategy: "drop-middle",
3310
3315
  threshold: 0.85,
@@ -3457,6 +3462,7 @@ async function agentLoop(options) {
3457
3462
  messages: normalizedMessages,
3458
3463
  system,
3459
3464
  tools: anthropicTools,
3465
+ ...options.runSignal !== void 0 ? { abortSignal: options.runSignal } : {},
3460
3466
  ...escalatedMaxTokens !== void 0 ? { maxTokens: escalatedMaxTokens } : {},
3461
3467
  ...options.toolChoice !== void 0 ? { toolChoice: options.toolChoice } : {}
3462
3468
  })) {
@@ -3470,6 +3476,9 @@ async function agentLoop(options) {
3470
3476
  }
3471
3477
  }
3472
3478
  } catch (err) {
3479
+ if (isAbortSignalAborted(options.runSignal)) {
3480
+ return failed(new RunTimeoutError(options.runTimeoutMs ?? 0), transcript);
3481
+ }
3473
3482
  if (isPromptTooLong(err) && !compactedThisTurn) {
3474
3483
  compactedThisTurn = true;
3475
3484
  const emergency = await compactIfNeeded({
@@ -10980,6 +10989,38 @@ var RunStateManager = class {
10980
10989
  await this.write(next);
10981
10990
  return next;
10982
10991
  }
10992
+ /**
10993
+ * Merge async lifecycle timing fields into the durable state.
10994
+ * Existing timestamps are preserved unless explicitly overwritten
10995
+ * by the caller.
10996
+ */
10997
+ async patchAsyncTiming(runId, nodeId, patch) {
10998
+ const current = await this.read(runId, nodeId);
10999
+ if (current === null) {
11000
+ throw new Error(`RunStateManager.patchAsyncTiming: no state found for ${runId}/${nodeId}`);
11001
+ }
11002
+ const next = {
11003
+ ...current,
11004
+ asyncTiming: { ...current.asyncTiming ?? {}, ...patch }
11005
+ };
11006
+ await this.write(next);
11007
+ return next;
11008
+ }
11009
+ async appendManualWebhookRetry(runId, nodeId, row) {
11010
+ const current = await this.read(runId, nodeId);
11011
+ if (current === null) {
11012
+ throw new Error(
11013
+ `RunStateManager.appendManualWebhookRetry: no state found for ${runId}/${nodeId}`
11014
+ );
11015
+ }
11016
+ const retries = current.manualWebhookRetries ?? [];
11017
+ const next = {
11018
+ ...current,
11019
+ manualWebhookRetries: [...retries, row]
11020
+ };
11021
+ await this.write(next);
11022
+ return next;
11023
+ }
10983
11024
  /**
10984
11025
  * Update just the heartbeat + progress (cheap, called every turn).
10985
11026
  */
@@ -11716,6 +11757,7 @@ ${inputJson}
11716
11757
  * uses fire-and-forget Promises which won't survive Worker request exit.
11717
11758
  */
11718
11759
  async start(options) {
11760
+ const startCalledAt = Date.now();
11719
11761
  const runId = options.runId ?? `run_${randomUUID()}`;
11720
11762
  const storage = await this.buildStorage();
11721
11763
  const stateManager = new RunStateManager(storage.workspace);
@@ -11727,19 +11769,62 @@ ${inputJson}
11727
11769
  deliveries: []
11728
11770
  } : void 0;
11729
11771
  const initial = RunStateManager.initial(runId, options.nodeId, webhook);
11730
- await stateManager.write(initial);
11772
+ await stateManager.write({
11773
+ ...initial,
11774
+ asyncTiming: { startCalledAt }
11775
+ });
11776
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11777
+ initialStateWrittenAt: Date.now()
11778
+ });
11731
11779
  const handoffEnabled = this.config.runner !== void 0;
11780
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11781
+ backgroundScheduledAt: Date.now()
11782
+ });
11732
11783
  this.backgroundExecutor.schedule(runId, async (signal) => {
11784
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11785
+ backgroundStartedAt: Date.now()
11786
+ });
11787
+ if (signal.aborted) return;
11733
11788
  await stateManager.update(runId, options.nodeId, { status: "running" });
11734
11789
  try {
11790
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11791
+ runCallStartedAt: Date.now()
11792
+ });
11735
11793
  const response = await this.run({ ...options, runId }, { handoffToRunner: handoffEnabled });
11794
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11795
+ runCallCompletedAt: Date.now()
11796
+ });
11736
11797
  if (signal.aborted) return;
11798
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11799
+ handoffStartedAt: Date.now()
11800
+ });
11737
11801
  const postHandoff = await this.maybeHandoffToRunner(runId, options.nodeId, response);
11802
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11803
+ handoffCompletedAt: Date.now(),
11804
+ finalizeStartedAt: Date.now()
11805
+ });
11806
+ if (await this.runnerAlreadyWroteTerminal(stateManager, runId, options.nodeId, postHandoff)) {
11807
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11808
+ finalizeCompletedAt: Date.now(),
11809
+ backgroundCompletedAt: Date.now()
11810
+ });
11811
+ return;
11812
+ }
11738
11813
  await stateManager.finalize(runId, options.nodeId, postHandoff);
11814
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11815
+ finalizeCompletedAt: Date.now()
11816
+ });
11739
11817
  await this.maybeFireWebhook(stateManager, runId, options.nodeId, postHandoff);
11818
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11819
+ backgroundCompletedAt: Date.now()
11820
+ });
11740
11821
  } catch (err) {
11741
11822
  if (signal.aborted) return;
11742
11823
  const errorMsg = err instanceof Error ? err.message : String(err);
11824
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11825
+ backgroundFailedAt: Date.now(),
11826
+ errorMessage: errorMsg
11827
+ });
11743
11828
  const failResponse = {
11744
11829
  runId,
11745
11830
  status: "failed",
@@ -11748,8 +11833,17 @@ ${inputJson}
11748
11833
  errors: [{ code: "RUN_FAILED", message: errorMsg }],
11749
11834
  timestamp: Date.now()
11750
11835
  };
11836
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11837
+ finalizeStartedAt: Date.now()
11838
+ });
11751
11839
  await stateManager.finalize(runId, options.nodeId, failResponse);
11840
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11841
+ finalizeCompletedAt: Date.now()
11842
+ });
11752
11843
  await this.maybeFireWebhook(stateManager, runId, options.nodeId, failResponse);
11844
+ await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
11845
+ backgroundCompletedAt: Date.now()
11846
+ });
11753
11847
  }
11754
11848
  });
11755
11849
  return { runId, nodeId: options.nodeId, status: "queued" };
@@ -11759,6 +11853,7 @@ ${inputJson}
11759
11853
  * dispatched via the background executor. Returns immediately.
11760
11854
  */
11761
11855
  async resumeAsync(options) {
11856
+ const startCalledAt = Date.now();
11762
11857
  const storage = await this.buildStorage();
11763
11858
  const stateManager = new RunStateManager(storage.workspace);
11764
11859
  let nodeId = options.nodeId;
@@ -11780,21 +11875,74 @@ ${inputJson}
11780
11875
  lastHeartbeat: Date.now(),
11781
11876
  response: null,
11782
11877
  // clear stale paused response so getStatus returns provisional
11878
+ asyncTiming: {
11879
+ ...existing.asyncTiming ?? {},
11880
+ startCalledAt
11881
+ },
11783
11882
  ...webhook !== void 0 ? { webhook } : {}
11784
- } : { ...RunStateManager.initial(options.runId, nodeId, webhook), status: "running" };
11883
+ } : {
11884
+ ...RunStateManager.initial(options.runId, nodeId, webhook),
11885
+ status: "running",
11886
+ asyncTiming: { startCalledAt }
11887
+ };
11785
11888
  await stateManager.write(next);
11889
+ await this.recordAsyncTiming(stateManager, options.runId, nodeId, {
11890
+ initialStateWrittenAt: Date.now()
11891
+ });
11786
11892
  const resumeNodeId = nodeId;
11787
11893
  const handoffEnabled = this.config.runner !== void 0;
11894
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11895
+ backgroundScheduledAt: Date.now()
11896
+ });
11788
11897
  this.backgroundExecutor.schedule(options.runId, async (signal) => {
11898
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11899
+ backgroundStartedAt: Date.now()
11900
+ });
11901
+ if (signal.aborted) return;
11789
11902
  try {
11903
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11904
+ runCallStartedAt: Date.now()
11905
+ });
11790
11906
  const response = await this.resume(options, { handoffToRunner: handoffEnabled });
11907
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11908
+ runCallCompletedAt: Date.now()
11909
+ });
11791
11910
  if (signal.aborted) return;
11911
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11912
+ handoffStartedAt: Date.now()
11913
+ });
11792
11914
  const postHandoff = await this.maybeHandoffToRunner(options.runId, resumeNodeId, response);
11915
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11916
+ handoffCompletedAt: Date.now(),
11917
+ finalizeStartedAt: Date.now()
11918
+ });
11919
+ if (await this.runnerAlreadyWroteTerminal(
11920
+ stateManager,
11921
+ options.runId,
11922
+ resumeNodeId,
11923
+ postHandoff
11924
+ )) {
11925
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11926
+ finalizeCompletedAt: Date.now(),
11927
+ backgroundCompletedAt: Date.now()
11928
+ });
11929
+ return;
11930
+ }
11793
11931
  await stateManager.finalize(options.runId, resumeNodeId, postHandoff);
11932
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11933
+ finalizeCompletedAt: Date.now()
11934
+ });
11794
11935
  await this.maybeFireWebhook(stateManager, options.runId, resumeNodeId, postHandoff);
11936
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11937
+ backgroundCompletedAt: Date.now()
11938
+ });
11795
11939
  } catch (err) {
11796
11940
  if (signal.aborted) return;
11797
11941
  const errorMsg = err instanceof Error ? err.message : String(err);
11942
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11943
+ backgroundFailedAt: Date.now(),
11944
+ errorMessage: errorMsg
11945
+ });
11798
11946
  const failResponse = {
11799
11947
  runId: options.runId,
11800
11948
  status: "failed",
@@ -11803,8 +11951,17 @@ ${inputJson}
11803
11951
  errors: [{ code: "RESUME_FAILED", message: errorMsg }],
11804
11952
  timestamp: Date.now()
11805
11953
  };
11954
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11955
+ finalizeStartedAt: Date.now()
11956
+ });
11806
11957
  await stateManager.finalize(options.runId, resumeNodeId, failResponse);
11958
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11959
+ finalizeCompletedAt: Date.now()
11960
+ });
11807
11961
  await this.maybeFireWebhook(stateManager, options.runId, resumeNodeId, failResponse);
11962
+ await this.recordAsyncTiming(stateManager, options.runId, resumeNodeId, {
11963
+ backgroundCompletedAt: Date.now()
11964
+ });
11808
11965
  }
11809
11966
  });
11810
11967
  return { runId: options.runId, nodeId, status: "running" };
@@ -11837,7 +11994,15 @@ ${inputJson}
11837
11994
  timestamp: Date.now()
11838
11995
  };
11839
11996
  }
11840
- if (state.response !== null) return state.response;
11997
+ if (state.response !== null) {
11998
+ return state.asyncTiming === void 0 ? state.response : {
11999
+ ...state.response,
12000
+ meta: {
12001
+ ...state.response.meta,
12002
+ asyncTiming: state.asyncTiming
12003
+ }
12004
+ };
12005
+ }
11841
12006
  return {
11842
12007
  runId: state.runId,
11843
12008
  status: state.status,
@@ -11847,7 +12012,8 @@ ${inputJson}
11847
12012
  turns: state.progress.turns,
11848
12013
  tokensUsed: state.progress.tokensUsed,
11849
12014
  activity: state.progress.currentActivity,
11850
- ...state.progress.lastTool !== void 0 ? { lastTool: state.progress.lastTool } : {}
12015
+ ...state.progress.lastTool !== void 0 ? { lastTool: state.progress.lastTool } : {},
12016
+ ...state.asyncTiming !== void 0 ? { asyncTiming: state.asyncTiming } : {}
11851
12017
  },
11852
12018
  errors: [],
11853
12019
  timestamp: state.lastHeartbeat
@@ -11856,6 +12022,11 @@ ${inputJson}
11856
12022
  /**
11857
12023
  * Poll until the run reaches a terminal state (done | failed | paused |
11858
12024
  * cancelled) or the timeout expires. Returns the final EngineResponse.
12025
+ *
12026
+ * For async paused runs, wait until the background wrapper has completed
12027
+ * its post-finalize bookkeeping before returning. Otherwise a caller can
12028
+ * immediately resume while the previous wrapper is still writing timing
12029
+ * diagnostics, allowing a stale paused state to overwrite the resumed run.
11859
12030
  */
11860
12031
  async waitFor(runId, opts = {}) {
11861
12032
  const pollInterval = opts.pollIntervalMs ?? 1e3;
@@ -11863,9 +12034,10 @@ ${inputJson}
11863
12034
  const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : Infinity;
11864
12035
  for (; ; ) {
11865
12036
  const resp = await this.getStatus(runId, opts.nodeId);
11866
- if (resp.status === "done" || resp.status === "failed" || resp.status === "paused") {
12037
+ if (resp.status === "done" || resp.status === "failed") {
11867
12038
  return resp;
11868
12039
  }
12040
+ if (resp.status === "paused" && this.pausedRunIsQuiesced(resp, opts)) return resp;
11869
12041
  if (Date.now() >= deadline) {
11870
12042
  return {
11871
12043
  runId,
@@ -11881,6 +12053,12 @@ ${inputJson}
11881
12053
  await new Promise((r) => setTimeout(r, pollInterval));
11882
12054
  }
11883
12055
  }
12056
+ pausedRunIsQuiesced(resp, opts) {
12057
+ if (resp.meta.pauseReason === "handoff_to_runner") return opts.waitForRunnerHandoff !== true;
12058
+ const timing = resp.meta.asyncTiming;
12059
+ if (timing === void 0) return true;
12060
+ return timing.backgroundCompletedAt !== void 0 || timing.backgroundFailedAt !== void 0;
12061
+ }
11884
12062
  /**
11885
12063
  * Cancel an async run. Aborts the background executor and marks the
11886
12064
  * state as cancelled. Idempotent — safe to call on already-terminal runs.
@@ -11937,14 +12115,31 @@ ${inputJson}
11937
12115
  if (original === void 0) {
11938
12116
  throw new Error(`retryWebhook: delivery ${deliveryId} not found`);
11939
12117
  }
11940
- await this.dispatchWebhookWithRetries(
11941
- stateManager,
11942
- runId,
11943
- targetNodeId,
11944
- original.event,
11945
- state.response,
11946
- 1
11947
- );
12118
+ const retryStartedAt = Date.now();
12119
+ try {
12120
+ await this.dispatchWebhookWithRetries(
12121
+ stateManager,
12122
+ runId,
12123
+ targetNodeId,
12124
+ original.event,
12125
+ state.response,
12126
+ 1
12127
+ );
12128
+ await this.recordManualWebhookRetry(stateManager, runId, targetNodeId, {
12129
+ deliveryId,
12130
+ startedAt: retryStartedAt,
12131
+ completedAt: Date.now()
12132
+ });
12133
+ } catch (err) {
12134
+ const errorMessage = err instanceof Error ? err.message : String(err);
12135
+ await this.recordManualWebhookRetry(stateManager, runId, targetNodeId, {
12136
+ deliveryId,
12137
+ startedAt: retryStartedAt,
12138
+ completedAt: Date.now(),
12139
+ errorMessage
12140
+ });
12141
+ throw err;
12142
+ }
11948
12143
  }
11949
12144
  /**
11950
12145
  * Scan all runs for stale heartbeats and mark them as failed. Clients
@@ -11981,6 +12176,12 @@ ${inputJson}
11981
12176
  return orphaned;
11982
12177
  }
11983
12178
  // ---------- runner handoff (Plan 019) ----------
12179
+ async runnerAlreadyWroteTerminal(stateManager, runId, nodeId, response) {
12180
+ if (response.status !== "paused") return false;
12181
+ if (response.meta.pauseReason !== "handoff_to_runner") return false;
12182
+ const latest = await stateManager.read(runId, nodeId);
12183
+ return latest?.status === "done" || latest?.status === "failed" || latest?.status === "cancelled";
12184
+ }
11984
12185
  /**
11985
12186
  * When the response indicates the run paused for runner handoff, POST
11986
12187
  * `{ runId }` to the configured runner URL. On success, return the
@@ -12046,7 +12247,22 @@ ${inputJson}
12046
12247
  }
12047
12248
  }
12048
12249
  // ---------- webhook helpers ----------
12250
+ async recordAsyncTiming(stateManager, runId, nodeId, patch) {
12251
+ try {
12252
+ await stateManager.patchAsyncTiming(runId, nodeId, patch);
12253
+ } catch {
12254
+ }
12255
+ }
12256
+ async recordManualWebhookRetry(stateManager, runId, nodeId, row) {
12257
+ try {
12258
+ await stateManager.appendManualWebhookRetry(runId, nodeId, row);
12259
+ } catch {
12260
+ }
12261
+ }
12049
12262
  async maybeFireWebhook(stateManager, runId, nodeId, response) {
12263
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12264
+ webhookCheckStartedAt: Date.now()
12265
+ });
12050
12266
  const state = await stateManager.read(runId, nodeId);
12051
12267
  if (state === null || state.webhook === void 0) return;
12052
12268
  const event = response.status === "done" ? "done" : response.status === "paused" ? "paused" : "failed";
@@ -12057,12 +12273,18 @@ ${inputJson}
12057
12273
  const state = await stateManager.read(runId, nodeId);
12058
12274
  if (state === null || state.webhook === void 0) return;
12059
12275
  const hook = state.webhook;
12276
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12277
+ webhookDispatchStartedAt: Date.now()
12278
+ });
12060
12279
  let attempt = startAttempt;
12061
12280
  while (attempt <= MAX_ATTEMPTS) {
12062
12281
  const delay = RETRY_DELAYS_MS[attempt - 1] ?? 0;
12063
12282
  if (delay > 0) {
12064
12283
  await new Promise((r) => setTimeout(r, delay));
12065
12284
  }
12285
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12286
+ webhookHttpStartedAt: Date.now()
12287
+ });
12066
12288
  const result = await this.webhookDispatcher.deliver({
12067
12289
  url: hook.url,
12068
12290
  event,
@@ -12071,6 +12293,9 @@ ${inputJson}
12071
12293
  ...hook.headers !== void 0 ? { headers: hook.headers } : {},
12072
12294
  attempt
12073
12295
  });
12296
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12297
+ webhookHttpCompletedAt: Date.now()
12298
+ });
12074
12299
  const latest = await stateManager.read(runId, nodeId);
12075
12300
  if (latest !== null && latest.webhook !== void 0) {
12076
12301
  const updated = {
@@ -12078,10 +12303,21 @@ ${inputJson}
12078
12303
  deliveries: [...latest.webhook.deliveries, result.delivery]
12079
12304
  };
12080
12305
  await stateManager.update(runId, nodeId, { webhook: updated });
12306
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12307
+ webhookStatePersistedAt: Date.now()
12308
+ });
12309
+ }
12310
+ if (!result.shouldRetry) {
12311
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12312
+ webhookDispatchCompletedAt: Date.now()
12313
+ });
12314
+ return;
12081
12315
  }
12082
- if (!result.shouldRetry) return;
12083
12316
  attempt += 1;
12084
12317
  }
12318
+ await this.recordAsyncTiming(stateManager, runId, nodeId, {
12319
+ webhookDispatchCompletedAt: Date.now()
12320
+ });
12085
12321
  }
12086
12322
  /**
12087
12323
  * Shut down engine-owned background resources — currently just the