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