agents 0.16.2 → 0.17.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 (122) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
  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-_E8wxUIK.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 +1918 -72
  26. package/dist/chat/index.js +1730 -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-CcbnKkNh.d.ts} +188 -14
  45. package/dist/index.d.ts +91 -71
  46. package/dist/index.js +562 -24
  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/serializable.d.ts +1 -1
  62. package/dist/skills/compile.js +1 -1
  63. package/dist/skills/compile.js.map +1 -1
  64. package/dist/skills/index.js +5 -5
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/sub-routing.d.ts +6 -6
  67. package/dist/utils.js +1 -1
  68. package/dist/utils.js.map +1 -1
  69. package/dist/vite.js +5 -5
  70. package/dist/vite.js.map +1 -1
  71. package/dist/wire-types-nflOzNuU.js +240 -0
  72. package/dist/wire-types-nflOzNuU.js.map +1 -0
  73. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  74. package/dist/workflow-types.d.ts +25 -21
  75. package/dist/workflow-types.js.map +1 -1
  76. package/dist/workflows.d.ts +22 -21
  77. package/dist/workflows.js +31 -7
  78. package/dist/workflows.js.map +1 -1
  79. package/docs/adding-to-existing-project.md +450 -0
  80. package/docs/agent-class.md +503 -0
  81. package/docs/agent-tools.md +552 -0
  82. package/docs/browse-the-web.md +430 -0
  83. package/docs/callable-methods.md +627 -0
  84. package/docs/chat-agents.md +1687 -0
  85. package/docs/chat-sdk.md +181 -0
  86. package/docs/client-sdk.md +520 -0
  87. package/docs/client-tools-continuation.md +177 -0
  88. package/docs/codemode.md +440 -0
  89. package/docs/configuration.md +775 -0
  90. package/docs/cross-domain-authentication.md +171 -0
  91. package/docs/durable-execution.md +537 -0
  92. package/docs/email.md +663 -0
  93. package/docs/get-current-agent.md +204 -0
  94. package/docs/getting-started.md +305 -0
  95. package/docs/http-websockets.md +668 -0
  96. package/docs/human-in-the-loop.md +661 -0
  97. package/docs/index.md +151 -0
  98. package/docs/long-running-agents.md +730 -0
  99. package/docs/mcp-client.md +620 -0
  100. package/docs/mcp-servers.md +526 -0
  101. package/docs/mcp-transports.md +308 -0
  102. package/docs/migration-to-ai-sdk-v5.md +96 -0
  103. package/docs/migration-to-ai-sdk-v6.md +163 -0
  104. package/docs/observability.md +261 -0
  105. package/docs/push-notifications.md +367 -0
  106. package/docs/queue.md +329 -0
  107. package/docs/readonly-connections.md +278 -0
  108. package/docs/resumable-streaming.md +127 -0
  109. package/docs/retries.md +444 -0
  110. package/docs/routing.md +749 -0
  111. package/docs/scheduling.md +898 -0
  112. package/docs/securing-mcp-servers.md +359 -0
  113. package/docs/server-driven-messages.md +477 -0
  114. package/docs/sessions.md +1024 -0
  115. package/docs/state.md +512 -0
  116. package/docs/sub-agents.md +389 -0
  117. package/docs/webhooks.md +604 -0
  118. package/docs/workflows.md +877 -0
  119. package/package.json +41 -14
  120. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  121. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  122. 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
8
  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";
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,9 @@ 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
316
331
  };
317
332
  /**
318
333
  * Parse the raw `retry_options` TEXT column from a SQLite row into a
@@ -437,7 +452,9 @@ var Agent = class Agent extends Server {
437
452
  fiberRecoveryScanDeadlineMs: ctor.options?.fiberRecoveryScanDeadlineMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryScanDeadlineMs,
438
453
  fiberRecoveryMaxAgeMs: ctor.options?.fiberRecoveryMaxAgeMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryMaxAgeMs,
439
454
  agentToolReattachNoProgressTimeoutMs: ctor.options?.agentToolReattachNoProgressTimeoutMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachNoProgressTimeoutMs,
440
- agentToolReattachMaxWindowMs: ctor.options?.agentToolReattachMaxWindowMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachMaxWindowMs
455
+ agentToolReattachMaxWindowMs: ctor.options?.agentToolReattachMaxWindowMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachMaxWindowMs,
456
+ detachedMaxBudgetMs: ctor.options?.detachedMaxBudgetMs ?? DEFAULT_AGENT_STATIC_OPTIONS.detachedMaxBudgetMs,
457
+ detachedNoProgressBudgetMs: ctor.options?.detachedNoProgressBudgetMs ?? DEFAULT_AGENT_STATIC_OPTIONS.detachedNoProgressBudgetMs
441
458
  };
442
459
  return this._cachedOptions;
443
460
  }
@@ -679,6 +696,17 @@ var Agent = class Agent extends Server {
679
696
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN output_json TEXT");
680
697
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN interrupted_reason TEXT");
681
698
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN child_still_running INTEGER");
699
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached INTEGER NOT NULL DEFAULT 0");
700
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_on_finish TEXT");
701
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_notify_source TEXT");
702
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_max_budget_at INTEGER");
703
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN finish_claimed_at INTEGER");
704
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN finish_delivered_at INTEGER");
705
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN give_up_claimed_at INTEGER");
706
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN give_up_delivered_at INTEGER");
707
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_no_progress_budget_ms INTEGER");
708
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN last_progress_at INTEGER");
709
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN detached_on_milestones TEXT");
682
710
  this.sql`
683
711
  INSERT OR REPLACE INTO cf_agents_state (id, state)
684
712
  VALUES (${SCHEMA_VERSION_ROW_ID}, ${String(CURRENT_SCHEMA_VERSION)})
@@ -707,6 +735,8 @@ var Agent = class Agent extends Server {
707
735
  this._managedFiberTerminalWaiters = /* @__PURE__ */ new Map();
708
736
  this._runFiberRecoveryInProgress = false;
709
737
  this._recoveryNoProgressScans = 0;
738
+ this._detachedBackboneArming = Promise.resolve();
739
+ this._detachedLiveCountWarned = false;
710
740
  this._ParentClass = Object.getPrototypeOf(this).constructor;
711
741
  this.initialState = DEFAULT_STATE;
712
742
  this.observability = genericObservability;
@@ -2641,7 +2671,7 @@ var Agent = class Agent extends Server {
2641
2671
  return true;
2642
2672
  }
2643
2673
  async deleteFibers(options) {
2644
- const terminalStatuses = [...this._normalizeFiberStatusFilter(options?.status) ?? new Set([
2674
+ const terminalStatuses = [...this._normalizeFiberStatusFilter(options?.status) ?? /* @__PURE__ */ new Set([
2645
2675
  "completed",
2646
2676
  "aborted",
2647
2677
  "error"
@@ -3138,6 +3168,25 @@ var Agent = class Agent extends Server {
3138
3168
  return (await this._cf_resolveSubAgent(next.className, next.name))._cf_dispatchScheduledCallback(ownerPath, row);
3139
3169
  }
3140
3170
  /**
3171
+ * Invoke an RPC method on this Agent or a descendant facet identified
3172
+ * by a root-first path. Used by AgentWorkflow to route callbacks and
3173
+ * `this.agent` calls back to the exact sub-agent that started a workflow.
3174
+ * @internal
3175
+ */
3176
+ async _cf_invokeAgentPath(targetPath, method, args) {
3177
+ await this.__unsafe_ensureInitialized();
3178
+ const selfPath = this.selfPath;
3179
+ if (!this._isSameAgentPathPrefix(selfPath, targetPath)) throw new Error(`Workflow origin path does not descend from ${JSON.stringify(selfPath)}.`);
3180
+ if (selfPath.length === targetPath.length) {
3181
+ const fn = this[method];
3182
+ if (isInternalJsStubProp(method) || method in Object.prototype || typeof fn !== "function") throw new Error(`Workflow origin method "${method}" is not callable on ${this.constructor.name}.`);
3183
+ return await fn.apply(this, args);
3184
+ }
3185
+ const next = targetPath[selfPath.length];
3186
+ if (!this.hasSubAgent(next.className, next.name)) throw new Error(`Workflow origin sub-agent ${next.className} "${next.name}" no longer exists.`);
3187
+ return await (await this._cf_resolveSubAgent(next.className, next.name))._cf_invokeAgentPath(targetPath, method, args);
3188
+ }
3189
+ /**
3141
3190
  * Recursively destroy a descendant facet identified by
3142
3191
  * `targetPath`. Walks down from `selfPath` until reaching the
3143
3192
  * target's immediate parent, where it cancels the target's
@@ -4075,11 +4124,40 @@ var Agent = class Agent extends Server {
4075
4124
  }
4076
4125
  async onAgentToolStart(_run) {}
4077
4126
  async onAgentToolFinish(_run, _result) {}
4127
+ /**
4128
+ * Parent hook fired (best-effort) whenever a child agent-tool run emits a
4129
+ * `reportProgress` signal that is forwarded through this parent's tail. Use it
4130
+ * to meter / steer / surface progress server-side. Fires for both awaited and
4131
+ * detached runs; it is NOT durable — after eviction a detached run's latest
4132
+ * snapshot is read from `inspectAgentToolRun().progress` on reconcile instead.
4133
+ */
4134
+ async onProgress(_run, _progress) {}
4135
+ /**
4136
+ * Emit an ephemeral progress signal from a sub-agent that is currently running
4137
+ * as an agent tool. Rides the child's active turn stream as a transient
4138
+ * `data-agent-progress` part (re-broadcast to the parent's clients + surfaced
4139
+ * in `useAgentToolEvents`) and persists a latest-wins snapshot for recovery /
4140
+ * inspection. A no-op (with a dev warning) on the base `Agent`, which has no
4141
+ * streaming turn — overridden by chat hosts (`@cloudflare/think`,
4142
+ * `AIChatAgent`). See `design/rfc-detached-agent-tools.md`.
4143
+ */
4144
+ async reportProgress(_progress, _options) {
4145
+ console.warn("[agents] reportProgress() is only supported on chat agents (@cloudflare/think, AIChatAgent) running as an agent tool; ignoring on base Agent.");
4146
+ }
4078
4147
  async runAgentTool(cls, options) {
4079
4148
  const runId = options.runId ?? nanoid(12);
4080
4149
  const agentType = cls.name;
4150
+ const detached = this._parseDetachedOption(options.detached);
4081
4151
  const existing = this._readAgentToolRun(runId);
4082
4152
  if (existing) {
4153
+ if (detached) {
4154
+ if (!this._isAgentToolRowHardTerminal(existing.status)) await this._armDetachedBackbone();
4155
+ return {
4156
+ runId,
4157
+ agentType,
4158
+ status: "running"
4159
+ };
4160
+ }
4083
4161
  if (existing.status === "completed" || existing.status === "error" || existing.status === "aborted") {
4084
4162
  if (existing.status === "completed" && existing.output_json == null) try {
4085
4163
  const child = await this.subAgent(cls, runId);
@@ -4151,14 +4229,22 @@ var Agent = class Agent extends Server {
4151
4229
  error
4152
4230
  };
4153
4231
  }
4232
+ const detachedMaxBudgetAt = detached ? startedAt + (detached.maxBudgetMs ?? this._resolvedOptions.detachedMaxBudgetMs) : null;
4233
+ const detachedNoProgressBudgetMs = detached ? detached.noProgressBudgetMs ?? this._resolvedOptions.detachedNoProgressBudgetMs : null;
4234
+ const detachedOnMilestonesJson = detached?.onMilestones ? JSON.stringify(detached.onMilestones) : null;
4154
4235
  this.sql`
4155
4236
  INSERT INTO cf_agent_tool_runs (
4156
4237
  run_id, parent_tool_call_id, agent_type, input_preview,
4157
- input_redacted, status, display_metadata, display_order, started_at
4238
+ input_redacted, status, display_metadata, display_order, started_at,
4239
+ detached, detached_on_finish, detached_notify_source,
4240
+ detached_max_budget_at, detached_no_progress_budget_ms,
4241
+ detached_on_milestones
4158
4242
  ) VALUES (
4159
4243
  ${runId}, ${options.parentToolCallId ?? null}, ${agentType},
4160
4244
  ${inputPreviewJson}, 1, 'starting', ${displayJson}, ${displayOrder},
4161
- ${startedAt}
4245
+ ${startedAt}, ${detached ? 1 : 0}, ${detached?.onFinishName ?? null},
4246
+ ${detached?.notifySource ?? null}, ${detachedMaxBudgetAt},
4247
+ ${detachedNoProgressBudgetMs}, ${detachedOnMilestonesJson}
4162
4248
  )
4163
4249
  `;
4164
4250
  const runInfo = {
@@ -4168,6 +4254,7 @@ var Agent = class Agent extends Server {
4168
4254
  inputPreview,
4169
4255
  status: "starting",
4170
4256
  display: options.display,
4257
+ ...detached?.notifySource !== void 0 ? { notifySource: detached.notifySource } : {},
4171
4258
  displayOrder,
4172
4259
  startedAt
4173
4260
  };
@@ -4184,6 +4271,17 @@ var Agent = class Agent extends Server {
4184
4271
  const adapter = this._asAgentToolChildAdapter(child);
4185
4272
  const childStart = await adapter.startAgentToolRun(options.input, { runId });
4186
4273
  this._markAgentToolRunning(runId);
4274
+ if (detached) {
4275
+ 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.`);
4276
+ await this._armDetachedBackbone({ resetCadence: true });
4277
+ this._maybeWarnDetachedLiveCount();
4278
+ this.ctx.waitUntil(this._detachedFastPath(runInfo, cls, runId));
4279
+ return {
4280
+ runId,
4281
+ agentType,
4282
+ status: "running"
4283
+ };
4284
+ }
4187
4285
  let sequence = 1;
4188
4286
  let parentAbortListener;
4189
4287
  if (options.signal) if (options.signal.aborted) {
@@ -4252,6 +4350,332 @@ var Agent = class Agent extends Server {
4252
4350
  if (parentAbortListener && options.signal) options.signal.removeEventListener("abort", parentAbortListener);
4253
4351
  }
4254
4352
  }
4353
+ /**
4354
+ * Cancel an agent-tool run by id. Idempotent: cancelling an already-terminal
4355
+ * run is a no-op. Detached runs deliver through the guarded ledger so a wired
4356
+ * `onFinish` fires once with `status: "aborted"`; awaited runs leave terminal
4357
+ * observation to the awaiting/recovery path, avoiding duplicate finish hooks.
4358
+ */
4359
+ async cancelAgentTool(runId, reason) {
4360
+ const row = this._readAgentToolRun(runId);
4361
+ if (!row) return;
4362
+ if (this._isAgentToolRowHardTerminal(row.status)) return;
4363
+ const isDetached = row.detached === 1;
4364
+ const message = reason instanceof Error ? reason.message : String(reason ?? "cancelled by parent");
4365
+ try {
4366
+ const child = await this._cf_resolveSubAgent(row.agent_type, runId);
4367
+ await this._asAgentToolChildAdapter(child).cancelAgentToolRun(runId, reason);
4368
+ } catch {}
4369
+ if (!isDetached) return;
4370
+ await this._deliverDetachedTerminal(runId, "finish", {
4371
+ runId,
4372
+ agentType: row.agent_type,
4373
+ status: "aborted",
4374
+ error: message
4375
+ });
4376
+ }
4377
+ /**
4378
+ * Parse + validate the `detached` option. Returns `null` for a non-detached
4379
+ * run, or the normalized config (with the validated `onFinish` method name)
4380
+ * for a detached one. Throws if `onFinish` does not name a method on this
4381
+ * agent — closures cannot survive Durable Object eviction, so the durable
4382
+ * hook is referenced by method name (the same contract as `schedule`).
4383
+ */
4384
+ _parseDetachedOption(detached) {
4385
+ if (!detached) return null;
4386
+ if (detached === true) return {};
4387
+ let onFinishName = detached.onFinish;
4388
+ const notifySource = typeof detached.notify === "object" ? detached.notify.source : void 0;
4389
+ if (onFinishName !== void 0) {
4390
+ 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.`);
4391
+ } else if (detached.notify) {
4392
+ if (typeof this[DETACHED_NOTIFY_CALLBACK] === "function") onFinishName = DETACHED_NOTIFY_CALLBACK;
4393
+ }
4394
+ return {
4395
+ ...onFinishName !== void 0 ? { onFinishName } : {},
4396
+ ...notifySource !== void 0 ? { notifySource } : {},
4397
+ ...detached.maxBudgetMs !== void 0 ? { maxBudgetMs: detached.maxBudgetMs } : {},
4398
+ ...detached.noProgressBudgetMs !== void 0 ? { noProgressBudgetMs: detached.noProgressBudgetMs } : {},
4399
+ ...(() => {
4400
+ const raw = detached.onMilestones;
4401
+ if (!raw) return {};
4402
+ const names = Array.isArray(raw) ? raw : raw.names;
4403
+ if (!Array.isArray(names) || names.length === 0) return {};
4404
+ return { onMilestones: {
4405
+ names,
4406
+ mode: Array.isArray(raw) ? "narrate" : raw.mode ?? "narrate"
4407
+ } };
4408
+ })()
4409
+ };
4410
+ }
4411
+ _isAgentToolRowHardTerminal(status) {
4412
+ return status === "completed" || status === "error" || status === "aborted";
4413
+ }
4414
+ _hasOutstandingDetachedRuns() {
4415
+ return (this.sql`
4416
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
4417
+ WHERE detached = 1 AND finish_delivered_at IS NULL
4418
+ `[0]?.n ?? 0) > 0;
4419
+ }
4420
+ /** Detached runs still holding a concurrency slot (non-terminal). */
4421
+ _liveDetachedRunCount() {
4422
+ return this.sql`
4423
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
4424
+ WHERE detached = 1 AND status IN ('starting', 'running')
4425
+ `[0]?.n ?? 0;
4426
+ }
4427
+ /**
4428
+ * Edge-triggered warning when live detached runs cross
4429
+ * `DETACHED_LIVE_COUNT_WARN_THRESHOLD`. Fires once on the up-crossing and
4430
+ * re-arms only after the count falls back below the threshold, so a parent
4431
+ * accumulating long-lived background runs surfaces a signal without spamming.
4432
+ */
4433
+ _maybeWarnDetachedLiveCount() {
4434
+ const liveCount = this._liveDetachedRunCount();
4435
+ if (liveCount < DETACHED_LIVE_COUNT_WARN_THRESHOLD) {
4436
+ this._detachedLiveCountWarned = false;
4437
+ return;
4438
+ }
4439
+ if (this._detachedLiveCountWarned) return;
4440
+ this._detachedLiveCountWarned = true;
4441
+ this._emit("agent_tool:detached:live_count_warning", {
4442
+ liveCount,
4443
+ threshold: DETACHED_LIVE_COUNT_WARN_THRESHOLD
4444
+ });
4445
+ 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\`.`);
4446
+ }
4447
+ /**
4448
+ * Warm fast path for a detached run: tail the child to terminal (so the
4449
+ * parent re-broadcasts its live stream to clients) and deliver the completion
4450
+ * with low latency while the isolate stays alive. Best-effort — the durable
4451
+ * `_cfDetachedReconcileTick` backbone is the guarantee; anything this misses
4452
+ * (eviction, a child that has not yet reached terminal) the backbone collects.
4453
+ */
4454
+ async _detachedFastPath(runInfo, cls, runId) {
4455
+ try {
4456
+ const child = await this.subAgent(cls, runId);
4457
+ const adapter = this._asAgentToolChildAdapter(child);
4458
+ let sequence = 1;
4459
+ if (adapter.tailAgentToolRun) {
4460
+ const stream = await adapter.tailAgentToolRun(runId, { afterSequence: -1 });
4461
+ sequence = (await this._forwardAgentToolStream(stream, runInfo.parentToolCallId, runId, sequence, void 0)).next;
4462
+ }
4463
+ const inspection = await adapter.inspectAgentToolRun(runId);
4464
+ if (inspection && this._isAgentToolRowHardTerminal(inspection.status)) {
4465
+ const result = this._terminalResultFromInspection(runInfo.agentType, inspection);
4466
+ await this._deliverDetachedTerminal(runId, "finish", result, {
4467
+ sequence,
4468
+ serialize: true
4469
+ }, inspection.completedAt);
4470
+ }
4471
+ } catch {}
4472
+ }
4473
+ /**
4474
+ * Single delivery funnel for a detached terminal. Both the warm fast path and
4475
+ * the durable backbone route through here, with INDEPENDENT ledger slots for
4476
+ * `finish` (the real terminal) vs `give_up` (budget exhausted). Each slot is
4477
+ * delivered at-least-once via a claim + lease:
4478
+ *
4479
+ * - Concurrent double-fire is prevented by the guarded CAS claim (RETURNING
4480
+ * yields the row only to the winner).
4481
+ * - A crash after the side effect but before `*_delivered_at` is written lets
4482
+ * the lease expire so a later reconcile re-delivers — hence handlers must be
4483
+ * idempotent.
4484
+ * - Two slots, not one, because `interrupted` is SOFT: a give-up followed by a
4485
+ * real completion is legitimate, and a single shared "delivered" bit would
4486
+ * dedupe the child's real late result away (the #1752 production incident).
4487
+ */
4488
+ async _deliverDetachedTerminal(runId, kind, result, options, completedAt = Date.now()) {
4489
+ const now = Date.now();
4490
+ const leaseFloor = now - DETACHED_DELIVERY_LEASE_MS;
4491
+ const claimQuery = kind === "finish" ? `UPDATE cf_agent_tool_runs
4492
+ SET finish_claimed_at = ?
4493
+ WHERE run_id = ?
4494
+ AND finish_delivered_at IS NULL
4495
+ AND (finish_claimed_at IS NULL OR finish_claimed_at < ?)` : `UPDATE cf_agent_tool_runs
4496
+ SET give_up_claimed_at = ?
4497
+ WHERE run_id = ?
4498
+ AND give_up_delivered_at IS NULL
4499
+ AND (give_up_claimed_at IS NULL OR give_up_claimed_at < ?)`;
4500
+ if (this.ctx.storage.sql.exec(claimQuery, now, runId, leaseFloor).rowsWritten === 0) return;
4501
+ const row = this._readAgentToolRun(runId);
4502
+ if (!row) return;
4503
+ this._updateAgentToolTerminal(runId, result, completedAt);
4504
+ this._broadcastAgentToolTerminal(row.parent_tool_call_id ?? void 0, options?.sequence ?? Date.now(), result);
4505
+ const runInfo = this._agentToolRunInfoFromRow(row, result.status, completedAt);
4506
+ const lifecycle = {
4507
+ status: result.status,
4508
+ ...result.summary !== void 0 ? { summary: result.summary } : {},
4509
+ ...result.error !== void 0 ? { error: result.error } : {},
4510
+ ...result.reason !== void 0 ? { reason: result.reason } : {},
4511
+ ...result.childStillRunning !== void 0 ? { childStillRunning: result.childStillRunning } : {}
4512
+ };
4513
+ const invoke = async () => {
4514
+ try {
4515
+ await this.onAgentToolFinish(runInfo, lifecycle);
4516
+ } catch (error) {
4517
+ await this._safeRunOnError(error);
4518
+ }
4519
+ const callbackName = row.detached_on_finish;
4520
+ if (callbackName) {
4521
+ const callback = this[callbackName];
4522
+ if (typeof callback === "function") try {
4523
+ await callback.bind(this)(runInfo, lifecycle);
4524
+ } catch (error) {
4525
+ this._emit("agent_tool:detached:delivery_failed", {
4526
+ runId,
4527
+ kind,
4528
+ status: result.status,
4529
+ callback: callbackName,
4530
+ error: error instanceof Error ? error.message : String(error)
4531
+ });
4532
+ await this._safeRunOnError(error);
4533
+ throw error;
4534
+ }
4535
+ }
4536
+ };
4537
+ await this._runDetachedDelivery(invoke, { serialize: options?.serialize });
4538
+ if (kind === "finish") this.sql`
4539
+ UPDATE cf_agent_tool_runs
4540
+ SET finish_delivered_at = ${Date.now()}
4541
+ WHERE run_id = ${runId}
4542
+ `;
4543
+ else this.sql`
4544
+ UPDATE cf_agent_tool_runs
4545
+ SET give_up_delivered_at = ${Date.now()}
4546
+ WHERE run_id = ${runId}
4547
+ `;
4548
+ }
4549
+ async _safeRunOnError(error) {
4550
+ try {
4551
+ await this.onError(error);
4552
+ } catch {}
4553
+ }
4554
+ /**
4555
+ * Run a detached terminal delivery (the `onAgentToolFinish` + per-run
4556
+ * `onFinish` callbacks) in an appropriate execution context. The base `Agent`
4557
+ * has no turn queue, so it only establishes `agentContext` — a handler that
4558
+ * calls `runAgentTool` / `setState` therefore works regardless of where the
4559
+ * delivery fired from.
4560
+ *
4561
+ * Chat-layer subclasses (`@cloudflare/think`, `@cloudflare/ai-chat`) override
4562
+ * this to additionally serialize delivery against their turn queue when
4563
+ * `serialize` is set: a fast-path push or backbone tick can land mid-turn, and
4564
+ * a state-mutating `onFinish` running concurrently with an active LLM turn is a
4565
+ * data race. The fast path and backbone never run synchronously inside a turn
4566
+ * (they fire from `waitUntil` / a scheduled alarm), so enqueuing them on the
4567
+ * turn queue is deadlock-free. An explicit `cancelAgentTool` runs with
4568
+ * `serialize` unset because it may be called from inside the very turn that
4569
+ * triggers it, where enqueuing would self-deadlock.
4570
+ */
4571
+ async _runDetachedDelivery(invoke, _options) {
4572
+ if (__DO_NOT_USE_WILL_BREAK__agentContext.getStore()?.agent) {
4573
+ await invoke();
4574
+ return;
4575
+ }
4576
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
4577
+ agent: this,
4578
+ connection: void 0,
4579
+ request: void 0,
4580
+ email: void 0
4581
+ }, invoke);
4582
+ }
4583
+ /**
4584
+ * Arm the self-scheduling detached reconcile backbone. Existing schedules are
4585
+ * reused for recovery/startup calls, but a fresh detached dispatch resets the
4586
+ * pending cadence to the fast end so new work is noticed promptly.
4587
+ */
4588
+ async _armDetachedBackbone(options) {
4589
+ const run = this._detachedBackboneArming.then(() => this._armDetachedBackboneInner(options));
4590
+ this._detachedBackboneArming = run.then(() => void 0, () => void 0);
4591
+ return run;
4592
+ }
4593
+ async _armDetachedBackboneInner(options) {
4594
+ const armed = (await this.listSchedules()).filter((schedule) => schedule.callback === DETACHED_RECONCILE_CALLBACK);
4595
+ if (armed.length > 0 && !options?.resetCadence) {
4596
+ for (const schedule of armed.slice(1)) await this.cancelSchedule(schedule.id);
4597
+ return;
4598
+ }
4599
+ for (const schedule of armed) await this.cancelSchedule(schedule.id);
4600
+ await this.schedule(DETACHED_BACKBONE_CADENCE_S[0], DETACHED_RECONCILE_CALLBACK, { cadenceIndex: 0 });
4601
+ }
4602
+ /**
4603
+ * Durable backbone for detached runs. Runs on a self-rescheduling alarm:
4604
+ * collects any detached run that has reached terminal but was not yet
4605
+ * delivered (e.g. the parent was evicted before the fast path landed), gives
4606
+ * up on any run past its absolute budget (tearing the child down), and
4607
+ * reschedules itself while any detached run remains undelivered — cancelling
4608
+ * itself once everything has settled (zero steady-state cost).
4609
+ */
4610
+ async _cfDetachedReconcileTick(payload) {
4611
+ const rows = this.sql`
4612
+ SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4613
+ summary, output_json, error_message, interrupted_reason,
4614
+ child_still_running, display_metadata, display_order,
4615
+ started_at, completed_at, detached, detached_on_finish,
4616
+ detached_notify_source, detached_max_budget_at,
4617
+ detached_no_progress_budget_ms, last_progress_at,
4618
+ detached_on_milestones,
4619
+ finish_claimed_at, finish_delivered_at, give_up_claimed_at,
4620
+ give_up_delivered_at
4621
+ FROM cf_agent_tool_runs
4622
+ WHERE detached = 1 AND finish_delivered_at IS NULL
4623
+ ORDER BY started_at ASC
4624
+ `;
4625
+ for (const row of rows) {
4626
+ const runId = row.run_id;
4627
+ let inspection = null;
4628
+ try {
4629
+ const child = await this._cf_resolveSubAgent(row.agent_type, runId);
4630
+ inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(runId);
4631
+ } catch {}
4632
+ if (inspection?.milestones && row.detached_on_milestones) {
4633
+ const milestoneRunInfo = this._agentToolRunInfoFromRow(row);
4634
+ for (const milestone of inspection.milestones) this._maybeDeliverDetachedMilestone(row, milestoneRunInfo, milestone);
4635
+ }
4636
+ if (inspection && this._isAgentToolRowHardTerminal(inspection.status)) {
4637
+ const result = this._terminalResultFromInspection(row.agent_type, inspection);
4638
+ await this._deliverDetachedTerminal(runId, "finish", result, {
4639
+ sequence: Date.now(),
4640
+ serialize: true
4641
+ }, inspection.completedAt);
4642
+ continue;
4643
+ }
4644
+ const now = Date.now();
4645
+ const budgetAt = row.detached_max_budget_at;
4646
+ const latestMilestone = inspection?.milestones?.length ? inspection.milestones[inspection.milestones.length - 1].at : void 0;
4647
+ const signalTimes = [
4648
+ inspection?.progress?.at,
4649
+ latestMilestone,
4650
+ row.last_progress_at
4651
+ ].filter((t) => typeof t === "number");
4652
+ const lastSignalAt = signalTimes.length > 0 ? Math.max(...signalTimes) : void 0;
4653
+ const noProgressBudgetMs = row.detached_no_progress_budget_ms;
4654
+ const overAbsolute = budgetAt !== null && now >= budgetAt;
4655
+ const overNoProgress = typeof noProgressBudgetMs === "number" && noProgressBudgetMs > 0 && Number.isFinite(noProgressBudgetMs) && typeof lastSignalAt === "number" && now - lastSignalAt >= noProgressBudgetMs;
4656
+ if ((overAbsolute || overNoProgress) && row.give_up_delivered_at === null) {
4657
+ let childTornDown = false;
4658
+ try {
4659
+ const child = await this._cf_resolveSubAgent(row.agent_type, runId);
4660
+ await this._asAgentToolChildAdapter(child).cancelAgentToolRun(runId, overAbsolute ? "detached budget exceeded" : "detached run went silent past its no-progress window");
4661
+ childTornDown = true;
4662
+ } catch {}
4663
+ await this._deliverDetachedTerminal(runId, "give_up", {
4664
+ runId,
4665
+ agentType: row.agent_type,
4666
+ status: "interrupted",
4667
+ error: overAbsolute ? "detached run exceeded its budget before completing" : "detached run went silent past its no-progress window",
4668
+ reason: overAbsolute ? "budget-exceeded" : "no-progress",
4669
+ childStillRunning: !childTornDown
4670
+ }, { serialize: true });
4671
+ }
4672
+ }
4673
+ if (this._hasOutstandingDetachedRuns()) {
4674
+ const currentIndex = typeof payload?.cadenceIndex === "number" ? payload.cadenceIndex : 0;
4675
+ const nextIndex = Math.min(currentIndex + 1, DETACHED_BACKBONE_CADENCE_S.length - 1);
4676
+ await this.schedule(DETACHED_BACKBONE_CADENCE_S[nextIndex], DETACHED_RECONCILE_CALLBACK, { cadenceIndex: nextIndex });
4677
+ }
4678
+ }
4255
4679
  hasAgentToolRun(classOrName, runId) {
4256
4680
  const agentType = typeof classOrName === "string" ? classOrName : classOrName.name;
4257
4681
  return (this.sql`
@@ -4311,7 +4735,10 @@ var Agent = class Agent extends Server {
4311
4735
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4312
4736
  summary, output_json, error_message, interrupted_reason,
4313
4737
  child_still_running, display_metadata, display_order,
4314
- started_at, completed_at
4738
+ started_at, completed_at, detached, detached_on_finish,
4739
+ detached_notify_source, detached_max_budget_at,
4740
+ finish_claimed_at, finish_delivered_at, give_up_claimed_at,
4741
+ give_up_delivered_at
4315
4742
  FROM cf_agent_tool_runs
4316
4743
  WHERE run_id = ${runId}
4317
4744
  LIMIT 1
@@ -4351,6 +4778,7 @@ var Agent = class Agent extends Server {
4351
4778
  inputPreview: this._parseAgentToolJson(row.input_preview),
4352
4779
  status,
4353
4780
  display: this._parseAgentToolJson(row.display_metadata),
4781
+ ...row.detached_notify_source != null ? { notifySource: row.detached_notify_source } : {},
4354
4782
  displayOrder: row.display_order,
4355
4783
  startedAt: row.started_at,
4356
4784
  completedAt
@@ -4509,6 +4937,7 @@ var Agent = class Agent extends Server {
4509
4937
  runId,
4510
4938
  body: chunk.body
4511
4939
  });
4940
+ this._observeForwardedProgress(runId, chunk.body);
4512
4941
  forwardedSinceProgress = true;
4513
4942
  armIdle();
4514
4943
  };
@@ -4598,6 +5027,76 @@ var Agent = class Agent extends Server {
4598
5027
  * called repeatedly while a child streams.
4599
5028
  */
4600
5029
  async _onAgentToolStreamProgress() {}
5030
+ /**
5031
+ * Best-effort observation of a forwarded child chunk: if it is a reserved
5032
+ * `data-agent-progress` frame, refresh the cached liveness timestamp on the
5033
+ * run row (a hint for a still-warm parent) and fire the public `onProgress`
5034
+ * hook. Never throws into the forward loop — the child's own persisted
5035
+ * snapshot (read via `inspectAgentToolRun`) remains authoritative for the
5036
+ * resetting no-progress budget after eviction.
5037
+ */
5038
+ _observeForwardedProgress(runId, body) {
5039
+ let parsed;
5040
+ try {
5041
+ parsed = JSON.parse(body);
5042
+ } catch {
5043
+ return;
5044
+ }
5045
+ if (!parsed) return;
5046
+ const isMilestone = parsed.type === AGENT_TOOL_MILESTONE_PART;
5047
+ if (parsed.type !== "data-agent-progress" && !isMilestone) return;
5048
+ const data = parsed.data ?? {};
5049
+ const at = Date.now();
5050
+ const snapshot = {
5051
+ ...typeof data.fraction === "number" ? { fraction: data.fraction } : {},
5052
+ ...typeof data.message === "string" ? { message: data.message } : {},
5053
+ ...typeof data.phase === "string" ? { phase: data.phase } : {},
5054
+ ...isMilestone && typeof data.name === "string" ? { milestone: data.name } : {},
5055
+ ...data.data !== void 0 ? { data: data.data } : {},
5056
+ at
5057
+ };
5058
+ const row = this._readAgentToolRun(runId);
5059
+ if (!row) return;
5060
+ if (row.detached) try {
5061
+ this.sql`
5062
+ UPDATE cf_agent_tool_runs SET last_progress_at = ${at}
5063
+ WHERE run_id = ${runId}
5064
+ `;
5065
+ } catch {}
5066
+ const runInfo = this._agentToolRunInfoFromRow(row);
5067
+ Promise.resolve(this.onProgress(runInfo, snapshot)).catch((error) => {
5068
+ console.error(`[agents] onProgress hook threw for run ${runId}:`, error instanceof Error ? error.message : String(error));
5069
+ });
5070
+ if (isMilestone && typeof data.name === "string") this._maybeDeliverDetachedMilestone(row, runInfo, {
5071
+ name: data.name,
5072
+ sequence: typeof data.sequence === "number" ? data.sequence : 0,
5073
+ at: typeof data.at === "number" ? data.at : at,
5074
+ ...data.data !== void 0 ? { data: data.data } : {}
5075
+ });
5076
+ }
5077
+ /**
5078
+ * Deliver a milestone notification IF this run opted into it via
5079
+ * `detached: { onMilestones }` and the milestone name is in that set. Routes
5080
+ * to the overridable `_deliverDetachedMilestone` seam (a no-op on the base
5081
+ * `Agent`; chat hosts inject an idempotent synthetic chat message).
5082
+ */
5083
+ _maybeDeliverDetachedMilestone(row, runInfo, milestone) {
5084
+ const configured = this._parseAgentToolJson(row.detached_on_milestones ?? null);
5085
+ const names = Array.isArray(configured) ? configured : configured?.names;
5086
+ const mode = Array.isArray(configured) ? "narrate" : configured?.mode ?? "narrate";
5087
+ if (!Array.isArray(names) || !names.includes(milestone.name)) return;
5088
+ Promise.resolve(this._deliverDetachedMilestone(runInfo, milestone, mode)).catch((error) => {
5089
+ console.error(`[agents] detached milestone delivery threw for run ${runInfo.runId} (${milestone.name}):`, error instanceof Error ? error.message : String(error));
5090
+ });
5091
+ }
5092
+ /**
5093
+ * Overridable seam for the `detached: { onMilestones }` convenience. The base
5094
+ * `Agent` has no chat surface, so this is a no-op; chat hosts
5095
+ * (`@cloudflare/think`, `AIChatAgent`) override it to submit an idempotent
5096
+ * synthetic message keyed on `(runId, milestone.name)`. Called from both the
5097
+ * warm tail and the backbone reconcile, so it MUST be idempotent.
5098
+ */
5099
+ async _deliverDetachedMilestone(_run, _milestone, _mode) {}
4601
5100
  _broadcastAgentToolTerminal(parentToolCallId, sequence, result, replay, connection) {
4602
5101
  if (result.status === "completed") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
4603
5102
  kind: "finished",
@@ -4828,7 +5327,7 @@ var Agent = class Agent extends Server {
4828
5327
  child_still_running, display_metadata, display_order,
4829
5328
  started_at, completed_at
4830
5329
  FROM cf_agent_tool_runs
4831
- WHERE status IN ('starting', 'running')
5330
+ WHERE status IN ('starting', 'running') AND detached = 0
4832
5331
  ORDER BY started_at ASC
4833
5332
  `;
4834
5333
  const runIds = options?.runIds !== void 0 ? new Set(options.runIds) : void 0;
@@ -4967,6 +5466,7 @@ var Agent = class Agent extends Server {
4967
5466
  runIds: options?.runIds
4968
5467
  });
4969
5468
  await this._runDeferredAgentToolFinishHooks(recoveredAgentToolFinishes);
5469
+ if (this._hasOutstandingDetachedRuns()) await this._armDetachedBackbone();
4970
5470
  })().catch(async (error) => {
4971
5471
  this._emit("agent_tool:recovery:failed", { error: error instanceof Error ? error.message : String(error) });
4972
5472
  try {
@@ -5305,10 +5805,29 @@ var Agent = class Agent extends Server {
5305
5805
  * Start a workflow and track it in this Agent's database.
5306
5806
  * Automatically injects agent identity into the workflow params.
5307
5807
  *
5808
+ * The originating Agent identity is persisted in the workflow params so
5809
+ * callbacks (`this.agent` RPC, progress/completion/error, state updates)
5810
+ * route back to the exact Agent or sub-agent facet that started the run.
5811
+ * Note the following constraints:
5812
+ *
5813
+ * - **Resolution is by name.** Callbacks re-resolve the originating Agent via
5814
+ * `getAgentByName(...)`. Agents addressed by a raw Durable Object id
5815
+ * (`idFromString`/`get(id)`) rather than by name will not receive
5816
+ * callbacks on the same instance.
5817
+ * - **Sub-agent runs are facet-local.** A workflow started from a sub-agent
5818
+ * is tracked in that facet's own storage; the parent's `getWorkflows()` /
5819
+ * `getWorkflowById()` do not see it. Aggregate across facets yourself if
5820
+ * you need a combined view.
5821
+ * - **Class names must survive bundling.** The originating path is keyed by
5822
+ * `constructor.name`. Ensure your bundler preserves class names
5823
+ * (e.g. esbuild `keepNames: true`) so callbacks can be routed.
5824
+ *
5308
5825
  * @template P - Type of params to pass to the workflow
5309
5826
  * @param workflowName - Name of the workflow binding in env (e.g., 'MY_WORKFLOW')
5310
5827
  * @param params - Params to pass to the workflow
5311
- * @param options - Optional workflow options
5828
+ * @param options - Optional workflow options. For sub-agents, pass
5829
+ * `agentBinding` as the **root** Agent's Durable Object binding name, not a
5830
+ * child binding.
5312
5831
  * @returns The workflow instance ID
5313
5832
  *
5314
5833
  * @example
@@ -5322,14 +5841,15 @@ var Agent = class Agent extends Server {
5322
5841
  async runWorkflow(workflowName, params, options) {
5323
5842
  const workflow = this._findWorkflowBindingByName(workflowName);
5324
5843
  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");
5844
+ const agentOrigin = this._workflowOrigin(options);
5845
+ if (!agentOrigin) throw new Error("Could not detect Agent binding name from class name. Pass it explicitly via options.agentBinding");
5327
5846
  const workflowId = options?.id ?? `wf_${nanoid()}`;
5328
5847
  const augmentedParams = {
5329
5848
  ...params,
5330
5849
  __agentName: this.name,
5331
- __agentBinding: agentBindingName,
5332
- __workflowName: workflowName
5850
+ __agentBinding: agentOrigin.kind === "agent" ? agentOrigin.binding : agentOrigin.rootBinding,
5851
+ __workflowName: workflowName,
5852
+ __agentOrigin: agentOrigin
5333
5853
  };
5334
5854
  const instance = await workflow.create({
5335
5855
  id: workflowId,
@@ -5871,12 +6391,28 @@ var Agent = class Agent extends Server {
5871
6391
  completedAt: row.completed_at ? /* @__PURE__ */ new Date(row.completed_at * 1e3) : null
5872
6392
  };
5873
6393
  }
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;
6394
+ _workflowOrigin(options) {
6395
+ if (this._isFacet) {
6396
+ const root = this._parentPath[0];
6397
+ const rootBindingName = options?.agentBinding ?? (root ? this._findAgentBindingNameForClass(root.className) : void 0);
6398
+ if (!rootBindingName) return void 0;
6399
+ return {
6400
+ kind: "facet",
6401
+ version: 1,
6402
+ rootBinding: rootBindingName,
6403
+ path: this.selfPath.map((step) => ({ ...step }))
6404
+ };
6405
+ }
6406
+ const agentBindingName = options?.agentBinding ?? this._findAgentBindingNameForClass(this._ParentClass.name);
6407
+ if (!agentBindingName) return void 0;
6408
+ return {
6409
+ kind: "agent",
6410
+ version: 1,
6411
+ binding: agentBindingName,
6412
+ name: this.name
6413
+ };
6414
+ }
6415
+ _findAgentBindingNameForClass(className) {
5880
6416
  for (const [key, value] of Object.entries(this.env)) if (value && typeof value === "object" && "idFromName" in value && typeof value.idFromName === "function") {
5881
6417
  if (key === className || camelCaseToKebabCase(key) === camelCaseToKebabCase(className)) return key;
5882
6418
  }
@@ -5914,7 +6450,9 @@ var Agent = class Agent extends Server {
5914
6450
  }
5915
6451
  /**
5916
6452
  * Handle a callback from a workflow.
5917
- * Called when the Agent receives a callback at /_workflow/callback.
6453
+ * Invoked via the internal `_workflow_handleCallback` RPC whenever an
6454
+ * {@link AgentWorkflow} reports progress, completion, an error, or a custom
6455
+ * event back to its originating Agent (or sub-agent facet).
5918
6456
  * Override this to handle all callback types in one place.
5919
6457
  *
5920
6458
  * @param callback - The callback payload
@@ -6448,6 +6986,6 @@ var StreamingResponse = class {
6448
6986
  }
6449
6987
  };
6450
6988
  //#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 };
6989
+ 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, isPlatformTransientError, normalizeServerId, parseSubAgentPath, routeAgentEmail, routeAgentRequest, routeSubAgentRequest, unstable_callable };
6452
6990
 
6453
6991
  //# sourceMappingURL=index.js.map