agents 0.3.10 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +2 -2
  2. package/dist/{index-N6791tVt.d.ts → agent-DY6QmSI_.d.ts} +3 -25
  3. package/dist/ai-types.js +1 -1
  4. package/dist/client-connection-CGMuV62J.js +472 -0
  5. package/dist/client-connection-CGMuV62J.js.map +1 -0
  6. package/dist/client-storage-Cvy5r9FG.d.ts +355 -0
  7. package/dist/client.d.ts +11 -7
  8. package/dist/client.js +6 -2
  9. package/dist/client.js.map +1 -1
  10. package/dist/email.d.ts +146 -16
  11. package/dist/email.js +222 -2
  12. package/dist/email.js.map +1 -0
  13. package/dist/index.d.ts +142 -41
  14. package/dist/index.js +2326 -6
  15. package/dist/index.js.map +1 -0
  16. package/dist/internal_context.d.ts +33 -6
  17. package/dist/internal_context.js +11 -2
  18. package/dist/internal_context.js.map +1 -0
  19. package/dist/mcp/client.d.ts +516 -2
  20. package/dist/mcp/client.js +662 -3
  21. package/dist/mcp/client.js.map +1 -0
  22. package/dist/mcp/do-oauth-client-provider.d.ts +61 -2
  23. package/dist/mcp/do-oauth-client-provider.js +154 -2
  24. package/dist/mcp/do-oauth-client-provider.js.map +1 -0
  25. package/dist/mcp/index.d.ts +3 -5
  26. package/dist/mcp/index.js +8 -7
  27. package/dist/mcp/index.js.map +1 -1
  28. package/dist/mcp/x402.d.ts +34 -14
  29. package/dist/mcp/x402.js +128 -66
  30. package/dist/mcp/x402.js.map +1 -1
  31. package/dist/{mcp-BwPscEiF.d.ts → mcp-Dw5vDrY8.d.ts} +1 -1
  32. package/dist/observability/index.d.ts +23 -2
  33. package/dist/observability/index.js +25 -6
  34. package/dist/observability/index.js.map +1 -0
  35. package/dist/react.d.ts +10 -10
  36. package/dist/react.js +6 -2
  37. package/dist/react.js.map +1 -1
  38. package/dist/types.d.ts +14 -1
  39. package/dist/types.js +16 -2
  40. package/dist/types.js.map +1 -0
  41. package/dist/utils.js +15 -2
  42. package/dist/utils.js.map +1 -0
  43. package/dist/workflow-types.d.ts +235 -23
  44. package/dist/workflows.d.ts +22 -24
  45. package/dist/workflows.js +2 -5
  46. package/dist/workflows.js.map +1 -1
  47. package/package.json +24 -28
  48. package/dist/client-CtC9E06G.js +0 -1122
  49. package/dist/client-CtC9E06G.js.map +0 -1
  50. package/dist/client-DV1CZKqa.d.ts +0 -969
  51. package/dist/do-oauth-client-provider-BqnOQzjy.d.ts +0 -70
  52. package/dist/do-oauth-client-provider-DDg8QrEA.js +0 -155
  53. package/dist/do-oauth-client-provider-DDg8QrEA.js.map +0 -1
  54. package/dist/email-8ljcpvwV.d.ts +0 -157
  55. package/dist/email-XHsSYsTO.js +0 -223
  56. package/dist/email-XHsSYsTO.js.map +0 -1
  57. package/dist/internal_context-CEu5ji80.d.ts +0 -29
  58. package/dist/internal_context-D9eKFth1.js +0 -8
  59. package/dist/internal_context-D9eKFth1.js.map +0 -1
  60. package/dist/src-i_UcyBYf.js +0 -2147
  61. package/dist/src-i_UcyBYf.js.map +0 -1
  62. package/dist/types-BITaDFf-.js +0 -16
  63. package/dist/types-BITaDFf-.js.map +0 -1
  64. package/dist/types-DSSHBW6w.d.ts +0 -14
  65. package/dist/utils-B49TmLCI.js +0 -16
  66. package/dist/utils-B49TmLCI.js.map +0 -1
  67. package/dist/workflow-types-Z_Oem1FJ.d.ts +0 -260
package/dist/index.js CHANGED
@@ -1,7 +1,2327 @@
1
- import { i as createHeaderBasedEmailResolver } from "./email-XHsSYsTO.js";
2
- import "./internal_context-D9eKFth1.js";
3
- import "./client-CtC9E06G.js";
4
- import "./do-oauth-client-provider-DDg8QrEA.js";
5
- import { a as callable, c as routeAgentEmail, i as StreamingResponse, l as routeAgentRequest, n as DEFAULT_AGENT_STATIC_OPTIONS, o as getAgentByName, r as SqlError, s as getCurrentAgent, t as Agent, u as unstable_callable } from "./src-i_UcyBYf.js";
1
+ import { MessageType } from "./types.js";
2
+ import { camelCaseToKebabCase } from "./utils.js";
3
+ import { createHeaderBasedEmailResolver, signAgentHeaders } from "./email.js";
4
+ import { __DO_NOT_USE_WILL_BREAK__agentContext } from "./internal_context.js";
5
+ import { i as DisposableStore, n as MCPConnectionState } from "./client-connection-CGMuV62J.js";
6
+ import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider.js";
7
+ import { MCPClientManager } from "./mcp/client.js";
8
+ import { genericObservability } from "./observability/index.js";
9
+ import { parseCronExpression } from "cron-schedule";
10
+ import { nanoid } from "nanoid";
11
+ import { EmailMessage } from "cloudflare:email";
12
+ import { Server, getServerByName, routePartykitRequest } from "partyserver";
6
13
 
7
- export { Agent, DEFAULT_AGENT_STATIC_OPTIONS, SqlError, StreamingResponse, callable, createHeaderBasedEmailResolver, getAgentByName, getCurrentAgent, routeAgentEmail, routeAgentRequest, unstable_callable };
14
+ //#region src/index.ts
15
+ /**
16
+ * Type guard for RPC request messages
17
+ */
18
+ function isRPCRequest(msg) {
19
+ return typeof msg === "object" && msg !== null && "type" in msg && msg.type === MessageType.RPC && "id" in msg && typeof msg.id === "string" && "method" in msg && typeof msg.method === "string" && "args" in msg && Array.isArray(msg.args);
20
+ }
21
+ /**
22
+ * Type guard for state update messages
23
+ */
24
+ function isStateUpdateMessage(msg) {
25
+ return typeof msg === "object" && msg !== null && "type" in msg && msg.type === MessageType.CF_AGENT_STATE && "state" in msg;
26
+ }
27
+ const callableMetadata = /* @__PURE__ */ new WeakMap();
28
+ /**
29
+ * Error class for SQL execution failures, containing the query that failed
30
+ */
31
+ var SqlError = class extends Error {
32
+ constructor(query, cause) {
33
+ const message = cause instanceof Error ? cause.message : String(cause);
34
+ super(`SQL query failed: ${message}`, { cause });
35
+ this.name = "SqlError";
36
+ this.query = query;
37
+ }
38
+ };
39
+ /**
40
+ * Decorator that marks a method as callable by clients
41
+ * @param metadata Optional metadata about the callable method
42
+ */
43
+ function callable(metadata = {}) {
44
+ return function callableDecorator(target, _context) {
45
+ if (!callableMetadata.has(target)) callableMetadata.set(target, metadata);
46
+ return target;
47
+ };
48
+ }
49
+ let didWarnAboutUnstableCallable = false;
50
+ /**
51
+ * Decorator that marks a method as callable by clients
52
+ * @deprecated this has been renamed to callable, and unstable_callable will be removed in the next major version
53
+ * @param metadata Optional metadata about the callable method
54
+ */
55
+ const unstable_callable = (metadata = {}) => {
56
+ if (!didWarnAboutUnstableCallable) {
57
+ didWarnAboutUnstableCallable = true;
58
+ console.warn("unstable_callable is deprecated, use callable instead. unstable_callable will be removed in the next major version.");
59
+ }
60
+ return callable(metadata);
61
+ };
62
+ function getNextCronTime(cron) {
63
+ return parseCronExpression(cron).getNextDate();
64
+ }
65
+ const STATE_ROW_ID = "cf_state_row_id";
66
+ const STATE_WAS_CHANGED = "cf_state_was_changed";
67
+ const DEFAULT_STATE = {};
68
+ /**
69
+ * Internal key used to store the readonly flag in connection state.
70
+ * Prefixed with _cf_ to avoid collision with user state keys.
71
+ */
72
+ const CF_READONLY_KEY = "_cf_readonly";
73
+ /**
74
+ * Tracks which agent constructors have already emitted the onStateUpdate
75
+ * deprecation warning, so it fires at most once per class.
76
+ */
77
+ const _onStateUpdateWarnedClasses = /* @__PURE__ */ new WeakSet();
78
+ /**
79
+ * Default options for Agent configuration.
80
+ * Child classes can override specific options without spreading.
81
+ */
82
+ const DEFAULT_AGENT_STATIC_OPTIONS = {
83
+ hibernate: true,
84
+ sendIdentityOnConnect: true,
85
+ hungScheduleTimeoutSeconds: 30
86
+ };
87
+ function getCurrentAgent() {
88
+ const store = __DO_NOT_USE_WILL_BREAK__agentContext.getStore();
89
+ if (!store) return {
90
+ agent: void 0,
91
+ connection: void 0,
92
+ request: void 0,
93
+ email: void 0
94
+ };
95
+ return store;
96
+ }
97
+ /**
98
+ * Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
99
+ * @param agent The agent instance
100
+ * @param method The method to wrap
101
+ * @returns A wrapped method that runs within the agent context
102
+ */
103
+ function withAgentContext(method) {
104
+ return function(...args) {
105
+ const { connection, request, email, agent } = getCurrentAgent();
106
+ if (agent === this) return method.apply(this, args);
107
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
108
+ agent: this,
109
+ connection,
110
+ request,
111
+ email
112
+ }, () => {
113
+ return method.apply(this, args);
114
+ });
115
+ };
116
+ }
117
+ /**
118
+ * Base class for creating Agent implementations
119
+ * @template Env Environment type containing bindings
120
+ * @template State State type to store within the Agent
121
+ */
122
+ var Agent = class Agent extends Server {
123
+ /**
124
+ * Current state of the Agent
125
+ */
126
+ get state() {
127
+ if (this._state !== DEFAULT_STATE) return this._state;
128
+ const wasChanged = this.sql`
129
+ SELECT state FROM cf_agents_state WHERE id = ${STATE_WAS_CHANGED}
130
+ `;
131
+ const result = this.sql`
132
+ SELECT state FROM cf_agents_state WHERE id = ${STATE_ROW_ID}
133
+ `;
134
+ if (wasChanged[0]?.state === "true" || result[0]?.state) {
135
+ const state = result[0]?.state;
136
+ try {
137
+ this._state = JSON.parse(state);
138
+ } catch (e) {
139
+ console.error("Failed to parse stored state, falling back to initialState:", e);
140
+ if (this.initialState !== DEFAULT_STATE) {
141
+ this._state = this.initialState;
142
+ this._setStateInternal(this.initialState);
143
+ } else {
144
+ this.sql`DELETE FROM cf_agents_state WHERE id = ${STATE_ROW_ID}`;
145
+ this.sql`DELETE FROM cf_agents_state WHERE id = ${STATE_WAS_CHANGED}`;
146
+ return;
147
+ }
148
+ }
149
+ return this._state;
150
+ }
151
+ if (this.initialState === DEFAULT_STATE) return;
152
+ this._setStateInternal(this.initialState);
153
+ return this.initialState;
154
+ }
155
+ static {
156
+ this.options = { hibernate: true };
157
+ }
158
+ /**
159
+ * Resolved options (merges defaults with subclass overrides)
160
+ */
161
+ get _resolvedOptions() {
162
+ const ctor = this.constructor;
163
+ return {
164
+ hibernate: ctor.options?.hibernate ?? DEFAULT_AGENT_STATIC_OPTIONS.hibernate,
165
+ sendIdentityOnConnect: ctor.options?.sendIdentityOnConnect ?? DEFAULT_AGENT_STATIC_OPTIONS.sendIdentityOnConnect,
166
+ hungScheduleTimeoutSeconds: ctor.options?.hungScheduleTimeoutSeconds ?? DEFAULT_AGENT_STATIC_OPTIONS.hungScheduleTimeoutSeconds
167
+ };
168
+ }
169
+ /**
170
+ * Execute SQL queries against the Agent's database
171
+ * @template T Type of the returned rows
172
+ * @param strings SQL query template strings
173
+ * @param values Values to be inserted into the query
174
+ * @returns Array of query results
175
+ */
176
+ sql(strings, ...values) {
177
+ let query = "";
178
+ try {
179
+ query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), "");
180
+ return [...this.ctx.storage.sql.exec(query, ...values)];
181
+ } catch (e) {
182
+ throw this.onError(new SqlError(query, e));
183
+ }
184
+ }
185
+ constructor(ctx, env) {
186
+ super(ctx, env);
187
+ this._state = DEFAULT_STATE;
188
+ this._disposables = new DisposableStore();
189
+ this._destroyed = false;
190
+ this._rawStateAccessors = /* @__PURE__ */ new WeakMap();
191
+ this._persistenceHookMode = "none";
192
+ this._ParentClass = Object.getPrototypeOf(this).constructor;
193
+ this.initialState = DEFAULT_STATE;
194
+ this.observability = genericObservability;
195
+ this._flushingQueue = false;
196
+ this.alarm = async () => {
197
+ const now = Math.floor(Date.now() / 1e3);
198
+ const result = this.sql`
199
+ SELECT * FROM cf_agents_schedules WHERE time <= ${now}
200
+ `;
201
+ if (result && Array.isArray(result)) for (const row of result) {
202
+ const callback = this[row.callback];
203
+ if (!callback) {
204
+ console.error(`callback ${row.callback} not found`);
205
+ continue;
206
+ }
207
+ if (row.type === "interval" && row.running === 1) {
208
+ const executionStartedAt = row.execution_started_at ?? 0;
209
+ const hungTimeoutSeconds = this._resolvedOptions.hungScheduleTimeoutSeconds;
210
+ const elapsedSeconds = now - executionStartedAt;
211
+ if (elapsedSeconds < hungTimeoutSeconds) {
212
+ console.warn(`Skipping interval schedule ${row.id}: previous execution still running`);
213
+ continue;
214
+ }
215
+ console.warn(`Forcing reset of hung interval schedule ${row.id} (started ${elapsedSeconds}s ago)`);
216
+ }
217
+ if (row.type === "interval") this.sql`UPDATE cf_agents_schedules SET running = 1, execution_started_at = ${now} WHERE id = ${row.id}`;
218
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
219
+ agent: this,
220
+ connection: void 0,
221
+ request: void 0,
222
+ email: void 0
223
+ }, async () => {
224
+ try {
225
+ this.observability?.emit({
226
+ displayMessage: `Schedule ${row.id} executed`,
227
+ id: nanoid(),
228
+ payload: {
229
+ callback: row.callback,
230
+ id: row.id
231
+ },
232
+ timestamp: Date.now(),
233
+ type: "schedule:execute"
234
+ }, this.ctx);
235
+ await callback.bind(this)(JSON.parse(row.payload), row);
236
+ } catch (e) {
237
+ console.error(`error executing callback "${row.callback}"`, e);
238
+ try {
239
+ await this.onError(e);
240
+ } catch {}
241
+ }
242
+ });
243
+ if (this._destroyed) return;
244
+ if (row.type === "cron") {
245
+ const nextExecutionTime = getNextCronTime(row.cron);
246
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
247
+ this.sql`
248
+ UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
249
+ `;
250
+ } else if (row.type === "interval") {
251
+ const nextTimestamp = Math.floor(Date.now() / 1e3) + (row.intervalSeconds ?? 0);
252
+ this.sql`
253
+ UPDATE cf_agents_schedules SET running = 0, time = ${nextTimestamp} WHERE id = ${row.id}
254
+ `;
255
+ } else this.sql`
256
+ DELETE FROM cf_agents_schedules WHERE id = ${row.id}
257
+ `;
258
+ }
259
+ if (this._destroyed) return;
260
+ await this._scheduleNextAlarm();
261
+ };
262
+ if (!wrappedClasses.has(this.constructor)) {
263
+ this._autoWrapCustomMethods();
264
+ wrappedClasses.add(this.constructor);
265
+ }
266
+ this.sql`
267
+ CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
268
+ id TEXT PRIMARY KEY NOT NULL,
269
+ name TEXT NOT NULL,
270
+ server_url TEXT NOT NULL,
271
+ callback_url TEXT NOT NULL,
272
+ client_id TEXT,
273
+ auth_url TEXT,
274
+ server_options TEXT
275
+ )
276
+ `;
277
+ this.sql`
278
+ CREATE TABLE IF NOT EXISTS cf_agents_state (
279
+ id TEXT PRIMARY KEY NOT NULL,
280
+ state TEXT
281
+ )
282
+ `;
283
+ this.sql`
284
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
285
+ id TEXT PRIMARY KEY NOT NULL,
286
+ payload TEXT,
287
+ callback TEXT,
288
+ created_at INTEGER DEFAULT (unixepoch())
289
+ )
290
+ `;
291
+ this.sql`
292
+ CREATE TABLE IF NOT EXISTS cf_agents_schedules (
293
+ id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
294
+ callback TEXT,
295
+ payload TEXT,
296
+ type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed', 'cron', 'interval')),
297
+ time INTEGER,
298
+ delayInSeconds INTEGER,
299
+ cron TEXT,
300
+ intervalSeconds INTEGER,
301
+ running INTEGER DEFAULT 0,
302
+ created_at INTEGER DEFAULT (unixepoch())
303
+ )
304
+ `;
305
+ const addColumnIfNotExists = (sql) => {
306
+ try {
307
+ this.ctx.storage.sql.exec(sql);
308
+ } catch (e) {
309
+ if (!(e instanceof Error ? e.message : String(e)).toLowerCase().includes("duplicate column")) throw e;
310
+ }
311
+ };
312
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN intervalSeconds INTEGER");
313
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN running INTEGER DEFAULT 0");
314
+ addColumnIfNotExists("ALTER TABLE cf_agents_schedules ADD COLUMN execution_started_at INTEGER");
315
+ this.sql`
316
+ CREATE TABLE IF NOT EXISTS cf_agents_workflows (
317
+ id TEXT PRIMARY KEY NOT NULL,
318
+ workflow_id TEXT NOT NULL UNIQUE,
319
+ workflow_name TEXT NOT NULL,
320
+ status TEXT NOT NULL CHECK(status IN (
321
+ 'queued', 'running', 'paused', 'errored',
322
+ 'terminated', 'complete', 'waiting',
323
+ 'waitingForPause', 'unknown'
324
+ )),
325
+ metadata TEXT,
326
+ error_name TEXT,
327
+ error_message TEXT,
328
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
329
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
330
+ completed_at INTEGER
331
+ )
332
+ `;
333
+ this.sql`
334
+ CREATE INDEX IF NOT EXISTS idx_workflows_status ON cf_agents_workflows(status)
335
+ `;
336
+ this.sql`
337
+ CREATE INDEX IF NOT EXISTS idx_workflows_name ON cf_agents_workflows(workflow_name)
338
+ `;
339
+ this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1", { storage: this.ctx.storage });
340
+ this._disposables.add(this.mcp.onServerStateChanged(async () => {
341
+ this.broadcastMcpServers();
342
+ }));
343
+ this._disposables.add(this.mcp.onObservabilityEvent((event) => {
344
+ this.observability?.emit(event);
345
+ }));
346
+ {
347
+ const proto = Object.getPrototypeOf(this);
348
+ const hasOwnNew = Object.prototype.hasOwnProperty.call(proto, "onStateChanged");
349
+ const hasOwnOld = Object.prototype.hasOwnProperty.call(proto, "onStateUpdate");
350
+ if (hasOwnNew && hasOwnOld) throw new Error("[Agent] Cannot override both onStateChanged and onStateUpdate. Remove onStateUpdate — it has been renamed to onStateChanged.");
351
+ if (hasOwnOld) {
352
+ const ctor = this.constructor;
353
+ if (!_onStateUpdateWarnedClasses.has(ctor)) {
354
+ _onStateUpdateWarnedClasses.add(ctor);
355
+ console.warn(`[Agent] onStateUpdate is deprecated. Rename to onStateChanged — the behavior is identical.`);
356
+ }
357
+ }
358
+ const base = Agent.prototype;
359
+ if (proto.onStateChanged !== base.onStateChanged) this._persistenceHookMode = "new";
360
+ else if (proto.onStateUpdate !== base.onStateUpdate) this._persistenceHookMode = "old";
361
+ }
362
+ const _onRequest = this.onRequest.bind(this);
363
+ this.onRequest = (request) => {
364
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
365
+ agent: this,
366
+ connection: void 0,
367
+ request,
368
+ email: void 0
369
+ }, async () => {
370
+ await this.mcp.ensureJsonSchema();
371
+ const oauthResponse = await this.handleMcpOAuthCallback(request);
372
+ if (oauthResponse) return oauthResponse;
373
+ return this._tryCatch(() => _onRequest(request));
374
+ });
375
+ };
376
+ const _onMessage = this.onMessage.bind(this);
377
+ this.onMessage = async (connection, message) => {
378
+ this._ensureConnectionWrapped(connection);
379
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
380
+ agent: this,
381
+ connection,
382
+ request: void 0,
383
+ email: void 0
384
+ }, async () => {
385
+ await this.mcp.ensureJsonSchema();
386
+ if (typeof message !== "string") return this._tryCatch(() => _onMessage(connection, message));
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(message);
390
+ } catch (_e) {
391
+ return this._tryCatch(() => _onMessage(connection, message));
392
+ }
393
+ if (isStateUpdateMessage(parsed)) {
394
+ if (this.isConnectionReadonly(connection)) {
395
+ connection.send(JSON.stringify({
396
+ type: MessageType.CF_AGENT_STATE_ERROR,
397
+ error: "Connection is readonly"
398
+ }));
399
+ return;
400
+ }
401
+ try {
402
+ this._setStateInternal(parsed.state, connection);
403
+ } catch (e) {
404
+ console.error("[Agent] State update rejected:", e);
405
+ connection.send(JSON.stringify({
406
+ type: MessageType.CF_AGENT_STATE_ERROR,
407
+ error: "State update rejected"
408
+ }));
409
+ }
410
+ return;
411
+ }
412
+ if (isRPCRequest(parsed)) {
413
+ try {
414
+ const { id, method, args } = parsed;
415
+ const methodFn = this[method];
416
+ if (typeof methodFn !== "function") throw new Error(`Method ${method} does not exist`);
417
+ if (!this._isCallable(method)) throw new Error(`Method ${method} is not callable`);
418
+ const metadata = callableMetadata.get(methodFn);
419
+ if (metadata?.streaming) {
420
+ const stream = new StreamingResponse(connection, id);
421
+ this.observability?.emit({
422
+ displayMessage: `RPC streaming call to ${method}`,
423
+ id: nanoid(),
424
+ payload: {
425
+ method,
426
+ streaming: true
427
+ },
428
+ timestamp: Date.now(),
429
+ type: "rpc"
430
+ }, this.ctx);
431
+ try {
432
+ await methodFn.apply(this, [stream, ...args]);
433
+ } catch (err) {
434
+ console.error(`Error in streaming method "${method}":`, err);
435
+ if (!stream.isClosed) stream.error(err instanceof Error ? err.message : String(err));
436
+ }
437
+ return;
438
+ }
439
+ const result = await methodFn.apply(this, args);
440
+ this.observability?.emit({
441
+ displayMessage: `RPC call to ${method}`,
442
+ id: nanoid(),
443
+ payload: {
444
+ method,
445
+ streaming: metadata?.streaming
446
+ },
447
+ timestamp: Date.now(),
448
+ type: "rpc"
449
+ }, this.ctx);
450
+ const response = {
451
+ done: true,
452
+ id,
453
+ result,
454
+ success: true,
455
+ type: MessageType.RPC
456
+ };
457
+ connection.send(JSON.stringify(response));
458
+ } catch (e) {
459
+ const response = {
460
+ error: e instanceof Error ? e.message : "Unknown error occurred",
461
+ id: parsed.id,
462
+ success: false,
463
+ type: MessageType.RPC
464
+ };
465
+ connection.send(JSON.stringify(response));
466
+ console.error("RPC error:", e);
467
+ }
468
+ return;
469
+ }
470
+ return this._tryCatch(() => _onMessage(connection, message));
471
+ });
472
+ };
473
+ const _onConnect = this.onConnect.bind(this);
474
+ this.onConnect = (connection, ctx) => {
475
+ this._ensureConnectionWrapped(connection);
476
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
477
+ agent: this,
478
+ connection,
479
+ request: ctx.request,
480
+ email: void 0
481
+ }, async () => {
482
+ if (this.shouldConnectionBeReadonly(connection, ctx)) this.setConnectionReadonly(connection, true);
483
+ if (this._resolvedOptions.sendIdentityOnConnect) connection.send(JSON.stringify({
484
+ name: this.name,
485
+ agent: camelCaseToKebabCase(this._ParentClass.name),
486
+ type: MessageType.CF_AGENT_IDENTITY
487
+ }));
488
+ if (this.state) connection.send(JSON.stringify({
489
+ state: this.state,
490
+ type: MessageType.CF_AGENT_STATE
491
+ }));
492
+ connection.send(JSON.stringify({
493
+ mcp: this.getMcpServers(),
494
+ type: MessageType.CF_AGENT_MCP_SERVERS
495
+ }));
496
+ this.observability?.emit({
497
+ displayMessage: "Connection established",
498
+ id: nanoid(),
499
+ payload: { connectionId: connection.id },
500
+ timestamp: Date.now(),
501
+ type: "connect"
502
+ }, this.ctx);
503
+ return this._tryCatch(() => _onConnect(connection, ctx));
504
+ });
505
+ };
506
+ const _onStart = this.onStart.bind(this);
507
+ this.onStart = async (props) => {
508
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
509
+ agent: this,
510
+ connection: void 0,
511
+ request: void 0,
512
+ email: void 0
513
+ }, async () => {
514
+ await this._tryCatch(async () => {
515
+ await this.mcp.restoreConnectionsFromStorage(this.name);
516
+ this.broadcastMcpServers();
517
+ this._checkOrphanedWorkflows();
518
+ return _onStart(props);
519
+ });
520
+ });
521
+ };
522
+ }
523
+ /**
524
+ * Check for workflows referencing unknown bindings and warn with migration suggestion.
525
+ */
526
+ _checkOrphanedWorkflows() {
527
+ const orphaned = this.sql`
528
+ SELECT
529
+ workflow_name,
530
+ COUNT(*) as total,
531
+ SUM(CASE WHEN status NOT IN ('complete', 'errored', 'terminated') THEN 1 ELSE 0 END) as active,
532
+ SUM(CASE WHEN status IN ('complete', 'errored', 'terminated') THEN 1 ELSE 0 END) as completed
533
+ FROM cf_agents_workflows
534
+ GROUP BY workflow_name
535
+ `.filter((row) => !this._findWorkflowBindingByName(row.workflow_name));
536
+ if (orphaned.length > 0) {
537
+ const currentBindings = this._getWorkflowBindingNames();
538
+ for (const { workflow_name: oldName, total, active, completed } of orphaned) {
539
+ const suggestion = currentBindings.length === 1 ? `this.migrateWorkflowBinding('${oldName}', '${currentBindings[0]}')` : `this.migrateWorkflowBinding('${oldName}', '<NEW_BINDING_NAME>')`;
540
+ const breakdown = active > 0 && completed > 0 ? ` (${active} active, ${completed} completed)` : active > 0 ? ` (${active} active)` : ` (${completed} completed)`;
541
+ console.warn(`[Agent] Found ${total} workflow(s) referencing unknown binding '${oldName}'${breakdown}. If you renamed the binding, call: ${suggestion}`);
542
+ }
543
+ }
544
+ }
545
+ _setStateInternal(nextState, source = "server") {
546
+ this.validateStateChange(nextState, source);
547
+ this._state = nextState;
548
+ this.sql`
549
+ INSERT OR REPLACE INTO cf_agents_state (id, state)
550
+ VALUES (${STATE_ROW_ID}, ${JSON.stringify(nextState)})
551
+ `;
552
+ this.sql`
553
+ INSERT OR REPLACE INTO cf_agents_state (id, state)
554
+ VALUES (${STATE_WAS_CHANGED}, ${JSON.stringify(true)})
555
+ `;
556
+ this.broadcast(JSON.stringify({
557
+ state: nextState,
558
+ type: MessageType.CF_AGENT_STATE
559
+ }), source !== "server" ? [source.id] : []);
560
+ const { connection, request, email } = __DO_NOT_USE_WILL_BREAK__agentContext.getStore() || {};
561
+ this.ctx.waitUntil((async () => {
562
+ try {
563
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
564
+ agent: this,
565
+ connection,
566
+ request,
567
+ email
568
+ }, async () => {
569
+ this.observability?.emit({
570
+ displayMessage: "State updated",
571
+ id: nanoid(),
572
+ payload: {},
573
+ timestamp: Date.now(),
574
+ type: "state:update"
575
+ }, this.ctx);
576
+ await this._callStatePersistenceHook(nextState, source);
577
+ });
578
+ } catch (e) {
579
+ try {
580
+ await this.onError(e);
581
+ } catch {}
582
+ }
583
+ })());
584
+ }
585
+ /**
586
+ * Update the Agent's state
587
+ * @param state New state to set
588
+ * @throws Error if called from a readonly connection context
589
+ */
590
+ setState(state) {
591
+ const store = __DO_NOT_USE_WILL_BREAK__agentContext.getStore();
592
+ if (store?.connection && this.isConnectionReadonly(store.connection)) throw new Error("Connection is readonly");
593
+ this._setStateInternal(state, "server");
594
+ }
595
+ /**
596
+ * Wraps connection.state and connection.setState so that the internal
597
+ * _cf_readonly flag is hidden from user code and cannot be accidentally
598
+ * overwritten. Must be called before any user code sees the connection.
599
+ *
600
+ * Idempotent — safe to call multiple times on the same connection.
601
+ */
602
+ _ensureConnectionWrapped(connection) {
603
+ if (this._rawStateAccessors.has(connection)) return;
604
+ const descriptor = Object.getOwnPropertyDescriptor(connection, "state");
605
+ let getRaw;
606
+ let setRaw;
607
+ if (descriptor?.get) {
608
+ getRaw = descriptor.get.bind(connection);
609
+ setRaw = connection.setState.bind(connection);
610
+ } else {
611
+ let rawState = connection.state ?? null;
612
+ getRaw = () => rawState;
613
+ setRaw = (state) => {
614
+ rawState = state;
615
+ return rawState;
616
+ };
617
+ }
618
+ this._rawStateAccessors.set(connection, {
619
+ getRaw,
620
+ setRaw
621
+ });
622
+ const CF_KEY = CF_READONLY_KEY;
623
+ Object.defineProperty(connection, "state", {
624
+ configurable: true,
625
+ enumerable: true,
626
+ get() {
627
+ const raw = getRaw();
628
+ if (raw != null && typeof raw === "object" && CF_KEY in raw) {
629
+ const { [CF_KEY]: _, ...userState } = raw;
630
+ return Object.keys(userState).length > 0 ? userState : null;
631
+ }
632
+ return raw;
633
+ }
634
+ });
635
+ Object.defineProperty(connection, "setState", {
636
+ configurable: true,
637
+ writable: true,
638
+ value(stateOrFn) {
639
+ const raw = getRaw();
640
+ const readonlyFlag = raw != null && typeof raw === "object" ? raw[CF_KEY] : void 0;
641
+ let newUserState;
642
+ if (typeof stateOrFn === "function") {
643
+ let userVisible = raw;
644
+ if (raw != null && typeof raw === "object" && CF_KEY in raw) {
645
+ const { [CF_KEY]: _, ...rest } = raw;
646
+ userVisible = Object.keys(rest).length > 0 ? rest : null;
647
+ }
648
+ newUserState = stateOrFn(userVisible);
649
+ } else newUserState = stateOrFn;
650
+ if (readonlyFlag !== void 0) {
651
+ if (newUserState != null && typeof newUserState === "object") return setRaw({
652
+ ...newUserState,
653
+ [CF_KEY]: readonlyFlag
654
+ });
655
+ return setRaw({ [CF_KEY]: readonlyFlag });
656
+ }
657
+ return setRaw(newUserState);
658
+ }
659
+ });
660
+ }
661
+ /**
662
+ * Mark a connection as readonly or readwrite
663
+ * @param connection The connection to mark
664
+ * @param readonly Whether the connection should be readonly (default: true)
665
+ */
666
+ setConnectionReadonly(connection, readonly = true) {
667
+ this._ensureConnectionWrapped(connection);
668
+ const accessors = this._rawStateAccessors.get(connection);
669
+ const raw = accessors.getRaw() ?? {};
670
+ if (readonly) accessors.setRaw({
671
+ ...raw,
672
+ [CF_READONLY_KEY]: true
673
+ });
674
+ else {
675
+ const { [CF_READONLY_KEY]: _, ...rest } = raw;
676
+ accessors.setRaw(Object.keys(rest).length > 0 ? rest : null);
677
+ }
678
+ }
679
+ /**
680
+ * Check if a connection is marked as readonly
681
+ * @param connection The connection to check
682
+ * @returns True if the connection is readonly
683
+ */
684
+ isConnectionReadonly(connection) {
685
+ const accessors = this._rawStateAccessors.get(connection);
686
+ if (accessors) return !!accessors.getRaw()?.[CF_READONLY_KEY];
687
+ return false;
688
+ }
689
+ /**
690
+ * Override this method to determine if a connection should be readonly on connect
691
+ * @param _connection The connection that is being established
692
+ * @param _ctx Connection context
693
+ * @returns True if the connection should be readonly
694
+ */
695
+ shouldConnectionBeReadonly(_connection, _ctx) {
696
+ return false;
697
+ }
698
+ /**
699
+ * Called before the Agent's state is persisted and broadcast.
700
+ * Override to validate or reject an update by throwing an error.
701
+ *
702
+ * IMPORTANT: This hook must be synchronous.
703
+ */
704
+ validateStateChange(nextState, source) {}
705
+ /**
706
+ * Called after the Agent's state has been persisted and broadcast to all clients.
707
+ * This is a notification hook — errors here are routed to onError and do not
708
+ * affect state persistence or client broadcasts.
709
+ *
710
+ * @param state Updated state
711
+ * @param source Source of the state update ("server" or a client connection)
712
+ */
713
+ onStateChanged(state, source) {}
714
+ /**
715
+ * @deprecated Renamed to `onStateChanged` — the behavior is identical.
716
+ * `onStateUpdate` will be removed in the next major version.
717
+ *
718
+ * Called after the Agent's state has been persisted and broadcast to all clients.
719
+ * This is a server-side notification hook. For the client-side state callback,
720
+ * see the `onStateUpdate` option in `useAgent` / `AgentClient`.
721
+ *
722
+ * @param state Updated state
723
+ * @param source Source of the state update ("server" or a client connection)
724
+ */
725
+ onStateUpdate(state, source) {}
726
+ /**
727
+ * Dispatch to the appropriate persistence hook based on the mode
728
+ * cached in the constructor. No prototype walks at call time.
729
+ */
730
+ async _callStatePersistenceHook(state, source) {
731
+ switch (this._persistenceHookMode) {
732
+ case "new":
733
+ await this.onStateChanged(state, source);
734
+ break;
735
+ case "old":
736
+ await this.onStateUpdate(state, source);
737
+ break;
738
+ }
739
+ }
740
+ /**
741
+ * Called when the Agent receives an email via routeAgentEmail()
742
+ * Override this method to handle incoming emails
743
+ * @param email Email message to process
744
+ */
745
+ async _onEmail(email) {
746
+ return __DO_NOT_USE_WILL_BREAK__agentContext.run({
747
+ agent: this,
748
+ connection: void 0,
749
+ request: void 0,
750
+ email
751
+ }, async () => {
752
+ if ("onEmail" in this && typeof this.onEmail === "function") return this._tryCatch(() => this.onEmail(email));
753
+ else {
754
+ console.log("Received email from:", email.from, "to:", email.to);
755
+ console.log("Subject:", email.headers.get("subject"));
756
+ console.log("Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails");
757
+ }
758
+ });
759
+ }
760
+ /**
761
+ * Reply to an email
762
+ * @param email The email to reply to
763
+ * @param options Options for the reply
764
+ * @param options.secret Secret for signing agent headers (enables secure reply routing).
765
+ * Required if the email was routed via createSecureReplyEmailResolver.
766
+ * Pass explicit `null` to opt-out of signing (not recommended for secure routing).
767
+ * @returns void
768
+ */
769
+ async replyToEmail(email, options) {
770
+ return this._tryCatch(async () => {
771
+ if (email._secureRouted && options.secret === void 0) throw new Error("This email was routed via createSecureReplyEmailResolver. You must pass a secret to replyToEmail() to sign replies, or pass explicit null to opt-out (not recommended).");
772
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
773
+ const agentId = this.name;
774
+ const { createMimeMessage } = await import("mimetext");
775
+ const msg = createMimeMessage();
776
+ msg.setSender({
777
+ addr: email.to,
778
+ name: options.fromName
779
+ });
780
+ msg.setRecipient(email.from);
781
+ msg.setSubject(options.subject || `Re: ${email.headers.get("subject")}` || "No subject");
782
+ msg.addMessage({
783
+ contentType: options.contentType || "text/plain",
784
+ data: options.body
785
+ });
786
+ const messageId = `<${agentId}@${email.from.split("@")[1]}>`;
787
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID"));
788
+ msg.setHeader("Message-ID", messageId);
789
+ msg.setHeader("X-Agent-Name", agentName);
790
+ msg.setHeader("X-Agent-ID", agentId);
791
+ if (typeof options.secret === "string") {
792
+ const signedHeaders = await signAgentHeaders(options.secret, agentName, agentId);
793
+ msg.setHeader("X-Agent-Sig", signedHeaders["X-Agent-Sig"]);
794
+ msg.setHeader("X-Agent-Sig-Ts", signedHeaders["X-Agent-Sig-Ts"]);
795
+ }
796
+ if (options.headers) for (const [key, value] of Object.entries(options.headers)) msg.setHeader(key, value);
797
+ await email.reply({
798
+ from: email.to,
799
+ raw: msg.asRaw(),
800
+ to: email.from
801
+ });
802
+ });
803
+ }
804
+ async _tryCatch(fn) {
805
+ try {
806
+ return await fn();
807
+ } catch (e) {
808
+ throw this.onError(e);
809
+ }
810
+ }
811
+ /**
812
+ * Automatically wrap custom methods with agent context
813
+ * This ensures getCurrentAgent() works in all custom methods without decorators
814
+ */
815
+ _autoWrapCustomMethods() {
816
+ const basePrototypes = [Agent.prototype, Server.prototype];
817
+ const baseMethods = /* @__PURE__ */ new Set();
818
+ for (const baseProto of basePrototypes) {
819
+ let proto = baseProto;
820
+ while (proto && proto !== Object.prototype) {
821
+ const methodNames = Object.getOwnPropertyNames(proto);
822
+ for (const methodName of methodNames) baseMethods.add(methodName);
823
+ proto = Object.getPrototypeOf(proto);
824
+ }
825
+ }
826
+ let proto = Object.getPrototypeOf(this);
827
+ let depth = 0;
828
+ while (proto && proto !== Object.prototype && depth < 10) {
829
+ const methodNames = Object.getOwnPropertyNames(proto);
830
+ for (const methodName of methodNames) {
831
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
832
+ if (baseMethods.has(methodName) || methodName.startsWith("_") || !descriptor || !!descriptor.get || typeof descriptor.value !== "function") continue;
833
+ const wrappedFunction = withAgentContext(this[methodName]);
834
+ if (this._isCallable(methodName)) callableMetadata.set(wrappedFunction, callableMetadata.get(this[methodName]));
835
+ this.constructor.prototype[methodName] = wrappedFunction;
836
+ }
837
+ proto = Object.getPrototypeOf(proto);
838
+ depth++;
839
+ }
840
+ }
841
+ onError(connectionOrError, error) {
842
+ let theError;
843
+ if (connectionOrError && error) {
844
+ theError = error;
845
+ console.error("Error on websocket connection:", connectionOrError.id, theError);
846
+ console.error("Override onError(connection, error) to handle websocket connection errors");
847
+ } else {
848
+ theError = connectionOrError;
849
+ console.error("Error on server:", theError);
850
+ console.error("Override onError(error) to handle server errors");
851
+ }
852
+ throw theError;
853
+ }
854
+ /**
855
+ * Render content (not implemented in base class)
856
+ */
857
+ render() {
858
+ throw new Error("Not implemented");
859
+ }
860
+ /**
861
+ * Queue a task to be executed in the future
862
+ * @param payload Payload to pass to the callback
863
+ * @param callback Name of the method to call
864
+ * @returns The ID of the queued task
865
+ */
866
+ async queue(callback, payload) {
867
+ const id = nanoid(9);
868
+ if (typeof callback !== "string") throw new Error("Callback must be a string");
869
+ if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
870
+ this.sql`
871
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
872
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
873
+ `;
874
+ this._flushQueue().catch((e) => {
875
+ console.error("Error flushing queue:", e);
876
+ });
877
+ return id;
878
+ }
879
+ async _flushQueue() {
880
+ if (this._flushingQueue) return;
881
+ this._flushingQueue = true;
882
+ try {
883
+ while (true) {
884
+ const result = this.sql`
885
+ SELECT * FROM cf_agents_queues
886
+ ORDER BY created_at ASC
887
+ `;
888
+ if (!result || result.length === 0) break;
889
+ for (const row of result || []) {
890
+ const callback = this[row.callback];
891
+ if (!callback) {
892
+ console.error(`callback ${row.callback} not found`);
893
+ await this.dequeue(row.id);
894
+ continue;
895
+ }
896
+ const { connection, request, email } = __DO_NOT_USE_WILL_BREAK__agentContext.getStore() || {};
897
+ try {
898
+ await __DO_NOT_USE_WILL_BREAK__agentContext.run({
899
+ agent: this,
900
+ connection,
901
+ request,
902
+ email
903
+ }, async () => {
904
+ await callback.bind(this)(JSON.parse(row.payload), row);
905
+ });
906
+ } catch (e) {
907
+ console.error(`Queue callback ${String(row.callback)} failed for row ${row.id}:`, e);
908
+ } finally {
909
+ await this.dequeue(row.id);
910
+ }
911
+ }
912
+ }
913
+ } finally {
914
+ this._flushingQueue = false;
915
+ }
916
+ }
917
+ /**
918
+ * Dequeue a task by ID
919
+ * @param id ID of the task to dequeue
920
+ */
921
+ async dequeue(id) {
922
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
923
+ }
924
+ /**
925
+ * Dequeue all tasks
926
+ */
927
+ async dequeueAll() {
928
+ this.sql`DELETE FROM cf_agents_queues`;
929
+ }
930
+ /**
931
+ * Dequeue all tasks by callback
932
+ * @param callback Name of the callback to dequeue
933
+ */
934
+ async dequeueAllByCallback(callback) {
935
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
936
+ }
937
+ /**
938
+ * Get a queued task by ID
939
+ * @param id ID of the task to get
940
+ * @returns The task or undefined if not found
941
+ */
942
+ async getQueue(id) {
943
+ const result = this.sql`
944
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
945
+ `;
946
+ return result ? {
947
+ ...result[0],
948
+ payload: JSON.parse(result[0].payload)
949
+ } : void 0;
950
+ }
951
+ /**
952
+ * Get all queues by key and value
953
+ * @param key Key to filter by
954
+ * @param value Value to filter by
955
+ * @returns Array of matching QueueItem objects
956
+ */
957
+ async getQueues(key, value) {
958
+ return this.sql`
959
+ SELECT * FROM cf_agents_queues
960
+ `.filter((row) => JSON.parse(row.payload)[key] === value);
961
+ }
962
+ /**
963
+ * Schedule a task to be executed in the future
964
+ * @template T Type of the payload data
965
+ * @param when When to execute the task (Date, seconds delay, or cron expression)
966
+ * @param callback Name of the method to call
967
+ * @param payload Data to pass to the callback
968
+ * @returns Schedule object representing the scheduled task
969
+ */
970
+ async schedule(when, callback, payload) {
971
+ const id = nanoid(9);
972
+ const emitScheduleCreate = (schedule) => this.observability?.emit({
973
+ displayMessage: `Schedule ${schedule.id} created`,
974
+ id: nanoid(),
975
+ payload: {
976
+ callback,
977
+ id
978
+ },
979
+ timestamp: Date.now(),
980
+ type: "schedule:create"
981
+ }, this.ctx);
982
+ if (typeof callback !== "string") throw new Error("Callback must be a string");
983
+ if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
984
+ if (when instanceof Date) {
985
+ const timestamp = Math.floor(when.getTime() / 1e3);
986
+ this.sql`
987
+ INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, time)
988
+ VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'scheduled', ${timestamp})
989
+ `;
990
+ await this._scheduleNextAlarm();
991
+ const schedule = {
992
+ callback,
993
+ id,
994
+ payload,
995
+ time: timestamp,
996
+ type: "scheduled"
997
+ };
998
+ emitScheduleCreate(schedule);
999
+ return schedule;
1000
+ }
1001
+ if (typeof when === "number") {
1002
+ const time = new Date(Date.now() + when * 1e3);
1003
+ const timestamp = Math.floor(time.getTime() / 1e3);
1004
+ this.sql`
1005
+ INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, delayInSeconds, time)
1006
+ VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'delayed', ${when}, ${timestamp})
1007
+ `;
1008
+ await this._scheduleNextAlarm();
1009
+ const schedule = {
1010
+ callback,
1011
+ delayInSeconds: when,
1012
+ id,
1013
+ payload,
1014
+ time: timestamp,
1015
+ type: "delayed"
1016
+ };
1017
+ emitScheduleCreate(schedule);
1018
+ return schedule;
1019
+ }
1020
+ if (typeof when === "string") {
1021
+ const nextExecutionTime = getNextCronTime(when);
1022
+ const timestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
1023
+ this.sql`
1024
+ INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, cron, time)
1025
+ VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'cron', ${when}, ${timestamp})
1026
+ `;
1027
+ await this._scheduleNextAlarm();
1028
+ const schedule = {
1029
+ callback,
1030
+ cron: when,
1031
+ id,
1032
+ payload,
1033
+ time: timestamp,
1034
+ type: "cron"
1035
+ };
1036
+ emitScheduleCreate(schedule);
1037
+ return schedule;
1038
+ }
1039
+ throw new Error(`Invalid schedule type: ${JSON.stringify(when)}(${typeof when}) trying to schedule ${callback}`);
1040
+ }
1041
+ /**
1042
+ * Schedule a task to run repeatedly at a fixed interval
1043
+ * @template T Type of the payload data
1044
+ * @param intervalSeconds Number of seconds between executions
1045
+ * @param callback Name of the method to call
1046
+ * @param payload Data to pass to the callback
1047
+ * @returns Schedule object representing the scheduled task
1048
+ */
1049
+ async scheduleEvery(intervalSeconds, callback, payload) {
1050
+ const MAX_INTERVAL_SECONDS = 720 * 60 * 60;
1051
+ if (typeof intervalSeconds !== "number" || intervalSeconds <= 0) throw new Error("intervalSeconds must be a positive number");
1052
+ if (intervalSeconds > MAX_INTERVAL_SECONDS) throw new Error(`intervalSeconds cannot exceed ${MAX_INTERVAL_SECONDS} seconds (30 days)`);
1053
+ if (typeof callback !== "string") throw new Error("Callback must be a string");
1054
+ if (typeof this[callback] !== "function") throw new Error(`this.${callback} is not a function`);
1055
+ const id = nanoid(9);
1056
+ const time = new Date(Date.now() + intervalSeconds * 1e3);
1057
+ const timestamp = Math.floor(time.getTime() / 1e3);
1058
+ this.sql`
1059
+ INSERT OR REPLACE INTO cf_agents_schedules (id, callback, payload, type, intervalSeconds, time, running)
1060
+ VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'interval', ${intervalSeconds}, ${timestamp}, 0)
1061
+ `;
1062
+ await this._scheduleNextAlarm();
1063
+ const schedule = {
1064
+ callback,
1065
+ id,
1066
+ intervalSeconds,
1067
+ payload,
1068
+ time: timestamp,
1069
+ type: "interval"
1070
+ };
1071
+ this.observability?.emit({
1072
+ displayMessage: `Schedule ${schedule.id} created`,
1073
+ id: nanoid(),
1074
+ payload: {
1075
+ callback,
1076
+ id
1077
+ },
1078
+ timestamp: Date.now(),
1079
+ type: "schedule:create"
1080
+ }, this.ctx);
1081
+ return schedule;
1082
+ }
1083
+ /**
1084
+ * Get a scheduled task by ID
1085
+ * @template T Type of the payload data
1086
+ * @param id ID of the scheduled task
1087
+ * @returns The Schedule object or undefined if not found
1088
+ */
1089
+ async getSchedule(id) {
1090
+ const result = this.sql`
1091
+ SELECT * FROM cf_agents_schedules WHERE id = ${id}
1092
+ `;
1093
+ if (!result || result.length === 0) return;
1094
+ return {
1095
+ ...result[0],
1096
+ payload: JSON.parse(result[0].payload)
1097
+ };
1098
+ }
1099
+ /**
1100
+ * Get scheduled tasks matching the given criteria
1101
+ * @template T Type of the payload data
1102
+ * @param criteria Criteria to filter schedules
1103
+ * @returns Array of matching Schedule objects
1104
+ */
1105
+ getSchedules(criteria = {}) {
1106
+ let query = "SELECT * FROM cf_agents_schedules WHERE 1=1";
1107
+ const params = [];
1108
+ if (criteria.id) {
1109
+ query += " AND id = ?";
1110
+ params.push(criteria.id);
1111
+ }
1112
+ if (criteria.type) {
1113
+ query += " AND type = ?";
1114
+ params.push(criteria.type);
1115
+ }
1116
+ if (criteria.timeRange) {
1117
+ query += " AND time >= ? AND time <= ?";
1118
+ const start = criteria.timeRange.start || /* @__PURE__ */ new Date(0);
1119
+ const end = criteria.timeRange.end || /* @__PURE__ */ new Date(999999999999999);
1120
+ params.push(Math.floor(start.getTime() / 1e3), Math.floor(end.getTime() / 1e3));
1121
+ }
1122
+ return this.ctx.storage.sql.exec(query, ...params).toArray().map((row) => ({
1123
+ ...row,
1124
+ payload: JSON.parse(row.payload)
1125
+ }));
1126
+ }
1127
+ /**
1128
+ * Cancel a scheduled task
1129
+ * @param id ID of the task to cancel
1130
+ * @returns true if the task was cancelled, false if the task was not found
1131
+ */
1132
+ async cancelSchedule(id) {
1133
+ const schedule = await this.getSchedule(id);
1134
+ if (!schedule) return false;
1135
+ this.observability?.emit({
1136
+ displayMessage: `Schedule ${id} cancelled`,
1137
+ id: nanoid(),
1138
+ payload: {
1139
+ callback: schedule.callback,
1140
+ id: schedule.id
1141
+ },
1142
+ timestamp: Date.now(),
1143
+ type: "schedule:cancel"
1144
+ }, this.ctx);
1145
+ this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
1146
+ await this._scheduleNextAlarm();
1147
+ return true;
1148
+ }
1149
+ async _scheduleNextAlarm() {
1150
+ const result = this.sql`
1151
+ SELECT time FROM cf_agents_schedules
1152
+ WHERE time >= ${Math.floor(Date.now() / 1e3)}
1153
+ ORDER BY time ASC
1154
+ LIMIT 1
1155
+ `;
1156
+ if (!result) return;
1157
+ if (result.length > 0 && "time" in result[0]) {
1158
+ const nextTime = result[0].time * 1e3;
1159
+ await this.ctx.storage.setAlarm(nextTime);
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Destroy the Agent, removing all state and scheduled tasks
1164
+ */
1165
+ async destroy() {
1166
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1167
+ this.sql`DROP TABLE IF EXISTS cf_agents_state`;
1168
+ this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1169
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
1170
+ this.sql`DROP TABLE IF EXISTS cf_agents_workflows`;
1171
+ await this.ctx.storage.deleteAlarm();
1172
+ await this.ctx.storage.deleteAll();
1173
+ this._disposables.dispose();
1174
+ await this.mcp.dispose();
1175
+ this._destroyed = true;
1176
+ setTimeout(() => {
1177
+ this.ctx.abort("destroyed");
1178
+ }, 0);
1179
+ this.observability?.emit({
1180
+ displayMessage: "Agent destroyed",
1181
+ id: nanoid(),
1182
+ payload: {},
1183
+ timestamp: Date.now(),
1184
+ type: "destroy"
1185
+ }, this.ctx);
1186
+ }
1187
+ /**
1188
+ * Check if a method is callable
1189
+ * @param method The method name to check
1190
+ * @returns True if the method is marked as callable
1191
+ */
1192
+ _isCallable(method) {
1193
+ return callableMetadata.has(this[method]);
1194
+ }
1195
+ /**
1196
+ * Get all methods marked as callable on this Agent
1197
+ * @returns A map of method names to their metadata
1198
+ */
1199
+ getCallableMethods() {
1200
+ const result = /* @__PURE__ */ new Map();
1201
+ let prototype = Object.getPrototypeOf(this);
1202
+ while (prototype && prototype !== Object.prototype) {
1203
+ for (const name of Object.getOwnPropertyNames(prototype)) {
1204
+ if (name === "constructor") continue;
1205
+ if (result.has(name)) continue;
1206
+ try {
1207
+ const fn = prototype[name];
1208
+ if (typeof fn === "function") {
1209
+ const meta = callableMetadata.get(fn);
1210
+ if (meta) result.set(name, meta);
1211
+ }
1212
+ } catch (e) {
1213
+ if (!(e instanceof TypeError)) throw e;
1214
+ }
1215
+ }
1216
+ prototype = Object.getPrototypeOf(prototype);
1217
+ }
1218
+ return result;
1219
+ }
1220
+ /**
1221
+ * Start a workflow and track it in this Agent's database.
1222
+ * Automatically injects agent identity into the workflow params.
1223
+ *
1224
+ * @template P - Type of params to pass to the workflow
1225
+ * @param workflowName - Name of the workflow binding in env (e.g., 'MY_WORKFLOW')
1226
+ * @param params - Params to pass to the workflow
1227
+ * @param options - Optional workflow options
1228
+ * @returns The workflow instance ID
1229
+ *
1230
+ * @example
1231
+ * ```typescript
1232
+ * const workflowId = await this.runWorkflow(
1233
+ * 'MY_WORKFLOW',
1234
+ * { taskId: '123', data: 'process this' }
1235
+ * );
1236
+ * ```
1237
+ */
1238
+ async runWorkflow(workflowName, params, options) {
1239
+ const workflow = this._findWorkflowBindingByName(workflowName);
1240
+ if (!workflow) throw new Error(`Workflow binding '${workflowName}' not found in environment`);
1241
+ const agentBindingName = options?.agentBinding ?? this._findAgentBindingName();
1242
+ if (!agentBindingName) throw new Error("Could not detect Agent binding name from class name. Pass it explicitly via options.agentBinding");
1243
+ const workflowId = options?.id ?? nanoid();
1244
+ const augmentedParams = {
1245
+ ...params,
1246
+ __agentName: this.name,
1247
+ __agentBinding: agentBindingName,
1248
+ __workflowName: workflowName
1249
+ };
1250
+ const instance = await workflow.create({
1251
+ id: workflowId,
1252
+ params: augmentedParams
1253
+ });
1254
+ const id = nanoid();
1255
+ const metadataJson = options?.metadata ? JSON.stringify(options.metadata) : null;
1256
+ try {
1257
+ this.sql`
1258
+ INSERT INTO cf_agents_workflows (id, workflow_id, workflow_name, status, metadata)
1259
+ VALUES (${id}, ${instance.id}, ${workflowName}, 'queued', ${metadataJson})
1260
+ `;
1261
+ } catch (e) {
1262
+ if (e instanceof Error && e.message.includes("UNIQUE constraint failed")) throw new Error(`Workflow with ID "${workflowId}" is already being tracked`);
1263
+ throw e;
1264
+ }
1265
+ this.observability?.emit({
1266
+ displayMessage: `Workflow ${instance.id} started`,
1267
+ id: nanoid(),
1268
+ payload: {
1269
+ workflowId: instance.id,
1270
+ workflowName
1271
+ },
1272
+ timestamp: Date.now(),
1273
+ type: "workflow:start"
1274
+ }, this.ctx);
1275
+ return instance.id;
1276
+ }
1277
+ /**
1278
+ * Send an event to a running workflow.
1279
+ * The workflow can wait for this event using step.waitForEvent().
1280
+ *
1281
+ * @param workflowName - Name of the workflow binding in env (e.g., 'MY_WORKFLOW')
1282
+ * @param workflowId - ID of the workflow instance
1283
+ * @param event - Event to send
1284
+ *
1285
+ * @example
1286
+ * ```typescript
1287
+ * await this.sendWorkflowEvent(
1288
+ * 'MY_WORKFLOW',
1289
+ * workflowId,
1290
+ * { type: 'approval', payload: { approved: true } }
1291
+ * );
1292
+ * ```
1293
+ */
1294
+ async sendWorkflowEvent(workflowName, workflowId, event) {
1295
+ const workflow = this._findWorkflowBindingByName(workflowName);
1296
+ if (!workflow) throw new Error(`Workflow binding '${workflowName}' not found in environment`);
1297
+ await (await workflow.get(workflowId)).sendEvent(event);
1298
+ this.observability?.emit({
1299
+ displayMessage: `Event sent to workflow ${workflowId}`,
1300
+ id: nanoid(),
1301
+ payload: {
1302
+ workflowId,
1303
+ eventType: event.type
1304
+ },
1305
+ timestamp: Date.now(),
1306
+ type: "workflow:event"
1307
+ }, this.ctx);
1308
+ }
1309
+ /**
1310
+ * Approve a waiting workflow.
1311
+ * Sends an approval event to the workflow that can be received by waitForApproval().
1312
+ *
1313
+ * @param workflowId - ID of the workflow to approve
1314
+ * @param data - Optional approval data (reason, metadata)
1315
+ *
1316
+ * @example
1317
+ * ```typescript
1318
+ * await this.approveWorkflow(workflowId, {
1319
+ * reason: 'Approved by admin',
1320
+ * metadata: { approvedBy: userId }
1321
+ * });
1322
+ * ```
1323
+ */
1324
+ async approveWorkflow(workflowId, data) {
1325
+ const workflowInfo = this.getWorkflow(workflowId);
1326
+ if (!workflowInfo) throw new Error(`Workflow ${workflowId} not found in tracking table`);
1327
+ await this.sendWorkflowEvent(workflowInfo.workflowName, workflowId, {
1328
+ type: "approval",
1329
+ payload: {
1330
+ approved: true,
1331
+ reason: data?.reason,
1332
+ metadata: data?.metadata
1333
+ }
1334
+ });
1335
+ this.observability?.emit({
1336
+ displayMessage: `Workflow ${workflowId} approved`,
1337
+ id: nanoid(),
1338
+ payload: {
1339
+ workflowId,
1340
+ reason: data?.reason
1341
+ },
1342
+ timestamp: Date.now(),
1343
+ type: "workflow:approved"
1344
+ }, this.ctx);
1345
+ }
1346
+ /**
1347
+ * Reject a waiting workflow.
1348
+ * Sends a rejection event to the workflow that will cause waitForApproval() to throw.
1349
+ *
1350
+ * @param workflowId - ID of the workflow to reject
1351
+ * @param data - Optional rejection data (reason)
1352
+ *
1353
+ * @example
1354
+ * ```typescript
1355
+ * await this.rejectWorkflow(workflowId, {
1356
+ * reason: 'Request denied by admin'
1357
+ * });
1358
+ * ```
1359
+ */
1360
+ async rejectWorkflow(workflowId, data) {
1361
+ const workflowInfo = this.getWorkflow(workflowId);
1362
+ if (!workflowInfo) throw new Error(`Workflow ${workflowId} not found in tracking table`);
1363
+ await this.sendWorkflowEvent(workflowInfo.workflowName, workflowId, {
1364
+ type: "approval",
1365
+ payload: {
1366
+ approved: false,
1367
+ reason: data?.reason
1368
+ }
1369
+ });
1370
+ this.observability?.emit({
1371
+ displayMessage: `Workflow ${workflowId} rejected`,
1372
+ id: nanoid(),
1373
+ payload: {
1374
+ workflowId,
1375
+ reason: data?.reason
1376
+ },
1377
+ timestamp: Date.now(),
1378
+ type: "workflow:rejected"
1379
+ }, this.ctx);
1380
+ }
1381
+ /**
1382
+ * Terminate a running workflow.
1383
+ * This immediately stops the workflow and sets its status to "terminated".
1384
+ *
1385
+ * @param workflowId - ID of the workflow to terminate (must be tracked via runWorkflow)
1386
+ * @throws Error if workflow not found in tracking table
1387
+ * @throws Error if workflow binding not found in environment
1388
+ * @throws Error if workflow is already completed/errored/terminated (from Cloudflare)
1389
+ *
1390
+ * @note `terminate()` is not yet supported in local development (wrangler dev).
1391
+ * It will throw an error locally but works when deployed to Cloudflare.
1392
+ *
1393
+ * @example
1394
+ * ```typescript
1395
+ * await this.terminateWorkflow(workflowId);
1396
+ * ```
1397
+ */
1398
+ async terminateWorkflow(workflowId) {
1399
+ const workflowInfo = this.getWorkflow(workflowId);
1400
+ if (!workflowInfo) throw new Error(`Workflow ${workflowId} not found in tracking table`);
1401
+ const workflow = this._findWorkflowBindingByName(workflowInfo.workflowName);
1402
+ if (!workflow) throw new Error(`Workflow binding '${workflowInfo.workflowName}' not found in environment`);
1403
+ const instance = await workflow.get(workflowId);
1404
+ try {
1405
+ await instance.terminate();
1406
+ } catch (err) {
1407
+ if (err instanceof Error && err.message.includes("Not implemented")) throw new Error("terminateWorkflow() is not supported in local development. Deploy to Cloudflare to use this feature. Follow https://github.com/cloudflare/agents/issues/823 for details and updates.");
1408
+ throw err;
1409
+ }
1410
+ const status = await instance.status();
1411
+ this._updateWorkflowTracking(workflowId, status);
1412
+ this.observability?.emit({
1413
+ displayMessage: `Workflow ${workflowId} terminated`,
1414
+ id: nanoid(),
1415
+ payload: {
1416
+ workflowId,
1417
+ workflowName: workflowInfo.workflowName
1418
+ },
1419
+ timestamp: Date.now(),
1420
+ type: "workflow:terminated"
1421
+ }, this.ctx);
1422
+ }
1423
+ /**
1424
+ * Pause a running workflow.
1425
+ * The workflow can be resumed later with resumeWorkflow().
1426
+ *
1427
+ * @param workflowId - ID of the workflow to pause (must be tracked via runWorkflow)
1428
+ * @throws Error if workflow not found in tracking table
1429
+ * @throws Error if workflow binding not found in environment
1430
+ * @throws Error if workflow is not running (from Cloudflare)
1431
+ *
1432
+ * @note `pause()` is not yet supported in local development (wrangler dev).
1433
+ * It will throw an error locally but works when deployed to Cloudflare.
1434
+ *
1435
+ * @example
1436
+ * ```typescript
1437
+ * await this.pauseWorkflow(workflowId);
1438
+ * ```
1439
+ */
1440
+ async pauseWorkflow(workflowId) {
1441
+ const workflowInfo = this.getWorkflow(workflowId);
1442
+ if (!workflowInfo) throw new Error(`Workflow ${workflowId} not found in tracking table`);
1443
+ const workflow = this._findWorkflowBindingByName(workflowInfo.workflowName);
1444
+ if (!workflow) throw new Error(`Workflow binding '${workflowInfo.workflowName}' not found in environment`);
1445
+ const instance = await workflow.get(workflowId);
1446
+ try {
1447
+ await instance.pause();
1448
+ } catch (err) {
1449
+ if (err instanceof Error && err.message.includes("Not implemented")) throw new Error("pauseWorkflow() is not supported in local development. Deploy to Cloudflare to use this feature. Follow https://github.com/cloudflare/agents/issues/823 for details and updates.");
1450
+ throw err;
1451
+ }
1452
+ const status = await instance.status();
1453
+ this._updateWorkflowTracking(workflowId, status);
1454
+ this.observability?.emit({
1455
+ displayMessage: `Workflow ${workflowId} paused`,
1456
+ id: nanoid(),
1457
+ payload: {
1458
+ workflowId,
1459
+ workflowName: workflowInfo.workflowName
1460
+ },
1461
+ timestamp: Date.now(),
1462
+ type: "workflow:paused"
1463
+ }, this.ctx);
1464
+ }
1465
+ /**
1466
+ * Resume a paused workflow.
1467
+ *
1468
+ * @param workflowId - ID of the workflow to resume (must be tracked via runWorkflow)
1469
+ * @throws Error if workflow not found in tracking table
1470
+ * @throws Error if workflow binding not found in environment
1471
+ * @throws Error if workflow is not paused (from Cloudflare)
1472
+ *
1473
+ * @note `resume()` is not yet supported in local development (wrangler dev).
1474
+ * It will throw an error locally but works when deployed to Cloudflare.
1475
+ *
1476
+ * @example
1477
+ * ```typescript
1478
+ * await this.resumeWorkflow(workflowId);
1479
+ * ```
1480
+ */
1481
+ async resumeWorkflow(workflowId) {
1482
+ const workflowInfo = this.getWorkflow(workflowId);
1483
+ if (!workflowInfo) throw new Error(`Workflow ${workflowId} not found in tracking table`);
1484
+ const workflow = this._findWorkflowBindingByName(workflowInfo.workflowName);
1485
+ if (!workflow) throw new Error(`Workflow binding '${workflowInfo.workflowName}' not found in environment`);
1486
+ const instance = await workflow.get(workflowId);
1487
+ try {
1488
+ await instance.resume();
1489
+ } catch (err) {
1490
+ if (err instanceof Error && err.message.includes("Not implemented")) throw new Error("resumeWorkflow() is not supported in local development. Deploy to Cloudflare to use this feature. Follow https://github.com/cloudflare/agents/issues/823 for details and updates.");
1491
+ throw err;
1492
+ }
1493
+ const status = await instance.status();
1494
+ this._updateWorkflowTracking(workflowId, status);
1495
+ this.observability?.emit({
1496
+ displayMessage: `Workflow ${workflowId} resumed`,
1497
+ id: nanoid(),
1498
+ payload: {
1499
+ workflowId,
1500
+ workflowName: workflowInfo.workflowName
1501
+ },
1502
+ timestamp: Date.now(),
1503
+ type: "workflow:resumed"
1504
+ }, this.ctx);
1505
+ }
1506
+ /**
1507
+ * Restart a workflow instance.
1508
+ * This re-runs the workflow from the beginning with the same ID.
1509
+ *
1510
+ * @param workflowId - ID of the workflow to restart (must be tracked via runWorkflow)
1511
+ * @param options - Optional settings
1512
+ * @param options.resetTracking - If true (default), resets created_at and clears error fields.
1513
+ * If false, preserves original timestamps.
1514
+ * @throws Error if workflow not found in tracking table
1515
+ * @throws Error if workflow binding not found in environment
1516
+ *
1517
+ * @note `restart()` is not yet supported in local development (wrangler dev).
1518
+ * It will throw an error locally but works when deployed to Cloudflare.
1519
+ *
1520
+ * @example
1521
+ * ```typescript
1522
+ * // Reset tracking (default)
1523
+ * await this.restartWorkflow(workflowId);
1524
+ *
1525
+ * // Preserve original timestamps
1526
+ * await this.restartWorkflow(workflowId, { resetTracking: false });
1527
+ * ```
1528
+ */
1529
+ async restartWorkflow(workflowId, options = {}) {
1530
+ const { resetTracking = true } = options;
1531
+ const workflowInfo = this.getWorkflow(workflowId);
1532
+ if (!workflowInfo) throw new Error(`Workflow ${workflowId} not found in tracking table`);
1533
+ const workflow = this._findWorkflowBindingByName(workflowInfo.workflowName);
1534
+ if (!workflow) throw new Error(`Workflow binding '${workflowInfo.workflowName}' not found in environment`);
1535
+ const instance = await workflow.get(workflowId);
1536
+ try {
1537
+ await instance.restart();
1538
+ } catch (err) {
1539
+ if (err instanceof Error && err.message.includes("Not implemented")) throw new Error("restartWorkflow() is not supported in local development. Deploy to Cloudflare to use this feature. Follow https://github.com/cloudflare/agents/issues/823 for details and updates.");
1540
+ throw err;
1541
+ }
1542
+ if (resetTracking) {
1543
+ const now = Math.floor(Date.now() / 1e3);
1544
+ this.sql`
1545
+ UPDATE cf_agents_workflows
1546
+ SET status = 'queued',
1547
+ created_at = ${now},
1548
+ updated_at = ${now},
1549
+ completed_at = NULL,
1550
+ error_name = NULL,
1551
+ error_message = NULL
1552
+ WHERE workflow_id = ${workflowId}
1553
+ `;
1554
+ } else {
1555
+ const status = await instance.status();
1556
+ this._updateWorkflowTracking(workflowId, status);
1557
+ }
1558
+ this.observability?.emit({
1559
+ displayMessage: `Workflow ${workflowId} restarted`,
1560
+ id: nanoid(),
1561
+ payload: {
1562
+ workflowId,
1563
+ workflowName: workflowInfo.workflowName
1564
+ },
1565
+ timestamp: Date.now(),
1566
+ type: "workflow:restarted"
1567
+ }, this.ctx);
1568
+ }
1569
+ /**
1570
+ * Find a workflow binding by its name.
1571
+ */
1572
+ _findWorkflowBindingByName(workflowName) {
1573
+ const binding = this.env[workflowName];
1574
+ if (binding && typeof binding === "object" && "create" in binding && "get" in binding) return binding;
1575
+ }
1576
+ /**
1577
+ * Get all workflow binding names from the environment.
1578
+ */
1579
+ _getWorkflowBindingNames() {
1580
+ const names = [];
1581
+ for (const [key, value] of Object.entries(this.env)) if (value && typeof value === "object" && "create" in value && "get" in value) names.push(key);
1582
+ return names;
1583
+ }
1584
+ /**
1585
+ * Get the status of a workflow and update the tracking record.
1586
+ *
1587
+ * @param workflowName - Name of the workflow binding in env (e.g., 'MY_WORKFLOW')
1588
+ * @param workflowId - ID of the workflow instance
1589
+ * @returns The workflow status
1590
+ */
1591
+ async getWorkflowStatus(workflowName, workflowId) {
1592
+ const workflow = this._findWorkflowBindingByName(workflowName);
1593
+ if (!workflow) throw new Error(`Workflow binding '${workflowName}' not found in environment`);
1594
+ const status = await (await workflow.get(workflowId)).status();
1595
+ this._updateWorkflowTracking(workflowId, status);
1596
+ return status;
1597
+ }
1598
+ /**
1599
+ * Get a tracked workflow by ID.
1600
+ *
1601
+ * @param workflowId - Workflow instance ID
1602
+ * @returns Workflow info or undefined if not found
1603
+ */
1604
+ getWorkflow(workflowId) {
1605
+ const rows = this.sql`
1606
+ SELECT * FROM cf_agents_workflows WHERE workflow_id = ${workflowId}
1607
+ `;
1608
+ if (!rows || rows.length === 0) return;
1609
+ return this._rowToWorkflowInfo(rows[0]);
1610
+ }
1611
+ /**
1612
+ * Query tracked workflows with cursor-based pagination.
1613
+ *
1614
+ * @param criteria - Query criteria including optional cursor for pagination
1615
+ * @returns WorkflowPage with workflows, total count, and next cursor
1616
+ *
1617
+ * @example
1618
+ * ```typescript
1619
+ * // First page
1620
+ * const page1 = this.getWorkflows({ status: 'running', limit: 20 });
1621
+ *
1622
+ * // Next page
1623
+ * if (page1.nextCursor) {
1624
+ * const page2 = this.getWorkflows({
1625
+ * status: 'running',
1626
+ * limit: 20,
1627
+ * cursor: page1.nextCursor
1628
+ * });
1629
+ * }
1630
+ * ```
1631
+ */
1632
+ getWorkflows(criteria = {}) {
1633
+ const limit = Math.min(criteria.limit ?? 50, 100);
1634
+ const isAsc = criteria.orderBy === "asc";
1635
+ const total = this._countWorkflows(criteria);
1636
+ let query = "SELECT * FROM cf_agents_workflows WHERE 1=1";
1637
+ const params = [];
1638
+ if (criteria.status) {
1639
+ const statuses = Array.isArray(criteria.status) ? criteria.status : [criteria.status];
1640
+ const placeholders = statuses.map(() => "?").join(", ");
1641
+ query += ` AND status IN (${placeholders})`;
1642
+ params.push(...statuses);
1643
+ }
1644
+ if (criteria.workflowName) {
1645
+ query += " AND workflow_name = ?";
1646
+ params.push(criteria.workflowName);
1647
+ }
1648
+ if (criteria.metadata) for (const [key, value] of Object.entries(criteria.metadata)) {
1649
+ query += ` AND json_extract(metadata, '$.' || ?) = ?`;
1650
+ params.push(key, value);
1651
+ }
1652
+ if (criteria.cursor) {
1653
+ const cursor = this._decodeCursor(criteria.cursor);
1654
+ if (isAsc) query += " AND (created_at > ? OR (created_at = ? AND workflow_id > ?))";
1655
+ else query += " AND (created_at < ? OR (created_at = ? AND workflow_id < ?))";
1656
+ params.push(cursor.createdAt, cursor.createdAt, cursor.workflowId);
1657
+ }
1658
+ query += ` ORDER BY created_at ${isAsc ? "ASC" : "DESC"}, workflow_id ${isAsc ? "ASC" : "DESC"}`;
1659
+ query += " LIMIT ?";
1660
+ params.push(limit + 1);
1661
+ const rows = this.ctx.storage.sql.exec(query, ...params).toArray();
1662
+ const hasMore = rows.length > limit;
1663
+ const workflows = (hasMore ? rows.slice(0, limit) : rows).map((row) => this._rowToWorkflowInfo(row));
1664
+ return {
1665
+ workflows,
1666
+ total,
1667
+ nextCursor: hasMore && workflows.length > 0 ? this._encodeCursor(workflows[workflows.length - 1]) : null
1668
+ };
1669
+ }
1670
+ /**
1671
+ * Count workflows matching criteria (for pagination total).
1672
+ */
1673
+ _countWorkflows(criteria) {
1674
+ let query = "SELECT COUNT(*) as count FROM cf_agents_workflows WHERE 1=1";
1675
+ const params = [];
1676
+ if (criteria.status) {
1677
+ const statuses = Array.isArray(criteria.status) ? criteria.status : [criteria.status];
1678
+ const placeholders = statuses.map(() => "?").join(", ");
1679
+ query += ` AND status IN (${placeholders})`;
1680
+ params.push(...statuses);
1681
+ }
1682
+ if (criteria.workflowName) {
1683
+ query += " AND workflow_name = ?";
1684
+ params.push(criteria.workflowName);
1685
+ }
1686
+ if (criteria.metadata) for (const [key, value] of Object.entries(criteria.metadata)) {
1687
+ query += ` AND json_extract(metadata, '$.' || ?) = ?`;
1688
+ params.push(key, value);
1689
+ }
1690
+ if (criteria.createdBefore) {
1691
+ query += " AND created_at < ?";
1692
+ params.push(Math.floor(criteria.createdBefore.getTime() / 1e3));
1693
+ }
1694
+ return this.ctx.storage.sql.exec(query, ...params).toArray()[0]?.count ?? 0;
1695
+ }
1696
+ /**
1697
+ * Encode a cursor from workflow info for pagination.
1698
+ * Stores createdAt as Unix timestamp in seconds (matching DB storage).
1699
+ */
1700
+ _encodeCursor(workflow) {
1701
+ return btoa(JSON.stringify({
1702
+ c: Math.floor(workflow.createdAt.getTime() / 1e3),
1703
+ i: workflow.workflowId
1704
+ }));
1705
+ }
1706
+ /**
1707
+ * Decode a pagination cursor.
1708
+ * Returns createdAt as Unix timestamp in seconds (matching DB storage).
1709
+ */
1710
+ _decodeCursor(cursor) {
1711
+ try {
1712
+ const data = JSON.parse(atob(cursor));
1713
+ if (typeof data.c !== "number" || typeof data.i !== "string") throw new Error("Invalid cursor structure");
1714
+ return {
1715
+ createdAt: data.c,
1716
+ workflowId: data.i
1717
+ };
1718
+ } catch {
1719
+ throw new Error("Invalid pagination cursor. The cursor may be malformed or corrupted.");
1720
+ }
1721
+ }
1722
+ /**
1723
+ * Delete a workflow tracking record.
1724
+ *
1725
+ * @param workflowId - ID of the workflow to delete
1726
+ * @returns true if a record was deleted, false if not found
1727
+ */
1728
+ deleteWorkflow(workflowId) {
1729
+ const existing = this.sql`
1730
+ SELECT COUNT(*) as count FROM cf_agents_workflows WHERE workflow_id = ${workflowId}
1731
+ `;
1732
+ if (!existing[0] || existing[0].count === 0) return false;
1733
+ this.sql`DELETE FROM cf_agents_workflows WHERE workflow_id = ${workflowId}`;
1734
+ return true;
1735
+ }
1736
+ /**
1737
+ * Delete workflow tracking records matching criteria.
1738
+ * Useful for cleaning up old completed/errored workflows.
1739
+ *
1740
+ * @param criteria - Criteria for which workflows to delete
1741
+ * @returns Number of records matching criteria (expected deleted count)
1742
+ *
1743
+ * @example
1744
+ * ```typescript
1745
+ * // Delete all completed workflows created more than 7 days ago
1746
+ * const deleted = this.deleteWorkflows({
1747
+ * status: 'complete',
1748
+ * createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
1749
+ * });
1750
+ *
1751
+ * // Delete all errored and terminated workflows
1752
+ * const deleted = this.deleteWorkflows({
1753
+ * status: ['errored', 'terminated']
1754
+ * });
1755
+ * ```
1756
+ */
1757
+ deleteWorkflows(criteria = {}) {
1758
+ let query = "DELETE FROM cf_agents_workflows WHERE 1=1";
1759
+ const params = [];
1760
+ if (criteria.status) {
1761
+ const statuses = Array.isArray(criteria.status) ? criteria.status : [criteria.status];
1762
+ const placeholders = statuses.map(() => "?").join(", ");
1763
+ query += ` AND status IN (${placeholders})`;
1764
+ params.push(...statuses);
1765
+ }
1766
+ if (criteria.workflowName) {
1767
+ query += " AND workflow_name = ?";
1768
+ params.push(criteria.workflowName);
1769
+ }
1770
+ if (criteria.metadata) for (const [key, value] of Object.entries(criteria.metadata)) {
1771
+ query += ` AND json_extract(metadata, '$.' || ?) = ?`;
1772
+ params.push(key, value);
1773
+ }
1774
+ if (criteria.createdBefore) {
1775
+ query += " AND created_at < ?";
1776
+ params.push(Math.floor(criteria.createdBefore.getTime() / 1e3));
1777
+ }
1778
+ return this.ctx.storage.sql.exec(query, ...params).rowsWritten;
1779
+ }
1780
+ /**
1781
+ * Migrate workflow tracking records from an old binding name to a new one.
1782
+ * Use this after renaming a workflow binding in wrangler.toml.
1783
+ *
1784
+ * @param oldName - Previous workflow binding name
1785
+ * @param newName - New workflow binding name
1786
+ * @returns Number of records migrated
1787
+ *
1788
+ * @example
1789
+ * ```typescript
1790
+ * // After renaming OLD_WORKFLOW to NEW_WORKFLOW in wrangler.toml
1791
+ * async onStart() {
1792
+ * const migrated = this.migrateWorkflowBinding('OLD_WORKFLOW', 'NEW_WORKFLOW');
1793
+ * }
1794
+ * ```
1795
+ */
1796
+ migrateWorkflowBinding(oldName, newName) {
1797
+ if (!this._findWorkflowBindingByName(newName)) throw new Error(`Workflow binding '${newName}' not found in environment`);
1798
+ const count = this.sql`
1799
+ SELECT COUNT(*) as count FROM cf_agents_workflows WHERE workflow_name = ${oldName}
1800
+ `[0]?.count ?? 0;
1801
+ if (count > 0) {
1802
+ this.sql`UPDATE cf_agents_workflows SET workflow_name = ${newName} WHERE workflow_name = ${oldName}`;
1803
+ console.log(`[Agent] Migrated ${count} workflow(s) from '${oldName}' to '${newName}'`);
1804
+ }
1805
+ return count;
1806
+ }
1807
+ /**
1808
+ * Update workflow tracking record from InstanceStatus
1809
+ */
1810
+ _updateWorkflowTracking(workflowId, status) {
1811
+ const statusName = status.status;
1812
+ const now = Math.floor(Date.now() / 1e3);
1813
+ const completedAt = [
1814
+ "complete",
1815
+ "errored",
1816
+ "terminated"
1817
+ ].includes(statusName) ? now : null;
1818
+ const errorName = status.error?.name ?? null;
1819
+ const errorMessage = status.error?.message ?? null;
1820
+ this.sql`
1821
+ UPDATE cf_agents_workflows
1822
+ SET status = ${statusName},
1823
+ error_name = ${errorName},
1824
+ error_message = ${errorMessage},
1825
+ updated_at = ${now},
1826
+ completed_at = ${completedAt}
1827
+ WHERE workflow_id = ${workflowId}
1828
+ `;
1829
+ }
1830
+ /**
1831
+ * Convert a database row to WorkflowInfo
1832
+ */
1833
+ _rowToWorkflowInfo(row) {
1834
+ return {
1835
+ id: row.id,
1836
+ workflowId: row.workflow_id,
1837
+ workflowName: row.workflow_name,
1838
+ status: row.status,
1839
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
1840
+ error: row.error_name ? {
1841
+ name: row.error_name,
1842
+ message: row.error_message ?? ""
1843
+ } : null,
1844
+ createdAt: /* @__PURE__ */ new Date(row.created_at * 1e3),
1845
+ updatedAt: /* @__PURE__ */ new Date(row.updated_at * 1e3),
1846
+ completedAt: row.completed_at ? /* @__PURE__ */ new Date(row.completed_at * 1e3) : null
1847
+ };
1848
+ }
1849
+ /**
1850
+ * Find the binding name for this Agent's namespace by matching class name.
1851
+ * Returns undefined if no match found - use options.agentBinding as fallback.
1852
+ */
1853
+ _findAgentBindingName() {
1854
+ const className = this._ParentClass.name;
1855
+ for (const [key, value] of Object.entries(this.env)) if (value && typeof value === "object" && "idFromName" in value && typeof value.idFromName === "function") {
1856
+ if (key === className || camelCaseToKebabCase(key) === camelCaseToKebabCase(className)) return key;
1857
+ }
1858
+ }
1859
+ /**
1860
+ * Handle a callback from a workflow.
1861
+ * Called when the Agent receives a callback at /_workflow/callback.
1862
+ * Override this to handle all callback types in one place.
1863
+ *
1864
+ * @param callback - The callback payload
1865
+ */
1866
+ async onWorkflowCallback(callback) {
1867
+ const now = Math.floor(Date.now() / 1e3);
1868
+ switch (callback.type) {
1869
+ case "progress":
1870
+ this.sql`
1871
+ UPDATE cf_agents_workflows
1872
+ SET status = 'running', updated_at = ${now}
1873
+ WHERE workflow_id = ${callback.workflowId} AND status IN ('queued', 'waiting')
1874
+ `;
1875
+ await this.onWorkflowProgress(callback.workflowName, callback.workflowId, callback.progress);
1876
+ break;
1877
+ case "complete":
1878
+ this.sql`
1879
+ UPDATE cf_agents_workflows
1880
+ SET status = 'complete', updated_at = ${now}, completed_at = ${now}
1881
+ WHERE workflow_id = ${callback.workflowId}
1882
+ AND status NOT IN ('terminated', 'paused')
1883
+ `;
1884
+ await this.onWorkflowComplete(callback.workflowName, callback.workflowId, callback.result);
1885
+ break;
1886
+ case "error":
1887
+ this.sql`
1888
+ UPDATE cf_agents_workflows
1889
+ SET status = 'errored', updated_at = ${now}, completed_at = ${now},
1890
+ error_name = 'WorkflowError', error_message = ${callback.error}
1891
+ WHERE workflow_id = ${callback.workflowId}
1892
+ AND status NOT IN ('terminated', 'paused')
1893
+ `;
1894
+ await this.onWorkflowError(callback.workflowName, callback.workflowId, callback.error);
1895
+ break;
1896
+ case "event":
1897
+ await this.onWorkflowEvent(callback.workflowName, callback.workflowId, callback.event);
1898
+ break;
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Called when a workflow reports progress.
1903
+ * Override to handle progress updates.
1904
+ *
1905
+ * @param workflowName - Workflow binding name
1906
+ * @param workflowId - ID of the workflow
1907
+ * @param progress - Typed progress data (default: DefaultProgress)
1908
+ */
1909
+ async onWorkflowProgress(_workflowName, _workflowId, _progress) {}
1910
+ /**
1911
+ * Called when a workflow completes successfully.
1912
+ * Override to handle completion.
1913
+ *
1914
+ * @param workflowName - Workflow binding name
1915
+ * @param workflowId - ID of the workflow
1916
+ * @param result - Optional result data
1917
+ */
1918
+ async onWorkflowComplete(_workflowName, _workflowId, _result) {}
1919
+ /**
1920
+ * Called when a workflow encounters an error.
1921
+ * Override to handle errors.
1922
+ *
1923
+ * @param workflowName - Workflow binding name
1924
+ * @param workflowId - ID of the workflow
1925
+ * @param error - Error message
1926
+ */
1927
+ async onWorkflowError(_workflowName, _workflowId, _error) {}
1928
+ /**
1929
+ * Called when a workflow sends a custom event.
1930
+ * Override to handle custom events.
1931
+ *
1932
+ * @param workflowName - Workflow binding name
1933
+ * @param workflowId - ID of the workflow
1934
+ * @param event - Custom event payload
1935
+ */
1936
+ async onWorkflowEvent(_workflowName, _workflowId, _event) {}
1937
+ /**
1938
+ * Handle a workflow callback via RPC.
1939
+ * @internal - Called by AgentWorkflow, do not call directly
1940
+ */
1941
+ async _workflow_handleCallback(callback) {
1942
+ await this.onWorkflowCallback(callback);
1943
+ }
1944
+ /**
1945
+ * Broadcast a message to all connected clients via RPC.
1946
+ * @internal - Called by AgentWorkflow, do not call directly
1947
+ */
1948
+ _workflow_broadcast(message) {
1949
+ this.broadcast(JSON.stringify(message));
1950
+ }
1951
+ /**
1952
+ * Update agent state via RPC.
1953
+ * @internal - Called by AgentWorkflow, do not call directly
1954
+ */
1955
+ _workflow_updateState(action, state) {
1956
+ if (action === "set") this.setState(state);
1957
+ else if (action === "merge") {
1958
+ const currentState = this.state ?? {};
1959
+ this.setState({
1960
+ ...currentState,
1961
+ ...state
1962
+ });
1963
+ } else if (action === "reset") this.setState(this.initialState);
1964
+ }
1965
+ /**
1966
+ * Connect to a new MCP Server
1967
+ *
1968
+ * @example
1969
+ * // Simple usage
1970
+ * await this.addMcpServer("github", "https://mcp.github.com");
1971
+ *
1972
+ * @example
1973
+ * // With options (preferred for custom headers, transport, etc.)
1974
+ * await this.addMcpServer("github", "https://mcp.github.com", {
1975
+ * transport: { headers: { "Authorization": "Bearer ..." } }
1976
+ * });
1977
+ *
1978
+ * @example
1979
+ * // Legacy 5-parameter signature (still supported)
1980
+ * await this.addMcpServer("github", url, callbackHost, agentsPrefix, options);
1981
+ *
1982
+ * @param serverName Name of the MCP server
1983
+ * @param url MCP Server URL
1984
+ * @param callbackHostOrOptions Options object, or callback host string (legacy)
1985
+ * @param agentsPrefix agents routing prefix if not using `agents` (legacy)
1986
+ * @param options MCP client and transport options (legacy)
1987
+ * @returns Server id and state - either "authenticating" with authUrl, or "ready"
1988
+ * @throws If connection or discovery fails
1989
+ */
1990
+ async addMcpServer(serverName, url, callbackHostOrOptions, agentsPrefix, options) {
1991
+ let resolvedCallbackHost;
1992
+ let resolvedAgentsPrefix;
1993
+ let resolvedOptions;
1994
+ let resolvedCallbackPath;
1995
+ if (typeof callbackHostOrOptions === "object" && callbackHostOrOptions !== null) {
1996
+ resolvedCallbackHost = callbackHostOrOptions.callbackHost;
1997
+ resolvedCallbackPath = callbackHostOrOptions.callbackPath;
1998
+ resolvedAgentsPrefix = callbackHostOrOptions.agentsPrefix ?? "agents";
1999
+ resolvedOptions = {
2000
+ client: callbackHostOrOptions.client,
2001
+ transport: callbackHostOrOptions.transport
2002
+ };
2003
+ } else {
2004
+ resolvedCallbackHost = callbackHostOrOptions;
2005
+ resolvedAgentsPrefix = agentsPrefix ?? "agents";
2006
+ resolvedOptions = options;
2007
+ }
2008
+ if (!this._resolvedOptions.sendIdentityOnConnect && !resolvedCallbackPath) throw new Error("callbackPath is required in addMcpServer options when sendIdentityOnConnect is false — the default callback URL would expose the instance name. Provide a callbackPath and route the callback request to this agent via getAgentByName.");
2009
+ if (!resolvedCallbackHost) {
2010
+ const { request } = getCurrentAgent();
2011
+ if (!request) throw new Error("callbackHost is required when not called within a request context");
2012
+ const requestUrl = new URL(request.url);
2013
+ resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
2014
+ }
2015
+ const normalizedHost = resolvedCallbackHost.replace(/\/$/, "");
2016
+ const callbackUrl = resolvedCallbackPath ? `${normalizedHost}/${resolvedCallbackPath.replace(/^\//, "")}` : `${normalizedHost}/${resolvedAgentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
2017
+ await this.mcp.ensureJsonSchema();
2018
+ const id = nanoid(8);
2019
+ const authProvider = this.createMcpOAuthProvider(callbackUrl);
2020
+ authProvider.serverId = id;
2021
+ const transportType = resolvedOptions?.transport?.type ?? "auto";
2022
+ let headerTransportOpts = {};
2023
+ if (resolvedOptions?.transport?.headers) headerTransportOpts = {
2024
+ eventSourceInit: { fetch: (url, init) => fetch(url, {
2025
+ ...init,
2026
+ headers: resolvedOptions?.transport?.headers
2027
+ }) },
2028
+ requestInit: { headers: resolvedOptions?.transport?.headers }
2029
+ };
2030
+ await this.mcp.registerServer(id, {
2031
+ url,
2032
+ name: serverName,
2033
+ callbackUrl,
2034
+ client: resolvedOptions?.client,
2035
+ transport: {
2036
+ ...headerTransportOpts,
2037
+ authProvider,
2038
+ type: transportType
2039
+ }
2040
+ });
2041
+ const result = await this.mcp.connectToServer(id);
2042
+ if (result.state === MCPConnectionState.FAILED) throw new Error(`Failed to connect to MCP server at ${url}: ${result.error}`);
2043
+ if (result.state === MCPConnectionState.AUTHENTICATING) return {
2044
+ id,
2045
+ state: result.state,
2046
+ authUrl: result.authUrl
2047
+ };
2048
+ const discoverResult = await this.mcp.discoverIfConnected(id);
2049
+ if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover MCP server capabilities: ${discoverResult.error}`);
2050
+ return {
2051
+ id,
2052
+ state: MCPConnectionState.READY
2053
+ };
2054
+ }
2055
+ async removeMcpServer(id) {
2056
+ await this.mcp.removeServer(id);
2057
+ }
2058
+ getMcpServers() {
2059
+ const mcpState = {
2060
+ prompts: this.mcp.listPrompts(),
2061
+ resources: this.mcp.listResources(),
2062
+ servers: {},
2063
+ tools: this.mcp.listTools()
2064
+ };
2065
+ const servers = this.mcp.listServers();
2066
+ if (servers && Array.isArray(servers) && servers.length > 0) for (const server of servers) {
2067
+ const serverConn = this.mcp.mcpConnections[server.id];
2068
+ let defaultState = "not-connected";
2069
+ if (!serverConn && server.auth_url) defaultState = "authenticating";
2070
+ mcpState.servers[server.id] = {
2071
+ auth_url: server.auth_url,
2072
+ capabilities: serverConn?.serverCapabilities ?? null,
2073
+ error: serverConn?.connectionError ?? null,
2074
+ instructions: serverConn?.instructions ?? null,
2075
+ name: server.name,
2076
+ server_url: server.server_url,
2077
+ state: serverConn?.connectionState ?? defaultState
2078
+ };
2079
+ }
2080
+ return mcpState;
2081
+ }
2082
+ /**
2083
+ * Create the OAuth provider used when connecting to MCP servers that require authentication.
2084
+ *
2085
+ * Override this method in a subclass to supply a custom OAuth provider implementation,
2086
+ * for example to use pre-registered client credentials, mTLS-based authentication,
2087
+ * or any other OAuth flow beyond dynamic client registration.
2088
+ *
2089
+ * @example
2090
+ * // Custom OAuth provider
2091
+ * class MyAgent extends Agent {
2092
+ * createMcpOAuthProvider(callbackUrl: string): AgentMcpOAuthProvider {
2093
+ * return new MyCustomOAuthProvider(
2094
+ * this.ctx.storage,
2095
+ * this.name,
2096
+ * callbackUrl
2097
+ * );
2098
+ * }
2099
+ * }
2100
+ *
2101
+ * @param callbackUrl The OAuth callback URL for the authorization flow
2102
+ * @returns An {@link AgentMcpOAuthProvider} instance used by {@link addMcpServer}
2103
+ */
2104
+ createMcpOAuthProvider(callbackUrl) {
2105
+ return new DurableObjectOAuthClientProvider(this.ctx.storage, this.name, callbackUrl);
2106
+ }
2107
+ broadcastMcpServers() {
2108
+ this.broadcast(JSON.stringify({
2109
+ mcp: this.getMcpServers(),
2110
+ type: MessageType.CF_AGENT_MCP_SERVERS
2111
+ }));
2112
+ }
2113
+ /**
2114
+ * Handle MCP OAuth callback request if it's an OAuth callback.
2115
+ *
2116
+ * This method encapsulates the entire OAuth callback flow:
2117
+ * 1. Checks if the request is an MCP OAuth callback
2118
+ * 2. Processes the OAuth code exchange
2119
+ * 3. Establishes the connection if successful
2120
+ * 4. Broadcasts MCP server state updates
2121
+ * 5. Returns the appropriate HTTP response
2122
+ *
2123
+ * @param request The incoming HTTP request
2124
+ * @returns Response if this was an OAuth callback, null otherwise
2125
+ */
2126
+ async handleMcpOAuthCallback(request) {
2127
+ if (!this.mcp.isCallbackRequest(request)) return null;
2128
+ const result = await this.mcp.handleCallbackRequest(request);
2129
+ if (result.authSuccess) this.mcp.establishConnection(result.serverId).catch((error) => {
2130
+ console.error("[Agent handleMcpOAuthCallback] Connection establishment failed:", error);
2131
+ });
2132
+ this.broadcastMcpServers();
2133
+ return this.handleOAuthCallbackResponse(result, request);
2134
+ }
2135
+ /**
2136
+ * Handle OAuth callback response using MCPClientManager configuration
2137
+ * @param result OAuth callback result
2138
+ * @param request The original request (needed for base URL)
2139
+ * @returns Response for the OAuth callback
2140
+ */
2141
+ handleOAuthCallbackResponse(result, request) {
2142
+ const config = this.mcp.getOAuthCallbackConfig();
2143
+ if (config?.customHandler) return config.customHandler(result);
2144
+ const baseOrigin = new URL(request.url).origin;
2145
+ if (config?.successRedirect && result.authSuccess) try {
2146
+ return Response.redirect(new URL(config.successRedirect, baseOrigin).href);
2147
+ } catch (e) {
2148
+ console.error("Invalid successRedirect URL:", config.successRedirect, e);
2149
+ return Response.redirect(baseOrigin);
2150
+ }
2151
+ if (config?.errorRedirect && !result.authSuccess) try {
2152
+ const errorUrl = `${config.errorRedirect}?error=${encodeURIComponent(result.authError || "Unknown error")}`;
2153
+ return Response.redirect(new URL(errorUrl, baseOrigin).href);
2154
+ } catch (e) {
2155
+ console.error("Invalid errorRedirect URL:", config.errorRedirect, e);
2156
+ return Response.redirect(baseOrigin);
2157
+ }
2158
+ return Response.redirect(baseOrigin);
2159
+ }
2160
+ };
2161
+ const wrappedClasses = /* @__PURE__ */ new Set();
2162
+ /**
2163
+ * Route a request to the appropriate Agent
2164
+ * @param request Request to route
2165
+ * @param env Environment containing Agent bindings
2166
+ * @param options Routing options
2167
+ * @returns Response from the Agent or undefined if no route matched
2168
+ */
2169
+ async function routeAgentRequest(request, env, options) {
2170
+ return routePartykitRequest(request, env, {
2171
+ prefix: "agents",
2172
+ ...options
2173
+ });
2174
+ }
2175
+ const agentMapCache = /* @__PURE__ */ new WeakMap();
2176
+ /**
2177
+ * Route an email to the appropriate Agent
2178
+ * @param email The email to route
2179
+ * @param env The environment containing the Agent bindings
2180
+ * @param options The options for routing the email
2181
+ * @returns A promise that resolves when the email has been routed
2182
+ */
2183
+ async function routeAgentEmail(email, env, options) {
2184
+ const routingInfo = await options.resolver(email, env);
2185
+ if (!routingInfo) {
2186
+ if (options.onNoRoute) await options.onNoRoute(email);
2187
+ else console.warn("No routing information found for email, dropping message");
2188
+ return;
2189
+ }
2190
+ if (!agentMapCache.has(env)) {
2191
+ const map = {};
2192
+ for (const [key, value] of Object.entries(env)) if (value && typeof value === "object" && "idFromName" in value && typeof value.idFromName === "function") {
2193
+ map[key] = value;
2194
+ map[camelCaseToKebabCase(key)] = value;
2195
+ }
2196
+ agentMapCache.set(env, map);
2197
+ }
2198
+ const agentMap = agentMapCache.get(env);
2199
+ const namespace = agentMap[routingInfo.agentName];
2200
+ if (!namespace) {
2201
+ const availableAgents = Object.keys(agentMap).filter((key) => !key.includes("-")).join(", ");
2202
+ throw new Error(`Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`);
2203
+ }
2204
+ const agent = await getAgentByName(namespace, routingInfo.agentId);
2205
+ const serialisableEmail = {
2206
+ getRaw: async () => {
2207
+ const reader = email.raw.getReader();
2208
+ const chunks = [];
2209
+ let done = false;
2210
+ while (!done) {
2211
+ const { value, done: readerDone } = await reader.read();
2212
+ done = readerDone;
2213
+ if (value) chunks.push(value);
2214
+ }
2215
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
2216
+ const combined = new Uint8Array(totalLength);
2217
+ let offset = 0;
2218
+ for (const chunk of chunks) {
2219
+ combined.set(chunk, offset);
2220
+ offset += chunk.length;
2221
+ }
2222
+ return combined;
2223
+ },
2224
+ headers: email.headers,
2225
+ rawSize: email.rawSize,
2226
+ setReject: (reason) => {
2227
+ email.setReject(reason);
2228
+ },
2229
+ forward: (rcptTo, headers) => {
2230
+ return email.forward(rcptTo, headers);
2231
+ },
2232
+ reply: (replyOptions) => {
2233
+ return email.reply(new EmailMessage(replyOptions.from, replyOptions.to, replyOptions.raw));
2234
+ },
2235
+ from: email.from,
2236
+ to: email.to,
2237
+ _secureRouted: routingInfo._secureRouted
2238
+ };
2239
+ await agent._onEmail(serialisableEmail);
2240
+ }
2241
+ /**
2242
+ * Get or create an Agent by name
2243
+ * @template Env Environment type containing bindings
2244
+ * @template T Type of the Agent class
2245
+ * @param namespace Agent namespace
2246
+ * @param name Name of the Agent instance
2247
+ * @param options Options for Agent creation
2248
+ * @returns Promise resolving to an Agent instance stub
2249
+ */
2250
+ async function getAgentByName(namespace, name, options) {
2251
+ return getServerByName(namespace, name, options);
2252
+ }
2253
+ /**
2254
+ * A wrapper for streaming responses in callable methods
2255
+ */
2256
+ var StreamingResponse = class {
2257
+ constructor(connection, id) {
2258
+ this._closed = false;
2259
+ this._connection = connection;
2260
+ this._id = id;
2261
+ }
2262
+ /**
2263
+ * Whether the stream has been closed (via end() or error())
2264
+ */
2265
+ get isClosed() {
2266
+ return this._closed;
2267
+ }
2268
+ /**
2269
+ * Send a chunk of data to the client
2270
+ * @param chunk The data to send
2271
+ * @returns false if stream is already closed (no-op), true if sent
2272
+ */
2273
+ send(chunk) {
2274
+ if (this._closed) {
2275
+ console.warn("StreamingResponse.send() called after stream was closed - data not sent");
2276
+ return false;
2277
+ }
2278
+ const response = {
2279
+ done: false,
2280
+ id: this._id,
2281
+ result: chunk,
2282
+ success: true,
2283
+ type: MessageType.RPC
2284
+ };
2285
+ this._connection.send(JSON.stringify(response));
2286
+ return true;
2287
+ }
2288
+ /**
2289
+ * End the stream and send the final chunk (if any)
2290
+ * @param finalChunk Optional final chunk of data to send
2291
+ * @returns false if stream is already closed (no-op), true if sent
2292
+ */
2293
+ end(finalChunk) {
2294
+ if (this._closed) return false;
2295
+ this._closed = true;
2296
+ const response = {
2297
+ done: true,
2298
+ id: this._id,
2299
+ result: finalChunk,
2300
+ success: true,
2301
+ type: MessageType.RPC
2302
+ };
2303
+ this._connection.send(JSON.stringify(response));
2304
+ return true;
2305
+ }
2306
+ /**
2307
+ * Send an error to the client and close the stream
2308
+ * @param message Error message to send
2309
+ * @returns false if stream is already closed (no-op), true if sent
2310
+ */
2311
+ error(message) {
2312
+ if (this._closed) return false;
2313
+ this._closed = true;
2314
+ const response = {
2315
+ error: message,
2316
+ id: this._id,
2317
+ success: false,
2318
+ type: MessageType.RPC
2319
+ };
2320
+ this._connection.send(JSON.stringify(response));
2321
+ return true;
2322
+ }
2323
+ };
2324
+
2325
+ //#endregion
2326
+ export { Agent, DEFAULT_AGENT_STATIC_OPTIONS, SqlError, StreamingResponse, __DO_NOT_USE_WILL_BREAK__agentContext, callable, createHeaderBasedEmailResolver, getAgentByName, getCurrentAgent, routeAgentEmail, routeAgentRequest, unstable_callable };
2327
+ //# sourceMappingURL=index.js.map