agents 0.11.9 → 0.12.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 (45) hide show
  1. package/README.md +47 -1
  2. package/dist/{index-DSwOzhhd.d.ts → agent-tool-types-CB7nISDE.d.ts} +706 -100
  3. package/dist/agent-tool-types.d.ts +34 -0
  4. package/dist/agent-tool-types.js +1 -0
  5. package/dist/agent-tools-BAdX1vdI.js +425 -0
  6. package/dist/agent-tools-BAdX1vdI.js.map +1 -0
  7. package/dist/agent-tools-Bb1O8blK.d.ts +14 -0
  8. package/dist/agent-tools.d.ts +68 -0
  9. package/dist/agent-tools.js +51 -0
  10. package/dist/agent-tools.js.map +1 -0
  11. package/dist/browser/ai.d.ts +1 -1
  12. package/dist/browser/ai.js +2 -2
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/tanstack-ai.d.ts +1 -1
  16. package/dist/browser/tanstack-ai.js +1 -1
  17. package/dist/chat/index.d.ts +4 -1
  18. package/dist/chat/index.js +34 -300
  19. package/dist/chat/index.js.map +1 -1
  20. package/dist/client.d.ts +2 -2
  21. package/dist/{compaction-helpers-C_cN3z55.js → compaction-helpers-CSaqCmdE.js} +1 -1
  22. package/dist/{compaction-helpers-C_cN3z55.js.map → compaction-helpers-CSaqCmdE.js.map} +1 -1
  23. package/dist/{compaction-helpers-YzCLvunJ.d.ts → compaction-helpers-D92Ipstp.d.ts} +1 -1
  24. package/dist/experimental/memory/session/index.d.ts +1 -1
  25. package/dist/experimental/memory/session/index.js +1 -1
  26. package/dist/experimental/memory/utils/index.d.ts +1 -1
  27. package/dist/experimental/memory/utils/index.js +1 -1
  28. package/dist/index.d.ts +74 -42
  29. package/dist/index.js +1803 -284
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp/client.d.ts +1 -1
  32. package/dist/mcp/index.d.ts +1 -1
  33. package/dist/react.d.ts +16 -2
  34. package/dist/react.js +51 -1
  35. package/dist/react.js.map +1 -1
  36. package/dist/{serializable-Bg8ARWlN.d.ts → serializable-Brg7fRds.d.ts} +1 -1
  37. package/dist/serializable.d.ts +1 -1
  38. package/dist/{shared-mfBbxjS1.js → shared-C6l4ZKRN.js} +1 -1
  39. package/dist/{shared-mfBbxjS1.js.map → shared-C6l4ZKRN.js.map} +1 -1
  40. package/dist/{shared-BUHZFGTk.d.ts → shared-Ch9slKdI.d.ts} +1 -1
  41. package/dist/sub-routing.d.ts +6 -6
  42. package/dist/sub-routing.js +7 -1
  43. package/dist/sub-routing.js.map +1 -1
  44. package/dist/workflows.d.ts +1 -1
  45. package/package.json +10 -5
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { __DO_NOT_USE_WILL_BREAK__agentContext } from "./internal_context.js";
1
2
  import { MessageType } from "./types.js";
2
3
  import { camelCaseToKebabCase } from "./utils.js";
3
4
  import { createHeaderBasedEmailResolver, signAgentHeaders } from "./email.js";
4
- import { __DO_NOT_USE_WILL_BREAK__agentContext } from "./internal_context.js";
5
5
  import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-Bqby-AHD.js";
6
6
  import { SUB_PREFIX, getSubAgentByName, parseSubAgentPath, routeSubAgentRequest } from "./sub-routing.js";
7
7
  import { isErrorRetryable, tryN, validateRetryOptions } from "./retries.js";
@@ -40,6 +40,52 @@ var SqlError = class extends Error {
40
40
  this.query = query;
41
41
  }
42
42
  };
43
+ var _connection2 = /* @__PURE__ */ new WeakMap();
44
+ var _broadcast = /* @__PURE__ */ new WeakMap();
45
+ var SubAgentConnectionBridge = class extends RpcTarget {
46
+ constructor(connection, broadcast) {
47
+ super();
48
+ _classPrivateFieldInitSpec(this, _connection2, void 0);
49
+ _classPrivateFieldInitSpec(this, _broadcast, void 0);
50
+ _classPrivateFieldSet2(_connection2, this, connection);
51
+ _classPrivateFieldSet2(_broadcast, this, broadcast);
52
+ }
53
+ send(message) {
54
+ _classPrivateFieldGet2(_connection2, this).send(message);
55
+ }
56
+ close(code, reason) {
57
+ _classPrivateFieldGet2(_connection2, this).close(code, reason);
58
+ }
59
+ setState(state) {
60
+ return _classPrivateFieldGet2(_connection2, this).setState(state);
61
+ }
62
+ broadcast(ownerPath, message, without) {
63
+ _classPrivateFieldGet2(_broadcast, this)?.call(this, ownerPath, message, without);
64
+ }
65
+ };
66
+ var _root = /* @__PURE__ */ new WeakMap();
67
+ var _connectionId = /* @__PURE__ */ new WeakMap();
68
+ var RootSubAgentConnectionBridge = class {
69
+ constructor(root, connectionId) {
70
+ _classPrivateFieldInitSpec(this, _root, void 0);
71
+ _classPrivateFieldInitSpec(this, _connectionId, void 0);
72
+ _classPrivateFieldSet2(_root, this, root);
73
+ _classPrivateFieldSet2(_connectionId, this, connectionId);
74
+ }
75
+ send(message) {
76
+ _classPrivateFieldGet2(_root, this)._cf_sendToSubAgentConnection(_classPrivateFieldGet2(_connectionId, this), message);
77
+ }
78
+ close(code, reason) {
79
+ _classPrivateFieldGet2(_root, this)._cf_closeSubAgentConnection(_classPrivateFieldGet2(_connectionId, this), code, reason);
80
+ }
81
+ setState(state) {
82
+ _classPrivateFieldGet2(_root, this)._cf_setSubAgentConnectionState(_classPrivateFieldGet2(_connectionId, this), state);
83
+ return state;
84
+ }
85
+ broadcast(ownerPath, message, without) {
86
+ _classPrivateFieldGet2(_root, this)._cf_broadcastToSubAgent(ownerPath, message, without);
87
+ }
88
+ };
43
89
  /**
44
90
  * Decorator that marks a method as callable by clients
45
91
  * @param metadata Optional metadata about the callable method
@@ -74,7 +120,7 @@ const DEFAULT_KEEP_ALIVE_INTERVAL_MS = 3e4;
74
120
  * The constructor stores this as a row in cf_agents_state and checks it
75
121
  * on wake to skip DDL on established DOs.
76
122
  */
77
- const CURRENT_SCHEMA_VERSION = 3;
123
+ const CURRENT_SCHEMA_VERSION = 7;
78
124
  const SCHEMA_VERSION_ROW_ID = "cf_schema_version";
79
125
  const STATE_ROW_ID = "cf_state_row_id";
80
126
  const STATE_WAS_CHANGED = "cf_state_was_changed";
@@ -99,13 +145,30 @@ const CF_READONLY_KEY = "_cf_readonly";
99
145
  */
100
146
  const CF_NO_PROTOCOL_KEY = "_cf_no_protocol";
101
147
  /**
148
+ * Internal key used to store voice call state in connection state.
149
+ * Used by the voice mixin to track whether a connection is in an active call.
150
+ */
151
+ const CF_VOICE_IN_CALL_KEY = "_cf_voiceInCall";
152
+ /**
153
+ * Internal key used to remember the outer `/sub/...` URL for a
154
+ * WebSocket accepted by the parent on behalf of a child facet.
155
+ * Hibernated events then wake the parent, which forwards frames to
156
+ * the child over serializable RPC while keeping native WebSocket I/O
157
+ * parent-owned.
158
+ */
159
+ const CF_SUB_AGENT_OUTER_URL_KEY = "_cf_subAgentOuterUrl";
160
+ const CF_SUB_AGENT_TAGS_KEY = "_cf_subAgentTags";
161
+ const SUB_AGENT_OUTER_URL_HEADER = "x-cf-agents-subagent-url";
162
+ /**
102
163
  * The set of all internal keys stored in connection state that must be
103
164
  * hidden from user code and preserved across setState calls.
104
165
  */
105
166
  const CF_INTERNAL_KEYS = new Set([
106
167
  CF_READONLY_KEY,
107
168
  CF_NO_PROTOCOL_KEY,
108
- "_cf_voiceInCall"
169
+ CF_VOICE_IN_CALL_KEY,
170
+ CF_SUB_AGENT_OUTER_URL_KEY,
171
+ CF_SUB_AGENT_TAGS_KEY
109
172
  ]);
110
173
  /** Check if a raw connection state object contains any internal keys. */
111
174
  function rawHasInternalKeys(raw) {
@@ -218,13 +281,13 @@ function getCurrentAgent() {
218
281
  */
219
282
  function withAgentContext(method) {
220
283
  return function(...args) {
221
- const { connection, request, email, agent } = getCurrentAgent();
284
+ const { agent } = getCurrentAgent();
222
285
  if (agent === this) return method.apply(this, args);
223
286
  return __DO_NOT_USE_WILL_BREAK__agentContext.run({
224
287
  agent: this,
225
- connection,
226
- request,
227
- email
288
+ connection: void 0,
289
+ request: void 0,
290
+ email: void 0
228
291
  }, () => {
229
292
  return method.apply(this, args);
230
293
  });
@@ -383,7 +446,9 @@ var Agent = class Agent extends Server {
383
446
  running INTEGER DEFAULT 0,
384
447
  created_at INTEGER DEFAULT (unixepoch()),
385
448
  execution_started_at INTEGER,
386
- retry_options TEXT
449
+ retry_options TEXT,
450
+ owner_path TEXT,
451
+ owner_path_key TEXT
387
452
  )
388
453
  `;
389
454
  const addColumnIfNotExists = (sql) => {
@@ -397,6 +462,8 @@ var Agent = class Agent extends Server {
397
462
  addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN running INTEGER DEFAULT 0");
398
463
  addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN execution_started_at INTEGER");
399
464
  addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN retry_options TEXT");
465
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN owner_path TEXT");
466
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN owner_path_key TEXT");
400
467
  addColumnIfNotExists("ALTER TABLE cf_agents_queues ADD COLUMN retry_options TEXT");
401
468
  {
402
469
  const rows = this.ctx.storage.sql.exec("SELECT sql FROM sqlite_master WHERE type='table' AND name='cf_agents_schedules'").toArray();
@@ -416,15 +483,19 @@ var Agent = class Agent extends Server {
416
483
  running INTEGER DEFAULT 0,
417
484
  created_at INTEGER DEFAULT (unixepoch()),
418
485
  execution_started_at INTEGER,
419
- retry_options TEXT
486
+ retry_options TEXT,
487
+ owner_path TEXT,
488
+ owner_path_key TEXT
420
489
  )
421
490
  `);
422
491
  this.ctx.storage.sql.exec(`
423
492
  INSERT INTO cf_agents_schedules_new
424
493
  (id, callback, payload, type, time, delayInSeconds, cron,
425
- intervalSeconds, running, created_at, execution_started_at, retry_options)
494
+ intervalSeconds, running, created_at, execution_started_at, retry_options,
495
+ owner_path, owner_path_key)
426
496
  SELECT id, callback, payload, type, time, delayInSeconds, cron,
427
- intervalSeconds, running, created_at, execution_started_at, retry_options
497
+ intervalSeconds, running, created_at, execution_started_at, retry_options,
498
+ owner_path, owner_path_key
428
499
  FROM cf_agents_schedules
429
500
  `);
430
501
  this.ctx.storage.sql.exec("DROP TABLE cf_agents_schedules");
@@ -467,6 +538,41 @@ var Agent = class Agent extends Server {
467
538
  )
468
539
  `;
469
540
  this.sql`
541
+ CREATE TABLE IF NOT EXISTS cf_agents_facet_runs (
542
+ owner_path TEXT NOT NULL,
543
+ owner_path_key TEXT NOT NULL,
544
+ run_id TEXT NOT NULL,
545
+ created_at INTEGER NOT NULL,
546
+ PRIMARY KEY (owner_path_key, run_id)
547
+ )
548
+ `;
549
+ this.sql`
550
+ CREATE INDEX IF NOT EXISTS idx_facet_runs_owner_path_key
551
+ ON cf_agents_facet_runs(owner_path_key)
552
+ `;
553
+ this.sql`
554
+ CREATE TABLE IF NOT EXISTS cf_agent_tool_runs (
555
+ run_id TEXT PRIMARY KEY,
556
+ parent_tool_call_id TEXT,
557
+ agent_type TEXT NOT NULL,
558
+ input_preview TEXT,
559
+ input_redacted INTEGER NOT NULL DEFAULT 1,
560
+ status TEXT NOT NULL,
561
+ summary TEXT,
562
+ output_json TEXT,
563
+ error_message TEXT,
564
+ display_metadata TEXT,
565
+ display_order INTEGER NOT NULL DEFAULT 0,
566
+ started_at INTEGER NOT NULL,
567
+ completed_at INTEGER
568
+ )
569
+ `;
570
+ this.sql`
571
+ CREATE INDEX IF NOT EXISTS idx_agent_tool_runs_parent_tool_call_id
572
+ ON cf_agent_tool_runs(parent_tool_call_id, display_order)
573
+ `;
574
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN output_json TEXT");
575
+ this.sql`
470
576
  INSERT OR REPLACE INTO cf_agents_state (id, state)
471
577
  VALUES (${SCHEMA_VERSION_ROW_ID}, ${String(CURRENT_SCHEMA_VERSION)})
472
578
  `;
@@ -480,16 +586,20 @@ var Agent = class Agent extends Server {
480
586
  this._rawStateAccessors = /* @__PURE__ */ new WeakMap();
481
587
  this._persistenceHookMode = "none";
482
588
  this._isFacet = false;
589
+ this._suppressProtocolBroadcasts = false;
590
+ this._cf_virtualSubAgentConnections = /* @__PURE__ */ new Map();
483
591
  this._parentPath = [];
484
592
  this._insideOnStart = false;
485
593
  this._warnedScheduleInOnStart = /* @__PURE__ */ new Set();
486
594
  this._keepAliveRefs = 0;
595
+ this._facetKeepAliveTokens = /* @__PURE__ */ new Set();
487
596
  this._runFiberActiveFibers = /* @__PURE__ */ new Set();
488
597
  this._runFiberRecoveryInProgress = false;
489
598
  this._ParentClass = Object.getPrototypeOf(this).constructor;
490
599
  this.initialState = DEFAULT_STATE;
491
600
  this.observability = genericObservability;
492
601
  this._flushingQueue = false;
602
+ this.maxConcurrentAgentTools = Infinity;
493
603
  this._subAgentRegistryReady = false;
494
604
  if (!wrappedClasses.has(this.constructor)) {
495
605
  this._autoWrapCustomMethods();
@@ -541,6 +651,7 @@ var Agent = class Agent extends Server {
541
651
  };
542
652
  const _onMessage = this.onMessage.bind(this);
543
653
  this.onMessage = async (connection, message) => {
654
+ if (await this._cf_forwardSubAgentWebSocketMessage(connection, message)) return;
544
655
  this._ensureConnectionWrapped(connection);
545
656
  return __DO_NOT_USE_WILL_BREAK__agentContext.run({
546
657
  agent: this,
@@ -632,8 +743,11 @@ var Agent = class Agent extends Server {
632
743
  });
633
744
  };
634
745
  const _onConnect = this.onConnect.bind(this);
635
- this.onConnect = (connection, ctx) => {
746
+ this.onConnect = async (connection, ctx) => {
636
747
  this._ensureConnectionWrapped(connection);
748
+ const subAgentOuterUrl = ctx.request.headers.get(SUB_AGENT_OUTER_URL_HEADER);
749
+ if (subAgentOuterUrl) this._unsafe_setConnectionFlag(connection, CF_SUB_AGENT_OUTER_URL_KEY, subAgentOuterUrl);
750
+ if (await this._cf_forwardSubAgentWebSocketConnect(connection, ctx.request, { gate: false })) return;
637
751
  return __DO_NOT_USE_WILL_BREAK__agentContext.run({
638
752
  agent: this,
639
753
  connection,
@@ -666,11 +780,13 @@ var Agent = class Agent extends Server {
666
780
  }));
667
781
  } else this._setConnectionNoProtocol(connection);
668
782
  this._emit("connect", { connectionId: connection.id });
783
+ await this._replayAgentToolRuns(connection);
669
784
  return this._tryCatch(() => _onConnect(connection, ctx));
670
785
  });
671
786
  };
672
787
  const _onClose = this.onClose.bind(this);
673
- this.onClose = (connection, code, reason, wasClean) => {
788
+ this.onClose = async (connection, code, reason, wasClean) => {
789
+ if (await this._cf_forwardSubAgentWebSocketClose(connection, code, reason, wasClean)) return;
674
790
  return __DO_NOT_USE_WILL_BREAK__agentContext.run({
675
791
  agent: this,
676
792
  connection,
@@ -696,12 +812,18 @@ var Agent = class Agent extends Server {
696
812
  if (await this.ctx.storage.get("cf_agents_is_facet")) this._isFacet = true;
697
813
  const storedParentPath = await this.ctx.storage.get("cf_agents_parent_path");
698
814
  if (isValidParentPath(storedParentPath)) this._parentPath = storedParentPath;
815
+ try {
816
+ await this._cf_hydrateSubAgentConnectionsFromRoot();
817
+ } catch (error) {
818
+ console.warn("[Agent] Unable to hydrate sub-agent WebSocket connections:", error);
819
+ }
699
820
  await this._tryCatch(async () => {
700
821
  await this.mcp.restoreConnectionsFromStorage(this.name);
701
822
  await this._restoreRpcMcpServers();
702
823
  this.broadcastMcpServers();
703
824
  this._checkOrphanedWorkflows();
704
825
  await this._checkRunFibers();
826
+ await this._reconcileAgentToolRuns();
705
827
  this._insideOnStart = true;
706
828
  this._warnedScheduleInOnStart.clear();
707
829
  try {
@@ -743,6 +865,7 @@ var Agent = class Agent extends Server {
743
865
  * @param excludeIds Additional connection IDs to exclude (e.g. the source)
744
866
  */
745
867
  _broadcastProtocol(msg, excludeIds = []) {
868
+ if (this._suppressProtocolBroadcasts) return;
746
869
  const exclude = [...excludeIds];
747
870
  for (const conn of this.getConnections()) if (!this.isConnectionProtocolEnabled(conn)) exclude.push(conn.id);
748
871
  this.broadcast(msg, exclude);
@@ -1374,39 +1497,60 @@ var Agent = class Agent extends Server {
1374
1497
  retry: parseRetryOptions(row)
1375
1498
  }));
1376
1499
  }
1377
- /**
1378
- * Schedule a task to be executed in the future
1379
- *
1380
- * Cron schedules are **idempotent by default** — calling `schedule("0 * * * *", "tick")`
1381
- * multiple times with the same callback, cron expression, and payload returns
1382
- * the existing schedule instead of creating a duplicate. Set `idempotent: false`
1383
- * to override this.
1384
- *
1385
- * For delayed and scheduled (Date) types, set `idempotent: true` to opt in
1386
- * to the same dedup behavior (matched on callback + payload). This is useful
1387
- * when calling `schedule()` in `onStart()` to avoid accumulating duplicate
1388
- * rows across Durable Object restarts.
1389
- *
1390
- * @template T Type of the payload data
1391
- * @param when When to execute the task (Date, seconds delay, or cron expression)
1392
- * @param callback Name of the method to call
1393
- * @param payload Data to pass to the callback
1394
- * @param options Options for the scheduled task
1395
- * @param options.retry Retry options for the callback execution
1396
- * @param options.idempotent Dedup by callback+payload. Defaults to `true` for cron, `false` otherwise.
1397
- * @returns Schedule object representing the scheduled task
1398
- */
1399
- async schedule(when, callback, payload, options) {
1400
- if (this._isFacet) throw new Error("Scheduling is not supported in sub-agents. Schedule from the parent agent instead.");
1500
+ _scheduleOwnerPathKey(path) {
1501
+ if (!path) return null;
1502
+ return path.map((step) => `${encodeURIComponent(step.className)}:${encodeURIComponent(step.name)}`).join("/");
1503
+ }
1504
+ _facetRunRowsForPrefix(ownerPath) {
1505
+ return this.sql`
1506
+ SELECT owner_path, owner_path_key, run_id, created_at
1507
+ FROM cf_agents_facet_runs
1508
+ `.filter((row) => {
1509
+ try {
1510
+ const rowOwnerPath = JSON.parse(row.owner_path);
1511
+ return this._isSameAgentPathPrefix(ownerPath, rowOwnerPath);
1512
+ } catch {
1513
+ return false;
1514
+ }
1515
+ });
1516
+ }
1517
+ _deleteFacetRunRowsForPrefix(ownerPath) {
1518
+ for (const row of this._facetRunRowsForPrefix(ownerPath)) this.sql`
1519
+ DELETE FROM cf_agents_facet_runs
1520
+ WHERE owner_path_key = ${row.owner_path_key}
1521
+ AND run_id = ${row.run_id}
1522
+ `;
1523
+ }
1524
+ async _rootAlarmOwner() {
1525
+ const root = this._parentPath[0];
1526
+ if (!root) throw new Error("Facet scheduler delegation requires a root parent.");
1527
+ const binding = this.ctx.exports?.[root.className];
1528
+ if (!binding) throw new Error(`Unable to resolve root scheduler "${root.className}" for sub-agent schedule delegation.`);
1529
+ return await getServerByName(binding, root.name);
1530
+ }
1531
+ _validateScheduleCallback(when, callback, options) {
1401
1532
  if (typeof callback !== "string") throw new Error("Callback must be a string");
1402
1533
  if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1403
1534
  if (options?.retry) validateRetryOptions(options.retry, this._resolvedOptions.retry);
1404
- const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1405
- const payloadJson = JSON.stringify(payload);
1406
1535
  if (this._insideOnStart && options?.idempotent === void 0 && typeof when !== "string" && !this._warnedScheduleInOnStart.has(callback)) {
1407
1536
  this._warnedScheduleInOnStart.add(callback);
1408
1537
  console.warn(`schedule("${callback}") called inside onStart() without { idempotent: true }. This creates a new row on every Durable Object restart, which can cause duplicate executions. Pass { idempotent: true } to deduplicate, or use scheduleEvery() for recurring tasks.`);
1409
1538
  }
1539
+ }
1540
+ /**
1541
+ * Insert (or, for idempotent calls, return the existing row for) a
1542
+ * schedule owned by either this top-level agent (`ownerPath === null`)
1543
+ * or a descendant facet. Returns `{ schedule, created }` — `created`
1544
+ * is `false` when an idempotent insert deduplicates onto an existing
1545
+ * row, so callers can suppress the `schedule:create` event in that
1546
+ * case to match historic semantics.
1547
+ * @internal
1548
+ */
1549
+ async _insertScheduleForOwner(ownerPath, when, callback, payload, options) {
1550
+ const ownerPathJson = ownerPath ? JSON.stringify(ownerPath) : null;
1551
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1552
+ const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1553
+ const payloadJson = JSON.stringify(payload);
1410
1554
  if (when instanceof Date) {
1411
1555
  const timestamp = Math.floor(when.getTime() / 1e3);
1412
1556
  if (options?.idempotent) {
@@ -1415,90 +1559,96 @@ var Agent = class Agent extends Server {
1415
1559
  WHERE type = 'scheduled'
1416
1560
  AND callback = ${callback}
1417
1561
  AND payload IS ${payloadJson}
1562
+ AND owner_path_key IS ${ownerPathKey}
1418
1563
  LIMIT 1
1419
1564
  `;
1420
1565
  if (existing.length > 0) {
1421
1566
  const row = existing[0];
1422
1567
  await this._scheduleNextAlarm();
1423
1568
  return {
1424
- callback: row.callback,
1425
- id: row.id,
1426
- payload: JSON.parse(row.payload),
1427
- retry: parseRetryOptions(row),
1428
- time: row.time,
1429
- type: "scheduled"
1569
+ schedule: {
1570
+ callback: row.callback,
1571
+ id: row.id,
1572
+ payload: JSON.parse(row.payload),
1573
+ retry: parseRetryOptions(row),
1574
+ time: row.time,
1575
+ type: "scheduled"
1576
+ },
1577
+ created: false
1430
1578
  };
1431
1579
  }
1432
1580
  }
1433
1581
  const id = nanoid(9);
1434
1582
  this.sql`
1435
- INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, time, retry_options)
1436
- VALUES (${id}, ${callback}, ${payloadJson}, 'scheduled', ${timestamp}, ${retryJson})
1583
+ INSERT OR REPLACE INTO cf_agents_schedules
1584
+ (id, callback, payload, type, time, retry_options, owner_path, owner_path_key)
1585
+ VALUES
1586
+ (${id}, ${callback}, ${payloadJson}, 'scheduled', ${timestamp}, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1437
1587
  `;
1438
1588
  await this._scheduleNextAlarm();
1439
- const schedule = {
1440
- callback,
1441
- id,
1442
- payload,
1443
- retry: options?.retry,
1444
- time: timestamp,
1445
- type: "scheduled"
1589
+ return {
1590
+ schedule: {
1591
+ callback,
1592
+ id,
1593
+ payload,
1594
+ retry: options?.retry,
1595
+ time: timestamp,
1596
+ type: "scheduled"
1597
+ },
1598
+ created: true
1446
1599
  };
1447
- this._emit("schedule:create", {
1448
- callback,
1449
- id
1450
- });
1451
- return schedule;
1452
1600
  }
1453
1601
  if (typeof when === "number") {
1454
- const time = new Date(Date.now() + when * 1e3);
1455
- const timestamp = Math.floor(time.getTime() / 1e3);
1602
+ const timestamp = Math.floor((Date.now() + when * 1e3) / 1e3);
1456
1603
  if (options?.idempotent) {
1457
1604
  const existing = this.sql`
1458
1605
  SELECT * FROM cf_agents_schedules
1459
1606
  WHERE type = 'delayed'
1460
1607
  AND callback = ${callback}
1461
1608
  AND payload IS ${payloadJson}
1609
+ AND owner_path_key IS ${ownerPathKey}
1462
1610
  LIMIT 1
1463
1611
  `;
1464
1612
  if (existing.length > 0) {
1465
1613
  const row = existing[0];
1466
1614
  await this._scheduleNextAlarm();
1467
1615
  return {
1468
- callback: row.callback,
1469
- delayInSeconds: row.delayInSeconds,
1470
- id: row.id,
1471
- payload: JSON.parse(row.payload),
1472
- retry: parseRetryOptions(row),
1473
- time: row.time,
1474
- type: "delayed"
1616
+ schedule: {
1617
+ callback: row.callback,
1618
+ delayInSeconds: row.delayInSeconds ?? 0,
1619
+ id: row.id,
1620
+ payload: JSON.parse(row.payload),
1621
+ retry: parseRetryOptions(row),
1622
+ time: row.time,
1623
+ type: "delayed"
1624
+ },
1625
+ created: false
1475
1626
  };
1476
1627
  }
1477
1628
  }
1478
1629
  const id = nanoid(9);
1479
1630
  this.sql`
1480
- INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, delayInSeconds, time, retry_options)
1481
- VALUES (${id}, ${callback}, ${payloadJson}, 'delayed', ${when}, ${timestamp}, ${retryJson})
1631
+ INSERT OR REPLACE INTO cf_agents_schedules
1632
+ (id, callback, payload, type, delayInSeconds, time, retry_options, owner_path, owner_path_key)
1633
+ VALUES
1634
+ (${id}, ${callback}, ${payloadJson}, 'delayed', ${when}, ${timestamp}, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1482
1635
  `;
1483
1636
  await this._scheduleNextAlarm();
1484
- const schedule = {
1485
- callback,
1486
- delayInSeconds: when,
1487
- id,
1488
- payload,
1489
- retry: options?.retry,
1490
- time: timestamp,
1491
- type: "delayed"
1637
+ return {
1638
+ schedule: {
1639
+ callback,
1640
+ delayInSeconds: when,
1641
+ id,
1642
+ payload,
1643
+ retry: options?.retry,
1644
+ time: timestamp,
1645
+ type: "delayed"
1646
+ },
1647
+ created: true
1492
1648
  };
1493
- this._emit("schedule:create", {
1494
- callback,
1495
- id
1496
- });
1497
- return schedule;
1498
1649
  }
1499
1650
  if (typeof when === "string") {
1500
- const nextExecutionTime = getNextCronTime(when);
1501
- const timestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
1651
+ const timestamp = Math.floor(getNextCronTime(when).getTime() / 1e3);
1502
1652
  if (options?.idempotent !== false) {
1503
1653
  const existing = this.sql`
1504
1654
  SELECT * FROM cf_agents_schedules
@@ -1506,79 +1656,70 @@ var Agent = class Agent extends Server {
1506
1656
  AND callback = ${callback}
1507
1657
  AND cron = ${when}
1508
1658
  AND payload IS ${payloadJson}
1659
+ AND owner_path_key IS ${ownerPathKey}
1509
1660
  LIMIT 1
1510
1661
  `;
1511
1662
  if (existing.length > 0) {
1512
1663
  const row = existing[0];
1513
1664
  await this._scheduleNextAlarm();
1514
1665
  return {
1515
- callback: row.callback,
1516
- cron: row.cron,
1517
- id: row.id,
1518
- payload: JSON.parse(row.payload),
1519
- retry: parseRetryOptions(row),
1520
- time: row.time,
1521
- type: "cron"
1666
+ schedule: {
1667
+ callback: row.callback,
1668
+ cron: row.cron ?? when,
1669
+ id: row.id,
1670
+ payload: JSON.parse(row.payload),
1671
+ retry: parseRetryOptions(row),
1672
+ time: row.time,
1673
+ type: "cron"
1674
+ },
1675
+ created: false
1522
1676
  };
1523
1677
  }
1524
1678
  }
1525
1679
  const id = nanoid(9);
1526
1680
  this.sql`
1527
- INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, cron, time, retry_options)
1528
- VALUES (${id}, ${callback}, ${payloadJson}, 'cron', ${when}, ${timestamp}, ${retryJson})
1681
+ INSERT OR REPLACE INTO cf_agents_schedules
1682
+ (id, callback, payload, type, cron, time, retry_options, owner_path, owner_path_key)
1683
+ VALUES
1684
+ (${id}, ${callback}, ${payloadJson}, 'cron', ${when}, ${timestamp}, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1529
1685
  `;
1530
1686
  await this._scheduleNextAlarm();
1531
- const schedule = {
1532
- callback,
1533
- cron: when,
1534
- id,
1535
- payload,
1536
- retry: options?.retry,
1537
- time: timestamp,
1538
- type: "cron"
1687
+ return {
1688
+ schedule: {
1689
+ callback,
1690
+ cron: when,
1691
+ id,
1692
+ payload,
1693
+ retry: options?.retry,
1694
+ time: timestamp,
1695
+ type: "cron"
1696
+ },
1697
+ created: true
1539
1698
  };
1540
- this._emit("schedule:create", {
1541
- callback,
1542
- id
1543
- });
1544
- return schedule;
1545
1699
  }
1546
1700
  throw new Error(`Invalid schedule type: ${JSON.stringify(when)}(${typeof when}) trying to schedule ${callback}`);
1547
1701
  }
1548
1702
  /**
1549
- * Schedule a task to run repeatedly at a fixed interval.
1550
- *
1551
- * This method is **idempotent** calling it multiple times with the same
1552
- * `callback`, `intervalSeconds`, and `payload` returns the existing schedule
1553
- * instead of creating a duplicate. A different interval or payload is
1554
- * treated as a distinct schedule and creates a new row.
1555
- *
1556
- * This makes it safe to call in `onStart()`, which runs on every Durable
1557
- * Object wake:
1558
- *
1559
- * ```ts
1560
- * async onStart() {
1561
- * // Only one schedule is created, no matter how many times the DO wakes
1562
- * await this.scheduleEvery(30, "tick");
1563
- * }
1564
- * ```
1565
- *
1566
- * @template T Type of the payload data
1567
- * @param intervalSeconds Number of seconds between executions
1568
- * @param callback Name of the method to call
1569
- * @param payload Data to pass to the callback
1570
- * @param options Options for the scheduled task
1571
- * @param options.retry Retry options for the callback execution
1572
- * @returns Schedule object representing the scheduled task
1703
+ * Insert a schedule row owned by a descendant facet. Called via RPC
1704
+ * from the facet's `schedule()`. Returns `{ schedule, created }`
1705
+ * so the originating facet can suppress `schedule:create` on
1706
+ * idempotent dedup. This method does not emit observability
1707
+ * events itself.
1708
+ * @internal
1573
1709
  */
1574
- async scheduleEvery(intervalSeconds, callback, payload, options) {
1575
- if (this._isFacet) throw new Error("Scheduling is not supported in sub-agents. Schedule from the parent agent instead.");
1576
- const MAX_INTERVAL_SECONDS = 720 * 60 * 60;
1577
- if (typeof intervalSeconds !== "number" || intervalSeconds <= 0) throw new Error("intervalSeconds must be a positive number");
1578
- if (intervalSeconds > MAX_INTERVAL_SECONDS) throw new Error(`intervalSeconds cannot exceed ${MAX_INTERVAL_SECONDS} seconds (30 days)`);
1579
- if (typeof callback !== "string") throw new Error("Callback must be a string");
1580
- if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1581
- if (options?.retry) validateRetryOptions(options.retry, this._resolvedOptions.retry);
1710
+ async _cf_scheduleForFacet(ownerPath, when, callback, payload, options) {
1711
+ return this._insertScheduleForOwner(ownerPath, when, callback, payload, options);
1712
+ }
1713
+ /**
1714
+ * Insert (or, for idempotent calls, return the existing row for) an
1715
+ * interval schedule. Mirrors {@link _insertScheduleForOwner}
1716
+ * returns `{ schedule, created }` so callers can suppress
1717
+ * `schedule:create` on dedup.
1718
+ * @internal
1719
+ */
1720
+ async _insertIntervalScheduleForOwner(ownerPath, intervalSeconds, callback, payload, options) {
1721
+ const ownerPathJson = ownerPath ? JSON.stringify(ownerPath) : null;
1722
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1582
1723
  const idempotent = options?._idempotent !== false;
1583
1724
  const payloadJson = JSON.stringify(payload);
1584
1725
  if (idempotent) {
@@ -1588,73 +1729,165 @@ var Agent = class Agent extends Server {
1588
1729
  AND callback = ${callback}
1589
1730
  AND intervalSeconds = ${intervalSeconds}
1590
1731
  AND payload IS ${payloadJson}
1732
+ AND owner_path_key IS ${ownerPathKey}
1591
1733
  LIMIT 1
1592
1734
  `;
1593
1735
  if (existing.length > 0) {
1594
1736
  const row = existing[0];
1595
1737
  await this._scheduleNextAlarm();
1596
1738
  return {
1597
- callback: row.callback,
1598
- id: row.id,
1599
- intervalSeconds: row.intervalSeconds,
1600
- payload: JSON.parse(row.payload),
1601
- retry: parseRetryOptions(row),
1602
- time: row.time,
1603
- type: "interval"
1739
+ schedule: {
1740
+ callback: row.callback,
1741
+ id: row.id,
1742
+ intervalSeconds: row.intervalSeconds ?? intervalSeconds,
1743
+ payload: JSON.parse(row.payload),
1744
+ retry: parseRetryOptions(row),
1745
+ time: row.time,
1746
+ type: "interval"
1747
+ },
1748
+ created: false
1604
1749
  };
1605
1750
  }
1606
1751
  }
1607
1752
  const id = nanoid(9);
1608
- const time = new Date(Date.now() + intervalSeconds * 1e3);
1609
- const timestamp = Math.floor(time.getTime() / 1e3);
1753
+ const timestamp = Math.floor((Date.now() + intervalSeconds * 1e3) / 1e3);
1610
1754
  const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1611
1755
  this.sql`
1612
- INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, intervalSeconds, time, running, retry_options)
1613
- VALUES (${id}, ${callback}, ${payloadJson}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson})
1756
+ INSERT OR REPLACE INTO cf_agents_schedules
1757
+ (id, callback, payload, type, intervalSeconds, time, running, retry_options, owner_path, owner_path_key)
1758
+ VALUES
1759
+ (${id}, ${callback}, ${payloadJson}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1614
1760
  `;
1615
1761
  await this._scheduleNextAlarm();
1616
- const schedule = {
1617
- callback,
1618
- id,
1619
- intervalSeconds,
1620
- payload,
1621
- retry: options?.retry,
1622
- time: timestamp,
1623
- type: "interval"
1762
+ return {
1763
+ schedule: {
1764
+ callback,
1765
+ id,
1766
+ intervalSeconds,
1767
+ payload,
1768
+ retry: options?.retry,
1769
+ time: timestamp,
1770
+ type: "interval"
1771
+ },
1772
+ created: true
1624
1773
  };
1625
- this._emit("schedule:create", {
1626
- callback,
1627
- id
1628
- });
1629
- return schedule;
1630
1774
  }
1631
1775
  /**
1632
- * Get a scheduled task by ID
1633
- * @template T Type of the payload data
1634
- * @param id ID of the scheduled task
1635
- * @returns The Schedule object or undefined if not found
1776
+ * Insert an interval schedule row owned by a descendant facet.
1777
+ * Called via RPC from the facet's `scheduleEvery()`. Returns
1778
+ * `{ schedule, created }` so the originating facet can suppress
1779
+ * `schedule:create` on idempotent dedup. This method does not
1780
+ * emit observability events itself.
1781
+ * @internal
1636
1782
  */
1637
- getSchedule(id) {
1783
+ async _cf_scheduleEveryForFacet(ownerPath, intervalSeconds, callback, payload, options) {
1784
+ return this._insertIntervalScheduleForOwner(ownerPath, intervalSeconds, callback, payload, options);
1785
+ }
1786
+ /**
1787
+ * Cancel a schedule row owned by a descendant facet, scoped by
1788
+ * `owner_path_key` so siblings can't reach each other's rows.
1789
+ * Returns the canceled row's callback name so the originating
1790
+ * facet can emit `schedule:cancel`. This method does not emit
1791
+ * observability events itself.
1792
+ * @internal
1793
+ */
1794
+ async _cf_cancelScheduleForFacet(ownerPath, id) {
1795
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1638
1796
  const result = this.sql`
1639
- SELECT * FROM cf_agents_schedules WHERE id = ${id}
1797
+ SELECT * FROM cf_agents_schedules
1798
+ WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
1640
1799
  `;
1641
- if (!result || result.length === 0) return;
1642
- const row = result[0];
1800
+ if (result.length === 0) return { ok: false };
1801
+ const callback = result[0].callback;
1802
+ this.sql`
1803
+ DELETE FROM cf_agents_schedules
1804
+ WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
1805
+ `;
1806
+ await this._scheduleNextAlarm();
1643
1807
  return {
1644
- ...row,
1645
- payload: JSON.parse(row.payload),
1646
- retry: parseRetryOptions(row)
1808
+ ok: true,
1809
+ callback
1647
1810
  };
1648
1811
  }
1649
1812
  /**
1650
- * Get scheduled tasks matching the given criteria
1651
- * @template T Type of the payload data
1652
- * @param criteria Criteria to filter schedules
1653
- * @returns Array of matching Schedule objects
1813
+ * Clean root-owned bookkeeping for a sub-tree of facets. This
1814
+ * bulk-cancels schedules whose `owner_path` starts with the given
1815
+ * prefix and deletes root-side facet fiber recovery leases for the
1816
+ * same sub-tree. Used by `deleteSubAgent` and recursive facet
1817
+ * destroy. Emits `schedule:cancel` on this agent (the alarm-owning
1818
+ * root) for each schedule row removed — the facets being torn down
1819
+ * may not be alive to receive the events themselves.
1820
+ * @internal
1654
1821
  */
1655
- getSchedules(criteria = {}) {
1656
- let query = "SELECT * FROM cf_agents_schedules WHERE 1=1";
1657
- const params = [];
1822
+ async _cf_cleanupFacetPrefix(ownerPath) {
1823
+ const rowsToDelete = this.sql`
1824
+ SELECT * FROM cf_agents_schedules
1825
+ WHERE owner_path IS NOT NULL
1826
+ `.filter((row) => {
1827
+ if (!row.owner_path) return false;
1828
+ try {
1829
+ const rowOwnerPath = JSON.parse(row.owner_path);
1830
+ return this._isSameAgentPathPrefix(ownerPath, rowOwnerPath);
1831
+ } catch {
1832
+ return false;
1833
+ }
1834
+ });
1835
+ for (const row of rowsToDelete) {
1836
+ this._emit("schedule:cancel", {
1837
+ callback: row.callback,
1838
+ id: row.id
1839
+ });
1840
+ this.sql`DELETE FROM cf_agents_schedules WHERE id = ${row.id}`;
1841
+ }
1842
+ this._deleteFacetRunRowsForPrefix(ownerPath);
1843
+ await this._scheduleNextAlarm();
1844
+ }
1845
+ _scheduleRowToSchedule(row) {
1846
+ const base = {
1847
+ callback: row.callback,
1848
+ id: row.id,
1849
+ payload: JSON.parse(row.payload),
1850
+ retry: parseRetryOptions(row)
1851
+ };
1852
+ switch (row.type) {
1853
+ case "scheduled": return {
1854
+ ...base,
1855
+ time: row.time,
1856
+ type: "scheduled"
1857
+ };
1858
+ case "delayed": return {
1859
+ ...base,
1860
+ delayInSeconds: row.delayInSeconds ?? 0,
1861
+ time: row.time,
1862
+ type: "delayed"
1863
+ };
1864
+ case "cron": return {
1865
+ ...base,
1866
+ cron: row.cron ?? "",
1867
+ time: row.time,
1868
+ type: "cron"
1869
+ };
1870
+ case "interval": return {
1871
+ ...base,
1872
+ intervalSeconds: row.intervalSeconds ?? 0,
1873
+ time: row.time,
1874
+ type: "interval"
1875
+ };
1876
+ }
1877
+ }
1878
+ _getScheduleForOwner(ownerPath, id) {
1879
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1880
+ const result = this.sql`
1881
+ SELECT * FROM cf_agents_schedules
1882
+ WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
1883
+ `;
1884
+ if (!result || result.length === 0) return;
1885
+ return this._scheduleRowToSchedule(result[0]);
1886
+ }
1887
+ _listSchedulesForOwner(ownerPath, criteria = {}) {
1888
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1889
+ let query = "SELECT * FROM cf_agents_schedules WHERE owner_path_key IS ?";
1890
+ const params = [ownerPathKey];
1658
1891
  if (criteria.id) {
1659
1892
  query += " AND id = ?";
1660
1893
  params.push(criteria.id);
@@ -1669,44 +1902,245 @@ var Agent = class Agent extends Server {
1669
1902
  const end = criteria.timeRange.end || /* @__PURE__ */ new Date(999999999999999);
1670
1903
  params.push(Math.floor(start.getTime() / 1e3), Math.floor(end.getTime() / 1e3));
1671
1904
  }
1672
- return this.ctx.storage.sql.exec(query, ...params).toArray().map((row) => ({
1673
- ...row,
1674
- payload: JSON.parse(row.payload),
1675
- retry: parseRetryOptions(row)
1676
- }));
1905
+ return this.ctx.storage.sql.exec(query, ...params).toArray().map((row) => this._scheduleRowToSchedule(row));
1677
1906
  }
1678
1907
  /**
1679
- * Cancel a scheduled task
1680
- * @param id ID of the task to cancel
1681
- * @returns true if the task was cancelled, false if the task was not found
1908
+ * Read a single schedule row owned by a descendant facet.
1909
+ * @internal
1682
1910
  */
1683
- async cancelSchedule(id) {
1684
- if (this._isFacet) throw new Error("Scheduling is not supported in sub-agents. Schedule from the parent agent instead.");
1685
- const schedule = this.getSchedule(id);
1686
- if (!schedule) return false;
1687
- this._emit("schedule:cancel", {
1688
- callback: schedule.callback,
1689
- id: schedule.id
1690
- });
1691
- this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
1692
- await this._scheduleNextAlarm();
1693
- return true;
1911
+ async _cf_getScheduleForFacet(ownerPath, id) {
1912
+ return this._getScheduleForOwner(ownerPath, id);
1694
1913
  }
1695
1914
  /**
1696
- * Keep the Durable Object alive via alarm heartbeats.
1697
- * Returns a disposer function that stops the heartbeat when called.
1698
- *
1699
- * Use this when you have long-running work and need to prevent the
1700
- * DO from going idle (eviction after ~70-140s of inactivity).
1701
- * The heartbeat fires every `keepAliveIntervalMs` (default 30s) via the
1915
+ * List schedule rows owned by a descendant facet, scoped by
1916
+ * `owner_path_key` so siblings remain isolated from each other.
1917
+ * @internal
1918
+ */
1919
+ async _cf_listSchedulesForFacet(ownerPath, criteria = {}) {
1920
+ return this._listSchedulesForOwner(ownerPath, criteria);
1921
+ }
1922
+ /**
1923
+ * Acquire a root-owned keepAlive ref on behalf of a descendant facet.
1924
+ * Facets share the root isolate but cannot set their own physical
1925
+ * alarm, so this lets facet work use the root alarm heartbeat.
1926
+ * @internal
1927
+ */
1928
+ async _cf_acquireFacetKeepAlive(ownerPath) {
1929
+ const token = `${this._scheduleOwnerPathKey(ownerPath) ?? "unknown"}:${nanoid(9)}`;
1930
+ this._facetKeepAliveTokens.add(token);
1931
+ this._keepAliveRefs++;
1932
+ if (this._keepAliveRefs === 1) await this._scheduleNextAlarm();
1933
+ return token;
1934
+ }
1935
+ /**
1936
+ * Release a root-owned keepAlive ref previously acquired for a facet.
1937
+ * Idempotent so disposer calls can safely race or run twice.
1938
+ * @internal
1939
+ */
1940
+ async _cf_releaseFacetKeepAlive(token) {
1941
+ if (!this._facetKeepAliveTokens.delete(token)) return;
1942
+ this._keepAliveRefs = Math.max(0, this._keepAliveRefs - 1);
1943
+ await this._scheduleNextAlarm();
1944
+ }
1945
+ /**
1946
+ * Register a facet's durable run row in the root-side index so root
1947
+ * alarm housekeeping can dispatch recovery checks into idle facets.
1948
+ * The facet remains authoritative for snapshots and recovery hooks.
1949
+ * @internal
1950
+ */
1951
+ async _cf_registerFacetRun(ownerPath, runId) {
1952
+ const ownerPathJson = JSON.stringify(ownerPath);
1953
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1954
+ if (!ownerPathKey) throw new Error("_cf_registerFacetRun requires a non-empty owner path.");
1955
+ this.sql`
1956
+ INSERT OR REPLACE INTO cf_agents_facet_runs
1957
+ (owner_path, owner_path_key, run_id, created_at)
1958
+ VALUES
1959
+ (${ownerPathJson}, ${ownerPathKey}, ${runId}, ${Date.now()})
1960
+ `;
1961
+ await this._scheduleNextAlarm();
1962
+ }
1963
+ /**
1964
+ * Remove a completed facet fiber from the root-side index.
1965
+ * @internal
1966
+ */
1967
+ async _cf_unregisterFacetRun(ownerPath, runId) {
1968
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1969
+ this.sql`
1970
+ DELETE FROM cf_agents_facet_runs
1971
+ WHERE owner_path_key IS ${ownerPathKey}
1972
+ AND run_id = ${runId}
1973
+ `;
1974
+ await this._scheduleNextAlarm();
1975
+ }
1976
+ /**
1977
+ * Schedule a task to be executed in the future
1978
+ *
1979
+ * Cron schedules are **idempotent by default** — calling `schedule("0 * * * *", "tick")`
1980
+ * multiple times with the same callback, cron expression, and payload returns
1981
+ * the existing schedule instead of creating a duplicate. Set `idempotent: false`
1982
+ * to override this.
1983
+ *
1984
+ * For delayed and scheduled (Date) types, set `idempotent: true` to opt in
1985
+ * to the same dedup behavior (matched on callback + payload). This is useful
1986
+ * when calling `schedule()` in `onStart()` to avoid accumulating duplicate
1987
+ * rows across Durable Object restarts.
1988
+ *
1989
+ * @template T Type of the payload data
1990
+ * @param when When to execute the task (Date, seconds delay, or cron expression)
1991
+ * @param callback Name of the method to call
1992
+ * @param payload Data to pass to the callback
1993
+ * @param options Options for the scheduled task
1994
+ * @param options.retry Retry options for the callback execution
1995
+ * @param options.idempotent Dedup by callback+payload. Defaults to `true` for cron, `false` otherwise.
1996
+ * @returns Schedule object representing the scheduled task
1997
+ */
1998
+ async schedule(when, callback, payload, options) {
1999
+ this._validateScheduleCallback(when, callback, options);
2000
+ const result = this._isFacet ? await (await this._rootAlarmOwner())._cf_scheduleForFacet(this.selfPath, when, callback, payload, options) : await this._insertScheduleForOwner(null, when, callback, payload, options);
2001
+ if (result.created) this._emit("schedule:create", {
2002
+ callback: result.schedule.callback,
2003
+ id: result.schedule.id
2004
+ });
2005
+ return result.schedule;
2006
+ }
2007
+ /**
2008
+ * Schedule a task to run repeatedly at a fixed interval.
2009
+ *
2010
+ * This method is **idempotent** — calling it multiple times with the same
2011
+ * `callback`, `intervalSeconds`, and `payload` returns the existing schedule
2012
+ * instead of creating a duplicate. A different interval or payload is
2013
+ * treated as a distinct schedule and creates a new row.
2014
+ *
2015
+ * This makes it safe to call in `onStart()`, which runs on every Durable
2016
+ * Object wake:
2017
+ *
2018
+ * ```ts
2019
+ * async onStart() {
2020
+ * // Only one schedule is created, no matter how many times the DO wakes
2021
+ * await this.scheduleEvery(30, "tick");
2022
+ * }
2023
+ * ```
2024
+ *
2025
+ * @template T Type of the payload data
2026
+ * @param intervalSeconds Number of seconds between executions
2027
+ * @param callback Name of the method to call
2028
+ * @param payload Data to pass to the callback
2029
+ * @param options Options for the scheduled task
2030
+ * @param options.retry Retry options for the callback execution
2031
+ * @returns Schedule object representing the scheduled task
2032
+ */
2033
+ async scheduleEvery(intervalSeconds, callback, payload, options) {
2034
+ const MAX_INTERVAL_SECONDS = 720 * 60 * 60;
2035
+ if (typeof intervalSeconds !== "number" || intervalSeconds <= 0) throw new Error("intervalSeconds must be a positive number");
2036
+ if (intervalSeconds > MAX_INTERVAL_SECONDS) throw new Error(`intervalSeconds cannot exceed ${MAX_INTERVAL_SECONDS} seconds (30 days)`);
2037
+ if (typeof callback !== "string") throw new Error("Callback must be a string");
2038
+ if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
2039
+ if (options?.retry) validateRetryOptions(options.retry, this._resolvedOptions.retry);
2040
+ const result = this._isFacet ? await (await this._rootAlarmOwner())._cf_scheduleEveryForFacet(this.selfPath, intervalSeconds, callback, payload, options) : await this._insertIntervalScheduleForOwner(null, intervalSeconds, callback, payload, options);
2041
+ if (result.created) this._emit("schedule:create", {
2042
+ callback: result.schedule.callback,
2043
+ id: result.schedule.id
2044
+ });
2045
+ return result.schedule;
2046
+ }
2047
+ /**
2048
+ * Get a scheduled task by ID
2049
+ * @template T Type of the payload data
2050
+ * @param id ID of the scheduled task
2051
+ * @returns The Schedule object or undefined if not found
2052
+ * @deprecated Use {@link getScheduleById}. This synchronous API cannot cross
2053
+ * Durable Object boundaries and throws inside sub-agents.
2054
+ */
2055
+ getSchedule(id) {
2056
+ if (this._isFacet) throw new Error("getSchedule() is synchronous and cannot read parent-owned sub-agent schedules. Use await this.getScheduleById(id) instead.");
2057
+ return this._getScheduleForOwner(null, id);
2058
+ }
2059
+ /**
2060
+ * Get a scheduled task by ID.
2061
+ *
2062
+ * Unlike the deprecated synchronous {@link getSchedule}, this works inside
2063
+ * sub-agents by delegating to the top-level parent that owns the alarm.
2064
+ *
2065
+ * @template T Type of the payload data
2066
+ * @param id ID of the scheduled task
2067
+ * @returns The Schedule object or undefined if not found
2068
+ */
2069
+ async getScheduleById(id) {
2070
+ if (this._isFacet) return (await this._rootAlarmOwner())._cf_getScheduleForFacet(this.selfPath, id);
2071
+ return this._getScheduleForOwner(null, id);
2072
+ }
2073
+ /**
2074
+ * Get scheduled tasks matching the given criteria
2075
+ * @template T Type of the payload data
2076
+ * @param criteria Criteria to filter schedules
2077
+ * @returns Array of matching Schedule objects
2078
+ * @deprecated Use {@link listSchedules}. This synchronous API cannot cross
2079
+ * Durable Object boundaries and throws inside sub-agents.
2080
+ */
2081
+ getSchedules(criteria = {}) {
2082
+ if (this._isFacet) throw new Error("getSchedules() is synchronous and cannot read parent-owned sub-agent schedules. Use await this.listSchedules(criteria) instead.");
2083
+ return this._listSchedulesForOwner(null, criteria);
2084
+ }
2085
+ /**
2086
+ * List scheduled tasks matching the given criteria.
2087
+ *
2088
+ * Unlike the deprecated synchronous {@link getSchedules}, this works inside
2089
+ * sub-agents by delegating to the top-level parent that owns the alarm.
2090
+ *
2091
+ * @template T Type of the payload data
2092
+ * @param criteria Criteria to filter schedules
2093
+ * @returns Array of matching Schedule objects
2094
+ */
2095
+ async listSchedules(criteria = {}) {
2096
+ if (this._isFacet) return (await this._rootAlarmOwner())._cf_listSchedulesForFacet(this.selfPath, criteria);
2097
+ return this._listSchedulesForOwner(null, criteria);
2098
+ }
2099
+ /**
2100
+ * Cancel a scheduled task.
2101
+ *
2102
+ * Schedules are isolated by owner: a top-level agent's
2103
+ * `cancelSchedule(id)` only matches its own schedules, and a
2104
+ * sub-agent's `cancelSchedule(id)` only matches schedules it
2105
+ * created. To clear every schedule under a sub-agent (and its
2106
+ * descendants), call `parent.deleteSubAgent(Cls, name)` from the
2107
+ * parent — that bulk-cleans root-owned bookkeeping via
2108
+ * {@link _cf_cleanupFacetPrefix}.
2109
+ *
2110
+ * @param id ID of the task to cancel
2111
+ * @returns true if the task was cancelled, false if the task was not found
2112
+ */
2113
+ async cancelSchedule(id) {
2114
+ if (this._isFacet) {
2115
+ const result = await (await this._rootAlarmOwner())._cf_cancelScheduleForFacet(this.selfPath, id);
2116
+ if (result.ok && result.callback) this._emit("schedule:cancel", {
2117
+ callback: result.callback,
2118
+ id
2119
+ });
2120
+ return result.ok;
2121
+ }
2122
+ const schedule = this._getScheduleForOwner(null, id);
2123
+ if (!schedule) return false;
2124
+ this._emit("schedule:cancel", {
2125
+ callback: schedule.callback,
2126
+ id: schedule.id
2127
+ });
2128
+ this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
2129
+ await this._scheduleNextAlarm();
2130
+ return true;
2131
+ }
2132
+ /**
2133
+ * Keep the Durable Object alive via alarm heartbeats.
2134
+ * Returns a disposer function that stops the heartbeat when called.
2135
+ *
2136
+ * Use this when you have long-running work and need to prevent the
2137
+ * DO from going idle (eviction after ~70-140s of inactivity).
2138
+ * The heartbeat fires every `keepAliveIntervalMs` (default 30s) via the
1702
2139
  * alarm system, without creating schedule rows or emitting observability
1703
2140
  * events. Configure via `static options = { keepAliveIntervalMs: 5000 }`.
1704
2141
  *
1705
- * No-op on facets. Facets share the parent's isolate and don't
1706
- * need a separate alarm heartbeat the parent's own activity,
1707
- * any open WebSocket to the facet, and any in-flight Promise
1708
- * already keep the shared machine alive for the duration of
1709
- * real work.
2142
+ * In facets, delegates the physical heartbeat to the root parent
2143
+ * because facets do not have independent alarm slots.
1710
2144
  *
1711
2145
  * @example
1712
2146
  * ```ts
@@ -1719,7 +2153,19 @@ var Agent = class Agent extends Server {
1719
2153
  * ```
1720
2154
  */
1721
2155
  async keepAlive() {
1722
- if (this._isFacet) return () => {};
2156
+ if (this._isFacet) {
2157
+ const root = await this._rootAlarmOwner();
2158
+ const token = await root._cf_acquireFacetKeepAlive(this.selfPath);
2159
+ let disposed = false;
2160
+ return () => {
2161
+ if (disposed) return;
2162
+ disposed = true;
2163
+ const release = root._cf_releaseFacetKeepAlive(token).catch((e) => {
2164
+ console.error("[Agent] Failed to release facet keepAlive:", e);
2165
+ });
2166
+ this.ctx.waitUntil(release);
2167
+ };
2168
+ }
1723
2169
  this._keepAliveRefs++;
1724
2170
  if (this._keepAliveRefs === 1) await this._scheduleNextAlarm();
1725
2171
  let disposed = false;
@@ -1773,8 +2219,16 @@ var Agent = class Agent extends Server {
1773
2219
  VALUES (${id}, ${name}, NULL, ${Date.now()})
1774
2220
  `;
1775
2221
  this._runFiberActiveFibers.add(id);
1776
- const dispose = await this.keepAlive();
2222
+ let root;
2223
+ let registeredFacetRun = false;
2224
+ let dispose = () => {};
1777
2225
  try {
2226
+ if (this._isFacet) {
2227
+ root = await this._rootAlarmOwner();
2228
+ await root._cf_registerFacetRun(this.selfPath, id);
2229
+ registeredFacetRun = true;
2230
+ }
2231
+ dispose = await this.keepAlive();
1778
2232
  const stash = (data) => {
1779
2233
  this.sql`
1780
2234
  UPDATE cf_agents_runs SET snapshot = ${JSON.stringify(data)}
@@ -1793,6 +2247,11 @@ var Agent = class Agent extends Server {
1793
2247
  this._runFiberActiveFibers.delete(id);
1794
2248
  this.sql`DELETE FROM cf_agents_runs WHERE id = ${id}`;
1795
2249
  dispose();
2250
+ if (root && registeredFacetRun) try {
2251
+ await root._cf_unregisterFacetRun(this.selfPath, id);
2252
+ } catch (e) {
2253
+ console.error("[Agent] Failed to unregister facet fiber:", e);
2254
+ }
1796
2255
  }
1797
2256
  }
1798
2257
  /**
@@ -1861,6 +2320,179 @@ var Agent = class Agent extends Server {
1861
2320
  /** @internal */
1862
2321
  async _onAlarmHousekeeping() {
1863
2322
  await this._checkRunFibers();
2323
+ await this._checkFacetRunFibers();
2324
+ }
2325
+ _isSameAgentPathPrefix(prefix, path) {
2326
+ if (prefix.length > path.length) return false;
2327
+ return prefix.every((step, index) => step.className === path[index].className && step.name === path[index].name);
2328
+ }
2329
+ /**
2330
+ * Root-side scan for durable fibers owned by descendant facets.
2331
+ * `cf_agents_facet_runs` is only an index; actual snapshots and
2332
+ * recovery hooks live in each facet's own `cf_agents_runs` table.
2333
+ * @internal
2334
+ */
2335
+ async _checkFacetRunFibers() {
2336
+ if (this._parentPath.length > 0) return;
2337
+ const rows = this.sql`
2338
+ SELECT owner_path, owner_path_key, run_id, created_at
2339
+ FROM cf_agents_facet_runs
2340
+ ORDER BY created_at ASC
2341
+ `;
2342
+ const firstRowByOwner = /* @__PURE__ */ new Map();
2343
+ for (const row of rows) if (!firstRowByOwner.has(row.owner_path_key)) firstRowByOwner.set(row.owner_path_key, row);
2344
+ for (const row of firstRowByOwner.values()) {
2345
+ let ownerPath;
2346
+ try {
2347
+ ownerPath = JSON.parse(row.owner_path);
2348
+ } catch (e) {
2349
+ console.warn(`[Agent] Corrupted facet fiber owner path for ${row.owner_path_key}; pruning stale lease.`, e);
2350
+ this.sql`
2351
+ DELETE FROM cf_agents_facet_runs
2352
+ WHERE owner_path_key = ${row.owner_path_key}
2353
+ `;
2354
+ continue;
2355
+ }
2356
+ try {
2357
+ if (await this._cf_checkRunFibersForFacet(ownerPath) === 0) this.sql`
2358
+ DELETE FROM cf_agents_facet_runs
2359
+ WHERE owner_path_key = ${row.owner_path_key}
2360
+ `;
2361
+ } catch (e) {
2362
+ console.error(`[Agent] Facet fiber recovery check failed for ${row.owner_path_key}:`, e);
2363
+ }
2364
+ }
2365
+ }
2366
+ /**
2367
+ * Dispatch a runFiber recovery check into the facet identified by
2368
+ * `ownerPath`. Returns the number of remaining local `cf_agents_runs`
2369
+ * rows on the target facet after recovery.
2370
+ * @internal
2371
+ */
2372
+ async _cf_checkRunFibersForFacet(ownerPath) {
2373
+ const selfPath = this.selfPath;
2374
+ if (!this._isSameAgentPathPrefix(selfPath, ownerPath)) throw new Error(`Facet fiber owner path does not descend from ${JSON.stringify(selfPath)}.`);
2375
+ if (selfPath.length === ownerPath.length) {
2376
+ await this._checkRunFibers();
2377
+ return this.sql`
2378
+ SELECT COUNT(*) as count FROM cf_agents_runs
2379
+ `[0]?.count ?? 0;
2380
+ }
2381
+ const next = ownerPath[selfPath.length];
2382
+ if (!this.hasSubAgent(next.className, next.name)) return 0;
2383
+ return (await this._cf_resolveSubAgent(next.className, next.name))._cf_checkRunFibersForFacet(ownerPath);
2384
+ }
2385
+ /**
2386
+ * Dispatch a scheduled callback into the facet identified by
2387
+ * `ownerPath`. Walks one step at a time: if `ownerPath` matches
2388
+ * `selfPath`, executes the callback locally; otherwise resolves
2389
+ * the next descendant facet and recurses through its own RPC.
2390
+ *
2391
+ * Called by the root's `alarm()` (which owns the physical alarm
2392
+ * for facet-owned schedules) and by intermediate facets while
2393
+ * walking down the chain.
2394
+ * @internal
2395
+ */
2396
+ async _cf_dispatchScheduledCallback(ownerPath, row) {
2397
+ const selfPath = this.selfPath;
2398
+ if (!this._isSameAgentPathPrefix(selfPath, ownerPath)) throw new Error(`Schedule owner path does not descend from ${JSON.stringify(selfPath)}.`);
2399
+ if (selfPath.length === ownerPath.length) {
2400
+ await this._executeScheduleCallback(row);
2401
+ return;
2402
+ }
2403
+ const next = ownerPath[selfPath.length];
2404
+ if (!this.hasSubAgent(next.className, next.name)) throw new Error(`Scheduled sub-agent ${next.className} "${next.name}" no longer exists.`);
2405
+ await (await this._cf_resolveSubAgent(next.className, next.name))._cf_dispatchScheduledCallback(ownerPath, row);
2406
+ }
2407
+ /**
2408
+ * Recursively destroy a descendant facet identified by
2409
+ * `targetPath`. Walks down from `selfPath` until reaching the
2410
+ * target's immediate parent, where it cancels the target's
2411
+ * parent-owned schedules (and any descendants), removes the
2412
+ * target from the registry, and calls `ctx.facets.delete` to
2413
+ * wipe the target's storage.
2414
+ *
2415
+ * Called by a facet's own `destroy()` (via the root) so that
2416
+ * `this.destroy()` inside a sub-agent results in the same
2417
+ * cleanup as `parent.deleteSubAgent(Cls, name)` from the parent.
2418
+ * @internal
2419
+ */
2420
+ async _cf_destroyDescendantFacet(targetPath) {
2421
+ const selfPath = this.selfPath;
2422
+ if (targetPath.length === 0) throw new Error("_cf_destroyDescendantFacet: target path must not be empty.");
2423
+ if (selfPath.length >= targetPath.length) throw new Error("_cf_destroyDescendantFacet: target must be a strict descendant.");
2424
+ if (!this._isSameAgentPathPrefix(selfPath, targetPath)) throw new Error("_cf_destroyDescendantFacet: target path does not descend from this agent.");
2425
+ if (this._parentPath.length === 0) await this._cf_cleanupFacetPrefix(targetPath);
2426
+ if (selfPath.length === targetPath.length - 1) {
2427
+ const target = targetPath[targetPath.length - 1];
2428
+ const ctx = this.ctx;
2429
+ if (!ctx.facets) throw new Error("destroy() (delegated from facet) is not supported in this runtime — `ctx.facets` is unavailable. Update to the latest `compatibility_date` in your wrangler.jsonc.");
2430
+ try {
2431
+ ctx.facets.delete(`${target.className}\0${target.name}`);
2432
+ } catch {}
2433
+ this._forgetSubAgent(target.className, target.name);
2434
+ return;
2435
+ }
2436
+ const next = targetPath[selfPath.length];
2437
+ if (!this.hasSubAgent(next.className, next.name)) return;
2438
+ await (await this._cf_resolveSubAgent(next.className, next.name))._cf_destroyDescendantFacet(targetPath);
2439
+ }
2440
+ async _executeScheduleCallback(row) {
2441
+ const callback = this[row.callback];
2442
+ if (!callback) {
2443
+ console.error(`callback ${row.callback} not found`);
2444
+ return;
2445
+ }
2446
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
2447
+ agent: this,
2448
+ connection: void 0,
2449
+ request: void 0,
2450
+ email: void 0
2451
+ }, async () => {
2452
+ const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
2453
+ let parsedPayload;
2454
+ try {
2455
+ parsedPayload = JSON.parse(row.payload);
2456
+ } catch (e) {
2457
+ console.error(`Failed to parse payload for schedule "${row.id}" (callback "${row.callback}")`, e);
2458
+ this._emit("schedule:error", {
2459
+ callback: row.callback,
2460
+ id: row.id,
2461
+ error: e instanceof Error ? e.message : String(e),
2462
+ attempts: 0
2463
+ });
2464
+ return;
2465
+ }
2466
+ try {
2467
+ this._emit("schedule:execute", {
2468
+ callback: row.callback,
2469
+ id: row.id
2470
+ });
2471
+ await tryN(maxAttempts, async (attempt) => {
2472
+ if (attempt > 1) this._emit("schedule:retry", {
2473
+ callback: row.callback,
2474
+ id: row.id,
2475
+ attempt,
2476
+ maxAttempts
2477
+ });
2478
+ await callback.bind(this)(parsedPayload, row);
2479
+ }, {
2480
+ baseDelayMs,
2481
+ maxDelayMs
2482
+ });
2483
+ } catch (e) {
2484
+ console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
2485
+ this._emit("schedule:error", {
2486
+ callback: row.callback,
2487
+ id: row.id,
2488
+ error: e instanceof Error ? e.message : String(e),
2489
+ attempts: maxAttempts
2490
+ });
2491
+ try {
2492
+ await this.onError(e);
2493
+ } catch {}
2494
+ }
2495
+ });
1864
2496
  }
1865
2497
  async _scheduleNextAlarm() {
1866
2498
  const nowMs = Date.now();
@@ -1891,6 +2523,12 @@ var Agent = class Agent extends Server {
1891
2523
  const keepAliveMs = nowMs + this._resolvedOptions.keepAliveIntervalMs;
1892
2524
  nextTimeMs = nextTimeMs === null ? keepAliveMs : Math.min(nextTimeMs, keepAliveMs);
1893
2525
  }
2526
+ if ((this.sql`
2527
+ SELECT COUNT(*) as count FROM cf_agents_facet_runs
2528
+ `[0]?.count ?? 0) > 0) {
2529
+ const facetRecoveryMs = nowMs + this._resolvedOptions.keepAliveIntervalMs;
2530
+ nextTimeMs = nextTimeMs === null ? facetRecoveryMs : Math.min(nextTimeMs, facetRecoveryMs);
2531
+ }
1894
2532
  if (nextTimeMs !== null) await this.ctx.storage.setAlarm(nextTimeMs);
1895
2533
  else await this.ctx.storage.deleteAlarm();
1896
2534
  }
@@ -1934,11 +2572,7 @@ var Agent = class Agent extends Server {
1934
2572
  });
1935
2573
  } catch {}
1936
2574
  for (const row of result) {
1937
- const callback = this[row.callback];
1938
- if (!callback) {
1939
- console.error(`callback ${row.callback} not found`);
1940
- continue;
1941
- }
2575
+ let executed = false;
1942
2576
  if (row.type === "interval" && row.running === 1) {
1943
2577
  const executionStartedAt = row.execution_started_at ?? 0;
1944
2578
  const hungTimeoutSeconds = this._resolvedOptions.hungScheduleTimeoutSeconds;
@@ -1950,59 +2584,31 @@ var Agent = class Agent extends Server {
1950
2584
  console.warn(`Forcing reset of hung interval schedule ${row.id} (started ${elapsedSeconds}s ago)`);
1951
2585
  }
1952
2586
  if (row.type === "interval") this.sql`UPDATE cf_agents_schedules SET running = 1, execution_started_at = ${now} WHERE id = ${row.id}`;
1953
- await __DO_NOT_USE_WILL_BREAK__agentContext.run({
1954
- agent: this,
1955
- connection: void 0,
1956
- request: void 0,
1957
- email: void 0
1958
- }, async () => {
1959
- const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
1960
- let parsedPayload;
1961
- try {
1962
- parsedPayload = JSON.parse(row.payload);
1963
- } catch (e) {
1964
- console.error(`Failed to parse payload for schedule "${row.id}" (callback "${row.callback}")`, e);
1965
- this._emit("schedule:error", {
1966
- callback: row.callback,
1967
- id: row.id,
1968
- error: e instanceof Error ? e.message : String(e),
1969
- attempts: 0
1970
- });
1971
- return;
1972
- }
2587
+ if (row.owner_path) try {
2588
+ const ownerPath = JSON.parse(row.owner_path);
2589
+ await this._cf_dispatchScheduledCallback(ownerPath, row);
2590
+ } catch (e) {
2591
+ console.error(`error dispatching scheduled callback "${row.callback}"`, e);
2592
+ this._emit("schedule:error", {
2593
+ callback: row.callback,
2594
+ id: row.id,
2595
+ error: e instanceof Error ? e.message : String(e),
2596
+ attempts: 0
2597
+ });
1973
2598
  try {
1974
- this._emit("schedule:execute", {
1975
- callback: row.callback,
1976
- id: row.id
1977
- });
1978
- await tryN(maxAttempts, async (attempt) => {
1979
- if (attempt > 1) this._emit("schedule:retry", {
1980
- callback: row.callback,
1981
- id: row.id,
1982
- attempt,
1983
- maxAttempts
1984
- });
1985
- await callback.bind(this)(parsedPayload, row);
1986
- }, {
1987
- baseDelayMs,
1988
- maxDelayMs
1989
- });
1990
- } catch (e) {
1991
- console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
1992
- this._emit("schedule:error", {
1993
- callback: row.callback,
1994
- id: row.id,
1995
- error: e instanceof Error ? e.message : String(e),
1996
- attempts: maxAttempts
1997
- });
1998
- try {
1999
- await this.onError(e);
2000
- } catch {}
2001
- }
2002
- });
2599
+ await this.onError(e);
2600
+ } catch {}
2601
+ if (row.type === "interval") this.sql`
2602
+ UPDATE cf_agents_schedules SET running = 0 WHERE id = ${row.id}
2603
+ `;
2604
+ continue;
2605
+ }
2606
+ else await this._executeScheduleCallback(row);
2607
+ executed = true;
2003
2608
  if (this._destroyed) return;
2609
+ if (!executed) continue;
2004
2610
  if (row.type === "cron") {
2005
- const nextExecutionTime = getNextCronTime(row.cron);
2611
+ const nextExecutionTime = getNextCronTime(row.cron ?? "");
2006
2612
  const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
2007
2613
  this.sql`
2008
2614
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
@@ -2045,8 +2651,349 @@ var Agent = class Agent extends Server {
2045
2651
  });
2046
2652
  if (decision instanceof Response) return decision;
2047
2653
  const forwardReq = decision instanceof Request ? decision : request;
2654
+ if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
2655
+ const acceptHeaders = new Headers(forwardReq.headers);
2656
+ const routedUrl = new URL(forwardReq.url);
2657
+ routedUrl.pathname = new URL(request.url).pathname;
2658
+ acceptHeaders.set(SUB_AGENT_OUTER_URL_HEADER, routedUrl.toString());
2659
+ return super.fetch(new Request(forwardReq, { headers: acceptHeaders }));
2660
+ }
2048
2661
  return this._cf_forwardToFacet(forwardReq, match);
2049
2662
  }
2663
+ broadcast(msg, without) {
2664
+ if (this._isFacet) {
2665
+ this._cf_broadcastToParentSubAgent(msg, without);
2666
+ return;
2667
+ }
2668
+ for (const connection of super.getConnections()) {
2669
+ if (without?.includes(connection.id)) continue;
2670
+ if (this._cf_connectionHasSubAgentTarget(connection)) continue;
2671
+ connection.send(msg);
2672
+ }
2673
+ }
2674
+ getConnection(id) {
2675
+ if (this._isFacet) {
2676
+ const stored = this._cf_virtualSubAgentConnections.get(id);
2677
+ if (stored) return this._cf_createSubAgentBridgeConnection(stored.bridge, stored.meta);
2678
+ }
2679
+ const connection = super.getConnection(id);
2680
+ if (!connection || this._cf_connectionHasSubAgentTarget(connection)) return;
2681
+ return connection;
2682
+ }
2683
+ *getConnections(tag) {
2684
+ if (this._isFacet) {
2685
+ for (const stored of this._cf_virtualSubAgentConnections.values()) if (!tag || stored.meta.tags.includes(tag)) yield this._cf_createSubAgentBridgeConnection(stored.bridge, stored.meta);
2686
+ }
2687
+ for (const connection of super.getConnections(tag)) {
2688
+ if (this._cf_connectionHasSubAgentTarget(connection)) continue;
2689
+ yield connection;
2690
+ }
2691
+ }
2692
+ async _cf_broadcastToParentSubAgent(message, without) {
2693
+ if (this._cf_currentSubAgentBridge) {
2694
+ this._cf_currentSubAgentBridge.broadcast(this.selfPath, message, without);
2695
+ return;
2696
+ }
2697
+ await (await this._rootAlarmOwner())._cf_broadcastToSubAgent(this.selfPath, message, without);
2698
+ }
2699
+ async _cf_broadcastToSubAgent(ownerPath, message, without) {
2700
+ if (this._isFacet && this._cf_currentSubAgentBridge) {
2701
+ this._cf_currentSubAgentBridge.broadcast(ownerPath, message, without);
2702
+ return;
2703
+ }
2704
+ for (const connection of super.getConnections()) {
2705
+ if (without?.includes(connection.id)) continue;
2706
+ const targetPath = this._cf_subAgentTargetPath(connection);
2707
+ if (!targetPath) continue;
2708
+ if (!this._isSameAgentPath(targetPath, ownerPath)) continue;
2709
+ connection.send(message);
2710
+ }
2711
+ }
2712
+ async _cf_subAgentConnectionMetas(ownerPath) {
2713
+ const metas = [];
2714
+ for (const connection of super.getConnections()) {
2715
+ const meta = this._cf_subAgentConnectionMetaForPath(connection, ownerPath);
2716
+ if (meta) metas.push(meta);
2717
+ }
2718
+ return metas;
2719
+ }
2720
+ async _cf_sendToSubAgentConnection(connectionId, message) {
2721
+ const connection = super.getConnection(connectionId);
2722
+ if (!connection || !this._cf_connectionHasSubAgentTarget(connection)) return;
2723
+ connection.send(message);
2724
+ }
2725
+ async _cf_closeSubAgentConnection(connectionId, code, reason) {
2726
+ const connection = super.getConnection(connectionId);
2727
+ if (!connection || !this._cf_connectionHasSubAgentTarget(connection)) return;
2728
+ connection.close(code, reason);
2729
+ }
2730
+ async _cf_setSubAgentConnectionState(connectionId, state) {
2731
+ const connection = super.getConnection(connectionId);
2732
+ if (!connection || !this._cf_connectionHasSubAgentTarget(connection)) return null;
2733
+ this._ensureConnectionWrapped(connection);
2734
+ connection.setState(state);
2735
+ return this._cf_getForwardedSubAgentState(connection);
2736
+ }
2737
+ _cf_subAgentConnectionMetaForPath(connection, ownerPath) {
2738
+ this._ensureConnectionWrapped(connection);
2739
+ const outerUri = this._unsafe_getConnectionFlag(connection, CF_SUB_AGENT_OUTER_URL_KEY);
2740
+ if (typeof outerUri !== "string") return null;
2741
+ const target = this._cf_subAgentPathFromOuterUri(outerUri, ownerPath);
2742
+ if (!target) return null;
2743
+ const raw = this._cf_getRawConnectionState(connection);
2744
+ const rawTags = raw != null && typeof raw === "object" ? raw[CF_SUB_AGENT_TAGS_KEY] : void 0;
2745
+ const tags = Array.isArray(rawTags) ? rawTags.filter((tag) => typeof tag === "string") : [...connection.tags];
2746
+ return {
2747
+ id: connection.id,
2748
+ uri: target.uri,
2749
+ tags,
2750
+ state: this._cf_getForwardedSubAgentState(connection)
2751
+ };
2752
+ }
2753
+ _cf_subAgentTargetPath(connection) {
2754
+ this._ensureConnectionWrapped(connection);
2755
+ const outerUri = this._unsafe_getConnectionFlag(connection, CF_SUB_AGENT_OUTER_URL_KEY);
2756
+ if (typeof outerUri !== "string") return null;
2757
+ return this._cf_subAgentPathFromOuterUri(outerUri)?.path ?? null;
2758
+ }
2759
+ _cf_subAgentPathFromOuterUri(outerUri, stopAt) {
2760
+ const ctx = this.ctx;
2761
+ const knownClasses = ctx.exports ? Object.keys(ctx.exports) : void 0;
2762
+ const path = [...this.selfPath];
2763
+ let currentUrl = outerUri;
2764
+ while (true) {
2765
+ const match = parseSubAgentPath(currentUrl, { knownClasses });
2766
+ if (!match) break;
2767
+ path.push({
2768
+ className: match.childClass,
2769
+ name: match.childName
2770
+ });
2771
+ const rewritten = new URL(currentUrl);
2772
+ rewritten.pathname = match.remainingPath;
2773
+ currentUrl = rewritten.toString();
2774
+ if (stopAt && this._isSameAgentPath(path, stopAt)) return {
2775
+ path,
2776
+ uri: currentUrl
2777
+ };
2778
+ }
2779
+ if (path.length === this.selfPath.length) return null;
2780
+ if (stopAt) return null;
2781
+ return {
2782
+ path,
2783
+ uri: currentUrl
2784
+ };
2785
+ }
2786
+ _isSameAgentPath(a, b) {
2787
+ if (a.length !== b.length) return false;
2788
+ return a.every((step, index) => step.className === b[index]?.className && step.name === b[index]?.name);
2789
+ }
2790
+ _cf_connectionHasSubAgentTarget(connection) {
2791
+ this._ensureConnectionWrapped(connection);
2792
+ return typeof this._unsafe_getConnectionFlag(connection, CF_SUB_AGENT_OUTER_URL_KEY) === "string";
2793
+ }
2794
+ _cf_connectionTargetsSubAgent(connection) {
2795
+ if (!connection.uri) return false;
2796
+ const ctx = this.ctx;
2797
+ return parseSubAgentPath(connection.uri, { knownClasses: ctx.exports ? Object.keys(ctx.exports) : void 0 }) !== null;
2798
+ }
2799
+ /**
2800
+ * Returns true when the current request is addressed to a child facet of
2801
+ * this agent rather than to this agent itself.
2802
+ *
2803
+ * Chat-style subclasses wrap `onConnect` before the base Agent forwarding
2804
+ * wrapper runs, so they need a request-level check to avoid sending their
2805
+ * own protocol frames on sockets that are about to be forwarded to a child.
2806
+ */
2807
+ _cf_requestTargetsSubAgent(request) {
2808
+ const ctx = this.ctx;
2809
+ return parseSubAgentPath(request.url, { knownClasses: ctx.exports ? Object.keys(ctx.exports) : void 0 }) !== null;
2810
+ }
2811
+ async _cf_forwardSubAgentWebSocketConnect(connection, request, options) {
2812
+ const routed = await this._cf_resolveSubAgentConnection(connection, request, options);
2813
+ if (!routed) return false;
2814
+ await routed.child._cf_handleSubAgentWebSocketConnect(this._cf_createSubAgentConnectionBridge(connection), routed.meta);
2815
+ return true;
2816
+ }
2817
+ _cf_createSubAgentConnectionBridge(connection) {
2818
+ return new SubAgentConnectionBridge(connection, (ownerPath, message, without) => {
2819
+ this._cf_broadcastToSubAgent(ownerPath, message, without);
2820
+ });
2821
+ }
2822
+ async _cf_forwardSubAgentWebSocketMessage(connection, message) {
2823
+ const routed = await this._cf_resolveSubAgentConnection(connection);
2824
+ if (!routed) return false;
2825
+ await routed.child._cf_handleSubAgentWebSocketMessage(message, this._cf_createSubAgentConnectionBridge(connection), routed.meta);
2826
+ return true;
2827
+ }
2828
+ async _cf_forwardSubAgentWebSocketClose(connection, code, reason, wasClean) {
2829
+ const routed = await this._cf_resolveSubAgentConnection(connection);
2830
+ if (!routed) return false;
2831
+ await routed.child._cf_handleSubAgentWebSocketClose(code, reason, wasClean, this._cf_createSubAgentConnectionBridge(connection), routed.meta);
2832
+ return true;
2833
+ }
2834
+ async _cf_resolveSubAgentConnection(connection, request, options = { gate: false }) {
2835
+ this._ensureConnectionWrapped(connection);
2836
+ const outerUri = this._unsafe_getConnectionFlag(connection, CF_SUB_AGENT_OUTER_URL_KEY);
2837
+ const uri = typeof outerUri === "string" ? outerUri : connection.uri;
2838
+ if (!uri) return null;
2839
+ const ctx = this.ctx;
2840
+ let match = parseSubAgentPath(uri, { knownClasses: ctx.exports ? Object.keys(ctx.exports) : void 0 });
2841
+ if (!match) return null;
2842
+ if (this._ParentClass.name === match.childClass && this.name === match.childName) {
2843
+ const tailUri = new URL(uri);
2844
+ tailUri.pathname = match.remainingPath;
2845
+ match = parseSubAgentPath(tailUri.toString(), { knownClasses: ctx.exports ? Object.keys(ctx.exports) : void 0 });
2846
+ if (!match) return null;
2847
+ }
2848
+ let forwardReq = request;
2849
+ if (request && options.gate) {
2850
+ const decision = await this.onBeforeSubAgent(request, {
2851
+ className: match.childClass,
2852
+ name: match.childName
2853
+ });
2854
+ if (decision instanceof Response) {
2855
+ connection.close(1008, "Sub-agent connection rejected");
2856
+ return null;
2857
+ }
2858
+ forwardReq = decision instanceof Request ? decision : request;
2859
+ }
2860
+ const child = await this._cf_resolveSubAgent(match.childClass, match.childName);
2861
+ const childUri = new URL(forwardReq?.url ?? uri);
2862
+ childUri.pathname = match.remainingPath;
2863
+ const raw = this._cf_getRawConnectionState(connection);
2864
+ const rawTags = raw != null && typeof raw === "object" ? raw[CF_SUB_AGENT_TAGS_KEY] : void 0;
2865
+ const tags = Array.isArray(rawTags) ? rawTags.filter((tag) => typeof tag === "string") : [...connection.tags];
2866
+ return {
2867
+ child,
2868
+ meta: {
2869
+ id: connection.id,
2870
+ uri: childUri.toString(),
2871
+ tags,
2872
+ state: this._cf_getForwardedSubAgentState(connection),
2873
+ requestHeaders: forwardReq ? [...forwardReq.headers] : void 0
2874
+ }
2875
+ };
2876
+ }
2877
+ async _cf_handleSubAgentWebSocketConnect(bridge, meta) {
2878
+ await this._cf_runWithSubAgentBridge(bridge, async () => {
2879
+ const connection = this._cf_createSubAgentBridgeConnection(bridge, meta);
2880
+ const request = new Request(meta.uri ?? "http://placeholder/", { headers: meta.requestHeaders });
2881
+ if (await this._cf_forwardSubAgentWebSocketConnect(connection, request, { gate: true })) return;
2882
+ if (this.shouldConnectionBeReadonly(connection, { request })) this.setConnectionReadonly(connection, true);
2883
+ if (!this.shouldSendProtocolMessages(connection, { request })) this._setConnectionNoProtocol(connection);
2884
+ const childTags = await this.getConnectionTags(connection, { request });
2885
+ connection.tags = [connection.id, ...childTags.filter((tag) => tag !== connection.id)];
2886
+ this._cf_storeVirtualSubAgentConnection(bridge, connection);
2887
+ await this.onConnect(connection, { request });
2888
+ this._cf_storeVirtualSubAgentConnection(bridge, connection);
2889
+ });
2890
+ }
2891
+ async _cf_handleSubAgentWebSocketMessage(message, bridge, meta) {
2892
+ const connection = this._cf_createSubAgentBridgeConnection(bridge, meta);
2893
+ this._cf_storeVirtualSubAgentConnection(bridge, connection);
2894
+ await this._cf_runWithSubAgentBridge(bridge, () => this.onMessage(connection, message));
2895
+ }
2896
+ async _cf_handleSubAgentWebSocketClose(code, reason, wasClean, bridge, meta) {
2897
+ const connection = this._cf_createSubAgentBridgeConnection(bridge, meta);
2898
+ this._cf_storeVirtualSubAgentConnection(bridge, connection);
2899
+ await this._cf_runWithSubAgentBridge(bridge, () => this.onClose(connection, code, reason, wasClean));
2900
+ this._cf_virtualSubAgentConnections.delete(meta.id);
2901
+ }
2902
+ async _cf_runWithSubAgentBridge(bridge, fn) {
2903
+ const previous = this._cf_currentSubAgentBridge;
2904
+ this._cf_currentSubAgentBridge = bridge;
2905
+ try {
2906
+ return await fn();
2907
+ } finally {
2908
+ this._cf_currentSubAgentBridge = previous;
2909
+ }
2910
+ }
2911
+ _cf_createSubAgentBridgeConnection(bridge, meta) {
2912
+ let stored = this._cf_virtualSubAgentConnections.get(meta.id);
2913
+ if (stored) {
2914
+ stored.bridge = bridge;
2915
+ stored.meta = meta;
2916
+ if (stored.connection) {
2917
+ stored.connection.uri = meta.uri;
2918
+ stored.connection.tags = meta.tags;
2919
+ return stored.connection;
2920
+ }
2921
+ } else {
2922
+ stored = {
2923
+ bridge,
2924
+ meta
2925
+ };
2926
+ this._cf_virtualSubAgentConnections.set(meta.id, stored);
2927
+ }
2928
+ const getStored = () => this._cf_virtualSubAgentConnections.get(meta.id) ?? stored;
2929
+ const updateStoredState = (nextState) => {
2930
+ const current = this._cf_virtualSubAgentConnections.get(meta.id);
2931
+ if (current) current.meta = {
2932
+ ...current.meta,
2933
+ state: nextState
2934
+ };
2935
+ };
2936
+ const connection = {
2937
+ id: meta.id,
2938
+ uri: meta.uri,
2939
+ tags: meta.tags,
2940
+ server: this.name,
2941
+ get state() {
2942
+ return getStored().meta.state;
2943
+ },
2944
+ setState(next) {
2945
+ const currentState = getStored().meta.state;
2946
+ const state = typeof next === "function" ? next(currentState) : next;
2947
+ updateStoredState(state);
2948
+ getStored().bridge.setState(state);
2949
+ return state;
2950
+ },
2951
+ send(message) {
2952
+ getStored().bridge.send(message);
2953
+ },
2954
+ close(code, reason) {
2955
+ getStored().bridge.close(code, reason);
2956
+ },
2957
+ addEventListener() {},
2958
+ removeEventListener() {}
2959
+ };
2960
+ stored.connection = connection;
2961
+ this._ensureConnectionWrapped(connection);
2962
+ return connection;
2963
+ }
2964
+ _cf_storeVirtualSubAgentConnection(bridge, connection) {
2965
+ this._unsafe_setConnectionFlag(connection, CF_SUB_AGENT_TAGS_KEY, [...connection.tags]);
2966
+ const stored = this._cf_virtualSubAgentConnections.get(connection.id);
2967
+ this._cf_virtualSubAgentConnections.set(connection.id, {
2968
+ bridge,
2969
+ meta: {
2970
+ id: connection.id,
2971
+ uri: connection.uri,
2972
+ tags: [...connection.tags],
2973
+ state: this._cf_getRawConnectionState(connection)
2974
+ },
2975
+ connection: stored?.connection ?? connection
2976
+ });
2977
+ }
2978
+ async _cf_hydrateSubAgentConnectionsFromRoot() {
2979
+ if (!this._isFacet || this._parentPath.length === 0) return;
2980
+ const root = await this._rootAlarmOwner();
2981
+ const metas = await root._cf_subAgentConnectionMetas(this.selfPath);
2982
+ for (const meta of metas) this._cf_virtualSubAgentConnections.set(meta.id, {
2983
+ bridge: new RootSubAgentConnectionBridge(root, meta.id),
2984
+ meta
2985
+ });
2986
+ }
2987
+ _cf_getRawConnectionState(connection) {
2988
+ this._ensureConnectionWrapped(connection);
2989
+ return this._rawStateAccessors.get(connection)?.getRaw() ?? null;
2990
+ }
2991
+ _cf_getForwardedSubAgentState(connection) {
2992
+ const raw = this._cf_getRawConnectionState(connection);
2993
+ if (raw == null || typeof raw !== "object") return raw;
2994
+ const { [CF_SUB_AGENT_OUTER_URL_KEY]: _, ...rest } = raw;
2995
+ return Object.keys(rest).length > 0 ? rest : null;
2996
+ }
2050
2997
  /**
2051
2998
  * Parent-side middleware hook. Fires before a request is
2052
2999
  * forwarded into a facet sub-agent. Mirrors `onBeforeConnect` /
@@ -2108,7 +3055,14 @@ var Agent = class Agent extends Server {
2108
3055
  }
2109
3056
  const rewritten = new URL(req.url);
2110
3057
  rewritten.pathname = match.remainingPath;
2111
- const forwarded = new Request(rewritten, req);
3058
+ const forwardedHeaders = new Headers(req.headers);
3059
+ const forwardedInit = {
3060
+ method: req.method,
3061
+ headers: forwardedHeaders
3062
+ };
3063
+ if (req.headers.get("Upgrade")?.toLowerCase() === "websocket") forwardedHeaders.set(SUB_AGENT_OUTER_URL_HEADER, req.url);
3064
+ if (req.body && req.method !== "GET" && req.method !== "HEAD") forwardedInit.body = await req.arrayBuffer();
3065
+ const forwarded = new Request(rewritten, forwardedInit);
2112
3066
  return fetcher.fetch(forwarded);
2113
3067
  }
2114
3068
  /**
@@ -2136,10 +3090,10 @@ var Agent = class Agent extends Server {
2136
3090
  * We set `_isFacet` eagerly (before `__unsafe_ensureInitialized`
2137
3091
  * runs `onStart()`) so any code that legitimately branches on it
2138
3092
  * — e.g. skipping parent-owned alarms in schedule guards — sees
2139
- * the flag during the first `onStart()` run. Broadcast paths no
2140
- * longer special-case facets, since facets can be directly
2141
- * addressed via sub-agent routing and have their own WebSocket
2142
- * connections.
3093
+ * the flag during the first `onStart()` run. Protocol broadcasts are
3094
+ * suppressed only during this bootstrap window; afterward, facets can
3095
+ * broadcast to their own WebSocket clients reached via sub-agent
3096
+ * routing.
2143
3097
  *
2144
3098
  * The facet's name (and `this.name` getter) is handled entirely by
2145
3099
  * partyserver via `ctx.id.name`, which is populated because the
@@ -2156,7 +3110,12 @@ var Agent = class Agent extends Server {
2156
3110
  this._isFacet = true;
2157
3111
  this._parentPath = parentPath;
2158
3112
  await Promise.all([this.ctx.storage.put("cf_agents_is_facet", true), this.ctx.storage.put("cf_agents_parent_path", parentPath)]);
2159
- await this.__unsafe_ensureInitialized();
3113
+ this._suppressProtocolBroadcasts = true;
3114
+ try {
3115
+ await this.__unsafe_ensureInitialized();
3116
+ } finally {
3117
+ this._suppressProtocolBroadcasts = false;
3118
+ }
2160
3119
  }
2161
3120
  /**
2162
3121
  * Ancestor chain for this agent, root-first. Empty for top-level
@@ -2260,6 +3219,529 @@ var Agent = class Agent extends Server {
2260
3219
  async subAgent(cls, name) {
2261
3220
  return await this._cf_resolveSubAgent(cls.name, name);
2262
3221
  }
3222
+ async onAgentToolStart(_run) {}
3223
+ async onAgentToolFinish(_run, _result) {}
3224
+ async runAgentTool(cls, options) {
3225
+ const runId = options.runId ?? nanoid(12);
3226
+ const agentType = cls.name;
3227
+ const existing = this._readAgentToolRun(runId);
3228
+ if (existing) {
3229
+ if (this._isAgentToolTerminal(existing.status)) {
3230
+ if (existing.status === "completed" && existing.output_json == null) try {
3231
+ const child = await this.subAgent(cls, runId);
3232
+ const inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(runId);
3233
+ if (inspection?.status === "completed") {
3234
+ const result = this._terminalResultFromInspection(agentType, inspection);
3235
+ this._updateAgentToolTerminal(runId, result, inspection.completedAt);
3236
+ return result;
3237
+ }
3238
+ } catch {}
3239
+ return this._resultFromAgentToolRow(existing);
3240
+ }
3241
+ return await this._replayAndInterruptAgentToolRun(cls, existing, "Agent tool run was still running, but live-tail reattachment is not supported in this runtime.");
3242
+ }
3243
+ const displayOrder = options.displayOrder ?? 0;
3244
+ const inputPreview = options.inputPreview ?? this._defaultAgentToolPreview(options.input);
3245
+ const displayJson = options.display !== void 0 ? JSON.stringify(options.display) : null;
3246
+ const inputPreviewJson = inputPreview !== void 0 ? JSON.stringify(inputPreview) : null;
3247
+ const startedAt = Date.now();
3248
+ if (this._activeAgentToolRunCount() >= this.maxConcurrentAgentTools) {
3249
+ const error = `maxConcurrentAgentTools (${this.maxConcurrentAgentTools}) exceeded`;
3250
+ this.sql`
3251
+ INSERT INTO cf_agent_tool_runs (
3252
+ run_id, parent_tool_call_id, agent_type, input_preview,
3253
+ input_redacted, status, error_message, display_metadata,
3254
+ display_order, started_at, completed_at
3255
+ ) VALUES (
3256
+ ${runId}, ${options.parentToolCallId ?? null}, ${agentType},
3257
+ ${inputPreviewJson}, 1, 'error', ${error}, ${displayJson},
3258
+ ${displayOrder}, ${startedAt}, ${Date.now()}
3259
+ )
3260
+ `;
3261
+ this._broadcastAgentToolEvent(options.parentToolCallId, 0, {
3262
+ kind: "started",
3263
+ runId,
3264
+ agentType,
3265
+ inputPreview,
3266
+ order: displayOrder,
3267
+ display: options.display
3268
+ });
3269
+ this._broadcastAgentToolEvent(options.parentToolCallId, 1, {
3270
+ kind: "error",
3271
+ runId,
3272
+ error
3273
+ });
3274
+ return {
3275
+ runId,
3276
+ agentType,
3277
+ status: "error",
3278
+ error
3279
+ };
3280
+ }
3281
+ this.sql`
3282
+ INSERT INTO cf_agent_tool_runs (
3283
+ run_id, parent_tool_call_id, agent_type, input_preview,
3284
+ input_redacted, status, display_metadata, display_order, started_at
3285
+ ) VALUES (
3286
+ ${runId}, ${options.parentToolCallId ?? null}, ${agentType},
3287
+ ${inputPreviewJson}, 1, 'starting', ${displayJson}, ${displayOrder},
3288
+ ${startedAt}
3289
+ )
3290
+ `;
3291
+ const runInfo = {
3292
+ runId,
3293
+ parentToolCallId: options.parentToolCallId,
3294
+ agentType,
3295
+ inputPreview,
3296
+ status: "starting",
3297
+ display: options.display,
3298
+ displayOrder,
3299
+ startedAt
3300
+ };
3301
+ await this.onAgentToolStart(runInfo);
3302
+ this._broadcastAgentToolEvent(options.parentToolCallId, 0, {
3303
+ kind: "started",
3304
+ runId,
3305
+ agentType,
3306
+ inputPreview,
3307
+ order: displayOrder,
3308
+ display: options.display
3309
+ });
3310
+ const child = await this.subAgent(cls, runId);
3311
+ const adapter = this._asAgentToolChildAdapter(child);
3312
+ const childStart = await adapter.startAgentToolRun(options.input, { runId });
3313
+ this._markAgentToolRunning(runId);
3314
+ let sequence = 1;
3315
+ let parentAbortListener;
3316
+ if (options.signal) if (options.signal.aborted) {
3317
+ await adapter.cancelAgentToolRun(runId, options.signal.reason);
3318
+ const result = {
3319
+ runId,
3320
+ agentType,
3321
+ status: "aborted",
3322
+ error: options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "cancelled")
3323
+ };
3324
+ this._updateAgentToolTerminal(runId, result);
3325
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
3326
+ await this.onAgentToolFinish({
3327
+ ...runInfo,
3328
+ status: "aborted",
3329
+ completedAt: Date.now()
3330
+ }, result);
3331
+ return result;
3332
+ } else {
3333
+ parentAbortListener = () => {
3334
+ adapter.cancelAgentToolRun(runId, options.signal?.reason);
3335
+ };
3336
+ options.signal.addEventListener("abort", parentAbortListener, { once: true });
3337
+ }
3338
+ try {
3339
+ if (adapter.tailAgentToolRun) {
3340
+ const stream = await adapter.tailAgentToolRun(runId, { afterSequence: -1 });
3341
+ sequence = await this._forwardAgentToolStream(stream, options.parentToolCallId, runId, sequence, options.signal);
3342
+ } else {
3343
+ const chunks = await adapter.getAgentToolChunks(runId);
3344
+ sequence = this._broadcastAgentToolChunks(options.parentToolCallId, runId, chunks, sequence);
3345
+ }
3346
+ if (options.signal?.aborted) {
3347
+ await adapter.cancelAgentToolRun(runId, options.signal.reason);
3348
+ const result = {
3349
+ runId,
3350
+ agentType,
3351
+ status: "aborted",
3352
+ error: options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "cancelled")
3353
+ };
3354
+ this._updateAgentToolTerminal(runId, result);
3355
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
3356
+ await this.onAgentToolFinish({
3357
+ ...runInfo,
3358
+ status: "aborted",
3359
+ completedAt: Date.now()
3360
+ }, result);
3361
+ return result;
3362
+ }
3363
+ const inspection = await adapter.inspectAgentToolRun(runId) ?? childStart;
3364
+ const result = this._terminalResultFromInspection(agentType, inspection);
3365
+ this._updateAgentToolTerminal(runId, result, inspection.completedAt);
3366
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
3367
+ await this.onAgentToolFinish({
3368
+ ...runInfo,
3369
+ status: result.status,
3370
+ completedAt: Date.now()
3371
+ }, result);
3372
+ return result;
3373
+ } catch (error) {
3374
+ if (options.signal?.aborted) {
3375
+ await adapter.cancelAgentToolRun(runId, options.signal.reason);
3376
+ const result = {
3377
+ runId,
3378
+ agentType,
3379
+ status: "aborted",
3380
+ error: options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "cancelled")
3381
+ };
3382
+ this._updateAgentToolTerminal(runId, result);
3383
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
3384
+ await this.onAgentToolFinish({
3385
+ ...runInfo,
3386
+ status: "aborted",
3387
+ completedAt: Date.now()
3388
+ }, result);
3389
+ return result;
3390
+ }
3391
+ const result = {
3392
+ runId,
3393
+ agentType,
3394
+ status: "error",
3395
+ error: error instanceof Error ? error.message : String(error)
3396
+ };
3397
+ this._updateAgentToolTerminal(runId, result);
3398
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
3399
+ await this.onAgentToolFinish({
3400
+ ...runInfo,
3401
+ status: "error",
3402
+ completedAt: Date.now()
3403
+ }, result);
3404
+ return result;
3405
+ } finally {
3406
+ if (parentAbortListener && options.signal) options.signal.removeEventListener("abort", parentAbortListener);
3407
+ }
3408
+ }
3409
+ hasAgentToolRun(classOrName, runId) {
3410
+ const agentType = typeof classOrName === "string" ? classOrName : classOrName.name;
3411
+ return (this.sql`
3412
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
3413
+ WHERE run_id = ${runId} AND agent_type = ${agentType}
3414
+ `[0]?.n ?? 0) > 0;
3415
+ }
3416
+ async clearAgentToolRuns(options) {
3417
+ const rows = this.sql`
3418
+ SELECT run_id, agent_type, status FROM cf_agent_tool_runs
3419
+ ORDER BY started_at ASC
3420
+ `;
3421
+ const statusFilter = options?.status ? new Set(options.status) : null;
3422
+ const retained = rows.filter((row) => {
3423
+ if (statusFilter && !statusFilter.has(row.status)) return false;
3424
+ if (options?.olderThan !== void 0) {
3425
+ const full = this._readAgentToolRun(row.run_id);
3426
+ if (!full || full.started_at >= options.olderThan) return false;
3427
+ }
3428
+ return true;
3429
+ });
3430
+ for (const row of retained) {
3431
+ try {
3432
+ const cls = this._agentToolClassByName(row.agent_type);
3433
+ if (row.status === "starting" || row.status === "running") {
3434
+ const child = await this.subAgent(cls, row.run_id);
3435
+ await this._asAgentToolChildAdapter(child).cancelAgentToolRun(row.run_id, "clearing agent tool run");
3436
+ }
3437
+ await this.deleteSubAgent(cls, row.run_id);
3438
+ } catch {}
3439
+ this.sql`
3440
+ DELETE FROM cf_agent_tool_runs WHERE run_id = ${row.run_id}
3441
+ `;
3442
+ }
3443
+ }
3444
+ _isAgentToolTerminal(status) {
3445
+ return status === "completed" || status === "error" || status === "aborted" || status === "interrupted";
3446
+ }
3447
+ _activeAgentToolRunCount() {
3448
+ return this.sql`
3449
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
3450
+ WHERE status IN ('starting', 'running')
3451
+ `[0]?.n ?? 0;
3452
+ }
3453
+ _defaultAgentToolPreview(input) {
3454
+ if (typeof input === "string") return input.slice(0, 500);
3455
+ if (input === null || input === void 0) return input;
3456
+ try {
3457
+ const json = JSON.stringify(input);
3458
+ return json.length > 500 ? `${json.slice(0, 497)}...` : json;
3459
+ } catch {
3460
+ return String(input).slice(0, 500);
3461
+ }
3462
+ }
3463
+ _readAgentToolRun(runId) {
3464
+ return this.sql`
3465
+ SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
3466
+ summary, output_json, error_message, display_metadata, display_order,
3467
+ started_at, completed_at
3468
+ FROM cf_agent_tool_runs
3469
+ WHERE run_id = ${runId}
3470
+ LIMIT 1
3471
+ `[0] ?? null;
3472
+ }
3473
+ _resultFromAgentToolRow(row) {
3474
+ const output = this._parseAgentToolJson(row.output_json);
3475
+ return {
3476
+ runId: row.run_id,
3477
+ agentType: row.agent_type,
3478
+ status: row.status,
3479
+ ...output !== void 0 ? { output } : {},
3480
+ ...row.summary !== null ? { summary: row.summary } : {},
3481
+ ...row.error_message !== null ? { error: row.error_message } : {}
3482
+ };
3483
+ }
3484
+ _terminalResultFromInspection(agentType, inspection) {
3485
+ if (inspection.status === "completed") return {
3486
+ runId: inspection.runId,
3487
+ agentType,
3488
+ status: "completed",
3489
+ output: inspection.output,
3490
+ summary: inspection.summary
3491
+ };
3492
+ if (inspection.status === "aborted") return {
3493
+ runId: inspection.runId,
3494
+ agentType,
3495
+ status: "aborted",
3496
+ error: inspection.error
3497
+ };
3498
+ return {
3499
+ runId: inspection.runId,
3500
+ agentType,
3501
+ status: "error",
3502
+ error: inspection.error ?? "Agent tool run failed"
3503
+ };
3504
+ }
3505
+ _updateAgentToolTerminal(runId, result, completedAt = Date.now()) {
3506
+ this.sql`
3507
+ UPDATE cf_agent_tool_runs
3508
+ SET status = ${result.status},
3509
+ summary = ${result.summary ?? null},
3510
+ output_json = ${this._stringifyAgentToolOutput(result.output)},
3511
+ error_message = ${result.error ?? null},
3512
+ completed_at = ${completedAt}
3513
+ WHERE run_id = ${runId}
3514
+ AND status NOT IN ('completed', 'error', 'aborted', 'interrupted')
3515
+ `;
3516
+ if (result.status === "completed" && result.output !== void 0) this.sql`
3517
+ UPDATE cf_agent_tool_runs
3518
+ SET output_json = COALESCE(output_json, ${this._stringifyAgentToolOutput(result.output)}),
3519
+ summary = COALESCE(summary, ${result.summary ?? null})
3520
+ WHERE run_id = ${runId} AND status = 'completed'
3521
+ `;
3522
+ }
3523
+ _markAgentToolRunning(runId) {
3524
+ this.sql`
3525
+ UPDATE cf_agent_tool_runs
3526
+ SET status = 'running'
3527
+ WHERE run_id = ${runId} AND status = 'starting'
3528
+ `;
3529
+ }
3530
+ _parseAgentToolJson(value) {
3531
+ if (value === null) return void 0;
3532
+ try {
3533
+ return JSON.parse(value);
3534
+ } catch {
3535
+ return value;
3536
+ }
3537
+ }
3538
+ _stringifyAgentToolOutput(output) {
3539
+ if (output === void 0) return null;
3540
+ const json = JSON.stringify(output);
3541
+ return json === void 0 ? null : json;
3542
+ }
3543
+ _broadcastAgentToolEvent(parentToolCallId, sequence, event, replay, connection) {
3544
+ const message = {
3545
+ type: "agent-tool-event",
3546
+ parentToolCallId,
3547
+ sequence,
3548
+ event,
3549
+ ...replay ? { replay } : {}
3550
+ };
3551
+ const body = JSON.stringify(message);
3552
+ if (connection) connection.send(body);
3553
+ else this.broadcast(body);
3554
+ }
3555
+ _broadcastAgentToolChunks(parentToolCallId, runId, chunks, sequence, replay, connection) {
3556
+ let next = sequence;
3557
+ for (const chunk of chunks) this._broadcastAgentToolEvent(parentToolCallId, next++, {
3558
+ kind: "chunk",
3559
+ runId,
3560
+ body: chunk.body
3561
+ }, replay, connection);
3562
+ return next;
3563
+ }
3564
+ async _forwardAgentToolStream(stream, parentToolCallId, runId, sequence, signal) {
3565
+ let next = sequence;
3566
+ if (signal?.aborted) return next;
3567
+ const reader = stream.getReader();
3568
+ const decoder = new TextDecoder();
3569
+ let bufferedBytes = "";
3570
+ let abortListener;
3571
+ if (signal) {
3572
+ abortListener = () => {};
3573
+ signal.addEventListener("abort", abortListener, { once: true });
3574
+ }
3575
+ try {
3576
+ const forwardChunk = (chunk) => {
3577
+ this._broadcastAgentToolEvent(parentToolCallId, next++, {
3578
+ kind: "chunk",
3579
+ runId,
3580
+ body: chunk.body
3581
+ });
3582
+ };
3583
+ const forwardLine = (line) => {
3584
+ try {
3585
+ const chunk = JSON.parse(line);
3586
+ if (typeof chunk.body === "string") forwardChunk(chunk);
3587
+ } catch {}
3588
+ };
3589
+ const flushBufferedBytes = (final = false) => {
3590
+ while (true) {
3591
+ const newline = bufferedBytes.indexOf("\n");
3592
+ if (newline === -1) break;
3593
+ const line = bufferedBytes.slice(0, newline).trim();
3594
+ bufferedBytes = bufferedBytes.slice(newline + 1);
3595
+ if (line.length > 0) forwardLine(line);
3596
+ }
3597
+ if (final && bufferedBytes.trim().length > 0) {
3598
+ forwardLine(bufferedBytes);
3599
+ bufferedBytes = "";
3600
+ }
3601
+ };
3602
+ while (true) {
3603
+ let readResult;
3604
+ try {
3605
+ readResult = await reader.read();
3606
+ } catch (error) {
3607
+ if (signal?.aborted) break;
3608
+ throw error;
3609
+ }
3610
+ const { done, value } = readResult;
3611
+ if (done) {
3612
+ bufferedBytes += decoder.decode();
3613
+ flushBufferedBytes(true);
3614
+ break;
3615
+ }
3616
+ if (value instanceof Uint8Array) {
3617
+ bufferedBytes += decoder.decode(value, { stream: true });
3618
+ flushBufferedBytes();
3619
+ } else forwardChunk(value);
3620
+ }
3621
+ } finally {
3622
+ if (abortListener && signal) signal.removeEventListener("abort", abortListener);
3623
+ reader.releaseLock();
3624
+ }
3625
+ return next;
3626
+ }
3627
+ _broadcastAgentToolTerminal(parentToolCallId, sequence, result, replay, connection) {
3628
+ if (result.status === "completed") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3629
+ kind: "finished",
3630
+ runId: result.runId,
3631
+ summary: result.summary ?? ""
3632
+ }, replay, connection);
3633
+ else if (result.status === "aborted") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3634
+ kind: "aborted",
3635
+ runId: result.runId,
3636
+ reason: result.error
3637
+ }, replay, connection);
3638
+ else if (result.status === "interrupted") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3639
+ kind: "interrupted",
3640
+ runId: result.runId,
3641
+ error: result.error ?? "Agent tool run was interrupted"
3642
+ }, replay, connection);
3643
+ else this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3644
+ kind: "error",
3645
+ runId: result.runId,
3646
+ error: result.error ?? "Agent tool run failed"
3647
+ }, replay, connection);
3648
+ }
3649
+ _asAgentToolChildAdapter(child) {
3650
+ const candidate = child;
3651
+ if (typeof candidate.startAgentToolRun !== "function" || typeof candidate.cancelAgentToolRun !== "function" || typeof candidate.inspectAgentToolRun !== "function" || typeof candidate.getAgentToolChunks !== "function") throw new Error("Agent tool child must implement the framework agent-tool adapter. Use a @cloudflare/think Think subclass or an AIChatAgent subclass.");
3652
+ return candidate;
3653
+ }
3654
+ _agentToolClassByName(className) {
3655
+ const cls = this.ctx.exports?.[className];
3656
+ if (!cls) throw new Error(`Agent tool class "${className}" is not exported.`);
3657
+ return cls;
3658
+ }
3659
+ async _replayAndInterruptAgentToolRun(cls, row, message) {
3660
+ const parentToolCallId = row.parent_tool_call_id ?? void 0;
3661
+ let sequence = 1;
3662
+ try {
3663
+ const child = await this.subAgent(cls, row.run_id);
3664
+ const chunks = await this._asAgentToolChildAdapter(child).getAgentToolChunks(row.run_id);
3665
+ sequence = this._broadcastAgentToolChunks(parentToolCallId, row.run_id, chunks, sequence);
3666
+ } catch {}
3667
+ const result = {
3668
+ runId: row.run_id,
3669
+ agentType: row.agent_type,
3670
+ status: "interrupted",
3671
+ error: message
3672
+ };
3673
+ this._updateAgentToolTerminal(row.run_id, result);
3674
+ this._broadcastAgentToolTerminal(parentToolCallId, sequence, result);
3675
+ return result;
3676
+ }
3677
+ async _replayAgentToolRuns(connection) {
3678
+ const rows = this.sql`
3679
+ SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
3680
+ summary, output_json, error_message, display_metadata, display_order
3681
+ FROM cf_agent_tool_runs
3682
+ ORDER BY started_at ASC
3683
+ `;
3684
+ for (const row of rows) {
3685
+ const parentToolCallId = row.parent_tool_call_id ?? void 0;
3686
+ let sequence = 0;
3687
+ this._broadcastAgentToolEvent(parentToolCallId, sequence++, {
3688
+ kind: "started",
3689
+ runId: row.run_id,
3690
+ agentType: row.agent_type,
3691
+ inputPreview: this._parseAgentToolJson(row.input_preview),
3692
+ order: row.display_order,
3693
+ display: this._parseAgentToolJson(row.display_metadata)
3694
+ }, true, connection);
3695
+ try {
3696
+ const child = await this.subAgent(this._agentToolClassByName(row.agent_type), row.run_id);
3697
+ const chunks = await this._asAgentToolChildAdapter(child).getAgentToolChunks(row.run_id);
3698
+ sequence = this._broadcastAgentToolChunks(parentToolCallId, row.run_id, chunks, sequence, true, connection);
3699
+ } catch {}
3700
+ if (this._isAgentToolTerminal(row.status)) this._broadcastAgentToolTerminal(parentToolCallId, sequence, {
3701
+ runId: row.run_id,
3702
+ agentType: row.agent_type,
3703
+ status: row.status,
3704
+ output: this._parseAgentToolJson(row.output_json),
3705
+ summary: row.summary ?? void 0,
3706
+ error: row.error_message ?? void 0
3707
+ }, true, connection);
3708
+ }
3709
+ }
3710
+ async _reconcileAgentToolRuns() {
3711
+ const rows = this.sql`
3712
+ SELECT run_id, agent_type FROM cf_agent_tool_runs
3713
+ WHERE status IN ('starting', 'running')
3714
+ ORDER BY started_at ASC
3715
+ `;
3716
+ for (const row of rows) try {
3717
+ const cls = this._agentToolClassByName(row.agent_type);
3718
+ if (!this.hasSubAgent(cls, row.run_id)) {
3719
+ this._updateAgentToolTerminal(row.run_id, {
3720
+ runId: row.run_id,
3721
+ agentType: row.agent_type,
3722
+ status: "interrupted",
3723
+ error: "Agent tool child was not found during parent recovery."
3724
+ });
3725
+ continue;
3726
+ }
3727
+ const child = await this.subAgent(cls, row.run_id);
3728
+ const inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(row.run_id);
3729
+ if (!inspection || inspection.status === "running" || inspection.status === "starting") this._updateAgentToolTerminal(row.run_id, {
3730
+ runId: row.run_id,
3731
+ agentType: row.agent_type,
3732
+ status: "interrupted",
3733
+ error: "Agent tool run was still running, but live-tail reattachment is not supported in this runtime."
3734
+ });
3735
+ else this._updateAgentToolTerminal(row.run_id, this._terminalResultFromInspection(row.agent_type, inspection), inspection.completedAt);
3736
+ } catch {
3737
+ this._updateAgentToolTerminal(row.run_id, {
3738
+ runId: row.run_id,
3739
+ agentType: row.agent_type,
3740
+ status: "interrupted",
3741
+ error: "Agent tool run could not be inspected during parent recovery."
3742
+ });
3743
+ }
3744
+ }
2263
3745
  /**
2264
3746
  * Shared facet resolution — takes a CamelCase class name string
2265
3747
  * (matching `ctx.exports`) rather than a class reference. Both
@@ -2289,7 +3771,14 @@ var Agent = class Agent extends Server {
2289
3771
  id: facetId
2290
3772
  }));
2291
3773
  const childParentPath = this.selfPath;
2292
- await stub._cf_initAsFacet(name, childParentPath);
3774
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
3775
+ agent: this,
3776
+ connection: void 0,
3777
+ request: void 0,
3778
+ email: void 0
3779
+ }, async () => {
3780
+ await stub._cf_initAsFacet(name, childParentPath);
3781
+ });
2293
3782
  this._recordSubAgent(className, name);
2294
3783
  return stub;
2295
3784
  }
@@ -2320,10 +3809,16 @@ var Agent = class Agent extends Server {
2320
3809
  * @param cls The Agent subclass used when creating the child
2321
3810
  * @param name Name of the child to delete
2322
3811
  */
2323
- deleteSubAgent(cls, name) {
3812
+ async deleteSubAgent(cls, name) {
2324
3813
  const ctx = this.ctx;
2325
3814
  if (!ctx.facets) throw new Error("deleteSubAgent() is not supported in this runtime — `ctx.facets` is unavailable. Update to the latest `compatibility_date` in your wrangler.jsonc.");
2326
3815
  const facetKey = `${cls.name}\0${name}`;
3816
+ const childPath = [...this.selfPath, {
3817
+ className: cls.name,
3818
+ name
3819
+ }];
3820
+ if (this._isFacet) await (await this._rootAlarmOwner())._cf_cleanupFacetPrefix(childPath);
3821
+ else await this._cf_cleanupFacetPrefix(childPath);
2327
3822
  try {
2328
3823
  ctx.facets.delete(facetKey);
2329
3824
  } catch {}
@@ -2383,16 +3878,28 @@ var Agent = class Agent extends Server {
2383
3878
  }));
2384
3879
  }
2385
3880
  /**
2386
- * Destroy the Agent, removing all state and scheduled tasks
3881
+ * Destroy the Agent, removing all state and scheduled tasks.
3882
+ *
3883
+ * On a top-level agent: drops every table, clears the alarm, and
3884
+ * aborts the isolate.
3885
+ *
3886
+ * On a sub-agent (facet): delegates teardown to the immediate
3887
+ * parent so the parent-owned schedule rows for this sub-agent
3888
+ * (and any of its descendants) are cancelled, the parent's
3889
+ * `cf_agents_sub_agents` registry entry is cleared, and
3890
+ * `ctx.facets.delete` wipes the facet's own storage. The
3891
+ * `ctx.facets.delete` call aborts this isolate, so this method
3892
+ * may not return cleanly when invoked from inside the facet —
3893
+ * callers should treat it as fire-and-forget.
2387
3894
  */
2388
3895
  async destroy() {
2389
- this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
2390
- this.sql`DROP TABLE IF EXISTS cf_agents_state`;
2391
- this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
2392
- this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
2393
- this.sql`DROP TABLE IF EXISTS cf_agents_workflows`;
2394
- this.sql`DROP TABLE IF EXISTS cf_agents_sub_agents`;
2395
- if (!this._isFacet) await this.ctx.storage.deleteAlarm();
3896
+ if (this._isFacet) {
3897
+ this._emit("destroy");
3898
+ await (await this._rootAlarmOwner())._cf_destroyDescendantFacet(this.selfPath);
3899
+ return;
3900
+ }
3901
+ this._dropInternalTablesForDestroy();
3902
+ await this.ctx.storage.deleteAlarm();
2396
3903
  await this.ctx.storage.deleteAll();
2397
3904
  this._disposables.dispose();
2398
3905
  await this.mcp.dispose();
@@ -2402,6 +3909,18 @@ var Agent = class Agent extends Server {
2402
3909
  }, 0);
2403
3910
  this._emit("destroy");
2404
3911
  }
3912
+ /** @internal Drop every internal Agents SDK table during top-level destroy. */
3913
+ _dropInternalTablesForDestroy() {
3914
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
3915
+ this.sql`DROP TABLE IF EXISTS cf_agents_state`;
3916
+ this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
3917
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
3918
+ this.sql`DROP TABLE IF EXISTS cf_agents_workflows`;
3919
+ this.sql`DROP TABLE IF EXISTS cf_agents_sub_agents`;
3920
+ this.sql`DROP TABLE IF EXISTS cf_agents_runs`;
3921
+ this.sql`DROP TABLE IF EXISTS cf_agents_facet_runs`;
3922
+ this.sql`DROP TABLE IF EXISTS cf_agent_tool_runs`;
3923
+ }
2405
3924
  /**
2406
3925
  * Check if a method is callable
2407
3926
  * @param method The method name to check