agents 0.11.8 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +37 -1
  2. package/dist/{index-DSwOzhhd.d.ts → agent-tool-types-tBGRsPm0.d.ts} +584 -99
  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-CIO14miM.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 +27 -1
  18. package/dist/chat/index.js +3 -263
  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 +1393 -296
  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 +42 -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/workflows.d.ts +1 -1
  43. package/package.json +9 -4
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";
@@ -74,7 +74,7 @@ const DEFAULT_KEEP_ALIVE_INTERVAL_MS = 3e4;
74
74
  * The constructor stores this as a row in cf_agents_state and checks it
75
75
  * on wake to skip DDL on established DOs.
76
76
  */
77
- const CURRENT_SCHEMA_VERSION = 3;
77
+ const CURRENT_SCHEMA_VERSION = 7;
78
78
  const SCHEMA_VERSION_ROW_ID = "cf_schema_version";
79
79
  const STATE_ROW_ID = "cf_state_row_id";
80
80
  const STATE_WAS_CHANGED = "cf_state_was_changed";
@@ -218,13 +218,13 @@ function getCurrentAgent() {
218
218
  */
219
219
  function withAgentContext(method) {
220
220
  return function(...args) {
221
- const { connection, request, email, agent } = getCurrentAgent();
221
+ const { agent } = getCurrentAgent();
222
222
  if (agent === this) return method.apply(this, args);
223
223
  return __DO_NOT_USE_WILL_BREAK__agentContext.run({
224
224
  agent: this,
225
- connection,
226
- request,
227
- email
225
+ connection: void 0,
226
+ request: void 0,
227
+ email: void 0
228
228
  }, () => {
229
229
  return method.apply(this, args);
230
230
  });
@@ -383,7 +383,9 @@ var Agent = class Agent extends Server {
383
383
  running INTEGER DEFAULT 0,
384
384
  created_at INTEGER DEFAULT (unixepoch()),
385
385
  execution_started_at INTEGER,
386
- retry_options TEXT
386
+ retry_options TEXT,
387
+ owner_path TEXT,
388
+ owner_path_key TEXT
387
389
  )
388
390
  `;
389
391
  const addColumnIfNotExists = (sql) => {
@@ -397,6 +399,8 @@ var Agent = class Agent extends Server {
397
399
  addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN running INTEGER DEFAULT 0");
398
400
  addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN execution_started_at INTEGER");
399
401
  addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN retry_options TEXT");
402
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN owner_path TEXT");
403
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN owner_path_key TEXT");
400
404
  addColumnIfNotExists("ALTER TABLE cf_agents_queues ADD COLUMN retry_options TEXT");
401
405
  {
402
406
  const rows = this.ctx.storage.sql.exec("SELECT sql FROM sqlite_master WHERE type='table' AND name='cf_agents_schedules'").toArray();
@@ -416,15 +420,19 @@ var Agent = class Agent extends Server {
416
420
  running INTEGER DEFAULT 0,
417
421
  created_at INTEGER DEFAULT (unixepoch()),
418
422
  execution_started_at INTEGER,
419
- retry_options TEXT
423
+ retry_options TEXT,
424
+ owner_path TEXT,
425
+ owner_path_key TEXT
420
426
  )
421
427
  `);
422
428
  this.ctx.storage.sql.exec(`
423
429
  INSERT INTO cf_agents_schedules_new
424
430
  (id, callback, payload, type, time, delayInSeconds, cron,
425
- intervalSeconds, running, created_at, execution_started_at, retry_options)
431
+ intervalSeconds, running, created_at, execution_started_at, retry_options,
432
+ owner_path, owner_path_key)
426
433
  SELECT id, callback, payload, type, time, delayInSeconds, cron,
427
- intervalSeconds, running, created_at, execution_started_at, retry_options
434
+ intervalSeconds, running, created_at, execution_started_at, retry_options,
435
+ owner_path, owner_path_key
428
436
  FROM cf_agents_schedules
429
437
  `);
430
438
  this.ctx.storage.sql.exec("DROP TABLE cf_agents_schedules");
@@ -467,6 +475,41 @@ var Agent = class Agent extends Server {
467
475
  )
468
476
  `;
469
477
  this.sql`
478
+ CREATE TABLE IF NOT EXISTS cf_agents_facet_runs (
479
+ owner_path TEXT NOT NULL,
480
+ owner_path_key TEXT NOT NULL,
481
+ run_id TEXT NOT NULL,
482
+ created_at INTEGER NOT NULL,
483
+ PRIMARY KEY (owner_path_key, run_id)
484
+ )
485
+ `;
486
+ this.sql`
487
+ CREATE INDEX IF NOT EXISTS idx_facet_runs_owner_path_key
488
+ ON cf_agents_facet_runs(owner_path_key)
489
+ `;
490
+ this.sql`
491
+ CREATE TABLE IF NOT EXISTS cf_agent_tool_runs (
492
+ run_id TEXT PRIMARY KEY,
493
+ parent_tool_call_id TEXT,
494
+ agent_type TEXT NOT NULL,
495
+ input_preview TEXT,
496
+ input_redacted INTEGER NOT NULL DEFAULT 1,
497
+ status TEXT NOT NULL,
498
+ summary TEXT,
499
+ output_json TEXT,
500
+ error_message TEXT,
501
+ display_metadata TEXT,
502
+ display_order INTEGER NOT NULL DEFAULT 0,
503
+ started_at INTEGER NOT NULL,
504
+ completed_at INTEGER
505
+ )
506
+ `;
507
+ this.sql`
508
+ CREATE INDEX IF NOT EXISTS idx_agent_tool_runs_parent_tool_call_id
509
+ ON cf_agent_tool_runs(parent_tool_call_id, display_order)
510
+ `;
511
+ addColumnIfNotExists("ALTER TABLE cf_agent_tool_runs ADD COLUMN output_json TEXT");
512
+ this.sql`
470
513
  INSERT OR REPLACE INTO cf_agents_state (id, state)
471
514
  VALUES (${SCHEMA_VERSION_ROW_ID}, ${String(CURRENT_SCHEMA_VERSION)})
472
515
  `;
@@ -480,16 +523,19 @@ var Agent = class Agent extends Server {
480
523
  this._rawStateAccessors = /* @__PURE__ */ new WeakMap();
481
524
  this._persistenceHookMode = "none";
482
525
  this._isFacet = false;
526
+ this._suppressProtocolBroadcasts = false;
483
527
  this._parentPath = [];
484
528
  this._insideOnStart = false;
485
529
  this._warnedScheduleInOnStart = /* @__PURE__ */ new Set();
486
530
  this._keepAliveRefs = 0;
531
+ this._facetKeepAliveTokens = /* @__PURE__ */ new Set();
487
532
  this._runFiberActiveFibers = /* @__PURE__ */ new Set();
488
533
  this._runFiberRecoveryInProgress = false;
489
534
  this._ParentClass = Object.getPrototypeOf(this).constructor;
490
535
  this.initialState = DEFAULT_STATE;
491
536
  this.observability = genericObservability;
492
537
  this._flushingQueue = false;
538
+ this.maxConcurrentAgentTools = Infinity;
493
539
  this._subAgentRegistryReady = false;
494
540
  if (!wrappedClasses.has(this.constructor)) {
495
541
  this._autoWrapCustomMethods();
@@ -666,6 +712,7 @@ var Agent = class Agent extends Server {
666
712
  }));
667
713
  } else this._setConnectionNoProtocol(connection);
668
714
  this._emit("connect", { connectionId: connection.id });
715
+ await this._replayAgentToolRuns(connection);
669
716
  return this._tryCatch(() => _onConnect(connection, ctx));
670
717
  });
671
718
  };
@@ -702,6 +749,7 @@ var Agent = class Agent extends Server {
702
749
  this.broadcastMcpServers();
703
750
  this._checkOrphanedWorkflows();
704
751
  await this._checkRunFibers();
752
+ await this._reconcileAgentToolRuns();
705
753
  this._insideOnStart = true;
706
754
  this._warnedScheduleInOnStart.clear();
707
755
  try {
@@ -743,6 +791,7 @@ var Agent = class Agent extends Server {
743
791
  * @param excludeIds Additional connection IDs to exclude (e.g. the source)
744
792
  */
745
793
  _broadcastProtocol(msg, excludeIds = []) {
794
+ if (this._suppressProtocolBroadcasts) return;
746
795
  const exclude = [...excludeIds];
747
796
  for (const conn of this.getConnections()) if (!this.isConnectionProtocolEnabled(conn)) exclude.push(conn.id);
748
797
  this.broadcast(msg, exclude);
@@ -1374,39 +1423,60 @@ var Agent = class Agent extends Server {
1374
1423
  retry: parseRetryOptions(row)
1375
1424
  }));
1376
1425
  }
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.");
1426
+ _scheduleOwnerPathKey(path) {
1427
+ if (!path) return null;
1428
+ return path.map((step) => `${encodeURIComponent(step.className)}:${encodeURIComponent(step.name)}`).join("/");
1429
+ }
1430
+ _facetRunRowsForPrefix(ownerPath) {
1431
+ return this.sql`
1432
+ SELECT owner_path, owner_path_key, run_id, created_at
1433
+ FROM cf_agents_facet_runs
1434
+ `.filter((row) => {
1435
+ try {
1436
+ const rowOwnerPath = JSON.parse(row.owner_path);
1437
+ return this._isSameAgentPathPrefix(ownerPath, rowOwnerPath);
1438
+ } catch {
1439
+ return false;
1440
+ }
1441
+ });
1442
+ }
1443
+ _deleteFacetRunRowsForPrefix(ownerPath) {
1444
+ for (const row of this._facetRunRowsForPrefix(ownerPath)) this.sql`
1445
+ DELETE FROM cf_agents_facet_runs
1446
+ WHERE owner_path_key = ${row.owner_path_key}
1447
+ AND run_id = ${row.run_id}
1448
+ `;
1449
+ }
1450
+ async _rootAlarmOwner() {
1451
+ const root = this._parentPath[0];
1452
+ if (!root) throw new Error("Facet scheduler delegation requires a root parent.");
1453
+ const binding = this.ctx.exports?.[root.className];
1454
+ if (!binding) throw new Error(`Unable to resolve root scheduler "${root.className}" for sub-agent schedule delegation.`);
1455
+ return await getServerByName(binding, root.name);
1456
+ }
1457
+ _validateScheduleCallback(when, callback, options) {
1401
1458
  if (typeof callback !== "string") throw new Error("Callback must be a string");
1402
1459
  if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1403
1460
  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
1461
  if (this._insideOnStart && options?.idempotent === void 0 && typeof when !== "string" && !this._warnedScheduleInOnStart.has(callback)) {
1407
1462
  this._warnedScheduleInOnStart.add(callback);
1408
1463
  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
1464
  }
1465
+ }
1466
+ /**
1467
+ * Insert (or, for idempotent calls, return the existing row for) a
1468
+ * schedule owned by either this top-level agent (`ownerPath === null`)
1469
+ * or a descendant facet. Returns `{ schedule, created }` — `created`
1470
+ * is `false` when an idempotent insert deduplicates onto an existing
1471
+ * row, so callers can suppress the `schedule:create` event in that
1472
+ * case to match historic semantics.
1473
+ * @internal
1474
+ */
1475
+ async _insertScheduleForOwner(ownerPath, when, callback, payload, options) {
1476
+ const ownerPathJson = ownerPath ? JSON.stringify(ownerPath) : null;
1477
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1478
+ const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1479
+ const payloadJson = JSON.stringify(payload);
1410
1480
  if (when instanceof Date) {
1411
1481
  const timestamp = Math.floor(when.getTime() / 1e3);
1412
1482
  if (options?.idempotent) {
@@ -1415,90 +1485,96 @@ var Agent = class Agent extends Server {
1415
1485
  WHERE type = 'scheduled'
1416
1486
  AND callback = ${callback}
1417
1487
  AND payload IS ${payloadJson}
1488
+ AND owner_path_key IS ${ownerPathKey}
1418
1489
  LIMIT 1
1419
1490
  `;
1420
1491
  if (existing.length > 0) {
1421
1492
  const row = existing[0];
1422
1493
  await this._scheduleNextAlarm();
1423
1494
  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"
1495
+ schedule: {
1496
+ callback: row.callback,
1497
+ id: row.id,
1498
+ payload: JSON.parse(row.payload),
1499
+ retry: parseRetryOptions(row),
1500
+ time: row.time,
1501
+ type: "scheduled"
1502
+ },
1503
+ created: false
1430
1504
  };
1431
1505
  }
1432
1506
  }
1433
1507
  const id = nanoid(9);
1434
1508
  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})
1509
+ INSERT OR REPLACE INTO cf_agents_schedules
1510
+ (id, callback, payload, type, time, retry_options, owner_path, owner_path_key)
1511
+ VALUES
1512
+ (${id}, ${callback}, ${payloadJson}, 'scheduled', ${timestamp}, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1437
1513
  `;
1438
1514
  await this._scheduleNextAlarm();
1439
- const schedule = {
1440
- callback,
1441
- id,
1442
- payload,
1443
- retry: options?.retry,
1444
- time: timestamp,
1445
- type: "scheduled"
1515
+ return {
1516
+ schedule: {
1517
+ callback,
1518
+ id,
1519
+ payload,
1520
+ retry: options?.retry,
1521
+ time: timestamp,
1522
+ type: "scheduled"
1523
+ },
1524
+ created: true
1446
1525
  };
1447
- this._emit("schedule:create", {
1448
- callback,
1449
- id
1450
- });
1451
- return schedule;
1452
1526
  }
1453
1527
  if (typeof when === "number") {
1454
- const time = new Date(Date.now() + when * 1e3);
1455
- const timestamp = Math.floor(time.getTime() / 1e3);
1528
+ const timestamp = Math.floor((Date.now() + when * 1e3) / 1e3);
1456
1529
  if (options?.idempotent) {
1457
1530
  const existing = this.sql`
1458
1531
  SELECT * FROM cf_agents_schedules
1459
1532
  WHERE type = 'delayed'
1460
1533
  AND callback = ${callback}
1461
1534
  AND payload IS ${payloadJson}
1535
+ AND owner_path_key IS ${ownerPathKey}
1462
1536
  LIMIT 1
1463
1537
  `;
1464
1538
  if (existing.length > 0) {
1465
1539
  const row = existing[0];
1466
1540
  await this._scheduleNextAlarm();
1467
1541
  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"
1542
+ schedule: {
1543
+ callback: row.callback,
1544
+ delayInSeconds: row.delayInSeconds ?? 0,
1545
+ id: row.id,
1546
+ payload: JSON.parse(row.payload),
1547
+ retry: parseRetryOptions(row),
1548
+ time: row.time,
1549
+ type: "delayed"
1550
+ },
1551
+ created: false
1475
1552
  };
1476
1553
  }
1477
1554
  }
1478
1555
  const id = nanoid(9);
1479
1556
  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})
1557
+ INSERT OR REPLACE INTO cf_agents_schedules
1558
+ (id, callback, payload, type, delayInSeconds, time, retry_options, owner_path, owner_path_key)
1559
+ VALUES
1560
+ (${id}, ${callback}, ${payloadJson}, 'delayed', ${when}, ${timestamp}, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1482
1561
  `;
1483
1562
  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"
1563
+ return {
1564
+ schedule: {
1565
+ callback,
1566
+ delayInSeconds: when,
1567
+ id,
1568
+ payload,
1569
+ retry: options?.retry,
1570
+ time: timestamp,
1571
+ type: "delayed"
1572
+ },
1573
+ created: true
1492
1574
  };
1493
- this._emit("schedule:create", {
1494
- callback,
1495
- id
1496
- });
1497
- return schedule;
1498
1575
  }
1499
1576
  if (typeof when === "string") {
1500
- const nextExecutionTime = getNextCronTime(when);
1501
- const timestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
1577
+ const timestamp = Math.floor(getNextCronTime(when).getTime() / 1e3);
1502
1578
  if (options?.idempotent !== false) {
1503
1579
  const existing = this.sql`
1504
1580
  SELECT * FROM cf_agents_schedules
@@ -1506,79 +1582,70 @@ var Agent = class Agent extends Server {
1506
1582
  AND callback = ${callback}
1507
1583
  AND cron = ${when}
1508
1584
  AND payload IS ${payloadJson}
1585
+ AND owner_path_key IS ${ownerPathKey}
1509
1586
  LIMIT 1
1510
1587
  `;
1511
1588
  if (existing.length > 0) {
1512
1589
  const row = existing[0];
1513
1590
  await this._scheduleNextAlarm();
1514
1591
  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"
1592
+ schedule: {
1593
+ callback: row.callback,
1594
+ cron: row.cron ?? when,
1595
+ id: row.id,
1596
+ payload: JSON.parse(row.payload),
1597
+ retry: parseRetryOptions(row),
1598
+ time: row.time,
1599
+ type: "cron"
1600
+ },
1601
+ created: false
1522
1602
  };
1523
1603
  }
1524
1604
  }
1525
1605
  const id = nanoid(9);
1526
1606
  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})
1607
+ INSERT OR REPLACE INTO cf_agents_schedules
1608
+ (id, callback, payload, type, cron, time, retry_options, owner_path, owner_path_key)
1609
+ VALUES
1610
+ (${id}, ${callback}, ${payloadJson}, 'cron', ${when}, ${timestamp}, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1529
1611
  `;
1530
1612
  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"
1613
+ return {
1614
+ schedule: {
1615
+ callback,
1616
+ cron: when,
1617
+ id,
1618
+ payload,
1619
+ retry: options?.retry,
1620
+ time: timestamp,
1621
+ type: "cron"
1622
+ },
1623
+ created: true
1539
1624
  };
1540
- this._emit("schedule:create", {
1541
- callback,
1542
- id
1543
- });
1544
- return schedule;
1545
1625
  }
1546
1626
  throw new Error(`Invalid schedule type: ${JSON.stringify(when)}(${typeof when}) trying to schedule ${callback}`);
1547
1627
  }
1548
1628
  /**
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
1629
+ * Insert a schedule row owned by a descendant facet. Called via RPC
1630
+ * from the facet's `schedule()`. Returns `{ schedule, created }`
1631
+ * so the originating facet can suppress `schedule:create` on
1632
+ * idempotent dedup. This method does not emit observability
1633
+ * events itself.
1634
+ * @internal
1573
1635
  */
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);
1636
+ async _cf_scheduleForFacet(ownerPath, when, callback, payload, options) {
1637
+ return this._insertScheduleForOwner(ownerPath, when, callback, payload, options);
1638
+ }
1639
+ /**
1640
+ * Insert (or, for idempotent calls, return the existing row for) an
1641
+ * interval schedule. Mirrors {@link _insertScheduleForOwner}
1642
+ * returns `{ schedule, created }` so callers can suppress
1643
+ * `schedule:create` on dedup.
1644
+ * @internal
1645
+ */
1646
+ async _insertIntervalScheduleForOwner(ownerPath, intervalSeconds, callback, payload, options) {
1647
+ const ownerPathJson = ownerPath ? JSON.stringify(ownerPath) : null;
1648
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1582
1649
  const idempotent = options?._idempotent !== false;
1583
1650
  const payloadJson = JSON.stringify(payload);
1584
1651
  if (idempotent) {
@@ -1588,73 +1655,165 @@ var Agent = class Agent extends Server {
1588
1655
  AND callback = ${callback}
1589
1656
  AND intervalSeconds = ${intervalSeconds}
1590
1657
  AND payload IS ${payloadJson}
1658
+ AND owner_path_key IS ${ownerPathKey}
1591
1659
  LIMIT 1
1592
1660
  `;
1593
1661
  if (existing.length > 0) {
1594
1662
  const row = existing[0];
1595
1663
  await this._scheduleNextAlarm();
1596
1664
  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"
1665
+ schedule: {
1666
+ callback: row.callback,
1667
+ id: row.id,
1668
+ intervalSeconds: row.intervalSeconds ?? intervalSeconds,
1669
+ payload: JSON.parse(row.payload),
1670
+ retry: parseRetryOptions(row),
1671
+ time: row.time,
1672
+ type: "interval"
1673
+ },
1674
+ created: false
1604
1675
  };
1605
1676
  }
1606
1677
  }
1607
1678
  const id = nanoid(9);
1608
- const time = new Date(Date.now() + intervalSeconds * 1e3);
1609
- const timestamp = Math.floor(time.getTime() / 1e3);
1679
+ const timestamp = Math.floor((Date.now() + intervalSeconds * 1e3) / 1e3);
1610
1680
  const retryJson = options?.retry ? JSON.stringify(options.retry) : null;
1611
1681
  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})
1682
+ INSERT OR REPLACE INTO cf_agents_schedules
1683
+ (id, callback, payload, type, intervalSeconds, time, running, retry_options, owner_path, owner_path_key)
1684
+ VALUES
1685
+ (${id}, ${callback}, ${payloadJson}, 'interval', ${intervalSeconds}, ${timestamp}, 0, ${retryJson}, ${ownerPathJson}, ${ownerPathKey})
1614
1686
  `;
1615
1687
  await this._scheduleNextAlarm();
1616
- const schedule = {
1617
- callback,
1618
- id,
1619
- intervalSeconds,
1620
- payload,
1621
- retry: options?.retry,
1622
- time: timestamp,
1623
- type: "interval"
1688
+ return {
1689
+ schedule: {
1690
+ callback,
1691
+ id,
1692
+ intervalSeconds,
1693
+ payload,
1694
+ retry: options?.retry,
1695
+ time: timestamp,
1696
+ type: "interval"
1697
+ },
1698
+ created: true
1624
1699
  };
1625
- this._emit("schedule:create", {
1626
- callback,
1627
- id
1628
- });
1629
- return schedule;
1630
1700
  }
1631
1701
  /**
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
1702
+ * Insert an interval schedule row owned by a descendant facet.
1703
+ * Called via RPC from the facet's `scheduleEvery()`. Returns
1704
+ * `{ schedule, created }` so the originating facet can suppress
1705
+ * `schedule:create` on idempotent dedup. This method does not
1706
+ * emit observability events itself.
1707
+ * @internal
1636
1708
  */
1637
- getSchedule(id) {
1709
+ async _cf_scheduleEveryForFacet(ownerPath, intervalSeconds, callback, payload, options) {
1710
+ return this._insertIntervalScheduleForOwner(ownerPath, intervalSeconds, callback, payload, options);
1711
+ }
1712
+ /**
1713
+ * Cancel a schedule row owned by a descendant facet, scoped by
1714
+ * `owner_path_key` so siblings can't reach each other's rows.
1715
+ * Returns the canceled row's callback name so the originating
1716
+ * facet can emit `schedule:cancel`. This method does not emit
1717
+ * observability events itself.
1718
+ * @internal
1719
+ */
1720
+ async _cf_cancelScheduleForFacet(ownerPath, id) {
1721
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1638
1722
  const result = this.sql`
1639
- SELECT * FROM cf_agents_schedules WHERE id = ${id}
1723
+ SELECT * FROM cf_agents_schedules
1724
+ WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
1640
1725
  `;
1641
- if (!result || result.length === 0) return;
1642
- const row = result[0];
1726
+ if (result.length === 0) return { ok: false };
1727
+ const callback = result[0].callback;
1728
+ this.sql`
1729
+ DELETE FROM cf_agents_schedules
1730
+ WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
1731
+ `;
1732
+ await this._scheduleNextAlarm();
1643
1733
  return {
1644
- ...row,
1645
- payload: JSON.parse(row.payload),
1646
- retry: parseRetryOptions(row)
1734
+ ok: true,
1735
+ callback
1647
1736
  };
1648
1737
  }
1649
1738
  /**
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
1739
+ * Clean root-owned bookkeeping for a sub-tree of facets. This
1740
+ * bulk-cancels schedules whose `owner_path` starts with the given
1741
+ * prefix and deletes root-side facet fiber recovery leases for the
1742
+ * same sub-tree. Used by `deleteSubAgent` and recursive facet
1743
+ * destroy. Emits `schedule:cancel` on this agent (the alarm-owning
1744
+ * root) for each schedule row removed — the facets being torn down
1745
+ * may not be alive to receive the events themselves.
1746
+ * @internal
1654
1747
  */
1655
- getSchedules(criteria = {}) {
1656
- let query = "SELECT * FROM cf_agents_schedules WHERE 1=1";
1657
- const params = [];
1748
+ async _cf_cleanupFacetPrefix(ownerPath) {
1749
+ const rowsToDelete = this.sql`
1750
+ SELECT * FROM cf_agents_schedules
1751
+ WHERE owner_path IS NOT NULL
1752
+ `.filter((row) => {
1753
+ if (!row.owner_path) return false;
1754
+ try {
1755
+ const rowOwnerPath = JSON.parse(row.owner_path);
1756
+ return this._isSameAgentPathPrefix(ownerPath, rowOwnerPath);
1757
+ } catch {
1758
+ return false;
1759
+ }
1760
+ });
1761
+ for (const row of rowsToDelete) {
1762
+ this._emit("schedule:cancel", {
1763
+ callback: row.callback,
1764
+ id: row.id
1765
+ });
1766
+ this.sql`DELETE FROM cf_agents_schedules WHERE id = ${row.id}`;
1767
+ }
1768
+ this._deleteFacetRunRowsForPrefix(ownerPath);
1769
+ await this._scheduleNextAlarm();
1770
+ }
1771
+ _scheduleRowToSchedule(row) {
1772
+ const base = {
1773
+ callback: row.callback,
1774
+ id: row.id,
1775
+ payload: JSON.parse(row.payload),
1776
+ retry: parseRetryOptions(row)
1777
+ };
1778
+ switch (row.type) {
1779
+ case "scheduled": return {
1780
+ ...base,
1781
+ time: row.time,
1782
+ type: "scheduled"
1783
+ };
1784
+ case "delayed": return {
1785
+ ...base,
1786
+ delayInSeconds: row.delayInSeconds ?? 0,
1787
+ time: row.time,
1788
+ type: "delayed"
1789
+ };
1790
+ case "cron": return {
1791
+ ...base,
1792
+ cron: row.cron ?? "",
1793
+ time: row.time,
1794
+ type: "cron"
1795
+ };
1796
+ case "interval": return {
1797
+ ...base,
1798
+ intervalSeconds: row.intervalSeconds ?? 0,
1799
+ time: row.time,
1800
+ type: "interval"
1801
+ };
1802
+ }
1803
+ }
1804
+ _getScheduleForOwner(ownerPath, id) {
1805
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1806
+ const result = this.sql`
1807
+ SELECT * FROM cf_agents_schedules
1808
+ WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
1809
+ `;
1810
+ if (!result || result.length === 0) return;
1811
+ return this._scheduleRowToSchedule(result[0]);
1812
+ }
1813
+ _listSchedulesForOwner(ownerPath, criteria = {}) {
1814
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1815
+ let query = "SELECT * FROM cf_agents_schedules WHERE owner_path_key IS ?";
1816
+ const params = [ownerPathKey];
1658
1817
  if (criteria.id) {
1659
1818
  query += " AND id = ?";
1660
1819
  params.push(criteria.id);
@@ -1669,61 +1828,274 @@ var Agent = class Agent extends Server {
1669
1828
  const end = criteria.timeRange.end || /* @__PURE__ */ new Date(999999999999999);
1670
1829
  params.push(Math.floor(start.getTime() / 1e3), Math.floor(end.getTime() / 1e3));
1671
1830
  }
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
- }));
1831
+ return this.ctx.storage.sql.exec(query, ...params).toArray().map((row) => this._scheduleRowToSchedule(row));
1677
1832
  }
1678
1833
  /**
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
1834
+ * Read a single schedule row owned by a descendant facet.
1835
+ * @internal
1682
1836
  */
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;
1837
+ async _cf_getScheduleForFacet(ownerPath, id) {
1838
+ return this._getScheduleForOwner(ownerPath, id);
1694
1839
  }
1695
1840
  /**
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
1702
- * alarm system, without creating schedule rows or emitting observability
1703
- * events. Configure via `static options = { keepAliveIntervalMs: 5000 }`.
1704
- *
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.
1710
- *
1711
- * @example
1712
- * ```ts
1713
- * const dispose = await this.keepAlive();
1714
- * try {
1715
- * // ... long-running work ...
1716
- * } finally {
1717
- * dispose();
1718
- * }
1719
- * ```
1841
+ * List schedule rows owned by a descendant facet, scoped by
1842
+ * `owner_path_key` so siblings remain isolated from each other.
1843
+ * @internal
1720
1844
  */
1721
- async keepAlive() {
1722
- if (this._isFacet) return () => {};
1845
+ async _cf_listSchedulesForFacet(ownerPath, criteria = {}) {
1846
+ return this._listSchedulesForOwner(ownerPath, criteria);
1847
+ }
1848
+ /**
1849
+ * Acquire a root-owned keepAlive ref on behalf of a descendant facet.
1850
+ * Facets share the root isolate but cannot set their own physical
1851
+ * alarm, so this lets facet work use the root alarm heartbeat.
1852
+ * @internal
1853
+ */
1854
+ async _cf_acquireFacetKeepAlive(ownerPath) {
1855
+ const token = `${this._scheduleOwnerPathKey(ownerPath) ?? "unknown"}:${nanoid(9)}`;
1856
+ this._facetKeepAliveTokens.add(token);
1723
1857
  this._keepAliveRefs++;
1724
1858
  if (this._keepAliveRefs === 1) await this._scheduleNextAlarm();
1725
- let disposed = false;
1726
- return () => {
1859
+ return token;
1860
+ }
1861
+ /**
1862
+ * Release a root-owned keepAlive ref previously acquired for a facet.
1863
+ * Idempotent so disposer calls can safely race or run twice.
1864
+ * @internal
1865
+ */
1866
+ async _cf_releaseFacetKeepAlive(token) {
1867
+ if (!this._facetKeepAliveTokens.delete(token)) return;
1868
+ this._keepAliveRefs = Math.max(0, this._keepAliveRefs - 1);
1869
+ await this._scheduleNextAlarm();
1870
+ }
1871
+ /**
1872
+ * Register a facet's durable run row in the root-side index so root
1873
+ * alarm housekeeping can dispatch recovery checks into idle facets.
1874
+ * The facet remains authoritative for snapshots and recovery hooks.
1875
+ * @internal
1876
+ */
1877
+ async _cf_registerFacetRun(ownerPath, runId) {
1878
+ const ownerPathJson = JSON.stringify(ownerPath);
1879
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1880
+ if (!ownerPathKey) throw new Error("_cf_registerFacetRun requires a non-empty owner path.");
1881
+ this.sql`
1882
+ INSERT OR REPLACE INTO cf_agents_facet_runs
1883
+ (owner_path, owner_path_key, run_id, created_at)
1884
+ VALUES
1885
+ (${ownerPathJson}, ${ownerPathKey}, ${runId}, ${Date.now()})
1886
+ `;
1887
+ await this._scheduleNextAlarm();
1888
+ }
1889
+ /**
1890
+ * Remove a completed facet fiber from the root-side index.
1891
+ * @internal
1892
+ */
1893
+ async _cf_unregisterFacetRun(ownerPath, runId) {
1894
+ const ownerPathKey = this._scheduleOwnerPathKey(ownerPath);
1895
+ this.sql`
1896
+ DELETE FROM cf_agents_facet_runs
1897
+ WHERE owner_path_key IS ${ownerPathKey}
1898
+ AND run_id = ${runId}
1899
+ `;
1900
+ await this._scheduleNextAlarm();
1901
+ }
1902
+ /**
1903
+ * Schedule a task to be executed in the future
1904
+ *
1905
+ * Cron schedules are **idempotent by default** — calling `schedule("0 * * * *", "tick")`
1906
+ * multiple times with the same callback, cron expression, and payload returns
1907
+ * the existing schedule instead of creating a duplicate. Set `idempotent: false`
1908
+ * to override this.
1909
+ *
1910
+ * For delayed and scheduled (Date) types, set `idempotent: true` to opt in
1911
+ * to the same dedup behavior (matched on callback + payload). This is useful
1912
+ * when calling `schedule()` in `onStart()` to avoid accumulating duplicate
1913
+ * rows across Durable Object restarts.
1914
+ *
1915
+ * @template T Type of the payload data
1916
+ * @param when When to execute the task (Date, seconds delay, or cron expression)
1917
+ * @param callback Name of the method to call
1918
+ * @param payload Data to pass to the callback
1919
+ * @param options Options for the scheduled task
1920
+ * @param options.retry Retry options for the callback execution
1921
+ * @param options.idempotent Dedup by callback+payload. Defaults to `true` for cron, `false` otherwise.
1922
+ * @returns Schedule object representing the scheduled task
1923
+ */
1924
+ async schedule(when, callback, payload, options) {
1925
+ this._validateScheduleCallback(when, callback, options);
1926
+ const result = this._isFacet ? await (await this._rootAlarmOwner())._cf_scheduleForFacet(this.selfPath, when, callback, payload, options) : await this._insertScheduleForOwner(null, when, callback, payload, options);
1927
+ if (result.created) this._emit("schedule:create", {
1928
+ callback: result.schedule.callback,
1929
+ id: result.schedule.id
1930
+ });
1931
+ return result.schedule;
1932
+ }
1933
+ /**
1934
+ * Schedule a task to run repeatedly at a fixed interval.
1935
+ *
1936
+ * This method is **idempotent** — calling it multiple times with the same
1937
+ * `callback`, `intervalSeconds`, and `payload` returns the existing schedule
1938
+ * instead of creating a duplicate. A different interval or payload is
1939
+ * treated as a distinct schedule and creates a new row.
1940
+ *
1941
+ * This makes it safe to call in `onStart()`, which runs on every Durable
1942
+ * Object wake:
1943
+ *
1944
+ * ```ts
1945
+ * async onStart() {
1946
+ * // Only one schedule is created, no matter how many times the DO wakes
1947
+ * await this.scheduleEvery(30, "tick");
1948
+ * }
1949
+ * ```
1950
+ *
1951
+ * @template T Type of the payload data
1952
+ * @param intervalSeconds Number of seconds between executions
1953
+ * @param callback Name of the method to call
1954
+ * @param payload Data to pass to the callback
1955
+ * @param options Options for the scheduled task
1956
+ * @param options.retry Retry options for the callback execution
1957
+ * @returns Schedule object representing the scheduled task
1958
+ */
1959
+ async scheduleEvery(intervalSeconds, callback, payload, options) {
1960
+ const MAX_INTERVAL_SECONDS = 720 * 60 * 60;
1961
+ if (typeof intervalSeconds !== "number" || intervalSeconds <= 0) throw new Error("intervalSeconds must be a positive number");
1962
+ if (intervalSeconds > MAX_INTERVAL_SECONDS) throw new Error(`intervalSeconds cannot exceed ${MAX_INTERVAL_SECONDS} seconds (30 days)`);
1963
+ if (typeof callback !== "string") throw new Error("Callback must be a string");
1964
+ if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1965
+ if (options?.retry) validateRetryOptions(options.retry, this._resolvedOptions.retry);
1966
+ const result = this._isFacet ? await (await this._rootAlarmOwner())._cf_scheduleEveryForFacet(this.selfPath, intervalSeconds, callback, payload, options) : await this._insertIntervalScheduleForOwner(null, intervalSeconds, callback, payload, options);
1967
+ if (result.created) this._emit("schedule:create", {
1968
+ callback: result.schedule.callback,
1969
+ id: result.schedule.id
1970
+ });
1971
+ return result.schedule;
1972
+ }
1973
+ /**
1974
+ * Get a scheduled task by ID
1975
+ * @template T Type of the payload data
1976
+ * @param id ID of the scheduled task
1977
+ * @returns The Schedule object or undefined if not found
1978
+ * @deprecated Use {@link getScheduleById}. This synchronous API cannot cross
1979
+ * Durable Object boundaries and throws inside sub-agents.
1980
+ */
1981
+ getSchedule(id) {
1982
+ if (this._isFacet) throw new Error("getSchedule() is synchronous and cannot read parent-owned sub-agent schedules. Use await this.getScheduleById(id) instead.");
1983
+ return this._getScheduleForOwner(null, id);
1984
+ }
1985
+ /**
1986
+ * Get a scheduled task by ID.
1987
+ *
1988
+ * Unlike the deprecated synchronous {@link getSchedule}, this works inside
1989
+ * sub-agents by delegating to the top-level parent that owns the alarm.
1990
+ *
1991
+ * @template T Type of the payload data
1992
+ * @param id ID of the scheduled task
1993
+ * @returns The Schedule object or undefined if not found
1994
+ */
1995
+ async getScheduleById(id) {
1996
+ if (this._isFacet) return (await this._rootAlarmOwner())._cf_getScheduleForFacet(this.selfPath, id);
1997
+ return this._getScheduleForOwner(null, id);
1998
+ }
1999
+ /**
2000
+ * Get scheduled tasks matching the given criteria
2001
+ * @template T Type of the payload data
2002
+ * @param criteria Criteria to filter schedules
2003
+ * @returns Array of matching Schedule objects
2004
+ * @deprecated Use {@link listSchedules}. This synchronous API cannot cross
2005
+ * Durable Object boundaries and throws inside sub-agents.
2006
+ */
2007
+ getSchedules(criteria = {}) {
2008
+ if (this._isFacet) throw new Error("getSchedules() is synchronous and cannot read parent-owned sub-agent schedules. Use await this.listSchedules(criteria) instead.");
2009
+ return this._listSchedulesForOwner(null, criteria);
2010
+ }
2011
+ /**
2012
+ * List scheduled tasks matching the given criteria.
2013
+ *
2014
+ * Unlike the deprecated synchronous {@link getSchedules}, this works inside
2015
+ * sub-agents by delegating to the top-level parent that owns the alarm.
2016
+ *
2017
+ * @template T Type of the payload data
2018
+ * @param criteria Criteria to filter schedules
2019
+ * @returns Array of matching Schedule objects
2020
+ */
2021
+ async listSchedules(criteria = {}) {
2022
+ if (this._isFacet) return (await this._rootAlarmOwner())._cf_listSchedulesForFacet(this.selfPath, criteria);
2023
+ return this._listSchedulesForOwner(null, criteria);
2024
+ }
2025
+ /**
2026
+ * Cancel a scheduled task.
2027
+ *
2028
+ * Schedules are isolated by owner: a top-level agent's
2029
+ * `cancelSchedule(id)` only matches its own schedules, and a
2030
+ * sub-agent's `cancelSchedule(id)` only matches schedules it
2031
+ * created. To clear every schedule under a sub-agent (and its
2032
+ * descendants), call `parent.deleteSubAgent(Cls, name)` from the
2033
+ * parent — that bulk-cleans root-owned bookkeeping via
2034
+ * {@link _cf_cleanupFacetPrefix}.
2035
+ *
2036
+ * @param id ID of the task to cancel
2037
+ * @returns true if the task was cancelled, false if the task was not found
2038
+ */
2039
+ async cancelSchedule(id) {
2040
+ if (this._isFacet) {
2041
+ const result = await (await this._rootAlarmOwner())._cf_cancelScheduleForFacet(this.selfPath, id);
2042
+ if (result.ok && result.callback) this._emit("schedule:cancel", {
2043
+ callback: result.callback,
2044
+ id
2045
+ });
2046
+ return result.ok;
2047
+ }
2048
+ const schedule = this._getScheduleForOwner(null, id);
2049
+ if (!schedule) return false;
2050
+ this._emit("schedule:cancel", {
2051
+ callback: schedule.callback,
2052
+ id: schedule.id
2053
+ });
2054
+ this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
2055
+ await this._scheduleNextAlarm();
2056
+ return true;
2057
+ }
2058
+ /**
2059
+ * Keep the Durable Object alive via alarm heartbeats.
2060
+ * Returns a disposer function that stops the heartbeat when called.
2061
+ *
2062
+ * Use this when you have long-running work and need to prevent the
2063
+ * DO from going idle (eviction after ~70-140s of inactivity).
2064
+ * The heartbeat fires every `keepAliveIntervalMs` (default 30s) via the
2065
+ * alarm system, without creating schedule rows or emitting observability
2066
+ * events. Configure via `static options = { keepAliveIntervalMs: 5000 }`.
2067
+ *
2068
+ * In facets, delegates the physical heartbeat to the root parent
2069
+ * because facets do not have independent alarm slots.
2070
+ *
2071
+ * @example
2072
+ * ```ts
2073
+ * const dispose = await this.keepAlive();
2074
+ * try {
2075
+ * // ... long-running work ...
2076
+ * } finally {
2077
+ * dispose();
2078
+ * }
2079
+ * ```
2080
+ */
2081
+ async keepAlive() {
2082
+ if (this._isFacet) {
2083
+ const root = await this._rootAlarmOwner();
2084
+ const token = await root._cf_acquireFacetKeepAlive(this.selfPath);
2085
+ let disposed = false;
2086
+ return () => {
2087
+ if (disposed) return;
2088
+ disposed = true;
2089
+ const release = root._cf_releaseFacetKeepAlive(token).catch((e) => {
2090
+ console.error("[Agent] Failed to release facet keepAlive:", e);
2091
+ });
2092
+ this.ctx.waitUntil(release);
2093
+ };
2094
+ }
2095
+ this._keepAliveRefs++;
2096
+ if (this._keepAliveRefs === 1) await this._scheduleNextAlarm();
2097
+ let disposed = false;
2098
+ return () => {
1727
2099
  if (disposed) return;
1728
2100
  disposed = true;
1729
2101
  this._keepAliveRefs = Math.max(0, this._keepAliveRefs - 1);
@@ -1773,8 +2145,16 @@ var Agent = class Agent extends Server {
1773
2145
  VALUES (${id}, ${name}, NULL, ${Date.now()})
1774
2146
  `;
1775
2147
  this._runFiberActiveFibers.add(id);
1776
- const dispose = await this.keepAlive();
2148
+ let root;
2149
+ let registeredFacetRun = false;
2150
+ let dispose = () => {};
1777
2151
  try {
2152
+ if (this._isFacet) {
2153
+ root = await this._rootAlarmOwner();
2154
+ await root._cf_registerFacetRun(this.selfPath, id);
2155
+ registeredFacetRun = true;
2156
+ }
2157
+ dispose = await this.keepAlive();
1778
2158
  const stash = (data) => {
1779
2159
  this.sql`
1780
2160
  UPDATE cf_agents_runs SET snapshot = ${JSON.stringify(data)}
@@ -1793,6 +2173,11 @@ var Agent = class Agent extends Server {
1793
2173
  this._runFiberActiveFibers.delete(id);
1794
2174
  this.sql`DELETE FROM cf_agents_runs WHERE id = ${id}`;
1795
2175
  dispose();
2176
+ if (root && registeredFacetRun) try {
2177
+ await root._cf_unregisterFacetRun(this.selfPath, id);
2178
+ } catch (e) {
2179
+ console.error("[Agent] Failed to unregister facet fiber:", e);
2180
+ }
1796
2181
  }
1797
2182
  }
1798
2183
  /**
@@ -1861,6 +2246,179 @@ var Agent = class Agent extends Server {
1861
2246
  /** @internal */
1862
2247
  async _onAlarmHousekeeping() {
1863
2248
  await this._checkRunFibers();
2249
+ await this._checkFacetRunFibers();
2250
+ }
2251
+ _isSameAgentPathPrefix(prefix, path) {
2252
+ if (prefix.length > path.length) return false;
2253
+ return prefix.every((step, index) => step.className === path[index].className && step.name === path[index].name);
2254
+ }
2255
+ /**
2256
+ * Root-side scan for durable fibers owned by descendant facets.
2257
+ * `cf_agents_facet_runs` is only an index; actual snapshots and
2258
+ * recovery hooks live in each facet's own `cf_agents_runs` table.
2259
+ * @internal
2260
+ */
2261
+ async _checkFacetRunFibers() {
2262
+ if (this._parentPath.length > 0) return;
2263
+ const rows = this.sql`
2264
+ SELECT owner_path, owner_path_key, run_id, created_at
2265
+ FROM cf_agents_facet_runs
2266
+ ORDER BY created_at ASC
2267
+ `;
2268
+ const firstRowByOwner = /* @__PURE__ */ new Map();
2269
+ for (const row of rows) if (!firstRowByOwner.has(row.owner_path_key)) firstRowByOwner.set(row.owner_path_key, row);
2270
+ for (const row of firstRowByOwner.values()) {
2271
+ let ownerPath;
2272
+ try {
2273
+ ownerPath = JSON.parse(row.owner_path);
2274
+ } catch (e) {
2275
+ console.warn(`[Agent] Corrupted facet fiber owner path for ${row.owner_path_key}; pruning stale lease.`, e);
2276
+ this.sql`
2277
+ DELETE FROM cf_agents_facet_runs
2278
+ WHERE owner_path_key = ${row.owner_path_key}
2279
+ `;
2280
+ continue;
2281
+ }
2282
+ try {
2283
+ if (await this._cf_checkRunFibersForFacet(ownerPath) === 0) this.sql`
2284
+ DELETE FROM cf_agents_facet_runs
2285
+ WHERE owner_path_key = ${row.owner_path_key}
2286
+ `;
2287
+ } catch (e) {
2288
+ console.error(`[Agent] Facet fiber recovery check failed for ${row.owner_path_key}:`, e);
2289
+ }
2290
+ }
2291
+ }
2292
+ /**
2293
+ * Dispatch a runFiber recovery check into the facet identified by
2294
+ * `ownerPath`. Returns the number of remaining local `cf_agents_runs`
2295
+ * rows on the target facet after recovery.
2296
+ * @internal
2297
+ */
2298
+ async _cf_checkRunFibersForFacet(ownerPath) {
2299
+ const selfPath = this.selfPath;
2300
+ if (!this._isSameAgentPathPrefix(selfPath, ownerPath)) throw new Error(`Facet fiber owner path does not descend from ${JSON.stringify(selfPath)}.`);
2301
+ if (selfPath.length === ownerPath.length) {
2302
+ await this._checkRunFibers();
2303
+ return this.sql`
2304
+ SELECT COUNT(*) as count FROM cf_agents_runs
2305
+ `[0]?.count ?? 0;
2306
+ }
2307
+ const next = ownerPath[selfPath.length];
2308
+ if (!this.hasSubAgent(next.className, next.name)) return 0;
2309
+ return (await this._cf_resolveSubAgent(next.className, next.name))._cf_checkRunFibersForFacet(ownerPath);
2310
+ }
2311
+ /**
2312
+ * Dispatch a scheduled callback into the facet identified by
2313
+ * `ownerPath`. Walks one step at a time: if `ownerPath` matches
2314
+ * `selfPath`, executes the callback locally; otherwise resolves
2315
+ * the next descendant facet and recurses through its own RPC.
2316
+ *
2317
+ * Called by the root's `alarm()` (which owns the physical alarm
2318
+ * for facet-owned schedules) and by intermediate facets while
2319
+ * walking down the chain.
2320
+ * @internal
2321
+ */
2322
+ async _cf_dispatchScheduledCallback(ownerPath, row) {
2323
+ const selfPath = this.selfPath;
2324
+ if (!this._isSameAgentPathPrefix(selfPath, ownerPath)) throw new Error(`Schedule owner path does not descend from ${JSON.stringify(selfPath)}.`);
2325
+ if (selfPath.length === ownerPath.length) {
2326
+ await this._executeScheduleCallback(row);
2327
+ return;
2328
+ }
2329
+ const next = ownerPath[selfPath.length];
2330
+ if (!this.hasSubAgent(next.className, next.name)) throw new Error(`Scheduled sub-agent ${next.className} "${next.name}" no longer exists.`);
2331
+ await (await this._cf_resolveSubAgent(next.className, next.name))._cf_dispatchScheduledCallback(ownerPath, row);
2332
+ }
2333
+ /**
2334
+ * Recursively destroy a descendant facet identified by
2335
+ * `targetPath`. Walks down from `selfPath` until reaching the
2336
+ * target's immediate parent, where it cancels the target's
2337
+ * parent-owned schedules (and any descendants), removes the
2338
+ * target from the registry, and calls `ctx.facets.delete` to
2339
+ * wipe the target's storage.
2340
+ *
2341
+ * Called by a facet's own `destroy()` (via the root) so that
2342
+ * `this.destroy()` inside a sub-agent results in the same
2343
+ * cleanup as `parent.deleteSubAgent(Cls, name)` from the parent.
2344
+ * @internal
2345
+ */
2346
+ async _cf_destroyDescendantFacet(targetPath) {
2347
+ const selfPath = this.selfPath;
2348
+ if (targetPath.length === 0) throw new Error("_cf_destroyDescendantFacet: target path must not be empty.");
2349
+ if (selfPath.length >= targetPath.length) throw new Error("_cf_destroyDescendantFacet: target must be a strict descendant.");
2350
+ if (!this._isSameAgentPathPrefix(selfPath, targetPath)) throw new Error("_cf_destroyDescendantFacet: target path does not descend from this agent.");
2351
+ if (this._parentPath.length === 0) await this._cf_cleanupFacetPrefix(targetPath);
2352
+ if (selfPath.length === targetPath.length - 1) {
2353
+ const target = targetPath[targetPath.length - 1];
2354
+ const ctx = this.ctx;
2355
+ 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.");
2356
+ try {
2357
+ ctx.facets.delete(`${target.className}\0${target.name}`);
2358
+ } catch {}
2359
+ this._forgetSubAgent(target.className, target.name);
2360
+ return;
2361
+ }
2362
+ const next = targetPath[selfPath.length];
2363
+ if (!this.hasSubAgent(next.className, next.name)) return;
2364
+ await (await this._cf_resolveSubAgent(next.className, next.name))._cf_destroyDescendantFacet(targetPath);
2365
+ }
2366
+ async _executeScheduleCallback(row) {
2367
+ const callback = this[row.callback];
2368
+ if (!callback) {
2369
+ console.error(`callback ${row.callback} not found`);
2370
+ return;
2371
+ }
2372
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
2373
+ agent: this,
2374
+ connection: void 0,
2375
+ request: void 0,
2376
+ email: void 0
2377
+ }, async () => {
2378
+ const { maxAttempts, baseDelayMs, maxDelayMs } = resolveRetryConfig(parseRetryOptions(row), this._resolvedOptions.retry);
2379
+ let parsedPayload;
2380
+ try {
2381
+ parsedPayload = JSON.parse(row.payload);
2382
+ } catch (e) {
2383
+ console.error(`Failed to parse payload for schedule "${row.id}" (callback "${row.callback}")`, e);
2384
+ this._emit("schedule:error", {
2385
+ callback: row.callback,
2386
+ id: row.id,
2387
+ error: e instanceof Error ? e.message : String(e),
2388
+ attempts: 0
2389
+ });
2390
+ return;
2391
+ }
2392
+ try {
2393
+ this._emit("schedule:execute", {
2394
+ callback: row.callback,
2395
+ id: row.id
2396
+ });
2397
+ await tryN(maxAttempts, async (attempt) => {
2398
+ if (attempt > 1) this._emit("schedule:retry", {
2399
+ callback: row.callback,
2400
+ id: row.id,
2401
+ attempt,
2402
+ maxAttempts
2403
+ });
2404
+ await callback.bind(this)(parsedPayload, row);
2405
+ }, {
2406
+ baseDelayMs,
2407
+ maxDelayMs
2408
+ });
2409
+ } catch (e) {
2410
+ console.error(`error executing callback "${row.callback}" after ${maxAttempts} attempts`, e);
2411
+ this._emit("schedule:error", {
2412
+ callback: row.callback,
2413
+ id: row.id,
2414
+ error: e instanceof Error ? e.message : String(e),
2415
+ attempts: maxAttempts
2416
+ });
2417
+ try {
2418
+ await this.onError(e);
2419
+ } catch {}
2420
+ }
2421
+ });
1864
2422
  }
1865
2423
  async _scheduleNextAlarm() {
1866
2424
  const nowMs = Date.now();
@@ -1891,6 +2449,12 @@ var Agent = class Agent extends Server {
1891
2449
  const keepAliveMs = nowMs + this._resolvedOptions.keepAliveIntervalMs;
1892
2450
  nextTimeMs = nextTimeMs === null ? keepAliveMs : Math.min(nextTimeMs, keepAliveMs);
1893
2451
  }
2452
+ if ((this.sql`
2453
+ SELECT COUNT(*) as count FROM cf_agents_facet_runs
2454
+ `[0]?.count ?? 0) > 0) {
2455
+ const facetRecoveryMs = nowMs + this._resolvedOptions.keepAliveIntervalMs;
2456
+ nextTimeMs = nextTimeMs === null ? facetRecoveryMs : Math.min(nextTimeMs, facetRecoveryMs);
2457
+ }
1894
2458
  if (nextTimeMs !== null) await this.ctx.storage.setAlarm(nextTimeMs);
1895
2459
  else await this.ctx.storage.deleteAlarm();
1896
2460
  }
@@ -1934,11 +2498,7 @@ var Agent = class Agent extends Server {
1934
2498
  });
1935
2499
  } catch {}
1936
2500
  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
- }
2501
+ let executed = false;
1942
2502
  if (row.type === "interval" && row.running === 1) {
1943
2503
  const executionStartedAt = row.execution_started_at ?? 0;
1944
2504
  const hungTimeoutSeconds = this._resolvedOptions.hungScheduleTimeoutSeconds;
@@ -1950,59 +2510,31 @@ var Agent = class Agent extends Server {
1950
2510
  console.warn(`Forcing reset of hung interval schedule ${row.id} (started ${elapsedSeconds}s ago)`);
1951
2511
  }
1952
2512
  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
- }
2513
+ if (row.owner_path) try {
2514
+ const ownerPath = JSON.parse(row.owner_path);
2515
+ await this._cf_dispatchScheduledCallback(ownerPath, row);
2516
+ } catch (e) {
2517
+ console.error(`error dispatching scheduled callback "${row.callback}"`, e);
2518
+ this._emit("schedule:error", {
2519
+ callback: row.callback,
2520
+ id: row.id,
2521
+ error: e instanceof Error ? e.message : String(e),
2522
+ attempts: 0
2523
+ });
1973
2524
  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
- });
2525
+ await this.onError(e);
2526
+ } catch {}
2527
+ if (row.type === "interval") this.sql`
2528
+ UPDATE cf_agents_schedules SET running = 0 WHERE id = ${row.id}
2529
+ `;
2530
+ continue;
2531
+ }
2532
+ else await this._executeScheduleCallback(row);
2533
+ executed = true;
2003
2534
  if (this._destroyed) return;
2535
+ if (!executed) continue;
2004
2536
  if (row.type === "cron") {
2005
- const nextExecutionTime = getNextCronTime(row.cron);
2537
+ const nextExecutionTime = getNextCronTime(row.cron ?? "");
2006
2538
  const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
2007
2539
  this.sql`
2008
2540
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
@@ -2136,10 +2668,10 @@ var Agent = class Agent extends Server {
2136
2668
  * We set `_isFacet` eagerly (before `__unsafe_ensureInitialized`
2137
2669
  * runs `onStart()`) so any code that legitimately branches on it
2138
2670
  * — 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.
2671
+ * the flag during the first `onStart()` run. Protocol broadcasts are
2672
+ * suppressed only during this bootstrap window; afterward, facets can
2673
+ * broadcast to their own WebSocket clients reached via sub-agent
2674
+ * routing.
2143
2675
  *
2144
2676
  * The facet's name (and `this.name` getter) is handled entirely by
2145
2677
  * partyserver via `ctx.id.name`, which is populated because the
@@ -2156,7 +2688,12 @@ var Agent = class Agent extends Server {
2156
2688
  this._isFacet = true;
2157
2689
  this._parentPath = parentPath;
2158
2690
  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();
2691
+ this._suppressProtocolBroadcasts = true;
2692
+ try {
2693
+ await this.__unsafe_ensureInitialized();
2694
+ } finally {
2695
+ this._suppressProtocolBroadcasts = false;
2696
+ }
2160
2697
  }
2161
2698
  /**
2162
2699
  * Ancestor chain for this agent, root-first. Empty for top-level
@@ -2260,6 +2797,529 @@ var Agent = class Agent extends Server {
2260
2797
  async subAgent(cls, name) {
2261
2798
  return await this._cf_resolveSubAgent(cls.name, name);
2262
2799
  }
2800
+ async onAgentToolStart(_run) {}
2801
+ async onAgentToolFinish(_run, _result) {}
2802
+ async runAgentTool(cls, options) {
2803
+ const runId = options.runId ?? nanoid(12);
2804
+ const agentType = cls.name;
2805
+ const existing = this._readAgentToolRun(runId);
2806
+ if (existing) {
2807
+ if (this._isAgentToolTerminal(existing.status)) {
2808
+ if (existing.status === "completed" && existing.output_json == null) try {
2809
+ const child = await this.subAgent(cls, runId);
2810
+ const inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(runId);
2811
+ if (inspection?.status === "completed") {
2812
+ const result = this._terminalResultFromInspection(agentType, inspection);
2813
+ this._updateAgentToolTerminal(runId, result, inspection.completedAt);
2814
+ return result;
2815
+ }
2816
+ } catch {}
2817
+ return this._resultFromAgentToolRow(existing);
2818
+ }
2819
+ return await this._replayAndInterruptAgentToolRun(cls, existing, "Agent tool run was still running, but live-tail reattachment is not supported in this runtime.");
2820
+ }
2821
+ const displayOrder = options.displayOrder ?? 0;
2822
+ const inputPreview = options.inputPreview ?? this._defaultAgentToolPreview(options.input);
2823
+ const displayJson = options.display !== void 0 ? JSON.stringify(options.display) : null;
2824
+ const inputPreviewJson = inputPreview !== void 0 ? JSON.stringify(inputPreview) : null;
2825
+ const startedAt = Date.now();
2826
+ if (this._activeAgentToolRunCount() >= this.maxConcurrentAgentTools) {
2827
+ const error = `maxConcurrentAgentTools (${this.maxConcurrentAgentTools}) exceeded`;
2828
+ this.sql`
2829
+ INSERT INTO cf_agent_tool_runs (
2830
+ run_id, parent_tool_call_id, agent_type, input_preview,
2831
+ input_redacted, status, error_message, display_metadata,
2832
+ display_order, started_at, completed_at
2833
+ ) VALUES (
2834
+ ${runId}, ${options.parentToolCallId ?? null}, ${agentType},
2835
+ ${inputPreviewJson}, 1, 'error', ${error}, ${displayJson},
2836
+ ${displayOrder}, ${startedAt}, ${Date.now()}
2837
+ )
2838
+ `;
2839
+ this._broadcastAgentToolEvent(options.parentToolCallId, 0, {
2840
+ kind: "started",
2841
+ runId,
2842
+ agentType,
2843
+ inputPreview,
2844
+ order: displayOrder,
2845
+ display: options.display
2846
+ });
2847
+ this._broadcastAgentToolEvent(options.parentToolCallId, 1, {
2848
+ kind: "error",
2849
+ runId,
2850
+ error
2851
+ });
2852
+ return {
2853
+ runId,
2854
+ agentType,
2855
+ status: "error",
2856
+ error
2857
+ };
2858
+ }
2859
+ this.sql`
2860
+ INSERT INTO cf_agent_tool_runs (
2861
+ run_id, parent_tool_call_id, agent_type, input_preview,
2862
+ input_redacted, status, display_metadata, display_order, started_at
2863
+ ) VALUES (
2864
+ ${runId}, ${options.parentToolCallId ?? null}, ${agentType},
2865
+ ${inputPreviewJson}, 1, 'starting', ${displayJson}, ${displayOrder},
2866
+ ${startedAt}
2867
+ )
2868
+ `;
2869
+ const runInfo = {
2870
+ runId,
2871
+ parentToolCallId: options.parentToolCallId,
2872
+ agentType,
2873
+ inputPreview,
2874
+ status: "starting",
2875
+ display: options.display,
2876
+ displayOrder,
2877
+ startedAt
2878
+ };
2879
+ await this.onAgentToolStart(runInfo);
2880
+ this._broadcastAgentToolEvent(options.parentToolCallId, 0, {
2881
+ kind: "started",
2882
+ runId,
2883
+ agentType,
2884
+ inputPreview,
2885
+ order: displayOrder,
2886
+ display: options.display
2887
+ });
2888
+ const child = await this.subAgent(cls, runId);
2889
+ const adapter = this._asAgentToolChildAdapter(child);
2890
+ const childStart = await adapter.startAgentToolRun(options.input, { runId });
2891
+ this._markAgentToolRunning(runId);
2892
+ let sequence = 1;
2893
+ let parentAbortListener;
2894
+ if (options.signal) if (options.signal.aborted) {
2895
+ await adapter.cancelAgentToolRun(runId, options.signal.reason);
2896
+ const result = {
2897
+ runId,
2898
+ agentType,
2899
+ status: "aborted",
2900
+ error: options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "cancelled")
2901
+ };
2902
+ this._updateAgentToolTerminal(runId, result);
2903
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
2904
+ await this.onAgentToolFinish({
2905
+ ...runInfo,
2906
+ status: "aborted",
2907
+ completedAt: Date.now()
2908
+ }, result);
2909
+ return result;
2910
+ } else {
2911
+ parentAbortListener = () => {
2912
+ adapter.cancelAgentToolRun(runId, options.signal?.reason);
2913
+ };
2914
+ options.signal.addEventListener("abort", parentAbortListener, { once: true });
2915
+ }
2916
+ try {
2917
+ if (adapter.tailAgentToolRun) {
2918
+ const stream = await adapter.tailAgentToolRun(runId, { afterSequence: -1 });
2919
+ sequence = await this._forwardAgentToolStream(stream, options.parentToolCallId, runId, sequence, options.signal);
2920
+ } else {
2921
+ const chunks = await adapter.getAgentToolChunks(runId);
2922
+ sequence = this._broadcastAgentToolChunks(options.parentToolCallId, runId, chunks, sequence);
2923
+ }
2924
+ if (options.signal?.aborted) {
2925
+ await adapter.cancelAgentToolRun(runId, options.signal.reason);
2926
+ const result = {
2927
+ runId,
2928
+ agentType,
2929
+ status: "aborted",
2930
+ error: options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "cancelled")
2931
+ };
2932
+ this._updateAgentToolTerminal(runId, result);
2933
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
2934
+ await this.onAgentToolFinish({
2935
+ ...runInfo,
2936
+ status: "aborted",
2937
+ completedAt: Date.now()
2938
+ }, result);
2939
+ return result;
2940
+ }
2941
+ const inspection = await adapter.inspectAgentToolRun(runId) ?? childStart;
2942
+ const result = this._terminalResultFromInspection(agentType, inspection);
2943
+ this._updateAgentToolTerminal(runId, result, inspection.completedAt);
2944
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
2945
+ await this.onAgentToolFinish({
2946
+ ...runInfo,
2947
+ status: result.status,
2948
+ completedAt: Date.now()
2949
+ }, result);
2950
+ return result;
2951
+ } catch (error) {
2952
+ if (options.signal?.aborted) {
2953
+ await adapter.cancelAgentToolRun(runId, options.signal.reason);
2954
+ const result = {
2955
+ runId,
2956
+ agentType,
2957
+ status: "aborted",
2958
+ error: options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "cancelled")
2959
+ };
2960
+ this._updateAgentToolTerminal(runId, result);
2961
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
2962
+ await this.onAgentToolFinish({
2963
+ ...runInfo,
2964
+ status: "aborted",
2965
+ completedAt: Date.now()
2966
+ }, result);
2967
+ return result;
2968
+ }
2969
+ const result = {
2970
+ runId,
2971
+ agentType,
2972
+ status: "error",
2973
+ error: error instanceof Error ? error.message : String(error)
2974
+ };
2975
+ this._updateAgentToolTerminal(runId, result);
2976
+ this._broadcastAgentToolTerminal(options.parentToolCallId, sequence, result);
2977
+ await this.onAgentToolFinish({
2978
+ ...runInfo,
2979
+ status: "error",
2980
+ completedAt: Date.now()
2981
+ }, result);
2982
+ return result;
2983
+ } finally {
2984
+ if (parentAbortListener && options.signal) options.signal.removeEventListener("abort", parentAbortListener);
2985
+ }
2986
+ }
2987
+ hasAgentToolRun(classOrName, runId) {
2988
+ const agentType = typeof classOrName === "string" ? classOrName : classOrName.name;
2989
+ return (this.sql`
2990
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
2991
+ WHERE run_id = ${runId} AND agent_type = ${agentType}
2992
+ `[0]?.n ?? 0) > 0;
2993
+ }
2994
+ async clearAgentToolRuns(options) {
2995
+ const rows = this.sql`
2996
+ SELECT run_id, agent_type, status FROM cf_agent_tool_runs
2997
+ ORDER BY started_at ASC
2998
+ `;
2999
+ const statusFilter = options?.status ? new Set(options.status) : null;
3000
+ const retained = rows.filter((row) => {
3001
+ if (statusFilter && !statusFilter.has(row.status)) return false;
3002
+ if (options?.olderThan !== void 0) {
3003
+ const full = this._readAgentToolRun(row.run_id);
3004
+ if (!full || full.started_at >= options.olderThan) return false;
3005
+ }
3006
+ return true;
3007
+ });
3008
+ for (const row of retained) {
3009
+ try {
3010
+ const cls = this._agentToolClassByName(row.agent_type);
3011
+ if (row.status === "starting" || row.status === "running") {
3012
+ const child = await this.subAgent(cls, row.run_id);
3013
+ await this._asAgentToolChildAdapter(child).cancelAgentToolRun(row.run_id, "clearing agent tool run");
3014
+ }
3015
+ await this.deleteSubAgent(cls, row.run_id);
3016
+ } catch {}
3017
+ this.sql`
3018
+ DELETE FROM cf_agent_tool_runs WHERE run_id = ${row.run_id}
3019
+ `;
3020
+ }
3021
+ }
3022
+ _isAgentToolTerminal(status) {
3023
+ return status === "completed" || status === "error" || status === "aborted" || status === "interrupted";
3024
+ }
3025
+ _activeAgentToolRunCount() {
3026
+ return this.sql`
3027
+ SELECT COUNT(*) AS n FROM cf_agent_tool_runs
3028
+ WHERE status IN ('starting', 'running')
3029
+ `[0]?.n ?? 0;
3030
+ }
3031
+ _defaultAgentToolPreview(input) {
3032
+ if (typeof input === "string") return input.slice(0, 500);
3033
+ if (input === null || input === void 0) return input;
3034
+ try {
3035
+ const json = JSON.stringify(input);
3036
+ return json.length > 500 ? `${json.slice(0, 497)}...` : json;
3037
+ } catch {
3038
+ return String(input).slice(0, 500);
3039
+ }
3040
+ }
3041
+ _readAgentToolRun(runId) {
3042
+ return this.sql`
3043
+ SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
3044
+ summary, output_json, error_message, display_metadata, display_order,
3045
+ started_at, completed_at
3046
+ FROM cf_agent_tool_runs
3047
+ WHERE run_id = ${runId}
3048
+ LIMIT 1
3049
+ `[0] ?? null;
3050
+ }
3051
+ _resultFromAgentToolRow(row) {
3052
+ const output = this._parseAgentToolJson(row.output_json);
3053
+ return {
3054
+ runId: row.run_id,
3055
+ agentType: row.agent_type,
3056
+ status: row.status,
3057
+ ...output !== void 0 ? { output } : {},
3058
+ ...row.summary !== null ? { summary: row.summary } : {},
3059
+ ...row.error_message !== null ? { error: row.error_message } : {}
3060
+ };
3061
+ }
3062
+ _terminalResultFromInspection(agentType, inspection) {
3063
+ if (inspection.status === "completed") return {
3064
+ runId: inspection.runId,
3065
+ agentType,
3066
+ status: "completed",
3067
+ output: inspection.output,
3068
+ summary: inspection.summary
3069
+ };
3070
+ if (inspection.status === "aborted") return {
3071
+ runId: inspection.runId,
3072
+ agentType,
3073
+ status: "aborted",
3074
+ error: inspection.error
3075
+ };
3076
+ return {
3077
+ runId: inspection.runId,
3078
+ agentType,
3079
+ status: "error",
3080
+ error: inspection.error ?? "Agent tool run failed"
3081
+ };
3082
+ }
3083
+ _updateAgentToolTerminal(runId, result, completedAt = Date.now()) {
3084
+ this.sql`
3085
+ UPDATE cf_agent_tool_runs
3086
+ SET status = ${result.status},
3087
+ summary = ${result.summary ?? null},
3088
+ output_json = ${this._stringifyAgentToolOutput(result.output)},
3089
+ error_message = ${result.error ?? null},
3090
+ completed_at = ${completedAt}
3091
+ WHERE run_id = ${runId}
3092
+ AND status NOT IN ('completed', 'error', 'aborted', 'interrupted')
3093
+ `;
3094
+ if (result.status === "completed" && result.output !== void 0) this.sql`
3095
+ UPDATE cf_agent_tool_runs
3096
+ SET output_json = COALESCE(output_json, ${this._stringifyAgentToolOutput(result.output)}),
3097
+ summary = COALESCE(summary, ${result.summary ?? null})
3098
+ WHERE run_id = ${runId} AND status = 'completed'
3099
+ `;
3100
+ }
3101
+ _markAgentToolRunning(runId) {
3102
+ this.sql`
3103
+ UPDATE cf_agent_tool_runs
3104
+ SET status = 'running'
3105
+ WHERE run_id = ${runId} AND status = 'starting'
3106
+ `;
3107
+ }
3108
+ _parseAgentToolJson(value) {
3109
+ if (value === null) return void 0;
3110
+ try {
3111
+ return JSON.parse(value);
3112
+ } catch {
3113
+ return value;
3114
+ }
3115
+ }
3116
+ _stringifyAgentToolOutput(output) {
3117
+ if (output === void 0) return null;
3118
+ const json = JSON.stringify(output);
3119
+ return json === void 0 ? null : json;
3120
+ }
3121
+ _broadcastAgentToolEvent(parentToolCallId, sequence, event, replay, connection) {
3122
+ const message = {
3123
+ type: "agent-tool-event",
3124
+ parentToolCallId,
3125
+ sequence,
3126
+ event,
3127
+ ...replay ? { replay } : {}
3128
+ };
3129
+ const body = JSON.stringify(message);
3130
+ if (connection) connection.send(body);
3131
+ else this.broadcast(body);
3132
+ }
3133
+ _broadcastAgentToolChunks(parentToolCallId, runId, chunks, sequence, replay, connection) {
3134
+ let next = sequence;
3135
+ for (const chunk of chunks) this._broadcastAgentToolEvent(parentToolCallId, next++, {
3136
+ kind: "chunk",
3137
+ runId,
3138
+ body: chunk.body
3139
+ }, replay, connection);
3140
+ return next;
3141
+ }
3142
+ async _forwardAgentToolStream(stream, parentToolCallId, runId, sequence, signal) {
3143
+ let next = sequence;
3144
+ if (signal?.aborted) return next;
3145
+ const reader = stream.getReader();
3146
+ const decoder = new TextDecoder();
3147
+ let bufferedBytes = "";
3148
+ let abortListener;
3149
+ if (signal) {
3150
+ abortListener = () => {};
3151
+ signal.addEventListener("abort", abortListener, { once: true });
3152
+ }
3153
+ try {
3154
+ const forwardChunk = (chunk) => {
3155
+ this._broadcastAgentToolEvent(parentToolCallId, next++, {
3156
+ kind: "chunk",
3157
+ runId,
3158
+ body: chunk.body
3159
+ });
3160
+ };
3161
+ const forwardLine = (line) => {
3162
+ try {
3163
+ const chunk = JSON.parse(line);
3164
+ if (typeof chunk.body === "string") forwardChunk(chunk);
3165
+ } catch {}
3166
+ };
3167
+ const flushBufferedBytes = (final = false) => {
3168
+ while (true) {
3169
+ const newline = bufferedBytes.indexOf("\n");
3170
+ if (newline === -1) break;
3171
+ const line = bufferedBytes.slice(0, newline).trim();
3172
+ bufferedBytes = bufferedBytes.slice(newline + 1);
3173
+ if (line.length > 0) forwardLine(line);
3174
+ }
3175
+ if (final && bufferedBytes.trim().length > 0) {
3176
+ forwardLine(bufferedBytes);
3177
+ bufferedBytes = "";
3178
+ }
3179
+ };
3180
+ while (true) {
3181
+ let readResult;
3182
+ try {
3183
+ readResult = await reader.read();
3184
+ } catch (error) {
3185
+ if (signal?.aborted) break;
3186
+ throw error;
3187
+ }
3188
+ const { done, value } = readResult;
3189
+ if (done) {
3190
+ bufferedBytes += decoder.decode();
3191
+ flushBufferedBytes(true);
3192
+ break;
3193
+ }
3194
+ if (value instanceof Uint8Array) {
3195
+ bufferedBytes += decoder.decode(value, { stream: true });
3196
+ flushBufferedBytes();
3197
+ } else forwardChunk(value);
3198
+ }
3199
+ } finally {
3200
+ if (abortListener && signal) signal.removeEventListener("abort", abortListener);
3201
+ reader.releaseLock();
3202
+ }
3203
+ return next;
3204
+ }
3205
+ _broadcastAgentToolTerminal(parentToolCallId, sequence, result, replay, connection) {
3206
+ if (result.status === "completed") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3207
+ kind: "finished",
3208
+ runId: result.runId,
3209
+ summary: result.summary ?? ""
3210
+ }, replay, connection);
3211
+ else if (result.status === "aborted") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3212
+ kind: "aborted",
3213
+ runId: result.runId,
3214
+ reason: result.error
3215
+ }, replay, connection);
3216
+ else if (result.status === "interrupted") this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3217
+ kind: "interrupted",
3218
+ runId: result.runId,
3219
+ error: result.error ?? "Agent tool run was interrupted"
3220
+ }, replay, connection);
3221
+ else this._broadcastAgentToolEvent(parentToolCallId, sequence, {
3222
+ kind: "error",
3223
+ runId: result.runId,
3224
+ error: result.error ?? "Agent tool run failed"
3225
+ }, replay, connection);
3226
+ }
3227
+ _asAgentToolChildAdapter(child) {
3228
+ const candidate = child;
3229
+ 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.");
3230
+ return candidate;
3231
+ }
3232
+ _agentToolClassByName(className) {
3233
+ const cls = this.ctx.exports?.[className];
3234
+ if (!cls) throw new Error(`Agent tool class "${className}" is not exported.`);
3235
+ return cls;
3236
+ }
3237
+ async _replayAndInterruptAgentToolRun(cls, row, message) {
3238
+ const parentToolCallId = row.parent_tool_call_id ?? void 0;
3239
+ let sequence = 1;
3240
+ try {
3241
+ const child = await this.subAgent(cls, row.run_id);
3242
+ const chunks = await this._asAgentToolChildAdapter(child).getAgentToolChunks(row.run_id);
3243
+ sequence = this._broadcastAgentToolChunks(parentToolCallId, row.run_id, chunks, sequence);
3244
+ } catch {}
3245
+ const result = {
3246
+ runId: row.run_id,
3247
+ agentType: row.agent_type,
3248
+ status: "interrupted",
3249
+ error: message
3250
+ };
3251
+ this._updateAgentToolTerminal(row.run_id, result);
3252
+ this._broadcastAgentToolTerminal(parentToolCallId, sequence, result);
3253
+ return result;
3254
+ }
3255
+ async _replayAgentToolRuns(connection) {
3256
+ const rows = this.sql`
3257
+ SELECT run_id, parent_tool_call_id, agent_type, input_preview, status,
3258
+ summary, output_json, error_message, display_metadata, display_order
3259
+ FROM cf_agent_tool_runs
3260
+ ORDER BY started_at ASC
3261
+ `;
3262
+ for (const row of rows) {
3263
+ const parentToolCallId = row.parent_tool_call_id ?? void 0;
3264
+ let sequence = 0;
3265
+ this._broadcastAgentToolEvent(parentToolCallId, sequence++, {
3266
+ kind: "started",
3267
+ runId: row.run_id,
3268
+ agentType: row.agent_type,
3269
+ inputPreview: this._parseAgentToolJson(row.input_preview),
3270
+ order: row.display_order,
3271
+ display: this._parseAgentToolJson(row.display_metadata)
3272
+ }, true, connection);
3273
+ try {
3274
+ const child = await this.subAgent(this._agentToolClassByName(row.agent_type), row.run_id);
3275
+ const chunks = await this._asAgentToolChildAdapter(child).getAgentToolChunks(row.run_id);
3276
+ sequence = this._broadcastAgentToolChunks(parentToolCallId, row.run_id, chunks, sequence, true, connection);
3277
+ } catch {}
3278
+ if (this._isAgentToolTerminal(row.status)) this._broadcastAgentToolTerminal(parentToolCallId, sequence, {
3279
+ runId: row.run_id,
3280
+ agentType: row.agent_type,
3281
+ status: row.status,
3282
+ output: this._parseAgentToolJson(row.output_json),
3283
+ summary: row.summary ?? void 0,
3284
+ error: row.error_message ?? void 0
3285
+ }, true, connection);
3286
+ }
3287
+ }
3288
+ async _reconcileAgentToolRuns() {
3289
+ const rows = this.sql`
3290
+ SELECT run_id, agent_type FROM cf_agent_tool_runs
3291
+ WHERE status IN ('starting', 'running')
3292
+ ORDER BY started_at ASC
3293
+ `;
3294
+ for (const row of rows) try {
3295
+ const cls = this._agentToolClassByName(row.agent_type);
3296
+ if (!this.hasSubAgent(cls, row.run_id)) {
3297
+ this._updateAgentToolTerminal(row.run_id, {
3298
+ runId: row.run_id,
3299
+ agentType: row.agent_type,
3300
+ status: "interrupted",
3301
+ error: "Agent tool child was not found during parent recovery."
3302
+ });
3303
+ continue;
3304
+ }
3305
+ const child = await this.subAgent(cls, row.run_id);
3306
+ const inspection = await this._asAgentToolChildAdapter(child).inspectAgentToolRun(row.run_id);
3307
+ if (!inspection || inspection.status === "running" || inspection.status === "starting") this._updateAgentToolTerminal(row.run_id, {
3308
+ runId: row.run_id,
3309
+ agentType: row.agent_type,
3310
+ status: "interrupted",
3311
+ error: "Agent tool run was still running, but live-tail reattachment is not supported in this runtime."
3312
+ });
3313
+ else this._updateAgentToolTerminal(row.run_id, this._terminalResultFromInspection(row.agent_type, inspection), inspection.completedAt);
3314
+ } catch {
3315
+ this._updateAgentToolTerminal(row.run_id, {
3316
+ runId: row.run_id,
3317
+ agentType: row.agent_type,
3318
+ status: "interrupted",
3319
+ error: "Agent tool run could not be inspected during parent recovery."
3320
+ });
3321
+ }
3322
+ }
2263
3323
  /**
2264
3324
  * Shared facet resolution — takes a CamelCase class name string
2265
3325
  * (matching `ctx.exports`) rather than a class reference. Both
@@ -2289,7 +3349,14 @@ var Agent = class Agent extends Server {
2289
3349
  id: facetId
2290
3350
  }));
2291
3351
  const childParentPath = this.selfPath;
2292
- await stub._cf_initAsFacet(name, childParentPath);
3352
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
3353
+ agent: this,
3354
+ connection: void 0,
3355
+ request: void 0,
3356
+ email: void 0
3357
+ }, async () => {
3358
+ await stub._cf_initAsFacet(name, childParentPath);
3359
+ });
2293
3360
  this._recordSubAgent(className, name);
2294
3361
  return stub;
2295
3362
  }
@@ -2320,10 +3387,16 @@ var Agent = class Agent extends Server {
2320
3387
  * @param cls The Agent subclass used when creating the child
2321
3388
  * @param name Name of the child to delete
2322
3389
  */
2323
- deleteSubAgent(cls, name) {
3390
+ async deleteSubAgent(cls, name) {
2324
3391
  const ctx = this.ctx;
2325
3392
  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
3393
  const facetKey = `${cls.name}\0${name}`;
3394
+ const childPath = [...this.selfPath, {
3395
+ className: cls.name,
3396
+ name
3397
+ }];
3398
+ if (this._isFacet) await (await this._rootAlarmOwner())._cf_cleanupFacetPrefix(childPath);
3399
+ else await this._cf_cleanupFacetPrefix(childPath);
2327
3400
  try {
2328
3401
  ctx.facets.delete(facetKey);
2329
3402
  } catch {}
@@ -2383,16 +3456,28 @@ var Agent = class Agent extends Server {
2383
3456
  }));
2384
3457
  }
2385
3458
  /**
2386
- * Destroy the Agent, removing all state and scheduled tasks
3459
+ * Destroy the Agent, removing all state and scheduled tasks.
3460
+ *
3461
+ * On a top-level agent: drops every table, clears the alarm, and
3462
+ * aborts the isolate.
3463
+ *
3464
+ * On a sub-agent (facet): delegates teardown to the immediate
3465
+ * parent so the parent-owned schedule rows for this sub-agent
3466
+ * (and any of its descendants) are cancelled, the parent's
3467
+ * `cf_agents_sub_agents` registry entry is cleared, and
3468
+ * `ctx.facets.delete` wipes the facet's own storage. The
3469
+ * `ctx.facets.delete` call aborts this isolate, so this method
3470
+ * may not return cleanly when invoked from inside the facet —
3471
+ * callers should treat it as fire-and-forget.
2387
3472
  */
2388
3473
  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();
3474
+ if (this._isFacet) {
3475
+ this._emit("destroy");
3476
+ await (await this._rootAlarmOwner())._cf_destroyDescendantFacet(this.selfPath);
3477
+ return;
3478
+ }
3479
+ this._dropInternalTablesForDestroy();
3480
+ await this.ctx.storage.deleteAlarm();
2396
3481
  await this.ctx.storage.deleteAll();
2397
3482
  this._disposables.dispose();
2398
3483
  await this.mcp.dispose();
@@ -2402,6 +3487,18 @@ var Agent = class Agent extends Server {
2402
3487
  }, 0);
2403
3488
  this._emit("destroy");
2404
3489
  }
3490
+ /** @internal Drop every internal Agents SDK table during top-level destroy. */
3491
+ _dropInternalTablesForDestroy() {
3492
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
3493
+ this.sql`DROP TABLE IF EXISTS cf_agents_state`;
3494
+ this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
3495
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
3496
+ this.sql`DROP TABLE IF EXISTS cf_agents_workflows`;
3497
+ this.sql`DROP TABLE IF EXISTS cf_agents_sub_agents`;
3498
+ this.sql`DROP TABLE IF EXISTS cf_agents_runs`;
3499
+ this.sql`DROP TABLE IF EXISTS cf_agents_facet_runs`;
3500
+ this.sql`DROP TABLE IF EXISTS cf_agent_tool_runs`;
3501
+ }
2405
3502
  /**
2406
3503
  * Check if a method is callable
2407
3504
  * @param method The method name to check