agents 0.13.2 → 0.14.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.
Files changed (74) hide show
  1. package/README.md +6 -4
  2. package/dist/{agent-tool-types-Dn9n-3SI.d.ts → agent-tool-types-LInzZfLo.d.ts} +511 -124
  3. package/dist/agent-tool-types.d.ts +13 -11
  4. package/dist/{agent-tools-B1ttU-pq.d.ts → agent-tools-BE9xosUG.d.ts} +2 -2
  5. package/dist/agent-tools.d.ts +14 -20
  6. package/dist/agent-tools.js +10 -6
  7. package/dist/agent-tools.js.map +1 -1
  8. package/dist/browser/ai.d.ts +1 -1
  9. package/dist/browser/ai.js +1 -1
  10. package/dist/browser/index.d.ts +1 -1
  11. package/dist/browser/index.js +1 -1
  12. package/dist/browser/tanstack-ai.d.ts +1 -1
  13. package/dist/browser/tanstack-ai.js +1 -1
  14. package/dist/chat/index.d.ts +194 -22
  15. package/dist/chat/index.js +144 -11
  16. package/dist/chat/index.js.map +1 -1
  17. package/dist/chat-sdk/index.d.ts +4 -4
  18. package/dist/classPrivateMethodInitSpec-bG0tD96O.js +7 -0
  19. package/dist/{client-D1kFXo80.js → client-NradHZZz.js} +206 -75
  20. package/dist/client-NradHZZz.js.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/{compaction-helpers-DvcZnvQ1.js → compaction-helpers-BjT2NKRZ.js} +37 -9
  23. package/dist/compaction-helpers-BjT2NKRZ.js.map +1 -0
  24. package/dist/{compaction-helpers-DAe-xiVY.d.ts → compaction-helpers-DpP_XP9J.d.ts} +86 -29
  25. package/dist/{do-oauth-client-provider-4OKQU9rT.d.ts → do-oauth-client-provider-CPm9rK5I.d.ts} +1 -1
  26. package/dist/{email-J0GGS3sa.d.ts → email-1fTSJwPm.d.ts} +1 -1
  27. package/dist/email.d.ts +2 -2
  28. package/dist/experimental/memory/session/index.d.ts +58 -23
  29. package/dist/experimental/memory/session/index.js +98 -9
  30. package/dist/experimental/memory/session/index.js.map +1 -1
  31. package/dist/experimental/memory/utils/index.d.ts +13 -11
  32. package/dist/experimental/memory/utils/index.js +2 -2
  33. package/dist/{index-DKey3P4s.d.ts → index-Brdu5nMI.d.ts} +270 -1
  34. package/dist/index.d.ts +74 -67
  35. package/dist/index.js +607 -97
  36. package/dist/index.js.map +1 -1
  37. package/dist/{internal_context-BZrMS0B5.d.ts → internal_context-CcZy2Em7.d.ts} +1 -1
  38. package/dist/internal_context.d.ts +1 -1
  39. package/dist/mcp/client.d.ts +17 -13
  40. package/dist/mcp/client.js +2 -2
  41. package/dist/mcp/do-oauth-client-provider.d.ts +1 -1
  42. package/dist/mcp/index.d.ts +35 -27
  43. package/dist/mcp/index.js +402 -69
  44. package/dist/mcp/index.js.map +1 -1
  45. package/dist/observability/index.d.ts +1 -1
  46. package/dist/observability/index.js +15 -1
  47. package/dist/observability/index.js.map +1 -1
  48. package/dist/react.d.ts +3 -3
  49. package/dist/{retries-BVdRl5ZE.d.ts → retries-ClWwxADl.d.ts} +1 -1
  50. package/dist/retries.d.ts +1 -1
  51. package/dist/serializable.d.ts +1 -1
  52. package/dist/{shared-Cvj92byG.d.ts → shared-CpY1FLvm.d.ts} +1 -1
  53. package/dist/{shared-CiKaIK4h.js → shared-DdOn6sp4.js} +3 -7
  54. package/dist/{shared-CiKaIK4h.js.map → shared-DdOn6sp4.js.map} +1 -1
  55. package/dist/skills/index.d.ts +236 -0
  56. package/dist/skills/index.js +1326 -0
  57. package/dist/skills/index.js.map +1 -0
  58. package/dist/sub-routing.d.ts +6 -6
  59. package/dist/{tool-output-truncation-CH-khbZ3.js → tool-output-truncation-BF4AZQlw.js} +1 -1
  60. package/dist/{tool-output-truncation-CH-khbZ3.js.map → tool-output-truncation-BF4AZQlw.js.map} +1 -1
  61. package/dist/{types-_JjKmv-l.d.ts → types-B0GymtN_.d.ts} +1 -1
  62. package/dist/types.d.ts +1 -1
  63. package/dist/vite.d.ts +1 -1
  64. package/dist/vite.js +248 -2
  65. package/dist/vite.js.map +1 -1
  66. package/dist/{workflow-types-Dkzg4hAx.d.ts → workflow-types-DPkuBi--.d.ts} +1 -1
  67. package/dist/workflow-types.d.ts +1 -1
  68. package/dist/workflows.d.ts +13 -3
  69. package/dist/workflows.js +10 -1
  70. package/dist/workflows.js.map +1 -1
  71. package/package.json +21 -3
  72. package/skills-module.d.ts +22 -0
  73. package/dist/client-D1kFXo80.js.map +0 -1
  74. package/dist/compaction-helpers-DvcZnvQ1.js.map +0 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { createHeaderBasedEmailResolver, signAgentHeaders } from "./email.js";
5
5
  import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-Evpt0SEr.js";
6
6
  import { SUB_PREFIX, getSubAgentByName, parseSubAgentPath, routeSubAgentRequest } from "./sub-routing.js";
7
7
  import { isErrorRetryable, tryN, validateRetryOptions } from "./retries.js";
8
- import { o as RPC_DO_PREFIX, r as MCPConnectionState, s as DisposableStore, t as MCPClientManager } from "./client-D1kFXo80.js";
8
+ import { a as MCPConnectionState, c as RPC_DO_PREFIX, i as normalizeServerId, l as DisposableStore, n as MCP_SERVER_ID_MAX_LENGTH, t as MCPClientManager } from "./client-NradHZZz.js";
9
9
  import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider.js";
10
10
  import { genericObservability } from "./observability/index.js";
11
11
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -114,6 +114,12 @@ function getNextCronTime(cron) {
114
114
  return parseCronExpression(cron).getNextDate();
115
115
  }
116
116
  const DEFAULT_KEEP_ALIVE_INTERVAL_MS = 3e4;
117
+ const DEFAULT_AGENT_TOOL_RECOVERY_TIMEOUT_MS = 2e3;
118
+ const DEFAULT_AGENT_TOOL_RECOVERY_TOTAL_TIMEOUT_MS = 5e3;
119
+ const DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS = 12e4;
120
+ const SUB_AGENT_IDENTITY_VERSION_LEGACY = "legacy";
121
+ const SUB_AGENT_IDENTITY_VERSION_PATH_V2 = "path-v2";
122
+ const SUB_AGENT_IDENTITY_PATH_V2_PREFIX = "cf-agents:v2:";
117
123
  /**
118
124
  * Schema version for the Agent's internal SQLite tables.
119
125
  * Bump this when adding new tables, columns, or migrations.
@@ -125,6 +131,25 @@ const SCHEMA_VERSION_ROW_ID = "cf_schema_version";
125
131
  const STATE_ROW_ID = "cf_state_row_id";
126
132
  const STATE_WAS_CHANGED = "cf_state_was_changed";
127
133
  const DEFAULT_STATE = {};
134
+ async function sha256Hex(value) {
135
+ const bytes = new TextEncoder().encode(value);
136
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
137
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
138
+ }
139
+ function pathV2IdentityName(logicalName, digest) {
140
+ return `${SUB_AGENT_IDENTITY_PATH_V2_PREFIX}${encodeURIComponent(logicalName)}:${digest}`;
141
+ }
142
+ function logicalNameFromPathV2Identity(identityName) {
143
+ if (!identityName.startsWith(SUB_AGENT_IDENTITY_PATH_V2_PREFIX)) return null;
144
+ const rest = identityName.slice(13);
145
+ const separator = rest.lastIndexOf(":");
146
+ if (separator === -1) return null;
147
+ try {
148
+ return decodeURIComponent(rest.slice(0, separator));
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
128
153
  /**
129
154
  * Validate that a stored `parentPath` has the expected shape. Used
130
155
  * when restoring from DO storage to guard against corrupted data.
@@ -240,7 +265,17 @@ const DEFAULT_AGENT_STATIC_OPTIONS = {
240
265
  maxAttempts: 3,
241
266
  baseDelayMs: 100,
242
267
  maxDelayMs: 3e3
243
- }
268
+ },
269
+ /** Timeout for internal framework fiber recovery hooks. */
270
+ fiberRecoveryHookTimeoutMs: 1e4,
271
+ /** Soft deadline for one interrupted-fiber recovery scan. */
272
+ fiberRecoveryScanDeadlineMs: 1e4,
273
+ /**
274
+ * Maximum age of an unmanaged interrupted-fiber row before recovery gives
275
+ * up. Bounds repeated retries of a `onFiberRecovered()` hook that keeps
276
+ * throwing so a poison row cannot re-trigger forever across boots.
277
+ */
278
+ fiberRecoveryMaxAgeMs: 1440 * 60 * 1e3
244
279
  };
245
280
  /**
246
281
  * Parse the raw `retry_options` TEXT column from a SQLite row into a
@@ -263,6 +298,19 @@ function resolveRetryConfig(taskRetry, defaults) {
263
298
  maxDelayMs: taskRetry?.maxDelayMs ?? defaults.maxDelayMs
264
299
  };
265
300
  }
301
+ /**
302
+ * Whether an error is a Durable Object reset caused by a code update (deploy).
303
+ *
304
+ * This is a transient, environmental failure: the invocation started on a
305
+ * superseded isolate, so every `ctx.storage` op throws this for the entire
306
+ * life of the invocation (code never reloads mid-invocation) — but the next
307
+ * fresh invocation runs the new code and succeeds. workerd surfaces it as a
308
+ * plain `Error` with this message, so a message match is the only signal.
309
+ */
310
+ function isDurableObjectCodeUpdateReset(error) {
311
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
312
+ return /reset because its code was updated/i.test(message);
313
+ }
266
314
  function getCurrentAgent() {
267
315
  const store = __DO_NOT_USE_WILL_BREAK__agentContext.getStore();
268
316
  if (!store) return {
@@ -360,7 +408,10 @@ var Agent = class Agent extends Server {
360
408
  maxAttempts: userRetry?.maxAttempts ?? DEFAULT_AGENT_STATIC_OPTIONS.retry.maxAttempts,
361
409
  baseDelayMs: userRetry?.baseDelayMs ?? DEFAULT_AGENT_STATIC_OPTIONS.retry.baseDelayMs,
362
410
  maxDelayMs: userRetry?.maxDelayMs ?? DEFAULT_AGENT_STATIC_OPTIONS.retry.maxDelayMs
363
- }
411
+ },
412
+ fiberRecoveryHookTimeoutMs: ctor.options?.fiberRecoveryHookTimeoutMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryHookTimeoutMs,
413
+ fiberRecoveryScanDeadlineMs: ctor.options?.fiberRecoveryScanDeadlineMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryScanDeadlineMs,
414
+ fiberRecoveryMaxAgeMs: ctor.options?.fiberRecoveryMaxAgeMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryMaxAgeMs
364
415
  };
365
416
  return this._cachedOptions;
366
417
  }
@@ -848,6 +899,8 @@ var Agent = class Agent extends Server {
848
899
  email: void 0
849
900
  }, async () => {
850
901
  if (await this.ctx.storage.get("cf_agents_is_facet")) this._isFacet = true;
902
+ const storedFacetName = await this.ctx.storage.get("cf_agents_facet_name");
903
+ if (typeof storedFacetName === "string") this._facetName = storedFacetName;
851
904
  const storedParentPath = await this.ctx.storage.get("cf_agents_parent_path");
852
905
  if (isValidParentPath(storedParentPath)) this._parentPath = storedParentPath;
853
906
  try {
@@ -861,7 +914,7 @@ var Agent = class Agent extends Server {
861
914
  this.broadcastMcpServers();
862
915
  this._checkOrphanedWorkflows();
863
916
  await this._checkRunFibers();
864
- const recoveredAgentToolFinishes = await this._reconcileAgentToolRuns({ deferFinishHooks: true });
917
+ const startupAgentToolRunIds = this._agentToolRunRecoveryRunIds();
865
918
  this._insideOnStart = true;
866
919
  this._warnedScheduleInOnStart.clear();
867
920
  let result;
@@ -870,7 +923,7 @@ var Agent = class Agent extends Server {
870
923
  } finally {
871
924
  this._insideOnStart = false;
872
925
  }
873
- await this._runDeferredAgentToolFinishHooks(recoveredAgentToolFinishes);
926
+ this._scheduleAgentToolRunRecovery({ runIds: startupAgentToolRunIds });
874
927
  return result;
875
928
  });
876
929
  });
@@ -1569,6 +1622,13 @@ var Agent = class Agent extends Server {
1569
1622
  if (!binding) throw new Error(`Unable to resolve root scheduler "${root.className}" for sub-agent schedule delegation.`);
1570
1623
  return await getServerByName(binding, root.name);
1571
1624
  }
1625
+ _cf_rootResolvesToSelf() {
1626
+ const root = this._parentPath[0];
1627
+ if (!root) return false;
1628
+ const binding = this.ctx.exports?.[root.className];
1629
+ if (!binding?.idFromName) return false;
1630
+ return binding.idFromName(root.name).equals(this.ctx.id);
1631
+ }
1572
1632
  _validateScheduleCallback(when, callback, options) {
1573
1633
  if (typeof callback !== "string") throw new Error("Callback must be a string");
1574
1634
  if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
@@ -2355,20 +2415,67 @@ var Agent = class Agent extends Server {
2355
2415
  return null;
2356
2416
  }
2357
2417
  }
2418
+ _fiberRecoveryPayload(ctx, managedRow, startedAt) {
2419
+ return {
2420
+ fiberId: ctx.id,
2421
+ fiberName: ctx.name,
2422
+ managed: managedRow !== null,
2423
+ recoveryReason: ctx.recoveryReason,
2424
+ elapsedMs: startedAt === void 0 ? void 0 : Date.now() - startedAt
2425
+ };
2426
+ }
2427
+ async _withFiberRecoveryTimeout(ctx, operation) {
2428
+ const timeoutMs = this._resolvedOptions.fiberRecoveryHookTimeoutMs;
2429
+ if (timeoutMs <= 0) return operation();
2430
+ let timer;
2431
+ try {
2432
+ return await Promise.race([operation(), new Promise((_, reject) => {
2433
+ timer = setTimeout(() => {
2434
+ reject(/* @__PURE__ */ new Error(`Fiber recovery hook timed out after ${timeoutMs}ms for "${ctx.name}" (${ctx.id})`));
2435
+ }, timeoutMs);
2436
+ })]);
2437
+ } finally {
2438
+ if (timer !== void 0) clearTimeout(timer);
2439
+ }
2440
+ }
2441
+ _recordFiberRecoveryFailure(ctx, managedRow, error, startedAt, reason = "handler_error") {
2442
+ const errorMessage = this._fiberErrorMessage(error);
2443
+ const completedAt = Date.now();
2444
+ if (managedRow) {
2445
+ this.sql`
2446
+ UPDATE cf_agents_fibers
2447
+ SET status = 'error',
2448
+ error_message = ${errorMessage},
2449
+ completed_at = ${completedAt}
2450
+ WHERE fiber_id = ${ctx.id}
2451
+ AND status = 'interrupted'
2452
+ `;
2453
+ this._notifyManagedFiberTerminal(ctx.id);
2454
+ }
2455
+ this._emit("fiber:recovery:failed", {
2456
+ ...this._fiberRecoveryPayload(ctx, managedRow, startedAt),
2457
+ error: errorMessage,
2458
+ reason
2459
+ });
2460
+ }
2358
2461
  async _runFiberRecoveryHook(ctx, managedRow) {
2462
+ const startedAt = Date.now();
2463
+ this._emit("fiber:recovery:attempt", this._fiberRecoveryPayload(ctx, managedRow));
2359
2464
  try {
2360
- if (!await this._handleInternalFiberRecovery(ctx)) {
2465
+ const handled = await this._withFiberRecoveryTimeout(ctx, () => this._handleInternalFiberRecovery(ctx));
2466
+ if (!handled) {
2361
2467
  const recoveryResult = await this.onFiberRecovered(ctx);
2362
2468
  if (managedRow && recoveryResult) this._applyManagedFiberRecoveryResult(ctx.id, recoveryResult);
2363
2469
  }
2470
+ this._emit("fiber:recovery:handled", {
2471
+ ...this._fiberRecoveryPayload(ctx, managedRow, startedAt),
2472
+ status: handled ? "internal" : managedRow ? "managed" : "user"
2473
+ });
2474
+ return true;
2364
2475
  } catch (e) {
2365
- if (managedRow) this.sql`
2366
- UPDATE cf_agents_fibers
2367
- SET error_message = ${this._fiberErrorMessage(e)}
2368
- WHERE fiber_id = ${ctx.id}
2369
- AND status = 'interrupted'
2370
- `;
2476
+ this._recordFiberRecoveryFailure(ctx, managedRow, e, startedAt);
2371
2477
  console.error(`[Agent] Fiber recovery failed for "${ctx.name}" (${ctx.id}):`, e);
2478
+ return false;
2372
2479
  }
2373
2480
  }
2374
2481
  _fiberInspectionFromRow(row) {
@@ -2545,6 +2652,18 @@ var Agent = class Agent extends Server {
2545
2652
  async runFiber(name, fn) {
2546
2653
  return this._runFiberInternal(nanoid(), name, fn);
2547
2654
  }
2655
+ /**
2656
+ * Internal framework entry point for fibers that need to compose their own
2657
+ * recovery metadata with user checkpoint data while preserving the public
2658
+ * `this.stash()` behavior.
2659
+ *
2660
+ * This deliberately stays protected/internal rather than becoming a public
2661
+ * `runFiber()` option until the durable execution API needs this generality.
2662
+ * @internal
2663
+ */
2664
+ async _runFiberWithStashWrapper(name, fn, options) {
2665
+ return this._runFiberInternal(nanoid(), name, fn, options);
2666
+ }
2548
2667
  async startFiber(name, fn, options) {
2549
2668
  const fiberId = options?.fiberId ?? nanoid();
2550
2669
  const idempotencyKey = options?.idempotencyKey;
@@ -2641,11 +2760,29 @@ var Agent = class Agent extends Server {
2641
2760
  INSERT INTO cf_agents_runs (id, name, snapshot, created_at)
2642
2761
  VALUES (${id}, ${name}, NULL, ${Date.now()})
2643
2762
  `;
2763
+ const startedAt = Date.now();
2764
+ this._emit("fiber:run:started", {
2765
+ fiberId: id,
2766
+ fiberName: name,
2767
+ managed: options?.managed === true
2768
+ });
2644
2769
  this._runFiberActiveFibers.add(id);
2770
+ const writeSnapshot = (data) => {
2771
+ const snapshot = JSON.stringify(data);
2772
+ this.sql`
2773
+ UPDATE cf_agents_runs SET snapshot = ${snapshot}
2774
+ WHERE id = ${id}
2775
+ `;
2776
+ if (options?.managed) this.sql`
2777
+ UPDATE cf_agents_fibers SET snapshot = ${snapshot}
2778
+ WHERE fiber_id = ${id}
2779
+ `;
2780
+ };
2645
2781
  let root;
2646
2782
  let registeredFacetRun = false;
2647
2783
  let dispose = () => {};
2648
2784
  try {
2785
+ if ("initialSnapshot" in (options ?? {})) writeSnapshot(options?.initialSnapshot);
2649
2786
  if (this._isFacet) {
2650
2787
  root = await this._rootAlarmOwner();
2651
2788
  await root._cf_registerFacetRun(this.selfPath, id);
@@ -2653,15 +2790,7 @@ var Agent = class Agent extends Server {
2653
2790
  }
2654
2791
  dispose = await this.keepAlive();
2655
2792
  const stash = (data) => {
2656
- const snapshot = JSON.stringify(data);
2657
- this.sql`
2658
- UPDATE cf_agents_runs SET snapshot = ${snapshot}
2659
- WHERE id = ${id}
2660
- `;
2661
- if (options?.managed) this.sql`
2662
- UPDATE cf_agents_fibers SET snapshot = ${snapshot}
2663
- WHERE fiber_id = ${id}
2664
- `;
2793
+ writeSnapshot(options?.wrapStash ? options.wrapStash(data) : data);
2665
2794
  };
2666
2795
  try {
2667
2796
  const result = await _fiberALS.run({
@@ -2675,12 +2804,25 @@ var Agent = class Agent extends Server {
2675
2804
  snapshot: null
2676
2805
  }));
2677
2806
  options?.beforeRunCleanup?.({ ok: true });
2807
+ this._emit("fiber:run:completed", {
2808
+ fiberId: id,
2809
+ fiberName: name,
2810
+ managed: options?.managed === true,
2811
+ elapsedMs: Date.now() - startedAt
2812
+ });
2678
2813
  return result;
2679
2814
  } catch (error) {
2680
2815
  options?.beforeRunCleanup?.({
2681
2816
  ok: false,
2682
2817
  error
2683
2818
  });
2819
+ this._emit("fiber:run:failed", {
2820
+ fiberId: id,
2821
+ fiberName: name,
2822
+ managed: options?.managed === true,
2823
+ error: this._fiberErrorMessage(error),
2824
+ elapsedMs: Date.now() - startedAt
2825
+ });
2684
2826
  throw error;
2685
2827
  }
2686
2828
  } finally {
@@ -2730,18 +2872,42 @@ var Agent = class Agent extends Server {
2730
2872
  async _checkRunFibers() {
2731
2873
  if (this._runFiberRecoveryInProgress) return;
2732
2874
  this._runFiberRecoveryInProgress = true;
2875
+ const scanStartedAt = Date.now();
2876
+ const scanDeadlineMs = this._resolvedOptions.fiberRecoveryScanDeadlineMs;
2877
+ const fiberRecoveryMaxAgeMs = this._resolvedOptions.fiberRecoveryMaxAgeMs;
2733
2878
  try {
2734
2879
  const rows = this.sql`SELECT id, name, snapshot, created_at FROM cf_agents_runs`;
2735
2880
  for (const row of rows) {
2881
+ if (scanDeadlineMs > 0 && Date.now() - scanStartedAt > scanDeadlineMs) {
2882
+ this._emit("fiber:recovery:skipped", {
2883
+ fiberId: row.id,
2884
+ fiberName: row.name,
2885
+ reason: "scan_deadline_exceeded",
2886
+ elapsedMs: Date.now() - scanStartedAt
2887
+ });
2888
+ break;
2889
+ }
2736
2890
  if (this._runFiberActiveFibers.has(row.id)) continue;
2737
2891
  const snapshot = this._parseFiberRecoverySnapshot(row.id, row.snapshot);
2738
2892
  const ctx = {
2739
2893
  id: row.id,
2740
2894
  name: row.name,
2741
2895
  snapshot,
2742
- createdAt: row.created_at
2896
+ createdAt: row.created_at,
2897
+ recoveryReason: "interrupted"
2743
2898
  };
2744
2899
  const managedRow = this._readFiber(row.id);
2900
+ this._emit("fiber:recovery:detected", {
2901
+ ...this._fiberRecoveryPayload(ctx, managedRow),
2902
+ elapsedMs: Date.now() - row.created_at
2903
+ });
2904
+ this._emit("fiber:run:interrupted", {
2905
+ fiberId: row.id,
2906
+ fiberName: row.name,
2907
+ managed: managedRow !== null,
2908
+ recoveryReason: "interrupted",
2909
+ elapsedMs: Date.now() - row.created_at
2910
+ });
2745
2911
  if (managedRow) {
2746
2912
  if (this._isTerminalFiberStatus(managedRow.status)) {
2747
2913
  this.sql`DELETE FROM cf_agents_runs WHERE id = ${row.id}`;
@@ -2761,8 +2927,17 @@ var Agent = class Agent extends Server {
2761
2927
  ctx.metadata = this._parseFiberJsonObject(managedRow.metadata_json);
2762
2928
  ctx.status = "interrupted";
2763
2929
  }
2764
- await this._runFiberRecoveryHook(ctx, managedRow);
2765
- this.sql`DELETE FROM cf_agents_runs WHERE id = ${row.id}`;
2930
+ const recovered = await this._runFiberRecoveryHook(ctx, managedRow);
2931
+ const tooOld = fiberRecoveryMaxAgeMs > 0 && Date.now() - row.created_at > fiberRecoveryMaxAgeMs;
2932
+ if (recovered || managedRow || tooOld) {
2933
+ if (!recovered && !managedRow && tooOld) this._emit("fiber:recovery:skipped", {
2934
+ fiberId: row.id,
2935
+ fiberName: row.name,
2936
+ reason: "max_age_exceeded",
2937
+ elapsedMs: Date.now() - row.created_at
2938
+ });
2939
+ this.sql`DELETE FROM cf_agents_runs WHERE id = ${row.id}`;
2940
+ }
2766
2941
  if (managedRow) this._notifyManagedFiberTerminal(row.id);
2767
2942
  }
2768
2943
  const ledgerOnlyRows = this.sql`
@@ -2775,6 +2950,16 @@ var Agent = class Agent extends Server {
2775
2950
  AND r.id IS NULL
2776
2951
  `;
2777
2952
  for (const row of ledgerOnlyRows) {
2953
+ if (scanDeadlineMs > 0 && Date.now() - scanStartedAt > scanDeadlineMs) {
2954
+ this._emit("fiber:recovery:skipped", {
2955
+ fiberId: row.fiber_id,
2956
+ fiberName: row.name,
2957
+ reason: "scan_deadline_exceeded",
2958
+ elapsedMs: Date.now() - scanStartedAt,
2959
+ managed: true
2960
+ });
2961
+ break;
2962
+ }
2778
2963
  if (this._runFiberActiveFibers.has(row.fiber_id)) continue;
2779
2964
  const snapshot = this._parseFiberRecoverySnapshot(row.fiber_id, row.snapshot);
2780
2965
  const completedAt = Date.now();
@@ -2785,15 +2970,28 @@ var Agent = class Agent extends Server {
2785
2970
  WHERE fiber_id = ${row.fiber_id}
2786
2971
  AND status IN ('pending', 'running')
2787
2972
  `;
2788
- await this._runFiberRecoveryHook({
2973
+ const ctx = {
2789
2974
  id: row.fiber_id,
2790
2975
  name: row.name,
2791
2976
  snapshot,
2792
2977
  createdAt: row.created_at,
2793
2978
  idempotencyKey: row.idempotency_key ?? void 0,
2794
2979
  metadata: this._parseFiberJsonObject(row.metadata_json),
2795
- status: "interrupted"
2796
- }, row);
2980
+ status: "interrupted",
2981
+ recoveryReason: "interrupted"
2982
+ };
2983
+ this._emit("fiber:recovery:detected", {
2984
+ ...this._fiberRecoveryPayload(ctx, row),
2985
+ elapsedMs: Date.now() - row.created_at
2986
+ });
2987
+ this._emit("fiber:run:interrupted", {
2988
+ fiberId: row.fiber_id,
2989
+ fiberName: row.name,
2990
+ managed: true,
2991
+ recoveryReason: "interrupted",
2992
+ elapsedMs: Date.now() - row.created_at
2993
+ });
2994
+ await this._runFiberRecoveryHook(ctx, row);
2797
2995
  this._notifyManagedFiberTerminal(row.fiber_id);
2798
2996
  }
2799
2997
  } finally {
@@ -2951,6 +3149,8 @@ var Agent = class Agent extends Server {
2951
3149
  });
2952
3150
  return;
2953
3151
  }
3152
+ const isOneShotSchedule = row.type === "delayed" || row.type === "scheduled";
3153
+ const shouldDeferReset = (error) => isOneShotSchedule && isDurableObjectCodeUpdateReset(error);
2954
3154
  try {
2955
3155
  this._emit("schedule:execute", {
2956
3156
  callback: row.callback,
@@ -2966,9 +3166,14 @@ var Agent = class Agent extends Server {
2966
3166
  await callback.bind(this)(parsedPayload, row);
2967
3167
  }, {
2968
3168
  baseDelayMs,
2969
- maxDelayMs
3169
+ maxDelayMs,
3170
+ shouldRetry: (error) => !shouldDeferReset(error)
2970
3171
  });
2971
3172
  } catch (e) {
3173
+ if (shouldDeferReset(e)) {
3174
+ console.warn(`Deferring scheduled callback "${row.callback}" to a fresh invocation after a Durable Object code-update reset; the one-shot row is preserved and the alarm will re-run on new code.`);
3175
+ throw e;
3176
+ }
2972
3177
  console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
2973
3178
  this._emit("schedule:error", {
2974
3179
  callback: row.callback,
@@ -3467,6 +3672,7 @@ var Agent = class Agent extends Server {
3467
3672
  }
3468
3673
  async _cf_hydrateSubAgentConnectionsFromRoot() {
3469
3674
  if (!this._isFacet || this._parentPath.length === 0) return;
3675
+ if (this._cf_rootResolvesToSelf()) return;
3470
3676
  const root = await this._rootAlarmOwner();
3471
3677
  const metas = await root._cf_subAgentConnectionMetas(this.selfPath);
3472
3678
  for (const meta of metas) this._cf_virtualSubAgentConnections.set(meta.id, {
@@ -3607,21 +3813,23 @@ var Agent = class Agent extends Server {
3607
3813
  * broadcast to their own WebSocket clients reached via sub-agent
3608
3814
  * routing.
3609
3815
  *
3610
- * The facet's name (and `this.name` getter) is handled entirely by
3611
- * partyserver via `ctx.id.name`, which is populated because the
3612
- * parent passed an explicit named Durable Object id to
3613
- * `ctx.facets.get()` — see {@link _cf_resolveSubAgent}. No
3614
- * `setName()` call or `__ps_name` storage write is needed; the
3615
- * facet's name survives cold wake automatically because the factory
3616
- * re-runs and `idFromName` is deterministic.
3816
+ * The facet's logical name is persisted separately from its routing id.
3817
+ * Legacy facets used the logical name directly as `ctx.id.name`; newer
3818
+ * facets can use path-scoped routing ids while preserving `this.name`.
3617
3819
  *
3618
3820
  * @internal Called by {@link subAgent}.
3619
3821
  */
3620
- async _cf_initAsFacet(name, parentPath = []) {
3621
- if (this.name !== name) throw new Error(`Facet bootstrap mismatch: expected this.name === "${name}" but got "${this.name}". This usually means the parent passed the wrong (or no) id to ctx.facets.get(). See _cf_resolveSubAgent.`);
3822
+ async _cf_initAsFacet(name, parentPath = [], identityName = name) {
3823
+ const routedName = super.name;
3824
+ if (routedName !== identityName) throw new Error(`Facet bootstrap mismatch: expected routed identity "${identityName}" but got "${routedName}". This usually means the parent passed the wrong id to ctx.facets.get(). See _cf_resolveSubAgent.`);
3622
3825
  this._isFacet = true;
3826
+ this._facetName = name;
3623
3827
  this._parentPath = parentPath;
3624
- await Promise.all([this.ctx.storage.put("cf_agents_is_facet", true), this.ctx.storage.put("cf_agents_parent_path", parentPath)]);
3828
+ await Promise.all([
3829
+ this.ctx.storage.put("cf_agents_is_facet", true),
3830
+ this.ctx.storage.put("cf_agents_facet_name", name),
3831
+ this.ctx.storage.put("cf_agents_parent_path", parentPath)
3832
+ ]);
3625
3833
  this._suppressProtocolBroadcasts = true;
3626
3834
  try {
3627
3835
  await this.__unsafe_ensureInitialized();
@@ -3629,6 +3837,9 @@ var Agent = class Agent extends Server {
3629
3837
  this._suppressProtocolBroadcasts = false;
3630
3838
  }
3631
3839
  }
3840
+ get name() {
3841
+ return this._facetName ?? logicalNameFromPathV2Identity(super.name) ?? super.name;
3842
+ }
3632
3843
  /**
3633
3844
  * Ancestor chain for this agent, root-first. Empty for top-level
3634
3845
  * DOs. Populated at facet init time; survives hibernation.
@@ -3782,7 +3993,7 @@ var Agent = class Agent extends Server {
3782
3993
  const agentType = cls.name;
3783
3994
  const existing = this._readAgentToolRun(runId);
3784
3995
  if (existing) {
3785
- if (this._isAgentToolTerminal(existing.status)) {
3996
+ if (existing.status === "completed" || existing.status === "error" || existing.status === "aborted") {
3786
3997
  if (existing.status === "completed" && existing.output_json == null) try {
3787
3998
  const child = await this.subAgent(cls, runId);
3788
3999
  const inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(runId);
@@ -3794,7 +4005,19 @@ var Agent = class Agent extends Server {
3794
4005
  } catch {}
3795
4006
  return this._resultFromAgentToolRow(existing);
3796
4007
  }
3797
- return await this._replayAndInterruptAgentToolRun(existing, "Agent tool run was still running, but live-tail reattachment is not supported in this runtime.");
4008
+ try {
4009
+ const child = await this.subAgent(cls, runId);
4010
+ const adapter = this._asAgentToolChildAdapter(child);
4011
+ const reattach = await this._reattachAgentToolRunToTerminal(adapter, existing, 1);
4012
+ if (reattach.result) {
4013
+ await this._finishAgentToolRun(this._agentToolRunInfoFromRow(existing), reattach.result, {
4014
+ sequence: reattach.sequence,
4015
+ completedAt: reattach.completedAt
4016
+ });
4017
+ return reattach.result;
4018
+ }
4019
+ } catch {}
4020
+ return await this._replayAndInterruptAgentToolRun(existing, "Agent tool run was still running and could not be re-attached to a terminal result within the recovery budget.");
3798
4021
  }
3799
4022
  const displayOrder = options.displayOrder ?? 0;
3800
4023
  const inputPreview = options.inputPreview ?? this._defaultAgentToolPreview(options.input);
@@ -4074,7 +4297,7 @@ var Agent = class Agent extends Server {
4074
4297
  error_message = ${result.error ?? null},
4075
4298
  completed_at = ${completedAt}
4076
4299
  WHERE run_id = ${runId}
4077
- AND status NOT IN ('completed', 'error', 'aborted', 'interrupted')
4300
+ AND status NOT IN ('completed', 'error', 'aborted')
4078
4301
  `;
4079
4302
  if (result.status === "completed" && result.output !== void 0) this.sql`
4080
4303
  UPDATE cf_agent_tool_runs
@@ -4126,7 +4349,12 @@ var Agent = class Agent extends Server {
4126
4349
  }
4127
4350
  async _broadcastAgentToolStoredChunks(row, sequence, replay, connection) {
4128
4351
  const child = await this._cf_resolveSubAgent(row.agent_type, row.run_id);
4129
- const chunks = await this._asAgentToolChildAdapter(child).getAgentToolChunks(row.run_id);
4352
+ const adapter = this._asAgentToolChildAdapter(child);
4353
+ return this._broadcastAgentToolStoredChunksFromAdapter(adapter, row, sequence, replay, connection);
4354
+ }
4355
+ async _broadcastAgentToolStoredChunksFromAdapter(adapter, row, sequence, replay, connection, timeoutMs) {
4356
+ const chunks = await this._getAgentToolChunksForRecovery(adapter, row.run_id, timeoutMs);
4357
+ if (!chunks) return sequence;
4130
4358
  return this._broadcastAgentToolChunks(row.parent_tool_call_id ?? void 0, row.run_id, chunks, sequence, replay, connection);
4131
4359
  }
4132
4360
  async _forwardAgentToolStream(stream, parentToolCallId, runId, sequence, signal) {
@@ -4135,11 +4363,17 @@ var Agent = class Agent extends Server {
4135
4363
  const reader = stream.getReader();
4136
4364
  const decoder = new TextDecoder();
4137
4365
  let bufferedBytes = "";
4366
+ let aborted = false;
4367
+ let resolveAbort;
4368
+ const abortPromise = new Promise((resolve) => {
4369
+ resolveAbort = resolve;
4370
+ });
4138
4371
  let abortListener;
4139
4372
  if (signal) {
4140
- abortListener = () => {};
4373
+ abortListener = () => resolveAbort?.();
4141
4374
  signal.addEventListener("abort", abortListener, { once: true });
4142
4375
  }
4376
+ let forwardedSinceProgress = false;
4143
4377
  try {
4144
4378
  const forwardChunk = (chunk) => {
4145
4379
  this._broadcastAgentToolEvent(parentToolCallId, next++, {
@@ -4147,6 +4381,7 @@ var Agent = class Agent extends Server {
4147
4381
  runId,
4148
4382
  body: chunk.body
4149
4383
  });
4384
+ forwardedSinceProgress = true;
4150
4385
  };
4151
4386
  const forwardLine = (line) => {
4152
4387
  try {
@@ -4168,14 +4403,17 @@ var Agent = class Agent extends Server {
4168
4403
  }
4169
4404
  };
4170
4405
  while (true) {
4171
- let readResult;
4172
- try {
4173
- readResult = await reader.read();
4174
- } catch (error) {
4175
- if (signal?.aborted) break;
4176
- throw error;
4406
+ const readPromise = reader.read();
4407
+ readPromise.catch(() => {});
4408
+ const raced = await Promise.race([readPromise.then((result) => ({
4409
+ kind: "read",
4410
+ result
4411
+ })), abortPromise.then(() => ({ kind: "abort" }))]);
4412
+ if (raced.kind === "abort") {
4413
+ aborted = true;
4414
+ break;
4177
4415
  }
4178
- const { done, value } = readResult;
4416
+ const { done, value } = raced.result;
4179
4417
  if (done) {
4180
4418
  bufferedBytes += decoder.decode();
4181
4419
  flushBufferedBytes(true);
@@ -4185,13 +4423,42 @@ var Agent = class Agent extends Server {
4185
4423
  bufferedBytes += decoder.decode(value, { stream: true });
4186
4424
  flushBufferedBytes();
4187
4425
  } else forwardChunk(value);
4426
+ if (forwardedSinceProgress) {
4427
+ forwardedSinceProgress = false;
4428
+ try {
4429
+ await this._onAgentToolStreamProgress();
4430
+ } catch {}
4431
+ }
4188
4432
  }
4189
4433
  } finally {
4190
4434
  if (abortListener && signal) signal.removeEventListener("abort", abortListener);
4191
- reader.releaseLock();
4435
+ if (!aborted) try {
4436
+ reader.releaseLock();
4437
+ } catch {}
4192
4438
  }
4193
4439
  return next;
4194
4440
  }
4441
+ /**
4442
+ * Hook invoked by `_forwardAgentToolStream` after a child produces output that
4443
+ * was forwarded to the parent's connections. Forwarding a sub-agent's stream
4444
+ * is genuine forward progress for the *parent* turn (the parent is
4445
+ * orchestrating the child), so chat-recovery subclasses (Think / AIChatAgent)
4446
+ * override this to advance their recovery progress marker.
4447
+ *
4448
+ * Without it, a parent whose turn merely `await`s a sub-agent banks zero
4449
+ * progress of its own, so under deploy churn the parent's no-progress recovery
4450
+ * window exhausts and abandons the turn as `interrupted` — even though the
4451
+ * child is healthily streaming and ultimately completes (observed in the
4452
+ * `deploy-churn --mode subagent` harness: `attempt 6/6, stable_timeout,
4453
+ * progress: 1`).
4454
+ *
4455
+ * Called ONLY after at least one chunk was actually forwarded — never merely
4456
+ * because a child is attached — so a silent / hung child still lets the parent
4457
+ * exhaust on its own timer. The base Agent has no recovery budget, so this is
4458
+ * a no-op; subclasses should throttle the (durable) bump since this can be
4459
+ * called repeatedly while a child streams.
4460
+ */
4461
+ async _onAgentToolStreamProgress() {}
4195
4462
  _broadcastAgentToolTerminal(parentToolCallId, sequence, result, replay, connection) {
4196
4463
  if (result.status === "completed") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
4197
4464
  kind: "finished",
@@ -4238,6 +4505,58 @@ var Agent = class Agent extends Server {
4238
4505
  await this._finishAgentToolRun(this._agentToolRunInfoFromRow(row), result, { sequence });
4239
4506
  return result;
4240
4507
  }
4508
+ /**
4509
+ * Re-attach to a still-running child agent-tool run and tail it to its real
4510
+ * terminal result, instead of abandoning it as `interrupted` (#1630). The
4511
+ * child is a separate facet with its own `chatRecovery`, so resolving it via
4512
+ * the adapter wakes it and lets it self-complete the interrupted turn; we tail
4513
+ * its live stream (forwarding chunks to the parent's connections) until it
4514
+ * reaches terminal, then inspect for the collected result.
4515
+ *
4516
+ * Bounded by {@link DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS}: a child that keeps
4517
+ * advancing toward terminal within the window is collected; a genuinely hung
4518
+ * child returns `{ result: undefined }` once the budget elapses so the caller
4519
+ * falls back to `interrupted` and recovery can never block forever.
4520
+ *
4521
+ * Returns the terminal `result` (and `completedAt`) when the child reaches a
4522
+ * terminal status within the budget, plus the advanced broadcast `sequence`.
4523
+ * Returns `{ result: undefined }` when there is no `tailAgentToolRun` adapter,
4524
+ * the budget is exhausted, or the child is still non-terminal.
4525
+ */
4526
+ async _reattachAgentToolRunToTerminal(adapter, row, sequence, budgetMs = DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS) {
4527
+ if (typeof adapter.tailAgentToolRun !== "function") return { sequence };
4528
+ const controller = new AbortController();
4529
+ let timeoutId;
4530
+ if (budgetMs > 0 && Number.isFinite(budgetMs)) timeoutId = setTimeout(() => controller.abort(), budgetMs);
4531
+ else if (budgetMs <= 0) controller.abort();
4532
+ this._emit("agent_tool:recovery:reattach", {
4533
+ runId: row.run_id,
4534
+ agentType: row.agent_type,
4535
+ budgetMs
4536
+ });
4537
+ let nextSequence = sequence;
4538
+ if (!controller.signal.aborted) try {
4539
+ let afterSequence = -1;
4540
+ try {
4541
+ const existing = await adapter.getAgentToolChunks(row.run_id);
4542
+ const last = existing[existing.length - 1];
4543
+ if (last) afterSequence = last.sequence;
4544
+ } catch {}
4545
+ const stream = await adapter.tailAgentToolRun(row.run_id, { afterSequence });
4546
+ nextSequence = await this._forwardAgentToolStream(stream, row.parent_tool_call_id ?? void 0, row.run_id, nextSequence, controller.signal);
4547
+ } catch {}
4548
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
4549
+ let inspection = null;
4550
+ try {
4551
+ inspection = await adapter.inspectAgentToolRun(row.run_id);
4552
+ } catch {}
4553
+ if (inspection && inspection.status !== "running" && inspection.status !== "starting") return {
4554
+ sequence: nextSequence,
4555
+ result: this._terminalResultFromInspection(row.agent_type, inspection),
4556
+ completedAt: inspection.completedAt
4557
+ };
4558
+ return { sequence: nextSequence };
4559
+ }
4241
4560
  async _replayAgentToolRuns(connection) {
4242
4561
  const rows = this.sql`
4243
4562
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
@@ -4270,6 +4589,10 @@ var Agent = class Agent extends Server {
4270
4589
  }
4271
4590
  }
4272
4591
  async _reconcileAgentToolRuns(options) {
4592
+ const reattachTimeoutMs = options?.reattachTimeoutMs ?? DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS;
4593
+ const startedAt = Date.now();
4594
+ const totalTimeoutMs = options?.totalRecoveryTimeoutMs ?? DEFAULT_AGENT_TOOL_RECOVERY_TOTAL_TIMEOUT_MS;
4595
+ const deadlineAt = totalTimeoutMs > 0 ? startedAt + totalTimeoutMs : Number.POSITIVE_INFINITY;
4273
4596
  const deferredFinishes = [];
4274
4597
  const rows = this.sql`
4275
4598
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
@@ -4279,43 +4602,158 @@ var Agent = class Agent extends Server {
4279
4602
  WHERE status IN ('starting', 'running')
4280
4603
  ORDER BY started_at ASC
4281
4604
  `;
4282
- for (const row of rows) {
4283
- let sequence = 1;
4284
- let completedAt;
4285
- let result;
4286
- try {
4287
- const child = await this._cf_resolveSubAgent(row.agent_type, row.run_id);
4288
- const inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(row.run_id);
4289
- try {
4290
- sequence = await this._broadcastAgentToolStoredChunks(row, sequence);
4291
- } catch {}
4292
- if (!inspection || inspection.status === "running" || inspection.status === "starting") result = {
4605
+ const runIds = options?.runIds !== void 0 ? new Set(options.runIds) : void 0;
4606
+ const recoveryRows = rows.filter((row) => !runIds || runIds.has(row.run_id));
4607
+ this._emit("agent_tool:recovery:begin", {
4608
+ runCount: recoveryRows.length,
4609
+ totalTimeoutMs
4610
+ });
4611
+ const finalizeRow = async (row, result, sequence, completedAt) => {
4612
+ this._emit("agent_tool:recovery:row", {
4613
+ runId: row.run_id,
4614
+ agentType: row.agent_type,
4615
+ status: result.status,
4616
+ reason: result.error,
4617
+ elapsedMs: Date.now() - startedAt
4618
+ });
4619
+ const deferredFinish = await this._finishAgentToolRun(this._agentToolRunInfoFromRow(row), result, {
4620
+ sequence,
4621
+ completedAt,
4622
+ deferFinishHook: options?.deferFinishHooks
4623
+ });
4624
+ if (deferredFinish) deferredFinishes.push(deferredFinish);
4625
+ };
4626
+ const reattachQueue = [];
4627
+ for (const row of recoveryRows) {
4628
+ const sequence = 1;
4629
+ const remainingMs = deadlineAt - Date.now();
4630
+ if (remainingMs <= 0) {
4631
+ this._emit("agent_tool:recovery:deadline", {
4632
+ runId: row.run_id,
4633
+ agentType: row.agent_type,
4634
+ elapsedMs: Date.now() - startedAt
4635
+ });
4636
+ await finalizeRow(row, {
4293
4637
  runId: row.run_id,
4294
4638
  agentType: row.agent_type,
4295
4639
  status: "interrupted",
4296
- error: "Agent tool run was still running, but live-tail reattachment is not supported in this runtime."
4297
- };
4298
- else {
4299
- result = this._terminalResultFromInspection(row.agent_type, inspection);
4300
- completedAt = inspection.completedAt;
4301
- }
4302
- } catch {
4303
- result = {
4640
+ error: "Agent tool run recovery deadline exceeded."
4641
+ }, sequence, void 0);
4642
+ continue;
4643
+ }
4644
+ const childTimeout = options?.childInspectionTimeoutMs ?? DEFAULT_AGENT_TOOL_RECOVERY_TIMEOUT_MS;
4645
+ const boundedChildTimeout = childTimeout > 0 ? Math.min(childTimeout, remainingMs) : remainingMs;
4646
+ const recovery = await this._inspectAgentToolRunForRecovery(row, sequence, boundedChildTimeout);
4647
+ if (recovery.status !== "inspected") {
4648
+ await finalizeRow(row, {
4304
4649
  runId: row.run_id,
4305
4650
  agentType: row.agent_type,
4306
4651
  status: "interrupted",
4307
- error: "Agent tool run could not be inspected during parent recovery."
4308
- };
4652
+ error: recovery.status === "timed-out" ? "Agent tool run inspection timed out during parent recovery." : "Agent tool run could not be inspected during parent recovery."
4653
+ }, sequence, void 0);
4654
+ continue;
4309
4655
  }
4310
- const deferredFinish = await this._finishAgentToolRun(this._agentToolRunInfoFromRow(row), result, {
4311
- sequence,
4312
- completedAt,
4313
- deferFinishHook: options?.deferFinishHooks
4314
- });
4315
- if (deferredFinish) deferredFinishes.push(deferredFinish);
4656
+ const inspection = recovery.inspection;
4657
+ const stillRunning = !inspection || inspection.status === "running" || inspection.status === "starting";
4658
+ if (stillRunning && typeof recovery.adapter.tailAgentToolRun === "function") {
4659
+ reattachQueue.push({
4660
+ row,
4661
+ adapter: recovery.adapter
4662
+ });
4663
+ continue;
4664
+ }
4665
+ let sequenceAfterReplay = sequence;
4666
+ try {
4667
+ sequenceAfterReplay = await this._broadcastAgentToolStoredChunksFromAdapter(recovery.adapter, row, sequence, void 0, void 0, boundedChildTimeout);
4668
+ } catch {}
4669
+ if (stillRunning) await finalizeRow(row, {
4670
+ runId: row.run_id,
4671
+ agentType: row.agent_type,
4672
+ status: "interrupted",
4673
+ error: "Agent tool run was still running, but live-tail reattachment is not supported in this runtime."
4674
+ }, sequenceAfterReplay, void 0);
4675
+ else await finalizeRow(row, this._terminalResultFromInspection(row.agent_type, inspection), sequenceAfterReplay, inspection.completedAt);
4316
4676
  }
4677
+ await Promise.all(reattachQueue.map(async ({ row, adapter }) => {
4678
+ const reattach = await this._reattachAgentToolRunToTerminal(adapter, row, 1, reattachTimeoutMs);
4679
+ await finalizeRow(row, reattach.result ?? {
4680
+ runId: row.run_id,
4681
+ agentType: row.agent_type,
4682
+ status: "interrupted",
4683
+ error: "Agent tool run was still running and did not reach a terminal result within the re-attach budget."
4684
+ }, reattach.sequence, reattach.completedAt);
4685
+ }));
4686
+ this._emit("agent_tool:recovery:complete", {
4687
+ runCount: recoveryRows.length,
4688
+ elapsedMs: Date.now() - startedAt
4689
+ });
4317
4690
  return deferredFinishes;
4318
4691
  }
4692
+ async _inspectAgentToolRunForRecovery(row, _sequence, timeoutMs = DEFAULT_AGENT_TOOL_RECOVERY_TIMEOUT_MS) {
4693
+ const inspect = (async () => {
4694
+ const child = await this._cf_resolveSubAgent(row.agent_type, row.run_id);
4695
+ const adapter = this._asAgentToolChildAdapter(child);
4696
+ return {
4697
+ status: "inspected",
4698
+ adapter,
4699
+ inspection: await adapter.inspectAgentToolRun(row.run_id)
4700
+ };
4701
+ })().catch(() => ({ status: "failed" }));
4702
+ if (timeoutMs <= 0) return inspect;
4703
+ let timeoutId;
4704
+ const timeout = new Promise((resolve) => {
4705
+ timeoutId = setTimeout(() => {
4706
+ resolve({ status: "timed-out" });
4707
+ }, timeoutMs);
4708
+ });
4709
+ const result = await Promise.race([inspect, timeout]);
4710
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
4711
+ return result;
4712
+ }
4713
+ _scheduleAgentToolRunRecovery(options) {
4714
+ if (this._agentToolRunRecoveryPromise) return this._agentToolRunRecoveryPromise;
4715
+ if (options?.runIds && options.runIds.length === 0) return Promise.resolve();
4716
+ const recovery = (async () => {
4717
+ await new Promise((resolve) => setTimeout(resolve, 0));
4718
+ const recoveredAgentToolFinishes = await this._reconcileAgentToolRuns({
4719
+ deferFinishHooks: true,
4720
+ childInspectionTimeoutMs: options?.childInspectionTimeoutMs,
4721
+ totalRecoveryTimeoutMs: options?.totalRecoveryTimeoutMs,
4722
+ reattachTimeoutMs: options?.reattachTimeoutMs,
4723
+ runIds: options?.runIds
4724
+ });
4725
+ await this._runDeferredAgentToolFinishHooks(recoveredAgentToolFinishes);
4726
+ })().catch(async (error) => {
4727
+ this._emit("agent_tool:recovery:failed", { error: error instanceof Error ? error.message : String(error) });
4728
+ try {
4729
+ await this.onError(error);
4730
+ } catch {}
4731
+ }).finally(() => {
4732
+ this._agentToolRunRecoveryPromise = void 0;
4733
+ });
4734
+ this._agentToolRunRecoveryPromise = recovery;
4735
+ this.ctx.waitUntil(recovery);
4736
+ return recovery;
4737
+ }
4738
+ _agentToolRunRecoveryRunIds() {
4739
+ return this.sql`
4740
+ SELECT run_id
4741
+ FROM cf_agent_tool_runs
4742
+ WHERE status IN ('starting', 'running')
4743
+ ORDER BY started_at ASC
4744
+ `.map((row) => row.run_id);
4745
+ }
4746
+ async _getAgentToolChunksForRecovery(adapter, runId, timeoutMs) {
4747
+ const chunks = adapter.getAgentToolChunks(runId).catch(() => void 0);
4748
+ if (timeoutMs === void 0 || timeoutMs <= 0) return chunks;
4749
+ let timeoutId;
4750
+ const timeout = new Promise((resolve) => {
4751
+ timeoutId = setTimeout(() => resolve(void 0), timeoutMs);
4752
+ });
4753
+ const result = await Promise.race([chunks, timeout]);
4754
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
4755
+ return result;
4756
+ }
4319
4757
  /**
4320
4758
  * Shared facet resolution — takes a CamelCase class name string
4321
4759
  * (matching `ctx.exports`) rather than a class reference. Both
@@ -4333,27 +4771,37 @@ var Agent = class Agent extends Server {
4333
4771
  if (!Cls) throw new Error(`Sub-agent class "${className}" not found in worker exports. Make sure the class is exported from your worker entry point and that the export name matches the class name.`);
4334
4772
  if (name.includes("\0")) throw new Error(`Sub-agent name contains null character (\\0), which is reserved.`);
4335
4773
  const facetKey = `${className}\0${name}`;
4774
+ const childParentPath = this.selfPath;
4775
+ const childPath = [...childParentPath, {
4776
+ className,
4777
+ name
4778
+ }];
4336
4779
  const rootClassName = this._parentPath[0]?.className ?? this.constructor.name;
4337
4780
  const rootNs = ctx.exports[rootClassName];
4338
4781
  if (!rootNs?.idFromName) {
4339
4782
  const minificationHint = /^_*[a-z][a-z0-9]{0,2}$/.test(rootClassName) ? ` The class name "${rootClassName}" looks minified — make sure your bundler preserves class names (e.g. esbuild's \`keepNames: true\`).` : "";
4340
4783
  throw new Error(`Sub-agent bootstrap requires the root agent class "${rootClassName}" to be available as a Durable Object namespace, but ctx.exports["${rootClassName}"] is missing or doesn't expose idFromName.${minificationHint} Make sure the root agent class is exported under that class name and registered in your wrangler.jsonc durable_objects.bindings.`);
4341
4784
  }
4342
- const facetId = rootNs.idFromName(name);
4785
+ const identity = await this._cf_subAgentIdentity(className, name, childPath);
4786
+ const facetId = rootNs.idFromName(identity.name);
4343
4787
  const stub = ctx.facets.get(facetKey, () => ({
4344
4788
  class: Cls,
4345
4789
  id: facetId
4346
4790
  }));
4347
- const childParentPath = this.selfPath;
4348
- await __DO_NOT_USE_WILL_BREAK__agentContext.run({
4349
- agent: this,
4350
- connection: void 0,
4351
- request: void 0,
4352
- email: void 0
4353
- }, async () => {
4354
- await stub._cf_initAsFacet(name, childParentPath);
4355
- });
4356
- this._recordSubAgent(className, name);
4791
+ this._recordSubAgent(className, name, identity);
4792
+ try {
4793
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
4794
+ agent: this,
4795
+ connection: void 0,
4796
+ request: void 0,
4797
+ email: void 0
4798
+ }, async () => {
4799
+ await stub._cf_initAsFacet(name, childParentPath, identity.name);
4800
+ });
4801
+ } catch (error) {
4802
+ if (!identity.existing) this._forgetSubAgent(className, name);
4803
+ throw error;
4804
+ }
4357
4805
  return stub;
4358
4806
  }
4359
4807
  /**
@@ -4398,6 +4846,13 @@ var Agent = class Agent extends Server {
4398
4846
  } catch {}
4399
4847
  this._forgetSubAgent(cls.name, name);
4400
4848
  }
4849
+ _addColumnIfNotExists(sql) {
4850
+ try {
4851
+ this.ctx.storage.sql.exec(sql);
4852
+ } catch (e) {
4853
+ if (!(e instanceof Error ? e.message : String(e)).toLowerCase().includes("duplicate column")) throw e;
4854
+ }
4855
+ }
4401
4856
  /** @internal */
4402
4857
  _ensureSubAgentRegistry() {
4403
4858
  if (this._subAgentRegistryReady) return;
@@ -4406,20 +4861,56 @@ var Agent = class Agent extends Server {
4406
4861
  class TEXT NOT NULL,
4407
4862
  name TEXT NOT NULL,
4408
4863
  created_at INTEGER NOT NULL,
4864
+ identity_version TEXT,
4865
+ identity_name TEXT,
4409
4866
  PRIMARY KEY (class, name)
4410
4867
  )
4411
4868
  `;
4869
+ this._addColumnIfNotExists("ALTER TABLE cf_agents_sub_agents ADD COLUMN identity_version TEXT");
4870
+ this._addColumnIfNotExists("ALTER TABLE cf_agents_sub_agents ADD COLUMN identity_name TEXT");
4412
4871
  this._subAgentRegistryReady = true;
4413
4872
  }
4414
4873
  /** @internal */
4415
- _recordSubAgent(className, name) {
4874
+ _recordSubAgent(className, name, identity) {
4416
4875
  this._ensureSubAgentRegistry();
4417
4876
  this.sql`
4418
- INSERT OR IGNORE INTO cf_agents_sub_agents (class, name, created_at)
4419
- VALUES (${className}, ${name}, ${Date.now()})
4877
+ INSERT OR IGNORE INTO cf_agents_sub_agents
4878
+ (class, name, created_at, identity_version, identity_name)
4879
+ VALUES
4880
+ (${className}, ${name}, ${Date.now()}, ${identity.version}, ${identity.name})
4420
4881
  `;
4421
4882
  }
4422
4883
  /** @internal */
4884
+ _subAgentRegistryRow(className, name) {
4885
+ this._ensureSubAgentRegistry();
4886
+ return this.sql`
4887
+ SELECT identity_version, identity_name
4888
+ FROM cf_agents_sub_agents
4889
+ WHERE class = ${className} AND name = ${name}
4890
+ LIMIT 1
4891
+ `[0] ?? null;
4892
+ }
4893
+ async _cf_subAgentIdentity(className, name, childPath) {
4894
+ const row = this._subAgentRegistryRow(className, name);
4895
+ if (row) {
4896
+ if (row.identity_version === SUB_AGENT_IDENTITY_VERSION_PATH_V2 && typeof row.identity_name === "string") return {
4897
+ version: SUB_AGENT_IDENTITY_VERSION_PATH_V2,
4898
+ name: row.identity_name,
4899
+ existing: true
4900
+ };
4901
+ return {
4902
+ version: SUB_AGENT_IDENTITY_VERSION_LEGACY,
4903
+ name,
4904
+ existing: true
4905
+ };
4906
+ }
4907
+ return {
4908
+ version: SUB_AGENT_IDENTITY_VERSION_PATH_V2,
4909
+ name: pathV2IdentityName(name, await sha256Hex(JSON.stringify(childPath))),
4910
+ existing: false
4911
+ };
4912
+ }
4913
+ /** @internal */
4423
4914
  _forgetSubAgent(className, name) {
4424
4915
  this._ensureSubAgentRegistry();
4425
4916
  this.sql`
@@ -4552,7 +5043,7 @@ var Agent = class Agent extends Server {
4552
5043
  if (!workflow) throw new Error(`Workflow binding '${workflowName}' not found in environment`);
4553
5044
  const agentBindingName = options?.agentBinding ?? this._findAgentBindingName();
4554
5045
  if (!agentBindingName) throw new Error("Could not detect Agent binding name from class name. Pass it explicitly via options.agentBinding");
4555
- const workflowId = options?.id ?? nanoid();
5046
+ const workflowId = options?.id ?? `wf_${nanoid()}`;
4556
5047
  const augmentedParams = {
4557
5048
  ...params,
4558
5049
  __agentName: this.name,
@@ -5254,7 +5745,26 @@ var Agent = class Agent extends Server {
5254
5745
  async addMcpServer(serverName, urlOrBinding, callbackHostOrOptions, agentsPrefix, options) {
5255
5746
  const isHttpTransport = typeof urlOrBinding === "string";
5256
5747
  const normalizedUrl = isHttpTransport ? new URL(urlOrBinding).href : void 0;
5257
- const existingServer = this.mcp.listServers().find((s) => s.name === serverName && (!isHttpTransport || new URL(s.server_url).href === normalizedUrl));
5748
+ let requestedId;
5749
+ if (typeof callbackHostOrOptions === "object" && callbackHostOrOptions !== null && typeof callbackHostOrOptions.id === "string") {
5750
+ const rawId = callbackHostOrOptions.id;
5751
+ requestedId = normalizeServerId(rawId);
5752
+ }
5753
+ const allServers = this.mcp.listServers();
5754
+ const existingServer = allServers.find((s) => s.name === serverName && (!isHttpTransport || new URL(s.server_url).href === normalizedUrl));
5755
+ if (requestedId) {
5756
+ const idConflict = allServers.find((s) => {
5757
+ if (s.id !== requestedId) return false;
5758
+ if (s.name !== serverName) return true;
5759
+ if (isHttpTransport) return new URL(s.server_url).href !== normalizedUrl;
5760
+ return false;
5761
+ });
5762
+ if (idConflict) throw new Error(`MCP server id "${requestedId}" is already in use by server "${idConflict.name}" (${idConflict.server_url}). Stable ids must be unique per (name, url).`);
5763
+ if (existingServer && existingServer.id !== requestedId) {
5764
+ await this.mcp.migrateServerId(existingServer.id, requestedId, this.name);
5765
+ existingServer.id = requestedId;
5766
+ }
5767
+ }
5258
5768
  if (existingServer && this.mcp.mcpConnections[existingServer.id]) {
5259
5769
  const conn = this.mcp.mcpConnections[existingServer.id];
5260
5770
  if (conn.connectionState === MCPConnectionState.AUTHENTICATING && conn.options.transport.authProvider?.authUrl) return {
@@ -5271,7 +5781,7 @@ var Agent = class Agent extends Server {
5271
5781
  if (typeof urlOrBinding !== "string") {
5272
5782
  const rpcOpts = callbackHostOrOptions;
5273
5783
  const normalizedName = serverName.toLowerCase().replace(/\s+/g, "-");
5274
- const reconnectId = existingServer?.id;
5784
+ const reconnectId = requestedId ?? existingServer?.id;
5275
5785
  const { id } = await this.mcp.connect(`${RPC_DO_PREFIX}${normalizedName}`, {
5276
5786
  reconnect: reconnectId ? { id: reconnectId } : void 0,
5277
5787
  transport: {
@@ -5328,7 +5838,7 @@ var Agent = class Agent extends Server {
5328
5838
  const normalizedHost = resolvedCallbackHost.replace(/\/$/, "");
5329
5839
  callbackUrl = resolvedCallbackPath ? `${normalizedHost}/${resolvedCallbackPath.replace(/^\//, "")}` : `${normalizedHost}/${resolvedAgentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
5330
5840
  }
5331
- const id = nanoid(8);
5841
+ const id = requestedId ?? nanoid(8);
5332
5842
  let authProvider;
5333
5843
  if (callbackUrl) {
5334
5844
  authProvider = this.createMcpOAuthProvider(callbackUrl);
@@ -5660,6 +6170,6 @@ var StreamingResponse = class {
5660
6170
  }
5661
6171
  };
5662
6172
  //#endregion
5663
- export { Agent, DEFAULT_AGENT_STATIC_OPTIONS, DurableObjectOAuthClientProvider, MessageType, SUB_PREFIX, SqlError, StreamingResponse, __DO_NOT_USE_WILL_BREAK__agentContext, callable, createHeaderBasedEmailResolver, getAgentByName, getCurrentAgent, getSubAgentByName, parseSubAgentPath, routeAgentEmail, routeAgentRequest, routeSubAgentRequest, unstable_callable };
6173
+ export { Agent, DEFAULT_AGENT_STATIC_OPTIONS, DurableObjectOAuthClientProvider, MCP_SERVER_ID_MAX_LENGTH, MessageType, SUB_PREFIX, SqlError, StreamingResponse, __DO_NOT_USE_WILL_BREAK__agentContext, callable, createHeaderBasedEmailResolver, getAgentByName, getCurrentAgent, getSubAgentByName, normalizeServerId, parseSubAgentPath, routeAgentEmail, routeAgentRequest, routeSubAgentRequest, unstable_callable };
5664
6174
 
5665
6175
  //# sourceMappingURL=index.js.map