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.
- package/README.md +37 -1
- package/dist/{index-DSwOzhhd.d.ts → agent-tool-types-tBGRsPm0.d.ts} +584 -99
- package/dist/agent-tool-types.d.ts +34 -0
- package/dist/agent-tool-types.js +1 -0
- package/dist/agent-tools-BAdX1vdI.js +425 -0
- package/dist/agent-tools-BAdX1vdI.js.map +1 -0
- package/dist/agent-tools-CIO14miM.d.ts +14 -0
- package/dist/agent-tools.d.ts +68 -0
- package/dist/agent-tools.js +51 -0
- package/dist/agent-tools.js.map +1 -0
- package/dist/browser/ai.d.ts +1 -1
- package/dist/browser/ai.js +2 -2
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/tanstack-ai.d.ts +1 -1
- package/dist/browser/tanstack-ai.js +1 -1
- package/dist/chat/index.d.ts +27 -1
- package/dist/chat/index.js +3 -263
- package/dist/chat/index.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/{compaction-helpers-C_cN3z55.js → compaction-helpers-CSaqCmdE.js} +1 -1
- package/dist/{compaction-helpers-C_cN3z55.js.map → compaction-helpers-CSaqCmdE.js.map} +1 -1
- package/dist/{compaction-helpers-YzCLvunJ.d.ts → compaction-helpers-D92Ipstp.d.ts} +1 -1
- package/dist/experimental/memory/session/index.d.ts +1 -1
- package/dist/experimental/memory/session/index.js +1 -1
- package/dist/experimental/memory/utils/index.d.ts +1 -1
- package/dist/experimental/memory/utils/index.js +1 -1
- package/dist/index.d.ts +74 -42
- package/dist/index.js +1393 -296
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/react.d.ts +16 -2
- package/dist/react.js +42 -1
- package/dist/react.js.map +1 -1
- package/dist/{serializable-Bg8ARWlN.d.ts → serializable-Brg7fRds.d.ts} +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/{shared-mfBbxjS1.js → shared-C6l4ZKRN.js} +1 -1
- package/dist/{shared-mfBbxjS1.js.map → shared-C6l4ZKRN.js.map} +1 -1
- package/dist/{shared-BUHZFGTk.d.ts → shared-Ch9slKdI.d.ts} +1 -1
- package/dist/sub-routing.d.ts +6 -6
- package/dist/workflows.d.ts +1 -1
- 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 =
|
|
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 {
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
|
1436
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
|
1481
|
-
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
|
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
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
|
1528
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
-
*
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
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
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
|
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
|
|
1613
|
-
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
*
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
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
|
-
|
|
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
|
|
1723
|
+
SELECT * FROM cf_agents_schedules
|
|
1724
|
+
WHERE id = ${id} AND owner_path_key IS ${ownerPathKey}
|
|
1640
1725
|
`;
|
|
1641
|
-
if (
|
|
1642
|
-
const
|
|
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
|
-
|
|
1645
|
-
|
|
1646
|
-
retry: parseRetryOptions(row)
|
|
1734
|
+
ok: true,
|
|
1735
|
+
callback
|
|
1647
1736
|
};
|
|
1648
1737
|
}
|
|
1649
1738
|
/**
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
*
|
|
1680
|
-
* @
|
|
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
|
|
1684
|
-
|
|
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
|
-
*
|
|
1697
|
-
*
|
|
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
|
|
1722
|
-
|
|
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
|
-
|
|
1726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
}
|
|
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.
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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.
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
this.
|
|
2395
|
-
|
|
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
|