agents 0.16.2 → 0.17.1

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 (126) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-CNyE1iz_.d.ts} +671 -138
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-CSnyGvJ2.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +2033 -77
  26. package/dist/chat/index.js +1828 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-BRnybD6X.d.ts} +197 -14
  45. package/dist/index.d.ts +95 -73
  46. package/dist/index.js +694 -25
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/{retries-CwlpAGet.d.ts → retries-CvHJwSuh.d.ts} +15 -6
  62. package/dist/retries.d.ts +8 -6
  63. package/dist/retries.js +44 -1
  64. package/dist/retries.js.map +1 -1
  65. package/dist/serializable.d.ts +1 -1
  66. package/dist/skills/compile.js +1 -1
  67. package/dist/skills/compile.js.map +1 -1
  68. package/dist/skills/index.js +5 -5
  69. package/dist/skills/index.js.map +1 -1
  70. package/dist/sub-routing.d.ts +6 -6
  71. package/dist/utils.js +1 -1
  72. package/dist/utils.js.map +1 -1
  73. package/dist/vite.js +5 -5
  74. package/dist/vite.js.map +1 -1
  75. package/dist/wire-types-nflOzNuU.js +240 -0
  76. package/dist/wire-types-nflOzNuU.js.map +1 -0
  77. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  78. package/dist/workflow-types.d.ts +25 -21
  79. package/dist/workflow-types.js.map +1 -1
  80. package/dist/workflows.d.ts +22 -21
  81. package/dist/workflows.js +31 -7
  82. package/dist/workflows.js.map +1 -1
  83. package/docs/adding-to-existing-project.md +450 -0
  84. package/docs/agent-class.md +503 -0
  85. package/docs/agent-tools.md +552 -0
  86. package/docs/browse-the-web.md +430 -0
  87. package/docs/callable-methods.md +627 -0
  88. package/docs/chat-agents.md +1696 -0
  89. package/docs/chat-sdk.md +181 -0
  90. package/docs/client-sdk.md +520 -0
  91. package/docs/client-tools-continuation.md +177 -0
  92. package/docs/codemode.md +440 -0
  93. package/docs/configuration.md +775 -0
  94. package/docs/cross-domain-authentication.md +171 -0
  95. package/docs/durable-execution.md +537 -0
  96. package/docs/email.md +663 -0
  97. package/docs/get-current-agent.md +204 -0
  98. package/docs/getting-started.md +305 -0
  99. package/docs/http-websockets.md +668 -0
  100. package/docs/human-in-the-loop.md +661 -0
  101. package/docs/index.md +151 -0
  102. package/docs/long-running-agents.md +730 -0
  103. package/docs/mcp-client.md +620 -0
  104. package/docs/mcp-servers.md +526 -0
  105. package/docs/mcp-transports.md +308 -0
  106. package/docs/migration-to-ai-sdk-v5.md +96 -0
  107. package/docs/migration-to-ai-sdk-v6.md +163 -0
  108. package/docs/observability.md +261 -0
  109. package/docs/push-notifications.md +367 -0
  110. package/docs/queue.md +329 -0
  111. package/docs/readonly-connections.md +278 -0
  112. package/docs/resumable-streaming.md +127 -0
  113. package/docs/retries.md +444 -0
  114. package/docs/routing.md +749 -0
  115. package/docs/scheduling.md +898 -0
  116. package/docs/securing-mcp-servers.md +359 -0
  117. package/docs/server-driven-messages.md +477 -0
  118. package/docs/sessions.md +1024 -0
  119. package/docs/state.md +512 -0
  120. package/docs/sub-agents.md +389 -0
  121. package/docs/webhooks.md +604 -0
  122. package/docs/workflows.md +877 -0
  123. package/package.json +41 -14
  124. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  125. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  126. package/dist/client-BXJ9n2f7.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
+ import { AGENT_TOOL_MILESTONE_PART, AGENT_TOOL_PROGRESS_PART } from "./agent-tool-types.js";
1
2
  import { __DO_NOT_USE_WILL_BREAK__agentContext } from "./internal_context.js";
2
3
  import { MessageType } from "./types.js";
3
4
  import { camelCaseToKebabCase, isInternalJsStubProp } from "./utils.js";
4
5
  import { createHeaderBasedEmailResolver, signAgentHeaders } from "./email.js";
5
- import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-CZ7QjTXN.js";
6
+ import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
6
7
  import { SUB_PREFIX, getSubAgentByName, parseSubAgentPath, routeSubAgentRequest } from "./sub-routing.js";
7
- import { isDurableObjectCodeUpdateReset, isErrorRetryable, isPlatformTransientError, tryN, validateRetryOptions } from "./retries.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-BXJ9n2f7.js";
8
+ import { isDurableObjectCodeUpdateReset, isDurableObjectMemoryLimitReset, isErrorRetryable, isPlatformTransientError, tryN, validateRetryOptions } from "./retries.js";
9
+ 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-BZ-B3NhC.js";
9
10
  import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider.js";
10
11
  import { genericObservability } from "./observability/index.js";
11
12
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -134,6 +135,18 @@ const FIBER_RECOVERY_MAX_BACKOFF_MS = 5 * 6e4;
134
135
  const FIBER_RECOVERY_BACKOFF_MAX_EXP = 20;
135
136
  const DEFAULT_AGENT_TOOL_REATTACH_NO_PROGRESS_TIMEOUT_MS = 12e4;
136
137
  const DEFAULT_AGENT_TOOL_REATTACH_MAX_WINDOW_MS = Number.POSITIVE_INFINITY;
138
+ const DEFAULT_DETACHED_MAX_BUDGET_MS = 1440 * 60 * 1e3;
139
+ const DEFAULT_DETACHED_NO_PROGRESS_BUDGET_MS = 3600 * 1e3;
140
+ const DETACHED_DELIVERY_LEASE_MS = 6e4;
141
+ const DETACHED_BACKBONE_CADENCE_S = [
142
+ 5,
143
+ 15,
144
+ 30,
145
+ 120
146
+ ];
147
+ const DETACHED_LIVE_COUNT_WARN_THRESHOLD = 50;
148
+ const DETACHED_RECONCILE_CALLBACK = "_cfDetachedReconcileTick";
149
+ const DETACHED_NOTIFY_CALLBACK = "_cfDetachedNotifyFinish";
137
150
  const SUB_AGENT_IDENTITY_VERSION_LEGACY = "legacy";
138
151
  const SUB_AGENT_IDENTITY_VERSION_PATH_V2 = "path-v2";
139
152
  const SUB_AGENT_IDENTITY_PATH_V2_PREFIX = "cf-agents:v2:";
@@ -143,7 +156,7 @@ const SUB_AGENT_IDENTITY_PATH_V2_PREFIX = "cf-agents:v2:";
143
156
  * The constructor stores this as a row in cf_agents_state and checks it
144
157
  * on wake to skip DDL on established DOs.
145
158
  */
146
- const CURRENT_SCHEMA_VERSION = 9;
159
+ const CURRENT_SCHEMA_VERSION = 11;
147
160
  const SCHEMA_VERSION_ROW_ID = "cf_schema_version";
148
161
  const STATE_ROW_ID = "cf_state_row_id";
149
162
  const STATE_WAS_CHANGED = "cf_state_was_changed";
@@ -205,7 +218,7 @@ const SUB_AGENT_OUTER_URL_HEADER = "x-cf-agents-subagent-url";
205
218
  * The set of all internal keys stored in connection state that must be
206
219
  * hidden from user code and preserved across setState calls.
207
220
  */
208
- const CF_INTERNAL_KEYS = new Set([
221
+ const CF_INTERNAL_KEYS = /* @__PURE__ */ new Set([
209
222
  CF_READONLY_KEY,
210
223
  CF_NO_PROTOCOL_KEY,
211
224
  CF_VOICE_IN_CALL_KEY,
@@ -312,7 +325,18 @@ const DEFAULT_AGENT_STATIC_OPTIONS = {
312
325
  * `maxRecoveryWork` / `shouldKeepRecovering`. Set a finite value to impose a
313
326
  * wall-clock cap (which also tears the child down on `window-exceeded`).
314
327
  */
315
- agentToolReattachMaxWindowMs: DEFAULT_AGENT_TOOL_REATTACH_MAX_WINDOW_MS
328
+ agentToolReattachMaxWindowMs: DEFAULT_AGENT_TOOL_REATTACH_MAX_WINDOW_MS,
329
+ detachedMaxBudgetMs: DEFAULT_DETACHED_MAX_BUDGET_MS,
330
+ detachedNoProgressBudgetMs: DEFAULT_DETACHED_NO_PROGRESS_BUDGET_MS,
331
+ /**
332
+ * Consecutive alarm invocations that may end in a Durable Object memory-limit
333
+ * reset (the isolate exceeded its 128 MB limit) before the alarm-boundary
334
+ * circuit breaker stops the platform's auto-retry loop and seals the looping
335
+ * work (#1825). A small budget tolerates a genuinely transient memory spike;
336
+ * a deterministic OOM (the work's footprint, not the platform, is the cause)
337
+ * is bounded here regardless of whether the in-DO recovery budgets could run.
338
+ */
339
+ maxAlarmMemoryLimitStrikes: 3
316
340
  };
317
341
  /**
318
342
  * Parse the raw `retry_options` TEXT column from a SQLite row into a
@@ -437,7 +461,10 @@ var Agent = class Agent extends Server {
437
461
  fiberRecoveryScanDeadlineMs: ctor.options?.fiberRecoveryScanDeadlineMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryScanDeadlineMs,
438
462
  fiberRecoveryMaxAgeMs: ctor.options?.fiberRecoveryMaxAgeMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryMaxAgeMs,
439
463
  agentToolReattachNoProgressTimeoutMs: ctor.options?.agentToolReattachNoProgressTimeoutMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachNoProgressTimeoutMs,
440
- agentToolReattachMaxWindowMs: ctor.options?.agentToolReattachMaxWindowMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachMaxWindowMs
464
+ agentToolReattachMaxWindowMs: ctor.options?.agentToolReattachMaxWindowMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachMaxWindowMs,
465
+ detachedMaxBudgetMs: ctor.options?.detachedMaxBudgetMs ?? DEFAULT_AGENT_STATIC_OPTIONS.detachedMaxBudgetMs,
466
+ detachedNoProgressBudgetMs: ctor.options?.detachedNoProgressBudgetMs ?? DEFAULT_AGENT_STATIC_OPTIONS.detachedNoProgressBudgetMs,
467
+ maxAlarmMemoryLimitStrikes: ctor.options?.maxAlarmMemoryLimitStrikes ?? DEFAULT_AGENT_STATIC_OPTIONS.maxAlarmMemoryLimitStrikes
441
468
  };
442
469
  return this._cachedOptions;
443
470
  }
@@ -679,6 +706,17 @@ var Agent = class Agent extends Server {
679
706
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN output_json TEXT");
680
707
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN interrupted_reason TEXT");
681
708
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN child_still_running INTEGER");
709
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached INTEGER NOT NULL DEFAULT 0");
710
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_on_finish TEXT");
711
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_notify_source TEXT");
712
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_max_budget_at INTEGER");
713
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN finish_claimed_at INTEGER");
714
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN finish_delivered_at INTEGER");
715
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN give_up_claimed_at INTEGER");
716
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN give_up_delivered_at INTEGER");
717
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_no_progress_budget_ms INTEGER");
718
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN last_progress_at INTEGER");
719
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_on_milestones TEXT");
682
720
  this.sql`
683
721
  INSERT OR REPLACE INTO cf_agents_state (id, state)
684
722
  VALUES (${SCHEMA_VERSION_ROW_ID}, ${String(CURRENT_SCHEMA_VERSION)})
@@ -707,6 +745,8 @@ var Agent = class Agent extends Server {
707
745
  this._managedFiberTerminalWaiters = /* @__PURE__ */ new Map();
708
746
  this._runFiberRecoveryInProgress = false;
709
747
  this._recoveryNoProgressScans = 0;
748
+ this._detachedBackboneArming = Promise.resolve();
749
+ this._detachedLiveCountWarned = false;
710
750
  this._ParentClass = Object.getPrototypeOf(this).constructor;
711
751
  this.initialState = DEFAULT_STATE;
712
752
  this.observability = genericObservability;
@@ -2641,7 +2681,7 @@ var Agent = class Agent extends Server {
2641
2681
  return true;
2642
2682
  }
2643
2683
  async deleteFibers(options) {
2644
- const terminalStatuses = [...this._normalizeFiberStatusFilter(options?.status) ?? new Set([
2684
+ const terminalStatuses = [...this._normalizeFiberStatusFilter(options?.status) ?? /* @__PURE__ */ new Set([
2645
2685
  "completed",
2646
2686
  "aborted",
2647
2687
  "error"
@@ -3138,6 +3178,25 @@ var Agent = class Agent extends Server {
3138
3178
  return (await this._cf_resolveSubAgent(next.className, next.name))._cf_dispatchScheduledCallback(ownerPath, row);
3139
3179
  }
3140
3180
  /**
3181
+ * Invoke an RPC method on this Agent or a descendant facet identified
3182
+ * by a root-first path. Used by AgentWorkflow to route callbacks and
3183
+ * `this.agent` calls back to the exact sub-agent that started a workflow.
3184
+ * @internal
3185
+ */
3186
+ async _cf_invokeAgentPath(targetPath, method, args) {
3187
+ await this.__unsafe_ensureInitialized();
3188
+ const selfPath = this.selfPath;
3189
+ if (!this._isSameAgentPathPrefix(selfPath, targetPath)) throw new Error(`Workflow origin path does not descend from ${JSON.stringify(selfPath)}.`);
3190
+ if (selfPath.length === targetPath.length) {
3191
+ const fn = this[method];
3192
+ if (isInternalJsStubProp(method) || method in Object.prototype || typeof fn !== "function") throw new Error(`Workflow origin method "${method}" is not callable on ${this.constructor.name}.`);
3193
+ return await fn.apply(this, args);
3194
+ }
3195
+ const next = targetPath[selfPath.length];
3196
+ if (!this.hasSubAgent(next.className, next.name)) throw new Error(`Workflow origin sub-agent ${next.className} "${next.name}" no longer exists.`);
3197
+ return await (await this._cf_resolveSubAgent(next.className, next.name))._cf_invokeAgentPath(targetPath, method, args);
3198
+ }
3199
+ /**
3141
3200
  * Recursively destroy a descendant facet identified by
3142
3201
  * `targetPath`. Walks down from `selfPath` until reaching the
3143
3202
  * target's immediate parent, where it cancels the target's
@@ -3199,6 +3258,7 @@ var Agent = class Agent extends Server {
3199
3258
  const isOneShotSchedule = row.type === "delayed" || row.type === "scheduled";
3200
3259
  const shouldDeferReset = (error) => isOneShotSchedule && isDurableObjectCodeUpdateReset(error);
3201
3260
  const shouldDeferOnExhaustion = (error) => isOneShotSchedule && isPlatformTransientError(error);
3261
+ const shouldDeferMemoryLimit = (error) => isOneShotSchedule && isDurableObjectMemoryLimitReset(error);
3202
3262
  try {
3203
3263
  this._emit("schedule:execute", {
3204
3264
  callback: row.callback,
@@ -3226,6 +3286,10 @@ var Agent = class Agent extends Server {
3226
3286
  console.warn(`Deferring scheduled callback "${row.callback}" after exhausting in-process retries on a transient platform error; the one-shot row is preserved and the alarm will re-run once the platform recovers.`);
3227
3287
  throw e;
3228
3288
  }
3289
+ if (shouldDeferMemoryLimit(e)) {
3290
+ console.warn(`Deferring scheduled callback "${row.callback}" to the alarm memory-limit circuit breaker after a Durable Object memory-limit reset; the one-shot row is preserved so the breaker can bound the retry loop and seal it (#1825).`);
3291
+ throw e;
3292
+ }
3229
3293
  console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
3230
3294
  this._emit("schedule:error", {
3231
3295
  callback: row.callback,
@@ -3337,6 +3401,20 @@ var Agent = class Agent extends Server {
3337
3401
  await this.destroy();
3338
3402
  return;
3339
3403
  }
3404
+ try {
3405
+ await this._cf_runAlarmBody();
3406
+ await this._cf_clearAlarmMemoryLimitStrikes();
3407
+ } catch (error) {
3408
+ if (!isDurableObjectMemoryLimitReset(error)) throw error;
3409
+ await this._cf_handleAlarmMemoryLimitReset(error);
3410
+ }
3411
+ }
3412
+ /**
3413
+ * The alarm body: PartyServer init + due-schedule processing + housekeeping +
3414
+ * next-alarm arm. Extracted from {@link alarm} so the memory-limit circuit
3415
+ * breaker can wrap it at the outermost frame (see {@link alarm}).
3416
+ */
3417
+ async _cf_runAlarmBody() {
3340
3418
  await super.alarm();
3341
3419
  const now = Math.floor(Date.now() / 1e3);
3342
3420
  const result = this.sql`
@@ -3387,7 +3465,9 @@ var Agent = class Agent extends Server {
3387
3465
  continue;
3388
3466
  }
3389
3467
  else {
3468
+ this._cf_executingScheduleRowId = row.id;
3390
3469
  await this._executeScheduleCallback(row);
3470
+ this._cf_executingScheduleRowId = void 0;
3391
3471
  executed = true;
3392
3472
  }
3393
3473
  if (this._destroyed) return;
@@ -3413,6 +3493,105 @@ var Agent = class Agent extends Server {
3413
3493
  await this._scheduleNextAlarm();
3414
3494
  }
3415
3495
  /**
3496
+ * The schedule-callback names whose alarm rows drive a recovery loop that can
3497
+ * deterministically OOM. The base agent has none; chat hosts (`Think`,
3498
+ * `AIChatAgent`) override this to return their recovery continuation callbacks
3499
+ * so the circuit breaker can surgically back them off / purge them WITHOUT
3500
+ * disturbing unrelated scheduled tasks. See {@link _cf_handleAlarmMemoryLimitReset}.
3501
+ */
3502
+ _cf_recoveryAlarmCallbacks() {
3503
+ return [];
3504
+ }
3505
+ /**
3506
+ * Hook for a host to terminalize ("seal") any in-flight recovery work as an
3507
+ * out-of-memory exhaustion when the alarm circuit breaker trips at its strike
3508
+ * budget (#1825). Runs at the outermost alarm frame (post-unwind, so writes
3509
+ * can land). Default: no-op. Chat hosts override to fire `onExhausted` + the
3510
+ * terminal banner and persist the sealed incident.
3511
+ */
3512
+ async _cf_sealMemoryLimitedRecovery() {}
3513
+ /**
3514
+ * Clear the durable memory-limit strike counter after a clean alarm so the
3515
+ * circuit breaker counts CONSECUTIVE resets rather than lifetime ones
3516
+ * (#1825). Reads first (cheap, usually cached) and only writes when a strike
3517
+ * is actually recorded, so the common no-strike path costs no write.
3518
+ * Best-effort: a stale strike only costs one extra tolerated spike later.
3519
+ */
3520
+ async _cf_clearAlarmMemoryLimitStrikes() {
3521
+ try {
3522
+ const prior = await this.ctx.storage.get(Agent._CF_OOM_ALARM_STRIKES_KEY);
3523
+ if (typeof prior === "number" && prior > 0) await this.ctx.storage.delete(Agent._CF_OOM_ALARM_STRIKES_KEY);
3524
+ } catch {}
3525
+ }
3526
+ /**
3527
+ * Alarm-boundary circuit breaker for Durable Object memory-limit resets
3528
+ * (#1825). The in-DO recovery budgets (`chatRecovery.maxOomRetries` /
3529
+ * `maxRecoveryWork`) only engage if their code runs AND its writes land; a
3530
+ * severe OOM can defeat both — thrown before the budget runs (boot hydration),
3531
+ * or its own small writes also OOM under memory pressure. In that case the
3532
+ * error reaches {@link alarm} and, unhandled, the platform auto-retries the
3533
+ * alarm indefinitely (re-running the doomed, billable turn each cycle).
3534
+ *
3535
+ * This runs at the OUTERMOST frame: the heavy turn has unwound and GC has
3536
+ * reclaimed its footprint, so the small writes here can land where mid-turn
3537
+ * ones (e.g. give-up's incident read) OOMed. A durable strike counter tolerates
3538
+ * a few resets (a transient spike may clear), backing off the recovery rows so
3539
+ * the retry is not a hot loop. At the `maxAlarmMemoryLimitStrikes` budget it
3540
+ * seals the recovery work and purges the looping rows so the loop — and the
3541
+ * bill — stops. Each step is best-effort: even these tiny writes can OOM, but
3542
+ * swallowing (not re-throwing) still halts the platform's auto-retry, and a
3543
+ * later wake re-arms legitimate schedules.
3544
+ */
3545
+ async _cf_handleAlarmMemoryLimitReset(error) {
3546
+ const key = Agent._CF_OOM_ALARM_STRIKES_KEY;
3547
+ let strikes = 1;
3548
+ try {
3549
+ const prior = await this.ctx.storage.get(key);
3550
+ strikes = (typeof prior === "number" ? prior : 0) + 1;
3551
+ await this.ctx.storage.put(key, strikes);
3552
+ } catch {}
3553
+ const limit = this._resolvedOptions.maxAlarmMemoryLimitStrikes;
3554
+ const sealed = strikes >= limit;
3555
+ const recoveryCallbacks = this._cf_recoveryAlarmCallbacks();
3556
+ const executingRowId = this._cf_executingScheduleRowId;
3557
+ this._cf_executingScheduleRowId = void 0;
3558
+ console.error(`Alarm hit a Durable Object memory-limit reset (strike ${strikes}/${limit}${sealed ? ", sealing recovery" : ", will retry with backoff"}). Breaking the platform alarm-retry loop (#1825).`, error instanceof Error ? error.message : String(error));
3559
+ if (sealed) {
3560
+ for (const cb of recoveryCallbacks) try {
3561
+ this.sql`DELETE FROM cf_agents_schedules WHERE callback = ${cb}`;
3562
+ } catch {}
3563
+ if (executingRowId) try {
3564
+ this.sql`DELETE FROM cf_agents_schedules WHERE id = ${executingRowId}`;
3565
+ } catch {}
3566
+ try {
3567
+ await this._cf_sealMemoryLimitedRecovery();
3568
+ } catch {}
3569
+ try {
3570
+ await this.ctx.storage.delete(key);
3571
+ } catch {}
3572
+ } else {
3573
+ const backoffSeconds = Math.min(300, 30 * strikes);
3574
+ const nextTime = Math.floor(Date.now() / 1e3) + backoffSeconds;
3575
+ for (const cb of recoveryCallbacks) try {
3576
+ this.sql`UPDATE cf_agents_schedules SET time = ${nextTime} WHERE callback = ${cb} AND time <= ${nextTime}`;
3577
+ } catch {}
3578
+ if (executingRowId) try {
3579
+ this.sql`UPDATE cf_agents_schedules SET time = ${nextTime} WHERE id = ${executingRowId} AND time <= ${nextTime}`;
3580
+ } catch {}
3581
+ }
3582
+ try {
3583
+ this._emit("alarm:memory_limit_reset", {
3584
+ strikes,
3585
+ limit,
3586
+ sealed,
3587
+ error: error instanceof Error ? error.message : String(error)
3588
+ });
3589
+ } catch {}
3590
+ try {
3591
+ await this._scheduleNextAlarm();
3592
+ } catch {}
3593
+ }
3594
+ /**
3416
3595
  * Intercept incoming HTTP/WS requests whose URL contains a
3417
3596
  * `/sub/{child-class}/{child-name}` marker and forward them to
3418
3597
  * the facet. The `onBeforeSubAgent` hook fires first (authorize,
@@ -4075,11 +4254,40 @@ var Agent = class Agent extends Server {
4075
4254
  }
4076
4255
  async onAgentToolStart(_run) {}
4077
4256
  async onAgentToolFinish(_run, _result) {}
4257
+ /**
4258
+ * Parent hook fired (best-effort) whenever a child agent-tool run emits a
4259
+ * `reportProgress` signal that is forwarded through this parent's tail. Use it
4260
+ * to meter / steer / surface progress server-side. Fires for both awaited and
4261
+ * detached runs; it is NOT durable — after eviction a detached run's latest
4262
+ * snapshot is read from `inspectAgentToolRun().progress` on reconcile instead.
4263
+ */
4264
+ async onProgress(_run, _progress) {}
4265
+ /**
4266
+ * Emit an ephemeral progress signal from a sub-agent that is currently running
4267
+ * as an agent tool. Rides the child's active turn stream as a transient
4268
+ * `data-agent-progress` part (re-broadcast to the parent's clients + surfaced
4269
+ * in `useAgentToolEvents`) and persists a latest-wins snapshot for recovery /
4270
+ * inspection. A no-op (with a dev warning) on the base `Agent`, which has no
4271
+ * streaming turn — overridden by chat hosts (`@cloudflare/think`,
4272
+ * `AIChatAgent`). See `design/rfc-detached-agent-tools.md`.
4273
+ */
4274
+ async reportProgress(_progress, _options) {
4275
+ console.warn("[agents] reportProgress() is only supported on chat agents (@cloudflare/think, AIChatAgent) running as an agent tool; ignoring on base Agent.");
4276
+ }
4078
4277
  async runAgentTool(cls, options) {
4079
4278
  const runId = options.runId ?? nanoid(12);
4080
4279
  const agentType = cls.name;
4280
+ const detached = this._parseDetachedOption(options.detached);
4081
4281
  const existing = this._readAgentToolRun(runId);
4082
4282
  if (existing) {
4283
+ if (detached) {
4284
+ if (!this._isAgentToolRowHardTerminal(existing.status)) await this._armDetachedBackbone();
4285
+ return {
4286
+ runId,
4287
+ agentType,
4288
+ status: "running"
4289
+ };
4290
+ }
4083
4291
  if (existing.status === "completed" || existing.status === "error" || existing.status === "aborted") {
4084
4292
  if (existing.status === "completed" && existing.output_json == null) try {
4085
4293
  const child = await this.subAgent(cls, runId);
@@ -4151,14 +4359,22 @@ var Agent = class Agent extends Server {
4151
4359
  error
4152
4360
  };
4153
4361
  }
4362
+ const detachedMaxBudgetAt = detached ? startedAt + (detached.maxBudgetMs ?? this._resolvedOptions.detachedMaxBudgetMs) : null;
4363
+ const detachedNoProgressBudgetMs = detached ? detached.noProgressBudgetMs ?? this._resolvedOptions.detachedNoProgressBudgetMs : null;
4364
+ const detachedOnMilestonesJson = detached?.onMilestones ? JSON.stringify(detached.onMilestones) : null;
4154
4365
  this.sql`
4155
4366
  INSERT INTO cf_agent_tool_runs (
4156
4367
  run_id, parent_tool_call_id, agent_type, input_preview,
4157
- input_redacted, status, display_metadata, display_order, started_at
4368
+ input_redacted, status, display_metadata, display_order, started_at,
4369
+ detached, detached_on_finish, detached_notify_source,
4370
+ detached_max_budget_at, detached_no_progress_budget_ms,
4371
+ detached_on_milestones
4158
4372
  ) VALUES (
4159
4373
  ${runId}, ${options.parentToolCallId ?? null}, ${agentType},
4160
4374
  ${inputPreviewJson}, 1, 'starting', ${displayJson}, ${displayOrder},
4161
- ${startedAt}
4375
+ ${startedAt}, ${detached ? 1 : 0}, ${detached?.onFinishName ?? null},
4376
+ ${detached?.notifySource ?? null}, ${detachedMaxBudgetAt},
4377
+ ${detachedNoProgressBudgetMs}, ${detachedOnMilestonesJson}
4162
4378
  )
4163
4379
  `;
4164
4380
  const runInfo = {
@@ -4168,6 +4384,7 @@ var Agent = class Agent extends Server {
4168
4384
  inputPreview,
4169
4385
  status: "starting",
4170
4386
  display: options.display,
4387
+ ...detached?.notifySource !== void 0 ? { notifySource: detached.notifySource } : {},
4171
4388
  displayOrder,
4172
4389
  startedAt
4173
4390
  };
@@ -4184,6 +4401,17 @@ var Agent = class Agent extends Server {
4184
4401
  const adapter = this._asAgentToolChildAdapter(child);
4185
4402
  const childStart = await adapter.startAgentToolRun(options.input, { runId });
4186
4403
  this._markAgentToolRunning(runId);
4404
+ if (detached) {
4405
+ if (options.signal) console.warn(`[agents] runAgentTool: \`signal\` is ignored for a detached run (${runId}); a detached child must outlive the spawning turn. Use cancelAgentTool(runId) to cancel it.`);
4406
+ await this._armDetachedBackbone({ resetCadence: true });
4407
+ this._maybeWarnDetachedLiveCount();
4408
+ this.ctx.waitUntil(this._detachedFastPath(runInfo, cls, runId));
4409
+ return {
4410
+ runId,
4411
+ agentType,
4412
+ status: "running"
4413
+ };
4414
+ }
4187
4415
  let sequence = 1;
4188
4416
  let parentAbortListener;
4189
4417
  if (options.signal) if (options.signal.aborted) {
@@ -4252,6 +4480,332 @@ var Agent = class Agent extends Server {
4252
4480
  if (parentAbortListener && options.signal) options.signal.removeEventListener("abort", parentAbortListener);
4253
4481
  }
4254
4482
  }
4483
+ /**
4484
+ * Cancel an agent-tool run by id. Idempotent: cancelling an already-terminal
4485
+ * run is a no-op. Detached runs deliver through the guarded ledger so a wired
4486
+ * `onFinish` fires once with `status: "aborted"`; awaited runs leave terminal
4487
+ * observation to the awaiting/recovery path, avoiding duplicate finish hooks.
4488
+ */
4489
+ async cancelAgentTool(runId, reason) {
4490
+ const row = this._readAgentToolRun(runId);
4491
+ if (!row) return;
4492
+ if (this._isAgentToolRowHardTerminal(row.status)) return;
4493
+ const isDetached = row.detached === 1;
4494
+ const message = reason instanceof Error ? reason.message : String(reason ?? "cancelled by parent");
4495
+ try {
4496
+ const child = await this._cf_resolveSubAgent(row.agent_type, runId);
4497
+ await this._asAgentToolChildAdapter(child).cancelAgentToolRun(runId, reason);
4498
+ } catch {}
4499
+ if (!isDetached) return;
4500
+ await this._deliverDetachedTerminal(runId, "finish", {
4501
+ runId,
4502
+ agentType: row.agent_type,
4503
+ status: "aborted",
4504
+ error: message
4505
+ });
4506
+ }
4507
+ /**
4508
+ * Parse + validate the `detached` option. Returns `null` for a non-detached
4509
+ * run, or the normalized config (with the validated `onFinish` method name)
4510
+ * for a detached one. Throws if `onFinish` does not name a method on this
4511
+ * agent — closures cannot survive Durable Object eviction, so the durable
4512
+ * hook is referenced by method name (the same contract as `schedule`).
4513
+ */
4514
+ _parseDetachedOption(detached) {
4515
+ if (!detached) return null;
4516
+ if (detached === true) return {};
4517
+ let onFinishName = detached.onFinish;
4518
+ const notifySource = typeof detached.notify === "object" ? detached.notify.source : void 0;
4519
+ if (onFinishName !== void 0) {
4520
+ if (typeof this[onFinishName] !== "function") throw new Error(`runAgentTool: detached.onFinish "${onFinishName}" is not a method on ${this.constructor.name}. Pass the NAME of a method (e.g. "onImportDone"), not a closure — closures cannot be rehydrated after the Durable Object is evicted.`);
4521
+ } else if (detached.notify) {
4522
+ if (typeof this[DETACHED_NOTIFY_CALLBACK] === "function") onFinishName = DETACHED_NOTIFY_CALLBACK;
4523
+ }
4524
+ return {
4525
+ ...onFinishName !== void 0 ? { onFinishName } : {},
4526
+ ...notifySource !== void 0 ? { notifySource } : {},
4527
+ ...detached.maxBudgetMs !== void 0 ? { maxBudgetMs: detached.maxBudgetMs } : {},
4528
+ ...detached.noProgressBudgetMs !== void 0 ? { noProgressBudgetMs: detached.noProgressBudgetMs } : {},
4529
+ ...(() => {
4530
+ const raw = detached.onMilestones;
4531
+ if (!raw) return {};
4532
+ const names = Array.isArray(raw) ? raw : raw.names;
4533
+ if (!Array.isArray(names) || names.length === 0) return {};
4534
+ return { onMilestones: {
4535
+ names,
4536
+ mode: Array.isArray(raw) ? "narrate" : raw.mode ?? "narrate"
4537
+ } };
4538
+ })()
4539
+ };
4540
+ }
4541
+ _isAgentToolRowHardTerminal(status) {
4542
+ return status === "completed" || status === "error" || status === "aborted";
4543
+ }
4544
+ _hasOutstandingDetachedRuns() {
4545
+ return (this.sql`
4546
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
4547
+ WHERE detached = 1 AND finish_delivered_at IS NULL
4548
+ `[0]?.n ?? 0) > 0;
4549
+ }
4550
+ /** Detached runs still holding a concurrency slot (non-terminal). */
4551
+ _liveDetachedRunCount() {
4552
+ return this.sql`
4553
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
4554
+ WHERE detached = 1 AND status IN ('starting', 'running')
4555
+ `[0]?.n ?? 0;
4556
+ }
4557
+ /**
4558
+ * Edge-triggered warning when live detached runs cross
4559
+ * `DETACHED_LIVE_COUNT_WARN_THRESHOLD`. Fires once on the up-crossing and
4560
+ * re-arms only after the count falls back below the threshold, so a parent
4561
+ * accumulating long-lived background runs surfaces a signal without spamming.
4562
+ */
4563
+ _maybeWarnDetachedLiveCount() {
4564
+ const liveCount = this._liveDetachedRunCount();
4565
+ if (liveCount < DETACHED_LIVE_COUNT_WARN_THRESHOLD) {
4566
+ this._detachedLiveCountWarned = false;
4567
+ return;
4568
+ }
4569
+ if (this._detachedLiveCountWarned) return;
4570
+ this._detachedLiveCountWarned = true;
4571
+ this._emit("agent_tool:detached:live_count_warning", {
4572
+ liveCount,
4573
+ threshold: DETACHED_LIVE_COUNT_WARN_THRESHOLD
4574
+ });
4575
+ console.warn(`[agents] ${liveCount} detached agent-tool runs are live on this agent (threshold ${DETACHED_LIVE_COUNT_WARN_THRESHOLD}). Detached runs hold a concurrency slot until they finish — make sure they are completing or being cancelled, or lower \`maxConcurrentAgentTools\`.`);
4576
+ }
4577
+ /**
4578
+ * Warm fast path for a detached run: tail the child to terminal (so the
4579
+ * parent re-broadcasts its live stream to clients) and deliver the completion
4580
+ * with low latency while the isolate stays alive. Best-effort — the durable
4581
+ * `_cfDetachedReconcileTick` backbone is the guarantee; anything this misses
4582
+ * (eviction, a child that has not yet reached terminal) the backbone collects.
4583
+ */
4584
+ async _detachedFastPath(runInfo, cls, runId) {
4585
+ try {
4586
+ const child = await this.subAgent(cls, runId);
4587
+ const adapter = this._asAgentToolChildAdapter(child);
4588
+ let sequence = 1;
4589
+ if (adapter.tailAgentToolRun) {
4590
+ const stream = await adapter.tailAgentToolRun(runId, { afterSequence: -1 });
4591
+ sequence = (await this._forwardAgentToolStream(stream, runInfo.parentToolCallId, runId, sequence, void 0)).next;
4592
+ }
4593
+ const inspection = await adapter.inspectAgentToolRun(runId);
4594
+ if (inspection && this._isAgentToolRowHardTerminal(inspection.status)) {
4595
+ const result = this._terminalResultFromInspection(runInfo.agentType, inspection);
4596
+ await this._deliverDetachedTerminal(runId, "finish", result, {
4597
+ sequence,
4598
+ serialize: true
4599
+ }, inspection.completedAt);
4600
+ }
4601
+ } catch {}
4602
+ }
4603
+ /**
4604
+ * Single delivery funnel for a detached terminal. Both the warm fast path and
4605
+ * the durable backbone route through here, with INDEPENDENT ledger slots for
4606
+ * `finish` (the real terminal) vs `give_up` (budget exhausted). Each slot is
4607
+ * delivered at-least-once via a claim + lease:
4608
+ *
4609
+ * - Concurrent double-fire is prevented by the guarded CAS claim (RETURNING
4610
+ * yields the row only to the winner).
4611
+ * - A crash after the side effect but before `*_delivered_at` is written lets
4612
+ * the lease expire so a later reconcile re-delivers — hence handlers must be
4613
+ * idempotent.
4614
+ * - Two slots, not one, because `interrupted` is SOFT: a give-up followed by a
4615
+ * real completion is legitimate, and a single shared "delivered" bit would
4616
+ * dedupe the child's real late result away (the #1752 production incident).
4617
+ */
4618
+ async _deliverDetachedTerminal(runId, kind, result, options, completedAt = Date.now()) {
4619
+ const now = Date.now();
4620
+ const leaseFloor = now - DETACHED_DELIVERY_LEASE_MS;
4621
+ const claimQuery = kind === "finish" ? `UPDATE cf_agent_tool_runs
4622
+ SET finish_claimed_at = ?
4623
+ WHERE run_id = ?
4624
+ AND finish_delivered_at IS NULL
4625
+ AND (finish_claimed_at IS NULL OR finish_claimed_at < ?)` : `UPDATE cf_agent_tool_runs
4626
+ SET give_up_claimed_at = ?
4627
+ WHERE run_id = ?
4628
+ AND give_up_delivered_at IS NULL
4629
+ AND (give_up_claimed_at IS NULL OR give_up_claimed_at < ?)`;
4630
+ if (this.ctx.storage.sql.exec(claimQuery, now, runId, leaseFloor).rowsWritten === 0) return;
4631
+ const row = this._readAgentToolRun(runId);
4632
+ if (!row) return;
4633
+ this._updateAgentToolTerminal(runId, result, completedAt);
4634
+ this._broadcastAgentToolTerminal(row.parent_tool_call_id ?? void 0, options?.sequence ?? Date.now(), result);
4635
+ const runInfo = this._agentToolRunInfoFromRow(row, result.status, completedAt);
4636
+ const lifecycle = {
4637
+ status: result.status,
4638
+ ...result.summary !== void 0 ? { summary: result.summary } : {},
4639
+ ...result.error !== void 0 ? { error: result.error } : {},
4640
+ ...result.reason !== void 0 ? { reason: result.reason } : {},
4641
+ ...result.childStillRunning !== void 0 ? { childStillRunning: result.childStillRunning } : {}
4642
+ };
4643
+ const invoke = async () => {
4644
+ try {
4645
+ await this.onAgentToolFinish(runInfo, lifecycle);
4646
+ } catch (error) {
4647
+ await this._safeRunOnError(error);
4648
+ }
4649
+ const callbackName = row.detached_on_finish;
4650
+ if (callbackName) {
4651
+ const callback = this[callbackName];
4652
+ if (typeof callback === "function") try {
4653
+ await callback.bind(this)(runInfo, lifecycle);
4654
+ } catch (error) {
4655
+ this._emit("agent_tool:detached:delivery_failed", {
4656
+ runId,
4657
+ kind,
4658
+ status: result.status,
4659
+ callback: callbackName,
4660
+ error: error instanceof Error ? error.message : String(error)
4661
+ });
4662
+ await this._safeRunOnError(error);
4663
+ throw error;
4664
+ }
4665
+ }
4666
+ };
4667
+ await this._runDetachedDelivery(invoke, { serialize: options?.serialize });
4668
+ if (kind === "finish") this.sql`
4669
+ UPDATE cf_agent_tool_runs
4670
+ SET finish_delivered_at = ${Date.now()}
4671
+ WHERE run_id = ${runId}
4672
+ `;
4673
+ else this.sql`
4674
+ UPDATE cf_agent_tool_runs
4675
+ SET give_up_delivered_at = ${Date.now()}
4676
+ WHERE run_id = ${runId}
4677
+ `;
4678
+ }
4679
+ async _safeRunOnError(error) {
4680
+ try {
4681
+ await this.onError(error);
4682
+ } catch {}
4683
+ }
4684
+ /**
4685
+ * Run a detached terminal delivery (the `onAgentToolFinish` + per-run
4686
+ * `onFinish` callbacks) in an appropriate execution context. The base `Agent`
4687
+ * has no turn queue, so it only establishes `agentContext` — a handler that
4688
+ * calls `runAgentTool` / `setState` therefore works regardless of where the
4689
+ * delivery fired from.
4690
+ *
4691
+ * Chat-layer subclasses (`@cloudflare/think`, `@cloudflare/ai-chat`) override
4692
+ * this to additionally serialize delivery against their turn queue when
4693
+ * `serialize` is set: a fast-path push or backbone tick can land mid-turn, and
4694
+ * a state-mutating `onFinish` running concurrently with an active LLM turn is a
4695
+ * data race. The fast path and backbone never run synchronously inside a turn
4696
+ * (they fire from `waitUntil` / a scheduled alarm), so enqueuing them on the
4697
+ * turn queue is deadlock-free. An explicit `cancelAgentTool` runs with
4698
+ * `serialize` unset because it may be called from inside the very turn that
4699
+ * triggers it, where enqueuing would self-deadlock.
4700
+ */
4701
+ async _runDetachedDelivery(invoke, _options) {
4702
+ if (__DO_NOT_USE_WILL_BREAK__agentContext.getStore()?.agent) {
4703
+ await invoke();
4704
+ return;
4705
+ }
4706
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
4707
+ agent: this,
4708
+ connection: void 0,
4709
+ request: void 0,
4710
+ email: void 0
4711
+ }, invoke);
4712
+ }
4713
+ /**
4714
+ * Arm the self-scheduling detached reconcile backbone. Existing schedules are
4715
+ * reused for recovery/startup calls, but a fresh detached dispatch resets the
4716
+ * pending cadence to the fast end so new work is noticed promptly.
4717
+ */
4718
+ async _armDetachedBackbone(options) {
4719
+ const run = this._detachedBackboneArming.then(() => this._armDetachedBackboneInner(options));
4720
+ this._detachedBackboneArming = run.then(() => void 0, () => void 0);
4721
+ return run;
4722
+ }
4723
+ async _armDetachedBackboneInner(options) {
4724
+ const armed = (await this.listSchedules()).filter((schedule) => schedule.callback === DETACHED_RECONCILE_CALLBACK);
4725
+ if (armed.length > 0 && !options?.resetCadence) {
4726
+ for (const schedule of armed.slice(1)) await this.cancelSchedule(schedule.id);
4727
+ return;
4728
+ }
4729
+ for (const schedule of armed) await this.cancelSchedule(schedule.id);
4730
+ await this.schedule(DETACHED_BACKBONE_CADENCE_S[0], DETACHED_RECONCILE_CALLBACK, { cadenceIndex: 0 });
4731
+ }
4732
+ /**
4733
+ * Durable backbone for detached runs. Runs on a self-rescheduling alarm:
4734
+ * collects any detached run that has reached terminal but was not yet
4735
+ * delivered (e.g. the parent was evicted before the fast path landed), gives
4736
+ * up on any run past its absolute budget (tearing the child down), and
4737
+ * reschedules itself while any detached run remains undelivered — cancelling
4738
+ * itself once everything has settled (zero steady-state cost).
4739
+ */
4740
+ async _cfDetachedReconcileTick(payload) {
4741
+ const rows = this.sql`
4742
+ SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4743
+ summary, output_json, error_message, interrupted_reason,
4744
+ child_still_running, display_metadata, display_order,
4745
+ started_at, completed_at, detached, detached_on_finish,
4746
+ detached_notify_source, detached_max_budget_at,
4747
+ detached_no_progress_budget_ms, last_progress_at,
4748
+ detached_on_milestones,
4749
+ finish_claimed_at, finish_delivered_at, give_up_claimed_at,
4750
+ give_up_delivered_at
4751
+ FROM cf_agent_tool_runs
4752
+ WHERE detached = 1 AND finish_delivered_at IS NULL
4753
+ ORDER BY started_at ASC
4754
+ `;
4755
+ for (const row of rows) {
4756
+ const runId = row.run_id;
4757
+ let inspection = null;
4758
+ try {
4759
+ const child = await this._cf_resolveSubAgent(row.agent_type, runId);
4760
+ inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(runId);
4761
+ } catch {}
4762
+ if (inspection?.milestones && row.detached_on_milestones) {
4763
+ const milestoneRunInfo = this._agentToolRunInfoFromRow(row);
4764
+ for (const milestone of inspection.milestones) this._maybeDeliverDetachedMilestone(row, milestoneRunInfo, milestone);
4765
+ }
4766
+ if (inspection && this._isAgentToolRowHardTerminal(inspection.status)) {
4767
+ const result = this._terminalResultFromInspection(row.agent_type, inspection);
4768
+ await this._deliverDetachedTerminal(runId, "finish", result, {
4769
+ sequence: Date.now(),
4770
+ serialize: true
4771
+ }, inspection.completedAt);
4772
+ continue;
4773
+ }
4774
+ const now = Date.now();
4775
+ const budgetAt = row.detached_max_budget_at;
4776
+ const latestMilestone = inspection?.milestones?.length ? inspection.milestones[inspection.milestones.length - 1].at : void 0;
4777
+ const signalTimes = [
4778
+ inspection?.progress?.at,
4779
+ latestMilestone,
4780
+ row.last_progress_at
4781
+ ].filter((t) => typeof t === "number");
4782
+ const lastSignalAt = signalTimes.length > 0 ? Math.max(...signalTimes) : void 0;
4783
+ const noProgressBudgetMs = row.detached_no_progress_budget_ms;
4784
+ const overAbsolute = budgetAt !== null && now >= budgetAt;
4785
+ const overNoProgress = typeof noProgressBudgetMs === "number" && noProgressBudgetMs > 0 && Number.isFinite(noProgressBudgetMs) && typeof lastSignalAt === "number" && now - lastSignalAt >= noProgressBudgetMs;
4786
+ if ((overAbsolute || overNoProgress) && row.give_up_delivered_at === null) {
4787
+ let childTornDown = false;
4788
+ try {
4789
+ const child = await this._cf_resolveSubAgent(row.agent_type, runId);
4790
+ await this._asAgentToolChildAdapter(child).cancelAgentToolRun(runId, overAbsolute ? "detached budget exceeded" : "detached run went silent past its no-progress window");
4791
+ childTornDown = true;
4792
+ } catch {}
4793
+ await this._deliverDetachedTerminal(runId, "give_up", {
4794
+ runId,
4795
+ agentType: row.agent_type,
4796
+ status: "interrupted",
4797
+ error: overAbsolute ? "detached run exceeded its budget before completing" : "detached run went silent past its no-progress window",
4798
+ reason: overAbsolute ? "budget-exceeded" : "no-progress",
4799
+ childStillRunning: !childTornDown
4800
+ }, { serialize: true });
4801
+ }
4802
+ }
4803
+ if (this._hasOutstandingDetachedRuns()) {
4804
+ const currentIndex = typeof payload?.cadenceIndex === "number" ? payload.cadenceIndex : 0;
4805
+ const nextIndex = Math.min(currentIndex + 1, DETACHED_BACKBONE_CADENCE_S.length - 1);
4806
+ await this.schedule(DETACHED_BACKBONE_CADENCE_S[nextIndex], DETACHED_RECONCILE_CALLBACK, { cadenceIndex: nextIndex });
4807
+ }
4808
+ }
4255
4809
  hasAgentToolRun(classOrName, runId) {
4256
4810
  const agentType = typeof classOrName === "string" ? classOrName : classOrName.name;
4257
4811
  return (this.sql`
@@ -4311,7 +4865,10 @@ var Agent = class Agent extends Server {
4311
4865
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4312
4866
  summary, output_json, error_message, interrupted_reason,
4313
4867
  child_still_running, display_metadata, display_order,
4314
- started_at, completed_at
4868
+ started_at, completed_at, detached, detached_on_finish,
4869
+ detached_notify_source, detached_max_budget_at,
4870
+ finish_claimed_at, finish_delivered_at, give_up_claimed_at,
4871
+ give_up_delivered_at
4315
4872
  FROM cf_agent_tool_runs
4316
4873
  WHERE run_id = ${runId}
4317
4874
  LIMIT 1
@@ -4351,6 +4908,7 @@ var Agent = class Agent extends Server {
4351
4908
  inputPreview: this._parseAgentToolJson(row.input_preview),
4352
4909
  status,
4353
4910
  display: this._parseAgentToolJson(row.display_metadata),
4911
+ ...row.detached_notify_source != null ? { notifySource: row.detached_notify_source } : {},
4354
4912
  displayOrder: row.display_order,
4355
4913
  startedAt: row.started_at,
4356
4914
  completedAt
@@ -4509,6 +5067,7 @@ var Agent = class Agent extends Server {
4509
5067
  runId,
4510
5068
  body: chunk.body
4511
5069
  });
5070
+ this._observeForwardedProgress(runId, chunk.body);
4512
5071
  forwardedSinceProgress = true;
4513
5072
  armIdle();
4514
5073
  };
@@ -4598,6 +5157,76 @@ var Agent = class Agent extends Server {
4598
5157
  * called repeatedly while a child streams.
4599
5158
  */
4600
5159
  async _onAgentToolStreamProgress() {}
5160
+ /**
5161
+ * Best-effort observation of a forwarded child chunk: if it is a reserved
5162
+ * `data-agent-progress` frame, refresh the cached liveness timestamp on the
5163
+ * run row (a hint for a still-warm parent) and fire the public `onProgress`
5164
+ * hook. Never throws into the forward loop — the child's own persisted
5165
+ * snapshot (read via `inspectAgentToolRun`) remains authoritative for the
5166
+ * resetting no-progress budget after eviction.
5167
+ */
5168
+ _observeForwardedProgress(runId, body) {
5169
+ let parsed;
5170
+ try {
5171
+ parsed = JSON.parse(body);
5172
+ } catch {
5173
+ return;
5174
+ }
5175
+ if (!parsed) return;
5176
+ const isMilestone = parsed.type === AGENT_TOOL_MILESTONE_PART;
5177
+ if (parsed.type !== "data-agent-progress" && !isMilestone) return;
5178
+ const data = parsed.data ?? {};
5179
+ const at = Date.now();
5180
+ const snapshot = {
5181
+ ...typeof data.fraction === "number" ? { fraction: data.fraction } : {},
5182
+ ...typeof data.message === "string" ? { message: data.message } : {},
5183
+ ...typeof data.phase === "string" ? { phase: data.phase } : {},
5184
+ ...isMilestone && typeof data.name === "string" ? { milestone: data.name } : {},
5185
+ ...data.data !== void 0 ? { data: data.data } : {},
5186
+ at
5187
+ };
5188
+ const row = this._readAgentToolRun(runId);
5189
+ if (!row) return;
5190
+ if (row.detached) try {
5191
+ this.sql`
5192
+ UPDATE cf_agent_tool_runs SET last_progress_at = ${at}
5193
+ WHERE run_id = ${runId}
5194
+ `;
5195
+ } catch {}
5196
+ const runInfo = this._agentToolRunInfoFromRow(row);
5197
+ Promise.resolve(this.onProgress(runInfo, snapshot)).catch((error) => {
5198
+ console.error(`[agents] onProgress hook threw for run ${runId}:`, error instanceof Error ? error.message : String(error));
5199
+ });
5200
+ if (isMilestone && typeof data.name === "string") this._maybeDeliverDetachedMilestone(row, runInfo, {
5201
+ name: data.name,
5202
+ sequence: typeof data.sequence === "number" ? data.sequence : 0,
5203
+ at: typeof data.at === "number" ? data.at : at,
5204
+ ...data.data !== void 0 ? { data: data.data } : {}
5205
+ });
5206
+ }
5207
+ /**
5208
+ * Deliver a milestone notification IF this run opted into it via
5209
+ * `detached: { onMilestones }` and the milestone name is in that set. Routes
5210
+ * to the overridable `_deliverDetachedMilestone` seam (a no-op on the base
5211
+ * `Agent`; chat hosts inject an idempotent synthetic chat message).
5212
+ */
5213
+ _maybeDeliverDetachedMilestone(row, runInfo, milestone) {
5214
+ const configured = this._parseAgentToolJson(row.detached_on_milestones ?? null);
5215
+ const names = Array.isArray(configured) ? configured : configured?.names;
5216
+ const mode = Array.isArray(configured) ? "narrate" : configured?.mode ?? "narrate";
5217
+ if (!Array.isArray(names) || !names.includes(milestone.name)) return;
5218
+ Promise.resolve(this._deliverDetachedMilestone(runInfo, milestone, mode)).catch((error) => {
5219
+ console.error(`[agents] detached milestone delivery threw for run ${runInfo.runId} (${milestone.name}):`, error instanceof Error ? error.message : String(error));
5220
+ });
5221
+ }
5222
+ /**
5223
+ * Overridable seam for the `detached: { onMilestones }` convenience. The base
5224
+ * `Agent` has no chat surface, so this is a no-op; chat hosts
5225
+ * (`@cloudflare/think`, `AIChatAgent`) override it to submit an idempotent
5226
+ * synthetic message keyed on `(runId, milestone.name)`. Called from both the
5227
+ * warm tail and the backbone reconcile, so it MUST be idempotent.
5228
+ */
5229
+ async _deliverDetachedMilestone(_run, _milestone, _mode) {}
4601
5230
  _broadcastAgentToolTerminal(parentToolCallId, sequence, result, replay, connection) {
4602
5231
  if (result.status === "completed") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
4603
5232
  kind: "finished",
@@ -4828,7 +5457,7 @@ var Agent = class Agent extends Server {
4828
5457
  child_still_running, display_metadata, display_order,
4829
5458
  started_at, completed_at
4830
5459
  FROM cf_agent_tool_runs
4831
- WHERE status IN ('starting', 'running')
5460
+ WHERE status IN ('starting', 'running') AND detached = 0
4832
5461
  ORDER BY started_at ASC
4833
5462
  `;
4834
5463
  const runIds = options?.runIds !== void 0 ? new Set(options.runIds) : void 0;
@@ -4967,6 +5596,7 @@ var Agent = class Agent extends Server {
4967
5596
  runIds: options?.runIds
4968
5597
  });
4969
5598
  await this._runDeferredAgentToolFinishHooks(recoveredAgentToolFinishes);
5599
+ if (this._hasOutstandingDetachedRuns()) await this._armDetachedBackbone();
4970
5600
  })().catch(async (error) => {
4971
5601
  this._emit("agent_tool:recovery:failed", { error: error instanceof Error ? error.message : String(error) });
4972
5602
  try {
@@ -5305,10 +5935,29 @@ var Agent = class Agent extends Server {
5305
5935
  * Start a workflow and track it in this Agent's database.
5306
5936
  * Automatically injects agent identity into the workflow params.
5307
5937
  *
5938
+ * The originating Agent identity is persisted in the workflow params so
5939
+ * callbacks (`this.agent` RPC, progress/completion/error, state updates)
5940
+ * route back to the exact Agent or sub-agent facet that started the run.
5941
+ * Note the following constraints:
5942
+ *
5943
+ * - **Resolution is by name.** Callbacks re-resolve the originating Agent via
5944
+ * `getAgentByName(...)`. Agents addressed by a raw Durable Object id
5945
+ * (`idFromString`/`get(id)`) rather than by name will not receive
5946
+ * callbacks on the same instance.
5947
+ * - **Sub-agent runs are facet-local.** A workflow started from a sub-agent
5948
+ * is tracked in that facet's own storage; the parent's `getWorkflows()` /
5949
+ * `getWorkflowById()` do not see it. Aggregate across facets yourself if
5950
+ * you need a combined view.
5951
+ * - **Class names must survive bundling.** The originating path is keyed by
5952
+ * `constructor.name`. Ensure your bundler preserves class names
5953
+ * (e.g. esbuild `keepNames: true`) so callbacks can be routed.
5954
+ *
5308
5955
  * @template P - Type of params to pass to the workflow
5309
5956
  * @param workflowName - Name of the workflow binding in env (e.g., 'MY_WORKFLOW')
5310
5957
  * @param params - Params to pass to the workflow
5311
- * @param options - Optional workflow options
5958
+ * @param options - Optional workflow options. For sub-agents, pass
5959
+ * `agentBinding` as the **root** Agent's Durable Object binding name, not a
5960
+ * child binding.
5312
5961
  * @returns The workflow instance ID
5313
5962
  *
5314
5963
  * @example
@@ -5322,14 +5971,15 @@ var Agent = class Agent extends Server {
5322
5971
  async runWorkflow(workflowName, params, options) {
5323
5972
  const workflow = this._findWorkflowBindingByName(workflowName);
5324
5973
  if (!workflow) throw new Error(`Workflow binding '${workflowName}' not found in environment`);
5325
- const agentBindingName = options?.agentBinding ?? this._findAgentBindingName();
5326
- if (!agentBindingName) throw new Error("Could not detect Agent binding name from class name. Pass it explicitly via options.agentBinding");
5974
+ const agentOrigin = this._workflowOrigin(options);
5975
+ if (!agentOrigin) throw new Error("Could not detect Agent binding name from class name. Pass it explicitly via options.agentBinding");
5327
5976
  const workflowId = options?.id ?? `wf_${nanoid()}`;
5328
5977
  const augmentedParams = {
5329
5978
  ...params,
5330
5979
  __agentName: this.name,
5331
- __agentBinding: agentBindingName,
5332
- __workflowName: workflowName
5980
+ __agentBinding: agentOrigin.kind === "agent" ? agentOrigin.binding : agentOrigin.rootBinding,
5981
+ __workflowName: workflowName,
5982
+ __agentOrigin: agentOrigin
5333
5983
  };
5334
5984
  const instance = await workflow.create({
5335
5985
  id: workflowId,
@@ -5871,12 +6521,28 @@ var Agent = class Agent extends Server {
5871
6521
  completedAt: row.completed_at ? /* @__PURE__ */ new Date(row.completed_at * 1e3) : null
5872
6522
  };
5873
6523
  }
5874
- /**
5875
- * Find the binding name for this Agent's namespace by matching class name.
5876
- * Returns undefined if no match found - use options.agentBinding as fallback.
5877
- */
5878
- _findAgentBindingName() {
5879
- const className = this._ParentClass.name;
6524
+ _workflowOrigin(options) {
6525
+ if (this._isFacet) {
6526
+ const root = this._parentPath[0];
6527
+ const rootBindingName = options?.agentBinding ?? (root ? this._findAgentBindingNameForClass(root.className) : void 0);
6528
+ if (!rootBindingName) return void 0;
6529
+ return {
6530
+ kind: "facet",
6531
+ version: 1,
6532
+ rootBinding: rootBindingName,
6533
+ path: this.selfPath.map((step) => ({ ...step }))
6534
+ };
6535
+ }
6536
+ const agentBindingName = options?.agentBinding ?? this._findAgentBindingNameForClass(this._ParentClass.name);
6537
+ if (!agentBindingName) return void 0;
6538
+ return {
6539
+ kind: "agent",
6540
+ version: 1,
6541
+ binding: agentBindingName,
6542
+ name: this.name
6543
+ };
6544
+ }
6545
+ _findAgentBindingNameForClass(className) {
5880
6546
  for (const [key, value] of Object.entries(this.env)) if (value && typeof value === "object" && "idFromName" in value && typeof value.idFromName === "function") {
5881
6547
  if (key === className || camelCaseToKebabCase(key) === camelCaseToKebabCase(className)) return key;
5882
6548
  }
@@ -5914,7 +6580,9 @@ var Agent = class Agent extends Server {
5914
6580
  }
5915
6581
  /**
5916
6582
  * Handle a callback from a workflow.
5917
- * Called when the Agent receives a callback at /_workflow/callback.
6583
+ * Invoked via the internal `_workflow_handleCallback` RPC whenever an
6584
+ * {@link AgentWorkflow} reports progress, completion, an error, or a custom
6585
+ * event back to its originating Agent (or sub-agent facet).
5918
6586
  * Override this to handle all callback types in one place.
5919
6587
  *
5920
6588
  * @param callback - The callback payload
@@ -6270,6 +6938,7 @@ var Agent = class Agent extends Server {
6270
6938
  }
6271
6939
  };
6272
6940
  Agent.options = { hibernate: true };
6941
+ Agent._CF_OOM_ALARM_STRIKES_KEY = "cf_agents:oom_alarm_strikes";
6273
6942
  const wrappedClasses = /* @__PURE__ */ new Set();
6274
6943
  /**
6275
6944
  * Route a request to the appropriate Agent
@@ -6448,6 +7117,6 @@ var StreamingResponse = class {
6448
7117
  }
6449
7118
  };
6450
7119
  //#endregion
6451
- export { Agent, DEFAULT_AGENT_STATIC_OPTIONS, DurableObjectOAuthClientProvider, MCP_SERVER_ID_MAX_LENGTH, MessageType, SUB_PREFIX, SqlError, StreamingResponse, __DO_NOT_USE_WILL_BREAK__agentContext, callable, camelCaseToKebabCase, createHeaderBasedEmailResolver, getAgentByName, getCurrentAgent, getSubAgentByName, isDurableObjectCodeUpdateReset, isPlatformTransientError, normalizeServerId, parseSubAgentPath, routeAgentEmail, routeAgentRequest, routeSubAgentRequest, unstable_callable };
7120
+ export { AGENT_TOOL_MILESTONE_PART, AGENT_TOOL_PROGRESS_PART, Agent, DEFAULT_AGENT_STATIC_OPTIONS, DurableObjectOAuthClientProvider, MCP_SERVER_ID_MAX_LENGTH, MessageType, SUB_PREFIX, SqlError, StreamingResponse, __DO_NOT_USE_WILL_BREAK__agentContext, callable, camelCaseToKebabCase, createHeaderBasedEmailResolver, getAgentByName, getCurrentAgent, getSubAgentByName, isDurableObjectCodeUpdateReset, isDurableObjectMemoryLimitReset, isPlatformTransientError, normalizeServerId, parseSubAgentPath, routeAgentEmail, routeAgentRequest, routeSubAgentRequest, unstable_callable };
6452
7121
 
6453
7122
  //# sourceMappingURL=index.js.map