agents 0.14.1 → 0.14.2

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 (58) hide show
  1. package/dist/{agent-tool-types-BAJWu8s4.d.ts → agent-tool-types-V25Z_HcX.d.ts} +258 -115
  2. package/dist/agent-tool-types.d.ts +13 -11
  3. package/dist/{agent-tools-DYrkT-Kx.js → agent-tools-3zLG7MgA.js} +4 -2
  4. package/dist/{agent-tools-DYrkT-Kx.js.map → agent-tools-3zLG7MgA.js.map} +1 -1
  5. package/dist/{agent-tools-0R6KEert.d.ts → agent-tools-C-9s151X.d.ts} +2 -2
  6. package/dist/agent-tools.d.ts +13 -11
  7. package/dist/agent-tools.js +8 -3
  8. package/dist/agent-tools.js.map +1 -1
  9. package/dist/browser/ai.js +1 -1
  10. package/dist/browser/ai.js.map +1 -1
  11. package/dist/browser/index.js +1 -1
  12. package/dist/browser/tanstack-ai.js +1 -1
  13. package/dist/browser/tanstack-ai.js.map +1 -1
  14. package/dist/chat/index.d.ts +91 -8
  15. package/dist/chat/index.js +8 -4
  16. package/dist/chat/index.js.map +1 -1
  17. package/dist/chat-sdk/index.d.ts +4 -4
  18. package/dist/chat-sdk/index.js.map +1 -1
  19. package/dist/{classPrivateFieldGet2-D_obpP6O.js → classPrivateFieldGet2-Beqsfu2Z.js} +5 -5
  20. package/dist/{classPrivateMethodInitSpec-10iTYB7F.js → classPrivateMethodInitSpec-B5ko1s2R.js} +2 -2
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/client-FUizKzj2.js.map +1 -1
  23. package/dist/client.d.ts +1 -1
  24. package/dist/compaction-helpers-iiKMr2TQ.js.map +1 -1
  25. package/dist/email.js.map +1 -1
  26. package/dist/experimental/memory/session/index.js.map +1 -1
  27. package/dist/experimental/webmcp.js.map +1 -1
  28. package/dist/{index-RJ4OxMOe.d.ts → index-CPe1OtI0.d.ts} +17 -1
  29. package/dist/index.d.ts +66 -64
  30. package/dist/index.js +263 -76
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp/client.d.ts +14 -14
  33. package/dist/mcp/do-oauth-client-provider.js.map +1 -1
  34. package/dist/mcp/index.d.ts +30 -30
  35. package/dist/mcp/index.js.map +1 -1
  36. package/dist/mcp/x402.js.map +1 -1
  37. package/dist/observability/index.d.ts +1 -1
  38. package/dist/observability/index.js.map +1 -1
  39. package/dist/react.d.ts +3 -3
  40. package/dist/react.js +1 -1
  41. package/dist/react.js.map +1 -1
  42. package/dist/schedule.js.map +1 -1
  43. package/dist/serializable.d.ts +1 -1
  44. package/dist/{shared-BIpUk4G5.js → shared-wyII629d.js} +3 -3
  45. package/dist/{shared-BIpUk4G5.js.map → shared-wyII629d.js.map} +1 -1
  46. package/dist/skills/index.js +4 -4
  47. package/dist/skills/index.js.map +1 -1
  48. package/dist/sub-routing.d.ts +6 -6
  49. package/dist/sub-routing.js.map +1 -1
  50. package/dist/tool-output-truncation-CNnnGZQ3.js.map +1 -1
  51. package/dist/utils.js.map +1 -1
  52. package/dist/vite.d.ts +15 -4
  53. package/dist/vite.js +37 -7
  54. package/dist/vite.js.map +1 -1
  55. package/dist/workflows.d.ts +10 -2
  56. package/dist/workflows.js +48 -22
  57. package/dist/workflows.js.map +1 -1
  58. package/package.json +7 -16
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { __DO_NOT_USE_WILL_BREAK__agentContext } from "./internal_context.js";
2
2
  import { MessageType } from "./types.js";
3
3
  import { camelCaseToKebabCase, isInternalJsStubProp } from "./utils.js";
4
4
  import { createHeaderBasedEmailResolver, signAgentHeaders } from "./email.js";
5
- import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-D_obpP6O.js";
5
+ import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-Beqsfu2Z.js";
6
6
  import { SUB_PREFIX, getSubAgentByName, parseSubAgentPath, routeSubAgentRequest } from "./sub-routing.js";
7
7
  import { isErrorRetryable, tryN, validateRetryOptions } from "./retries.js";
8
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-FUizKzj2.js";
@@ -116,7 +116,8 @@ function getNextCronTime(cron) {
116
116
  const DEFAULT_KEEP_ALIVE_INTERVAL_MS = 3e4;
117
117
  const DEFAULT_AGENT_TOOL_RECOVERY_TIMEOUT_MS = 2e3;
118
118
  const DEFAULT_AGENT_TOOL_RECOVERY_TOTAL_TIMEOUT_MS = 5e3;
119
- const DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS = 12e4;
119
+ const DEFAULT_AGENT_TOOL_REATTACH_NO_PROGRESS_TIMEOUT_MS = 12e4;
120
+ const DEFAULT_AGENT_TOOL_REATTACH_MAX_WINDOW_MS = Number.POSITIVE_INFINITY;
120
121
  const SUB_AGENT_IDENTITY_VERSION_LEGACY = "legacy";
121
122
  const SUB_AGENT_IDENTITY_VERSION_PATH_V2 = "path-v2";
122
123
  const SUB_AGENT_IDENTITY_PATH_V2_PREFIX = "cf-agents:v2:";
@@ -126,7 +127,7 @@ const SUB_AGENT_IDENTITY_PATH_V2_PREFIX = "cf-agents:v2:";
126
127
  * The constructor stores this as a row in cf_agents_state and checks it
127
128
  * on wake to skip DDL on established DOs.
128
129
  */
129
- const CURRENT_SCHEMA_VERSION = 8;
130
+ const CURRENT_SCHEMA_VERSION = 9;
130
131
  const SCHEMA_VERSION_ROW_ID = "cf_schema_version";
131
132
  const STATE_ROW_ID = "cf_state_row_id";
132
133
  const STATE_WAS_CHANGED = "cf_state_was_changed";
@@ -275,7 +276,27 @@ const DEFAULT_AGENT_STATIC_OPTIONS = {
275
276
  * up. Bounds repeated retries of a `onFiberRecovered()` hook that keeps
276
277
  * throwing so a poison row cannot re-trigger forever across boots.
277
278
  */
278
- fiberRecoveryMaxAgeMs: 1440 * 60 * 1e3
279
+ fiberRecoveryMaxAgeMs: 1440 * 60 * 1e3,
280
+ /**
281
+ * No-progress budget (ms) for re-attaching to a still-running agent-tool
282
+ * child after a deploy / parent recovery (#1630). Bounds how long the parent
283
+ * waits with NO forward progress from the child; it resets on every forwarded
284
+ * chunk, so a child that keeps streaming is never abandoned mid-flight. Only a
285
+ * genuinely silent/hung child seals `interrupted` after a full window. Raise
286
+ * for children with long quiet stretches between outputs.
287
+ */
288
+ agentToolReattachNoProgressTimeoutMs: DEFAULT_AGENT_TOOL_REATTACH_NO_PROGRESS_TIMEOUT_MS,
289
+ /**
290
+ * Optional hard wall-clock ceiling (ms) on a single agent-tool re-attach
291
+ * (#1630). Caps the total wait even as the no-progress budget re-arms across
292
+ * stream-closes. Defaults to `Infinity` (no implicit cap), mirroring
293
+ * chat-recovery's `maxRecoveryWork` (#1672): a healthy, still-advancing child
294
+ * is followed for as long as it makes progress — a hung child is bounded by
295
+ * the no-progress budget, and a content-runaway by the child's own
296
+ * `maxRecoveryWork` / `shouldKeepRecovering`. Set a finite value to impose a
297
+ * wall-clock cap (which also tears the child down on `window-exceeded`).
298
+ */
299
+ agentToolReattachMaxWindowMs: DEFAULT_AGENT_TOOL_REATTACH_MAX_WINDOW_MS
279
300
  };
280
301
  /**
281
302
  * Parse the raw `retry_options` TEXT column from a SQLite row into a
@@ -428,7 +449,9 @@ var Agent = class Agent extends Server {
428
449
  },
429
450
  fiberRecoveryHookTimeoutMs: ctor.options?.fiberRecoveryHookTimeoutMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryHookTimeoutMs,
430
451
  fiberRecoveryScanDeadlineMs: ctor.options?.fiberRecoveryScanDeadlineMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryScanDeadlineMs,
431
- fiberRecoveryMaxAgeMs: ctor.options?.fiberRecoveryMaxAgeMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryMaxAgeMs
452
+ fiberRecoveryMaxAgeMs: ctor.options?.fiberRecoveryMaxAgeMs ?? DEFAULT_AGENT_STATIC_OPTIONS.fiberRecoveryMaxAgeMs,
453
+ agentToolReattachNoProgressTimeoutMs: ctor.options?.agentToolReattachNoProgressTimeoutMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachNoProgressTimeoutMs,
454
+ agentToolReattachMaxWindowMs: ctor.options?.agentToolReattachMaxWindowMs ?? DEFAULT_AGENT_STATIC_OPTIONS.agentToolReattachMaxWindowMs
432
455
  };
433
456
  return this._cachedOptions;
434
457
  }
@@ -655,6 +678,8 @@ var Agent = class Agent extends Server {
655
678
  summary TEXT,
656
679
  output_json TEXT,
657
680
  error_message TEXT,
681
+ interrupted_reason TEXT,
682
+ child_still_running INTEGER,
658
683
  display_metadata TEXT,
659
684
  display_order INTEGER NOT NULL DEFAULT 0,
660
685
  started_at INTEGER NOT NULL,
@@ -666,6 +691,8 @@ var Agent = class Agent extends Server {
666
691
  ON cf_agent_tool_runs(parent_tool_call_id, display_order)
667
692
  `;
668
693
  addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN output_json TEXT");
694
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN interrupted_reason TEXT");
695
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN child_still_running INTEGER");
669
696
  this.sql`
670
697
  INSERT OR REPLACE INTO cf_agents_state (id, state)
671
698
  VALUES (${SCHEMA_VERSION_ROW_ID}, ${String(CURRENT_SCHEMA_VERSION)})
@@ -680,12 +707,12 @@ var Agent = class Agent extends Server {
680
707
  this._rawStateAccessors = /* @__PURE__ */ new WeakMap();
681
708
  this._persistenceHookMode = "none";
682
709
  this._isFacet = false;
683
- this._suppressProtocolBroadcasts = false;
684
710
  this._protocolBroadcastExcludeIds = /* @__PURE__ */ new Set();
685
711
  this._cf_virtualSubAgentConnections = /* @__PURE__ */ new Map();
686
712
  this._parentPath = [];
687
713
  this._insideOnStart = false;
688
714
  this._warnedScheduleInOnStart = /* @__PURE__ */ new Set();
715
+ this._warnedChatRecoveryInOnStart = false;
689
716
  this._keepAliveRefs = 0;
690
717
  this._facetKeepAliveTokens = /* @__PURE__ */ new Set();
691
718
  this._runFiberActiveFibers = /* @__PURE__ */ new Set();
@@ -932,6 +959,7 @@ var Agent = class Agent extends Server {
932
959
  this._checkOrphanedWorkflows();
933
960
  await this._checkRunFibers();
934
961
  const startupAgentToolRunIds = this._agentToolRunRecoveryRunIds();
962
+ const chatRecoveryBefore = this.chatRecovery;
935
963
  this._insideOnStart = true;
936
964
  this._warnedScheduleInOnStart.clear();
937
965
  let result;
@@ -940,6 +968,12 @@ var Agent = class Agent extends Server {
940
968
  } finally {
941
969
  this._insideOnStart = false;
942
970
  }
971
+ const chatRecoveryAfter = this.chatRecovery;
972
+ const chatRecoveryAfterMatters = typeof chatRecoveryAfter === "object" && chatRecoveryAfter !== null || chatRecoveryAfter === true;
973
+ if (!this._warnedChatRecoveryInOnStart && chatRecoveryBefore !== chatRecoveryAfter && chatRecoveryAfterMatters) {
974
+ this._warnedChatRecoveryInOnStart = true;
975
+ console.warn("[Agent] `chatRecovery` was assigned during onStart(). Chat recovery evaluates its budgets (and may seal an interrupted turn, firing onExhausted) on wake BEFORE onStart() runs, so a config set here is applied too late and the built-in defaults are used for the recovery that matters. Assign `chatRecovery` as a class field or in the constructor instead.");
976
+ }
943
977
  this._scheduleAgentToolRunRecovery({ runIds: startupAgentToolRunIds });
944
978
  return result;
945
979
  });
@@ -976,7 +1010,6 @@ var Agent = class Agent extends Server {
976
1010
  * @param excludeIds Additional connection IDs to exclude (e.g. the source)
977
1011
  */
978
1012
  _broadcastProtocol(msg, excludeIds = []) {
979
- if (this._suppressProtocolBroadcasts) return;
980
1013
  const exclude = [...excludeIds, ...this._protocolBroadcastExcludeIds];
981
1014
  for (const conn of this.getConnections()) if (!this.isConnectionProtocolEnabled(conn)) exclude.push(conn.id);
982
1015
  this.broadcast(msg, exclude);
@@ -3387,6 +3420,7 @@ var Agent = class Agent extends Server {
3387
3420
  if (this._isFacet) {
3388
3421
  const stored = this._cf_virtualSubAgentConnections.get(id);
3389
3422
  if (stored) return this._cf_createSubAgentBridgeConnection(stored.bridge, stored.meta);
3423
+ return;
3390
3424
  }
3391
3425
  const connection = super.getConnection(id);
3392
3426
  if (!connection || this._cf_connectionHasSubAgentTarget(connection)) return;
@@ -3395,6 +3429,7 @@ var Agent = class Agent extends Server {
3395
3429
  *getConnections(tag) {
3396
3430
  if (this._isFacet) {
3397
3431
  for (const stored of this._cf_virtualSubAgentConnections.values()) if (!tag || stored.meta.tags.includes(tag)) yield this._cf_createSubAgentBridgeConnection(stored.bridge, stored.meta);
3432
+ return;
3398
3433
  }
3399
3434
  for (const connection of super.getConnections(tag)) {
3400
3435
  if (this._cf_connectionHasSubAgentTarget(connection)) continue;
@@ -3847,12 +3882,7 @@ var Agent = class Agent extends Server {
3847
3882
  this.ctx.storage.put("cf_agents_facet_name", name),
3848
3883
  this.ctx.storage.put("cf_agents_parent_path", parentPath)
3849
3884
  ]);
3850
- this._suppressProtocolBroadcasts = true;
3851
- try {
3852
- await this.__unsafe_ensureInitialized();
3853
- } finally {
3854
- this._suppressProtocolBroadcasts = false;
3855
- }
3885
+ await this.__unsafe_ensureInitialized();
3856
3886
  }
3857
3887
  get name() {
3858
3888
  return this._facetName ?? logicalNameFromPathV2Identity(super.name) ?? super.name;
@@ -4022,10 +4052,12 @@ var Agent = class Agent extends Server {
4022
4052
  } catch {}
4023
4053
  return this._resultFromAgentToolRow(existing);
4024
4054
  }
4055
+ let reattachReason;
4056
+ let childTornDown = false;
4025
4057
  try {
4026
4058
  const child = await this.subAgent(cls, runId);
4027
4059
  const adapter = this._asAgentToolChildAdapter(child);
4028
- const reattach = await this._reattachAgentToolRunToTerminal(adapter, existing, 1);
4060
+ const reattach = await this._reattachAgentToolRunToTerminal(adapter, existing, 1, this._resolvedOptions.agentToolReattachNoProgressTimeoutMs, this._resolvedOptions.agentToolReattachMaxWindowMs);
4029
4061
  if (reattach.result) {
4030
4062
  await this._finishAgentToolRun(this._agentToolRunInfoFromRow(existing), reattach.result, {
4031
4063
  sequence: reattach.sequence,
@@ -4033,8 +4065,13 @@ var Agent = class Agent extends Server {
4033
4065
  });
4034
4066
  return reattach.result;
4035
4067
  }
4068
+ reattachReason = reattach.reason;
4069
+ childTornDown = await this._teardownGivenUpAgentToolChild(adapter, runId, reattach.reason);
4036
4070
  } catch {}
4037
- return await this._replayAndInterruptAgentToolRun(existing, "Agent tool run was still running and could not be re-attached to a terminal result within the recovery budget.");
4071
+ return await this._replayAndInterruptAgentToolRun(existing, this._interruptedMessageForReason(reattachReason), {
4072
+ reason: reattachReason,
4073
+ childStillRunning: !childTornDown
4074
+ });
4038
4075
  }
4039
4076
  const displayOrder = options.displayOrder ?? 0;
4040
4077
  const inputPreview = options.inputPreview ?? this._defaultAgentToolPreview(options.input);
@@ -4128,7 +4165,7 @@ var Agent = class Agent extends Server {
4128
4165
  try {
4129
4166
  if (adapter.tailAgentToolRun) {
4130
4167
  const stream = await adapter.tailAgentToolRun(runId, { afterSequence: -1 });
4131
- sequence = await this._forwardAgentToolStream(stream, options.parentToolCallId, runId, sequence, options.signal);
4168
+ sequence = (await this._forwardAgentToolStream(stream, options.parentToolCallId, runId, sequence, options.signal)).next;
4132
4169
  } else {
4133
4170
  const chunks = await adapter.getAgentToolChunks(runId);
4134
4171
  sequence = this._broadcastAgentToolChunks(options.parentToolCallId, runId, chunks, sequence);
@@ -4232,13 +4269,28 @@ var Agent = class Agent extends Server {
4232
4269
  _readAgentToolRun(runId) {
4233
4270
  return this.sql`
4234
4271
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4235
- summary, output_json, error_message, display_metadata, display_order,
4272
+ summary, output_json, error_message, interrupted_reason,
4273
+ child_still_running, display_metadata, display_order,
4236
4274
  started_at, completed_at
4237
4275
  FROM cf_agent_tool_runs
4238
4276
  WHERE run_id = ${runId}
4239
4277
  LIMIT 1
4240
4278
  `[0] ?? null;
4241
4279
  }
4280
+ /**
4281
+ * Reconstruct the typed interrupted cause (`reason` / `childStillRunning`,
4282
+ * #1630 follow-up) from a stored row so a row→result/event rebuild — e.g. a
4283
+ * reconnect replay — carries the same fields a live client saw. Only
4284
+ * `interrupted` rows store a cause; everything else yields `{}` (the columns
4285
+ * are cleared whenever a row settles to a hard terminal).
4286
+ */
4287
+ _agentToolInterruptedExtrasFromRow(row) {
4288
+ if (row.status !== "interrupted") return {};
4289
+ return {
4290
+ ...row.interrupted_reason !== null ? { reason: row.interrupted_reason } : {},
4291
+ ...row.child_still_running !== null ? { childStillRunning: row.child_still_running !== 0 } : {}
4292
+ };
4293
+ }
4242
4294
  _resultFromAgentToolRow(row) {
4243
4295
  const output = this._parseAgentToolJson(row.output_json);
4244
4296
  return {
@@ -4247,7 +4299,8 @@ var Agent = class Agent extends Server {
4247
4299
  status: row.status,
4248
4300
  ...output !== void 0 ? { output } : {},
4249
4301
  ...row.summary !== null ? { summary: row.summary } : {},
4250
- ...row.error_message !== null ? { error: row.error_message } : {}
4302
+ ...row.error_message !== null ? { error: row.error_message } : {},
4303
+ ...this._agentToolInterruptedExtrasFromRow(row)
4251
4304
  };
4252
4305
  }
4253
4306
  _agentToolRunInfoFromRow(row, status = row.status, completedAt = row.completed_at ?? void 0) {
@@ -4306,12 +4359,15 @@ var Agent = class Agent extends Server {
4306
4359
  }
4307
4360
  }
4308
4361
  _updateAgentToolTerminal(runId, result, completedAt = Date.now()) {
4362
+ const childStillRunning = result.childStillRunning === void 0 ? null : result.childStillRunning ? 1 : 0;
4309
4363
  this.sql`
4310
4364
  UPDATE cf_agent_tool_runs
4311
4365
  SET status = ${result.status},
4312
4366
  summary = ${result.summary ?? null},
4313
4367
  output_json = ${this._stringifyAgentToolOutput(result.output)},
4314
4368
  error_message = ${result.error ?? null},
4369
+ interrupted_reason = ${result.reason ?? null},
4370
+ child_still_running = ${childStillRunning},
4315
4371
  completed_at = ${completedAt}
4316
4372
  WHERE run_id = ${runId}
4317
4373
  AND status NOT IN ('completed', 'error', 'aborted')
@@ -4374,9 +4430,13 @@ var Agent = class Agent extends Server {
4374
4430
  if (!chunks) return sequence;
4375
4431
  return this._broadcastAgentToolChunks(row.parent_tool_call_id ?? void 0, row.run_id, chunks, sequence, replay, connection);
4376
4432
  }
4377
- async _forwardAgentToolStream(stream, parentToolCallId, runId, sequence, signal) {
4433
+ async _forwardAgentToolStream(stream, parentToolCallId, runId, sequence, signal, idleTimeoutMs) {
4378
4434
  let next = sequence;
4379
- if (signal?.aborted) return next;
4435
+ if (signal?.aborted) return {
4436
+ next,
4437
+ ended: "aborted"
4438
+ };
4439
+ let ended = "done";
4380
4440
  const reader = stream.getReader();
4381
4441
  const decoder = new TextDecoder();
4382
4442
  let bufferedBytes = "";
@@ -4390,6 +4450,17 @@ var Agent = class Agent extends Server {
4390
4450
  abortListener = () => resolveAbort?.();
4391
4451
  signal.addEventListener("abort", abortListener, { once: true });
4392
4452
  }
4453
+ const idleEnabled = typeof idleTimeoutMs === "number" && idleTimeoutMs > 0 && Number.isFinite(idleTimeoutMs);
4454
+ let resolveIdle;
4455
+ let idleTimer;
4456
+ const idlePromise = new Promise((resolve) => {
4457
+ resolveIdle = resolve;
4458
+ });
4459
+ const armIdle = () => {
4460
+ if (!idleEnabled) return;
4461
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
4462
+ idleTimer = setTimeout(() => resolveIdle?.(), idleTimeoutMs);
4463
+ };
4393
4464
  let forwardedSinceProgress = false;
4394
4465
  try {
4395
4466
  const forwardChunk = (chunk) => {
@@ -4399,6 +4470,7 @@ var Agent = class Agent extends Server {
4399
4470
  body: chunk.body
4400
4471
  });
4401
4472
  forwardedSinceProgress = true;
4473
+ armIdle();
4402
4474
  };
4403
4475
  const forwardLine = (line) => {
4404
4476
  try {
@@ -4419,15 +4491,21 @@ var Agent = class Agent extends Server {
4419
4491
  bufferedBytes = "";
4420
4492
  }
4421
4493
  };
4494
+ armIdle();
4422
4495
  while (true) {
4423
4496
  const readPromise = reader.read();
4424
4497
  readPromise.catch(() => {});
4425
- const raced = await Promise.race([readPromise.then((result) => ({
4426
- kind: "read",
4427
- result
4428
- })), abortPromise.then(() => ({ kind: "abort" }))]);
4429
- if (raced.kind === "abort") {
4498
+ const raced = await Promise.race([
4499
+ readPromise.then((result) => ({
4500
+ kind: "read",
4501
+ result
4502
+ })),
4503
+ abortPromise.then(() => ({ kind: "abort" })),
4504
+ idlePromise.then(() => ({ kind: "idle" }))
4505
+ ]);
4506
+ if (raced.kind === "abort" || raced.kind === "idle") {
4430
4507
  aborted = true;
4508
+ ended = raced.kind === "idle" ? "idle" : "aborted";
4431
4509
  break;
4432
4510
  }
4433
4511
  const { done, value } = raced.result;
@@ -4448,12 +4526,16 @@ var Agent = class Agent extends Server {
4448
4526
  }
4449
4527
  }
4450
4528
  } finally {
4529
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
4451
4530
  if (abortListener && signal) signal.removeEventListener("abort", abortListener);
4452
4531
  if (!aborted) try {
4453
4532
  reader.releaseLock();
4454
4533
  } catch {}
4455
4534
  }
4456
- return next;
4535
+ return {
4536
+ next,
4537
+ ended
4538
+ };
4457
4539
  }
4458
4540
  /**
4459
4541
  * Hook invoked by `_forwardAgentToolStream` after a child produces output that
@@ -4490,7 +4572,9 @@ var Agent = class Agent extends Server {
4490
4572
  else if (result.status === "interrupted") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
4491
4573
  kind: "interrupted",
4492
4574
  runId: result.runId,
4493
- error: result.error ?? "Agent tool run was interrupted"
4575
+ error: result.error ?? "Agent tool run was interrupted",
4576
+ ...result.reason !== void 0 ? { reason: result.reason } : {},
4577
+ ...result.childStillRunning !== void 0 ? { childStillRunning: result.childStillRunning } : {}
4494
4578
  }, replay, connection);
4495
4579
  else this._broadcastAgentToolEvent(parentToolCallId, sequence, {
4496
4580
  kind: "error",
@@ -4508,7 +4592,7 @@ var Agent = class Agent extends Server {
4508
4592
  if (!cls) throw new Error(`Agent tool class "${className}" is not exported.`);
4509
4593
  return cls;
4510
4594
  }
4511
- async _replayAndInterruptAgentToolRun(row, message) {
4595
+ async _replayAndInterruptAgentToolRun(row, message, extra) {
4512
4596
  let sequence = 1;
4513
4597
  try {
4514
4598
  sequence = await this._broadcastAgentToolStoredChunks(row, sequence);
@@ -4517,12 +4601,53 @@ var Agent = class Agent extends Server {
4517
4601
  runId: row.run_id,
4518
4602
  agentType: row.agent_type,
4519
4603
  status: "interrupted",
4520
- error: message
4604
+ error: message,
4605
+ ...extra?.reason !== void 0 ? { reason: extra.reason } : {},
4606
+ ...extra?.childStillRunning !== void 0 ? { childStillRunning: extra.childStillRunning } : {}
4521
4607
  };
4522
4608
  await this._finishAgentToolRun(this._agentToolRunInfoFromRow(row), result, { sequence });
4523
4609
  return result;
4524
4610
  }
4525
4611
  /**
4612
+ * Human-readable prose for an `interrupted` seal. Kept in sync with
4613
+ * {@link AgentToolInterruptedReason}; callers branch on the typed `reason`
4614
+ * field, not this string.
4615
+ */
4616
+ _interruptedMessageForReason(reason) {
4617
+ switch (reason) {
4618
+ case "no-progress": return "Agent tool run was still running but made no forward progress within the re-attach no-progress budget; the parent gave up.";
4619
+ case "window-exceeded": return "Agent tool run did not reach a terminal result within the maximum re-attach window; the parent gave up.";
4620
+ case "not-tailable": return "Agent tool run was still running, but live-tail reattachment is not supported in this runtime.";
4621
+ case "inspect-timeout": return "Agent tool run inspection timed out during parent recovery.";
4622
+ case "inspect-failed": return "Agent tool run could not be inspected during parent recovery.";
4623
+ case "recovery-deadline": return "Agent tool run recovery deadline exceeded.";
4624
+ default: return "Agent tool run was still running and did not reach a terminal result.";
4625
+ }
4626
+ }
4627
+ /**
4628
+ * Tear down a child agent-tool run the parent has genuinely given up on
4629
+ * (#1630 follow-up). Teardown is scoped to `window-exceeded` ONLY — the hard
4630
+ * ceiling, where the child has had its full recovery window and is therefore
4631
+ * truly exhausted, so cancelling it reclaims its fiber / keep-alive. Every
4632
+ * other give-up is deliberately left repairable: `no-progress` seals stay
4633
+ * SOFT (`interrupted`, `childStillRunning: true`) so a re-issue can still
4634
+ * re-attach and collect the child if it self-heals — tearing those down would
4635
+ * defeat the repair-on-re-issue path and convert a retryable interrupt into a
4636
+ * non-retryable `aborted`. Reasons where the child's state is unknown
4637
+ * (`inspect-*`, `recovery-deadline`, `not-tailable`) are also left alone.
4638
+ * Returns whether the child was torn down (so the caller reports
4639
+ * `childStillRunning: false`).
4640
+ */
4641
+ async _teardownGivenUpAgentToolChild(adapter, runId, reason) {
4642
+ if (reason !== "window-exceeded") return false;
4643
+ try {
4644
+ await adapter.cancelAgentToolRun(runId, `agent tool run given up by parent recovery: ${reason}`);
4645
+ return true;
4646
+ } catch {
4647
+ return false;
4648
+ }
4649
+ }
4650
+ /**
4526
4651
  * Re-attach to a still-running child agent-tool run and tail it to its real
4527
4652
  * terminal result, instead of abandoning it as `interrupted` (#1630). The
4528
4653
  * child is a separate facet with its own `chatRecovery`, so resolving it via
@@ -4530,54 +4655,98 @@ var Agent = class Agent extends Server {
4530
4655
  * its live stream (forwarding chunks to the parent's connections) until it
4531
4656
  * reaches terminal, then inspect for the collected result.
4532
4657
  *
4533
- * Bounded by {@link DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS}: a child that keeps
4534
- * advancing toward terminal within the window is collected; a genuinely hung
4535
- * child returns `{ result: undefined }` once the budget elapses so the caller
4536
- * falls back to `interrupted` and recovery can never block forever.
4658
+ * The wait is PROGRESS-KEYED, not a flat wall clock (which previously abandoned
4659
+ * healthy, still-advancing children whose recovery simply outran a fixed
4660
+ * budget). `noProgressTimeoutMs` bounds how long the parent waits with NO
4661
+ * forward progress; it is reset on every forwarded chunk. As long as the child
4662
+ * keeps streaming it is followed through to terminal. The loop also RE-ARMS
4663
+ * across stream-closes (a child re-evicted mid-recovery, or a tail that ends
4664
+ * before terminal) as long as the prior attempt made progress, so a child that
4665
+ * dies and recovers again during deploy churn is still collected. A genuinely
4666
+ * silent/hung child can never block recovery forever: it seals `interrupted`
4667
+ * after one `noProgressTimeoutMs` window. `maxWindowMs` is an OPTIONAL hard
4668
+ * wall-clock ceiling (default `Infinity` — uncapped, mirroring #1672's
4669
+ * `maxRecoveryWork`); set it finite to also bound a child that keeps
4670
+ * progressing, which seals `window-exceeded` and tears the child down.
4537
4671
  *
4538
4672
  * Returns the terminal `result` (and `completedAt`) when the child reaches a
4539
- * terminal status within the budget, plus the advanced broadcast `sequence`.
4540
- * Returns `{ result: undefined }` when there is no `tailAgentToolRun` adapter,
4541
- * the budget is exhausted, or the child is still non-terminal.
4673
+ * terminal status, plus the advanced broadcast `sequence`. Returns
4674
+ * `{ result: undefined }` when there is no `tailAgentToolRun` adapter, the
4675
+ * child makes no progress within a full no-progress window, or the ceiling is
4676
+ * reached while the child is still non-terminal — the caller then seals
4677
+ * `interrupted`.
4542
4678
  */
4543
- async _reattachAgentToolRunToTerminal(adapter, row, sequence, budgetMs = DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS) {
4544
- if (typeof adapter.tailAgentToolRun !== "function") return { sequence };
4545
- const controller = new AbortController();
4546
- let timeoutId;
4547
- if (budgetMs > 0 && Number.isFinite(budgetMs)) timeoutId = setTimeout(() => controller.abort(), budgetMs);
4548
- else if (budgetMs <= 0) controller.abort();
4679
+ async _reattachAgentToolRunToTerminal(adapter, row, sequence, noProgressTimeoutMs = DEFAULT_AGENT_TOOL_REATTACH_NO_PROGRESS_TIMEOUT_MS, maxWindowMs = DEFAULT_AGENT_TOOL_REATTACH_MAX_WINDOW_MS) {
4680
+ if (typeof adapter.tailAgentToolRun !== "function") return {
4681
+ sequence,
4682
+ reason: "not-tailable"
4683
+ };
4549
4684
  this._emit("agent_tool:recovery:reattach", {
4550
4685
  runId: row.run_id,
4551
4686
  agentType: row.agent_type,
4552
- budgetMs
4687
+ budgetMs: noProgressTimeoutMs
4553
4688
  });
4554
- let nextSequence = sequence;
4555
- if (!controller.signal.aborted) try {
4556
- let afterSequence = -1;
4689
+ const collectTerminal = async (seq) => {
4690
+ let inspection = null;
4557
4691
  try {
4558
- const existing = await adapter.getAgentToolChunks(row.run_id);
4559
- const last = existing[existing.length - 1];
4560
- if (last) afterSequence = last.sequence;
4561
- } catch {}
4562
- const stream = await adapter.tailAgentToolRun(row.run_id, { afterSequence });
4563
- nextSequence = await this._forwardAgentToolStream(stream, row.parent_tool_call_id ?? void 0, row.run_id, nextSequence, controller.signal);
4564
- } catch {}
4565
- if (timeoutId !== void 0) clearTimeout(timeoutId);
4566
- let inspection = null;
4692
+ inspection = await adapter.inspectAgentToolRun(row.run_id);
4693
+ } catch {
4694
+ return null;
4695
+ }
4696
+ if (inspection && inspection.status !== "running" && inspection.status !== "starting") return {
4697
+ sequence: seq,
4698
+ result: this._terminalResultFromInspection(row.agent_type, inspection),
4699
+ completedAt: inspection.completedAt
4700
+ };
4701
+ return null;
4702
+ };
4703
+ let nextSequence = sequence;
4704
+ if (!(noProgressTimeoutMs > 0)) return await collectTerminal(nextSequence) ?? {
4705
+ sequence: nextSequence,
4706
+ reason: "no-progress"
4707
+ };
4708
+ const ceilingController = new AbortController();
4709
+ let ceilingTimer;
4710
+ if (maxWindowMs > 0 && Number.isFinite(maxWindowMs)) ceilingTimer = setTimeout(() => ceilingController.abort(), maxWindowMs);
4711
+ let reason = "no-progress";
4567
4712
  try {
4568
- inspection = await adapter.inspectAgentToolRun(row.run_id);
4569
- } catch {}
4570
- if (inspection && inspection.status !== "running" && inspection.status !== "starting") return {
4713
+ while (!ceilingController.signal.aborted) {
4714
+ let afterSequence = -1;
4715
+ try {
4716
+ const existing = await adapter.getAgentToolChunks(row.run_id);
4717
+ const last = existing[existing.length - 1];
4718
+ if (last) afterSequence = last.sequence;
4719
+ } catch {}
4720
+ const beforeSequence = nextSequence;
4721
+ let streamEnded = "idle";
4722
+ try {
4723
+ const stream = await adapter.tailAgentToolRun(row.run_id, { afterSequence });
4724
+ const forwarded = await this._forwardAgentToolStream(stream, row.parent_tool_call_id ?? void 0, row.run_id, nextSequence, ceilingController.signal, noProgressTimeoutMs);
4725
+ nextSequence = forwarded.next;
4726
+ streamEnded = forwarded.ended;
4727
+ } catch {}
4728
+ const terminal = await collectTerminal(nextSequence);
4729
+ if (terminal) return terminal;
4730
+ if (ceilingController.signal.aborted) {
4731
+ reason = "window-exceeded";
4732
+ break;
4733
+ }
4734
+ if (streamEnded !== "done") break;
4735
+ if (nextSequence <= beforeSequence) break;
4736
+ }
4737
+ } finally {
4738
+ if (ceilingTimer !== void 0) clearTimeout(ceilingTimer);
4739
+ }
4740
+ return {
4571
4741
  sequence: nextSequence,
4572
- result: this._terminalResultFromInspection(row.agent_type, inspection),
4573
- completedAt: inspection.completedAt
4742
+ reason
4574
4743
  };
4575
- return { sequence: nextSequence };
4576
4744
  }
4577
4745
  async _replayAgentToolRuns(connection) {
4578
4746
  const rows = this.sql`
4579
4747
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4580
- summary, output_json, error_message, display_metadata, display_order
4748
+ summary, output_json, error_message, interrupted_reason,
4749
+ child_still_running, display_metadata, display_order
4581
4750
  FROM cf_agent_tool_runs
4582
4751
  ORDER BY started_at ASC
4583
4752
  `;
@@ -4601,19 +4770,22 @@ var Agent = class Agent extends Server {
4601
4770
  status: row.status,
4602
4771
  output: this._parseAgentToolJson(row.output_json),
4603
4772
  summary: row.summary ?? void 0,
4604
- error: row.error_message ?? void 0
4773
+ error: row.error_message ?? void 0,
4774
+ ...this._agentToolInterruptedExtrasFromRow(row)
4605
4775
  }, true, connection);
4606
4776
  }
4607
4777
  }
4608
4778
  async _reconcileAgentToolRuns(options) {
4609
- const reattachTimeoutMs = options?.reattachTimeoutMs ?? DEFAULT_AGENT_TOOL_REATTACH_TIMEOUT_MS;
4779
+ const reattachTimeoutMs = options?.reattachTimeoutMs ?? this._resolvedOptions.agentToolReattachNoProgressTimeoutMs;
4780
+ const reattachMaxWindowMs = options?.reattachMaxWindowMs ?? this._resolvedOptions.agentToolReattachMaxWindowMs;
4610
4781
  const startedAt = Date.now();
4611
4782
  const totalTimeoutMs = options?.totalRecoveryTimeoutMs ?? DEFAULT_AGENT_TOOL_RECOVERY_TOTAL_TIMEOUT_MS;
4612
4783
  const deadlineAt = totalTimeoutMs > 0 ? startedAt + totalTimeoutMs : Number.POSITIVE_INFINITY;
4613
4784
  const deferredFinishes = [];
4614
4785
  const rows = this.sql`
4615
4786
  SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
4616
- summary, output_json, error_message, display_metadata, display_order,
4787
+ summary, output_json, error_message, interrupted_reason,
4788
+ child_still_running, display_metadata, display_order,
4617
4789
  started_at, completed_at
4618
4790
  FROM cf_agent_tool_runs
4619
4791
  WHERE status IN ('starting', 'running')
@@ -4654,7 +4826,8 @@ var Agent = class Agent extends Server {
4654
4826
  runId: row.run_id,
4655
4827
  agentType: row.agent_type,
4656
4828
  status: "interrupted",
4657
- error: "Agent tool run recovery deadline exceeded."
4829
+ reason: "recovery-deadline",
4830
+ error: this._interruptedMessageForReason("recovery-deadline")
4658
4831
  }, sequence, void 0);
4659
4832
  continue;
4660
4833
  }
@@ -4662,12 +4835,16 @@ var Agent = class Agent extends Server {
4662
4835
  const boundedChildTimeout = childTimeout > 0 ? Math.min(childTimeout, remainingMs) : remainingMs;
4663
4836
  const recovery = await this._inspectAgentToolRunForRecovery(row, sequence, boundedChildTimeout);
4664
4837
  if (recovery.status !== "inspected") {
4665
- await finalizeRow(row, {
4666
- runId: row.run_id,
4667
- agentType: row.agent_type,
4668
- status: "interrupted",
4669
- error: recovery.status === "timed-out" ? "Agent tool run inspection timed out during parent recovery." : "Agent tool run could not be inspected during parent recovery."
4670
- }, sequence, void 0);
4838
+ await finalizeRow(row, (() => {
4839
+ const reason = recovery.status === "timed-out" ? "inspect-timeout" : "inspect-failed";
4840
+ return {
4841
+ runId: row.run_id,
4842
+ agentType: row.agent_type,
4843
+ status: "interrupted",
4844
+ reason,
4845
+ error: this._interruptedMessageForReason(reason)
4846
+ };
4847
+ })(), sequence, void 0);
4671
4848
  continue;
4672
4849
  }
4673
4850
  const inspection = recovery.inspection;
@@ -4687,17 +4864,26 @@ var Agent = class Agent extends Server {
4687
4864
  runId: row.run_id,
4688
4865
  agentType: row.agent_type,
4689
4866
  status: "interrupted",
4690
- error: "Agent tool run was still running, but live-tail reattachment is not supported in this runtime."
4867
+ reason: "not-tailable",
4868
+ childStillRunning: true,
4869
+ error: this._interruptedMessageForReason("not-tailable")
4691
4870
  }, sequenceAfterReplay, void 0);
4692
4871
  else await finalizeRow(row, this._terminalResultFromInspection(row.agent_type, inspection), sequenceAfterReplay, inspection.completedAt);
4693
4872
  }
4694
4873
  await Promise.all(reattachQueue.map(async ({ row, adapter }) => {
4695
- const reattach = await this._reattachAgentToolRunToTerminal(adapter, row, 1, reattachTimeoutMs);
4696
- await finalizeRow(row, reattach.result ?? {
4874
+ const reattach = await this._reattachAgentToolRunToTerminal(adapter, row, 1, reattachTimeoutMs, reattachMaxWindowMs);
4875
+ if (reattach.result) {
4876
+ await finalizeRow(row, reattach.result, reattach.sequence, reattach.completedAt);
4877
+ return;
4878
+ }
4879
+ const tornDown = await this._teardownGivenUpAgentToolChild(adapter, row.run_id, reattach.reason);
4880
+ await finalizeRow(row, {
4697
4881
  runId: row.run_id,
4698
4882
  agentType: row.agent_type,
4699
4883
  status: "interrupted",
4700
- error: "Agent tool run was still running and did not reach a terminal result within the re-attach budget."
4884
+ reason: reattach.reason,
4885
+ childStillRunning: !tornDown,
4886
+ error: this._interruptedMessageForReason(reattach.reason)
4701
4887
  }, reattach.sequence, reattach.completedAt);
4702
4888
  }));
4703
4889
  this._emit("agent_tool:recovery:complete", {
@@ -4737,6 +4923,7 @@ var Agent = class Agent extends Server {
4737
4923
  childInspectionTimeoutMs: options?.childInspectionTimeoutMs,
4738
4924
  totalRecoveryTimeoutMs: options?.totalRecoveryTimeoutMs,
4739
4925
  reattachTimeoutMs: options?.reattachTimeoutMs,
4926
+ reattachMaxWindowMs: options?.reattachMaxWindowMs,
4740
4927
  runIds: options?.runIds
4741
4928
  });
4742
4929
  await this._runDeferredAgentToolFinishHooks(recoveredAgentToolFinishes);