agents 0.0.0-b25bc37 → 0.0.0-b299d20

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 (73) hide show
  1. package/README.md +255 -27
  2. package/dist/_esm-LV5FJ3HK.js +3922 -0
  3. package/dist/_esm-LV5FJ3HK.js.map +1 -0
  4. package/dist/ai-chat-agent.d.ts +56 -7
  5. package/dist/ai-chat-agent.js +285 -93
  6. package/dist/ai-chat-agent.js.map +1 -1
  7. package/dist/ai-chat-v5-migration.d.ts +152 -0
  8. package/dist/ai-chat-v5-migration.js +20 -0
  9. package/dist/ai-react.d.ts +77 -67
  10. package/dist/ai-react.js +274 -114
  11. package/dist/ai-react.js.map +1 -1
  12. package/dist/ai-types.d.ts +41 -18
  13. package/dist/ai-types.js +7 -0
  14. package/dist/ccip-CMBYN64O.js +15 -0
  15. package/dist/ccip-CMBYN64O.js.map +1 -0
  16. package/dist/chunk-5Y6BEZDY.js +276 -0
  17. package/dist/chunk-5Y6BEZDY.js.map +1 -0
  18. package/dist/chunk-BER7KXUJ.js +18 -0
  19. package/dist/chunk-BER7KXUJ.js.map +1 -0
  20. package/dist/chunk-JJBFIGUC.js +5202 -0
  21. package/dist/chunk-JJBFIGUC.js.map +1 -0
  22. package/dist/chunk-PR4QN5HX.js +43 -0
  23. package/dist/chunk-PR4QN5HX.js.map +1 -0
  24. package/dist/chunk-QEPGNUG6.js +650 -0
  25. package/dist/chunk-QEPGNUG6.js.map +1 -0
  26. package/dist/chunk-QEVM4BVL.js +116 -0
  27. package/dist/chunk-QEVM4BVL.js.map +1 -0
  28. package/dist/chunk-RS5OCNEQ.js +1323 -0
  29. package/dist/chunk-RS5OCNEQ.js.map +1 -0
  30. package/dist/chunk-TYAY6AU6.js +159 -0
  31. package/dist/chunk-TYAY6AU6.js.map +1 -0
  32. package/dist/chunk-UJVEAURM.js +150 -0
  33. package/dist/chunk-UJVEAURM.js.map +1 -0
  34. package/dist/chunk-XFS5ERG3.js +127 -0
  35. package/dist/chunk-XFS5ERG3.js.map +1 -0
  36. package/dist/client-BohGLma8.d.ts +5041 -0
  37. package/dist/client.d.ts +16 -2
  38. package/dist/client.js +8 -126
  39. package/dist/client.js.map +1 -1
  40. package/dist/index.d.ts +289 -26
  41. package/dist/index.js +18 -8
  42. package/dist/mcp/client.d.ts +11 -714
  43. package/dist/mcp/client.js +4 -465
  44. package/dist/mcp/client.js.map +1 -1
  45. package/dist/mcp/do-oauth-client-provider.d.ts +50 -0
  46. package/dist/mcp/do-oauth-client-provider.js +8 -0
  47. package/dist/mcp/do-oauth-client-provider.js.map +1 -0
  48. package/dist/mcp/index.d.ts +70 -37
  49. package/dist/mcp/index.js +1016 -266
  50. package/dist/mcp/index.js.map +1 -1
  51. package/dist/mcp/x402.d.ts +39 -0
  52. package/dist/mcp/x402.js +3195 -0
  53. package/dist/mcp/x402.js.map +1 -0
  54. package/dist/observability/index.d.ts +46 -0
  55. package/dist/observability/index.js +12 -0
  56. package/dist/observability/index.js.map +1 -0
  57. package/dist/react.d.ts +89 -5
  58. package/dist/react.js +24 -9
  59. package/dist/react.js.map +1 -1
  60. package/dist/schedule.d.ts +81 -7
  61. package/dist/schedule.js +20 -7
  62. package/dist/schedule.js.map +1 -1
  63. package/dist/secp256k1-M22GZP2U.js +2193 -0
  64. package/dist/secp256k1-M22GZP2U.js.map +1 -0
  65. package/dist/serializable.d.ts +32 -0
  66. package/dist/serializable.js +1 -0
  67. package/dist/serializable.js.map +1 -0
  68. package/package.json +93 -52
  69. package/src/index.ts +1224 -156
  70. package/dist/chunk-HMLY7DHA.js +0 -16
  71. package/dist/chunk-YMUU7QHV.js +0 -595
  72. package/dist/chunk-YMUU7QHV.js.map +0 -1
  73. /package/dist/{chunk-HMLY7DHA.js.map → ai-chat-v5-migration.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,21 +1,34 @@
1
+ import type { env } from "cloudflare:workers";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
5
+
6
+ import type {
7
+ Prompt,
8
+ Resource,
9
+ ServerCapabilities,
10
+ Tool
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import { parseCronExpression } from "cron-schedule";
13
+ import { nanoid } from "nanoid";
14
+ import { EmailMessage } from "cloudflare:email";
1
15
  import {
2
- Server,
3
- routePartykitRequest,
4
- type PartyServerOptions,
5
- getServerByName,
6
16
  type Connection,
7
17
  type ConnectionContext,
18
+ type PartyServerOptions,
19
+ Server,
8
20
  type WSMessage,
21
+ getServerByName,
22
+ routePartykitRequest
9
23
  } from "partyserver";
24
+ import { camelCaseToKebabCase } from "./client";
25
+ import { MCPClientManager } from "./mcp/client";
26
+ import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
27
+ import type { TransportType } from "./mcp/types";
28
+ import { genericObservability, type Observability } from "./observability";
29
+ import { MessageType } from "./ai-types";
10
30
 
11
- import { parseCronExpression } from "cron-schedule";
12
- import { nanoid } from "nanoid";
13
-
14
- import { AsyncLocalStorage } from "node:async_hooks";
15
-
16
- export type { Connection, WSMessage, ConnectionContext } from "partyserver";
17
-
18
- import { WorkflowEntrypoint as CFWorkflowEntrypoint } from "cloudflare:workers";
31
+ export type { Connection, ConnectionContext, WSMessage } from "partyserver";
19
32
 
20
33
  /**
21
34
  * RPC request message from client
@@ -31,7 +44,7 @@ export type RPCRequest = {
31
44
  * State update message from client
32
45
  */
33
46
  export type StateUpdateMessage = {
34
- type: "cf_agent_state";
47
+ type: MessageType.CF_AGENT_STATE;
35
48
  state: unknown;
36
49
  };
37
50
 
@@ -39,7 +52,7 @@ export type StateUpdateMessage = {
39
52
  * RPC response message to client
40
53
  */
41
54
  export type RPCResponse = {
42
- type: "rpc";
55
+ type: MessageType.RPC;
43
56
  id: string;
44
57
  } & (
45
58
  | {
@@ -66,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
66
79
  typeof msg === "object" &&
67
80
  msg !== null &&
68
81
  "type" in msg &&
69
- msg.type === "rpc" &&
82
+ msg.type === MessageType.RPC &&
70
83
  "id" in msg &&
71
84
  typeof msg.id === "string" &&
72
85
  "method" in msg &&
@@ -84,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
84
97
  typeof msg === "object" &&
85
98
  msg !== null &&
86
99
  "type" in msg &&
87
- msg.type === "cf_agent_state" &&
100
+ msg.type === MessageType.CF_AGENT_STATE &&
88
101
  "state" in msg
89
102
  );
90
103
  }
@@ -99,16 +112,16 @@ export type CallableMetadata = {
99
112
  streaming?: boolean;
100
113
  };
101
114
 
102
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
103
115
  const callableMetadata = new Map<Function, CallableMetadata>();
104
116
 
105
117
  /**
106
118
  * Decorator that marks a method as callable by clients
107
119
  * @param metadata Optional metadata about the callable method
108
120
  */
109
- export function unstable_callable(metadata: CallableMetadata = {}) {
121
+ export function callable(metadata: CallableMetadata = {}) {
110
122
  return function callableDecorator<This, Args extends unknown[], Return>(
111
123
  target: (this: This, ...args: Args) => Return,
124
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
112
125
  context: ClassMethodDecoratorContext
113
126
  ) {
114
127
  if (!callableMetadata.has(target)) {
@@ -119,10 +132,29 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
119
132
  };
120
133
  }
121
134
 
135
+ let didWarnAboutUnstableCallable = false;
136
+
122
137
  /**
123
- * A class for creating workflow entry points that can be used with Cloudflare Workers
138
+ * Decorator that marks a method as callable by clients
139
+ * @deprecated this has been renamed to callable, and unstable_callable will be removed in the next major version
140
+ * @param metadata Optional metadata about the callable method
124
141
  */
125
- export class WorkflowEntrypoint extends CFWorkflowEntrypoint {}
142
+ export const unstable_callable = (metadata: CallableMetadata = {}) => {
143
+ if (!didWarnAboutUnstableCallable) {
144
+ didWarnAboutUnstableCallable = true;
145
+ console.warn(
146
+ "unstable_callable is deprecated, use callable instead. unstable_callable will be removed in the next major version."
147
+ );
148
+ }
149
+ callable(metadata);
150
+ };
151
+
152
+ export type QueueItem<T = string> = {
153
+ id: string;
154
+ payload: T;
155
+ callback: keyof Agent<unknown>;
156
+ created_at: number;
157
+ };
126
158
 
127
159
  /**
128
160
  * Represents a scheduled task within an Agent
@@ -165,24 +197,133 @@ function getNextCronTime(cron: string) {
165
197
  return interval.getNextDate();
166
198
  }
167
199
 
200
+ export type { TransportType } from "./mcp/types";
201
+
202
+ /**
203
+ * MCP Server state update message from server -> Client
204
+ */
205
+ export type MCPServerMessage = {
206
+ type: MessageType.CF_AGENT_MCP_SERVERS;
207
+ mcp: MCPServersState;
208
+ };
209
+
210
+ export type MCPServersState = {
211
+ servers: {
212
+ [id: string]: MCPServer;
213
+ };
214
+ tools: Tool[];
215
+ prompts: Prompt[];
216
+ resources: Resource[];
217
+ };
218
+
219
+ export type MCPServer = {
220
+ name: string;
221
+ server_url: string;
222
+ auth_url: string | null;
223
+ // This state is specifically about the temporary process of getting a token (if needed).
224
+ // Scope outside of that can't be relied upon because when the DO sleeps, there's no way
225
+ // to communicate a change to a non-ready state.
226
+ state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
227
+ instructions: string | null;
228
+ capabilities: ServerCapabilities | null;
229
+ };
230
+
231
+ /**
232
+ * MCP Server data stored in DO SQL for resuming MCP Server connections
233
+ */
234
+ type MCPServerRow = {
235
+ id: string;
236
+ name: string;
237
+ server_url: string;
238
+ client_id: string | null;
239
+ auth_url: string | null;
240
+ callback_url: string;
241
+ server_options: string;
242
+ };
243
+
168
244
  const STATE_ROW_ID = "cf_state_row_id";
169
245
  const STATE_WAS_CHANGED = "cf_state_was_changed";
170
246
 
171
247
  const DEFAULT_STATE = {} as unknown;
172
248
 
173
- export const unstable_context = new AsyncLocalStorage<{
174
- agent: Agent<unknown>;
249
+ const agentContext = new AsyncLocalStorage<{
250
+ agent: Agent<unknown, unknown>;
175
251
  connection: Connection | undefined;
176
252
  request: Request | undefined;
253
+ email: AgentEmail | undefined;
177
254
  }>();
178
255
 
256
+ export function getCurrentAgent<
257
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
258
+ >(): {
259
+ agent: T | undefined;
260
+ connection: Connection | undefined;
261
+ request: Request | undefined;
262
+ email: AgentEmail | undefined;
263
+ } {
264
+ const store = agentContext.getStore() as
265
+ | {
266
+ agent: T;
267
+ connection: Connection | undefined;
268
+ request: Request | undefined;
269
+ email: AgentEmail | undefined;
270
+ }
271
+ | undefined;
272
+ if (!store) {
273
+ return {
274
+ agent: undefined,
275
+ connection: undefined,
276
+ request: undefined,
277
+ email: undefined
278
+ };
279
+ }
280
+ return store;
281
+ }
282
+
283
+ /**
284
+ * Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
285
+ * @param agent The agent instance
286
+ * @param method The method to wrap
287
+ * @returns A wrapped method that runs within the agent context
288
+ */
289
+
290
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
291
+ function withAgentContext<T extends (...args: any[]) => any>(
292
+ method: T
293
+ ): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
294
+ return function (...args: Parameters<T>): ReturnType<T> {
295
+ const { connection, request, email, agent } = getCurrentAgent();
296
+
297
+ if (agent === this) {
298
+ // already wrapped, so we can just call the method
299
+ return method.apply(this, args);
300
+ }
301
+ // not wrapped, so we need to wrap it
302
+ return agentContext.run({ agent: this, connection, request, email }, () => {
303
+ return method.apply(this, args);
304
+ });
305
+ };
306
+ }
307
+
179
308
  /**
180
309
  * Base class for creating Agent implementations
181
310
  * @template Env Environment type containing bindings
182
311
  * @template State State type to store within the Agent
183
312
  */
184
- export class Agent<Env, State = unknown> extends Server<Env> {
185
- #state = DEFAULT_STATE as State;
313
+ export class Agent<
314
+ Env = typeof env,
315
+ State = unknown,
316
+ Props extends Record<string, unknown> = Record<string, unknown>
317
+ > extends Server<Env, Props> {
318
+ private _state = DEFAULT_STATE as State;
319
+
320
+ private _ParentClass: typeof Agent<Env, State> =
321
+ Object.getPrototypeOf(this).constructor;
322
+
323
+ readonly mcp: MCPClientManager = new MCPClientManager(
324
+ this._ParentClass.name,
325
+ "0.0.1"
326
+ );
186
327
 
187
328
  /**
188
329
  * Initial state for the Agent
@@ -194,9 +335,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
194
335
  * Current state of the Agent
195
336
  */
196
337
  get state(): State {
197
- if (this.#state !== DEFAULT_STATE) {
338
+ if (this._state !== DEFAULT_STATE) {
198
339
  // state was previously set, and populated internal state
199
- return this.#state;
340
+ return this._state;
200
341
  }
201
342
  // looks like this is the first time the state is being accessed
202
343
  // check if the state was set in a previous life
@@ -216,8 +357,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
216
357
  ) {
217
358
  const state = result[0]?.state as string; // could be null?
218
359
 
219
- this.#state = JSON.parse(state);
220
- return this.#state;
360
+ this._state = JSON.parse(state);
361
+ return this._state;
221
362
  }
222
363
 
223
364
  // ok, this is the first time the state is being accessed
@@ -238,9 +379,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
238
379
  */
239
380
  static options = {
240
381
  /** Whether the Agent should hibernate when inactive */
241
- hibernate: true, // default to hibernate
382
+ hibernate: true // default to hibernate
242
383
  };
243
384
 
385
+ /**
386
+ * The observability implementation to use for the Agent
387
+ */
388
+ observability?: Observability = genericObservability;
389
+
244
390
  /**
245
391
  * Execute SQL queries against the Agent's database
246
392
  * @template T Type of the returned rows
@@ -270,6 +416,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
270
416
  constructor(ctx: AgentContext, env: Env) {
271
417
  super(ctx, env);
272
418
 
419
+ if (!wrappedClasses.has(this.constructor)) {
420
+ // Auto-wrap custom methods with agent context
421
+ this._autoWrapCustomMethods();
422
+ wrappedClasses.add(this.constructor);
423
+ }
424
+
273
425
  this.sql`
274
426
  CREATE TABLE IF NOT EXISTS cf_agents_state (
275
427
  id TEXT PRIMARY KEY NOT NULL,
@@ -277,8 +429,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
277
429
  )
278
430
  `;
279
431
 
432
+ this.sql`
433
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
434
+ id TEXT PRIMARY KEY NOT NULL,
435
+ payload TEXT,
436
+ callback TEXT,
437
+ created_at INTEGER DEFAULT (unixepoch())
438
+ )
439
+ `;
440
+
280
441
  void this.ctx.blockConcurrencyWhile(async () => {
281
- return this.#tryCatch(async () => {
442
+ return this._tryCatch(async () => {
282
443
  // Create alarms table if it doesn't exist
283
444
  this.sql`
284
445
  CREATE TABLE IF NOT EXISTS cf_agents_schedules (
@@ -298,25 +459,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
298
459
  });
299
460
  });
300
461
 
462
+ this.sql`
463
+ CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
464
+ id TEXT PRIMARY KEY NOT NULL,
465
+ name TEXT NOT NULL,
466
+ server_url TEXT NOT NULL,
467
+ callback_url TEXT NOT NULL,
468
+ client_id TEXT,
469
+ auth_url TEXT,
470
+ server_options TEXT
471
+ )
472
+ `;
473
+
474
+ const _onRequest = this.onRequest.bind(this);
475
+ this.onRequest = (request: Request) => {
476
+ return agentContext.run(
477
+ { agent: this, connection: undefined, request, email: undefined },
478
+ async () => {
479
+ if (this.mcp.isCallbackRequest(request)) {
480
+ await this.mcp.handleCallbackRequest(request);
481
+
482
+ // after the MCP connection handshake, we can send updated mcp state
483
+ this.broadcast(
484
+ JSON.stringify({
485
+ mcp: this.getMcpServers(),
486
+ type: MessageType.CF_AGENT_MCP_SERVERS
487
+ })
488
+ );
489
+
490
+ // We probably should let the user configure this response/redirect, but this is fine for now.
491
+ return new Response("<script>window.close();</script>", {
492
+ headers: { "content-type": "text/html" },
493
+ status: 200
494
+ });
495
+ }
496
+
497
+ return this._tryCatch(() => _onRequest(request));
498
+ }
499
+ );
500
+ };
501
+
301
502
  const _onMessage = this.onMessage.bind(this);
302
503
  this.onMessage = async (connection: Connection, message: WSMessage) => {
303
- return unstable_context.run(
304
- { agent: this, connection, request: undefined },
504
+ return agentContext.run(
505
+ { agent: this, connection, request: undefined, email: undefined },
305
506
  async () => {
306
507
  if (typeof message !== "string") {
307
- return this.#tryCatch(() => _onMessage(connection, message));
508
+ return this._tryCatch(() => _onMessage(connection, message));
308
509
  }
309
510
 
310
511
  let parsed: unknown;
311
512
  try {
312
513
  parsed = JSON.parse(message);
313
- } catch (e) {
514
+ } catch (_e) {
314
515
  // silently fail and let the onMessage handler handle it
315
- return this.#tryCatch(() => _onMessage(connection, message));
516
+ return this._tryCatch(() => _onMessage(connection, message));
316
517
  }
317
518
 
318
519
  if (isStateUpdateMessage(parsed)) {
319
- this.#setStateInternal(parsed.state as State, connection);
520
+ this._setStateInternal(parsed.state as State, connection);
320
521
  return;
321
522
  }
322
523
 
@@ -330,11 +531,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
330
531
  throw new Error(`Method ${method} does not exist`);
331
532
  }
332
533
 
333
- if (!this.#isCallable(method)) {
534
+ if (!this._isCallable(method)) {
334
535
  throw new Error(`Method ${method} is not callable`);
335
536
  }
336
537
 
337
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
338
538
  const metadata = callableMetadata.get(methodFn as Function);
339
539
 
340
540
  // For streaming methods, pass a StreamingResponse object
@@ -346,22 +546,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
346
546
 
347
547
  // For regular methods, execute and send response
348
548
  const result = await methodFn.apply(this, args);
549
+
550
+ this.observability?.emit(
551
+ {
552
+ displayMessage: `RPC call to ${method}`,
553
+ id: nanoid(),
554
+ payload: {
555
+ method,
556
+ streaming: metadata?.streaming
557
+ },
558
+ timestamp: Date.now(),
559
+ type: "rpc"
560
+ },
561
+ this.ctx
562
+ );
563
+
349
564
  const response: RPCResponse = {
350
- type: "rpc",
565
+ done: true,
351
566
  id,
352
- success: true,
353
567
  result,
354
- done: true,
568
+ success: true,
569
+ type: MessageType.RPC
355
570
  };
356
571
  connection.send(JSON.stringify(response));
357
572
  } catch (e) {
358
573
  // Send error response
359
574
  const response: RPCResponse = {
360
- type: "rpc",
361
- id: parsed.id,
362
- success: false,
363
575
  error:
364
576
  e instanceof Error ? e.message : "Unknown error occurred",
577
+ id: parsed.id,
578
+ success: false,
579
+ type: MessageType.RPC
365
580
  };
366
581
  connection.send(JSON.stringify(response));
367
582
  console.error("RPC error:", e);
@@ -369,7 +584,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
369
584
  return;
370
585
  }
371
586
 
372
- return this.#tryCatch(() => _onMessage(connection, message));
587
+ return this._tryCatch(() => _onMessage(connection, message));
373
588
  }
374
589
  );
375
590
  };
@@ -378,27 +593,122 @@ export class Agent<Env, State = unknown> extends Server<Env> {
378
593
  this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
379
594
  // TODO: This is a hack to ensure the state is sent after the connection is established
380
595
  // must fix this
381
- return unstable_context.run(
382
- { agent: this, connection, request: ctx.request },
596
+ return agentContext.run(
597
+ { agent: this, connection, request: ctx.request, email: undefined },
598
+ () => {
599
+ if (this.state) {
600
+ connection.send(
601
+ JSON.stringify({
602
+ state: this.state,
603
+ type: MessageType.CF_AGENT_STATE
604
+ })
605
+ );
606
+ }
607
+
608
+ connection.send(
609
+ JSON.stringify({
610
+ mcp: this.getMcpServers(),
611
+ type: MessageType.CF_AGENT_MCP_SERVERS
612
+ })
613
+ );
614
+
615
+ this.observability?.emit(
616
+ {
617
+ displayMessage: "Connection established",
618
+ id: nanoid(),
619
+ payload: {
620
+ connectionId: connection.id
621
+ },
622
+ timestamp: Date.now(),
623
+ type: "connect"
624
+ },
625
+ this.ctx
626
+ );
627
+ return this._tryCatch(() => _onConnect(connection, ctx));
628
+ }
629
+ );
630
+ };
631
+
632
+ const _onStart = this.onStart.bind(this);
633
+ this.onStart = async (props?: Props) => {
634
+ return agentContext.run(
635
+ {
636
+ agent: this,
637
+ connection: undefined,
638
+ request: undefined,
639
+ email: undefined
640
+ },
383
641
  async () => {
384
- setTimeout(() => {
385
- if (this.state) {
386
- connection.send(
387
- JSON.stringify({
388
- type: "cf_agent_state",
389
- state: this.state,
390
- })
391
- );
642
+ await this._tryCatch(() => {
643
+ const servers = this.sql<MCPServerRow>`
644
+ SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
645
+ `;
646
+
647
+ this.broadcast(
648
+ JSON.stringify({
649
+ mcp: this.getMcpServers(),
650
+ type: MessageType.CF_AGENT_MCP_SERVERS
651
+ })
652
+ );
653
+
654
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
655
+ if (servers && Array.isArray(servers) && servers.length > 0) {
656
+ // Restore callback URLs for OAuth-enabled servers
657
+ servers.forEach((server) => {
658
+ if (server.callback_url) {
659
+ this.mcp.registerCallbackUrl(server.callback_url);
660
+ }
661
+ });
662
+
663
+ servers.forEach((server) => {
664
+ this._connectToMcpServerInternal(
665
+ server.name,
666
+ server.server_url,
667
+ server.callback_url,
668
+ server.server_options
669
+ ? JSON.parse(server.server_options)
670
+ : undefined,
671
+ {
672
+ id: server.id,
673
+ oauthClientId: server.client_id ?? undefined
674
+ }
675
+ )
676
+ .then(() => {
677
+ // Broadcast updated MCP servers state after each server connects
678
+ this.broadcast(
679
+ JSON.stringify({
680
+ mcp: this.getMcpServers(),
681
+ type: MessageType.CF_AGENT_MCP_SERVERS
682
+ })
683
+ );
684
+ })
685
+ .catch((error) => {
686
+ console.error(
687
+ `Error connecting to MCP server: ${server.name} (${server.server_url})`,
688
+ error
689
+ );
690
+ // Still broadcast even if connection fails, so clients know about the failure
691
+ this.broadcast(
692
+ JSON.stringify({
693
+ mcp: this.getMcpServers(),
694
+ type: MessageType.CF_AGENT_MCP_SERVERS
695
+ })
696
+ );
697
+ });
698
+ });
392
699
  }
393
- return this.#tryCatch(() => _onConnect(connection, ctx));
394
- }, 20);
700
+ return _onStart(props);
701
+ });
395
702
  }
396
703
  );
397
704
  };
398
705
  }
399
706
 
400
- #setStateInternal(state: State, source: Connection | "server" = "server") {
401
- this.#state = state;
707
+ private _setStateInternal(
708
+ state: State,
709
+ source: Connection | "server" = "server"
710
+ ) {
711
+ this._state = state;
402
712
  this.sql`
403
713
  INSERT OR REPLACE INTO cf_agents_state (id, state)
404
714
  VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
@@ -409,16 +719,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
409
719
  `;
410
720
  this.broadcast(
411
721
  JSON.stringify({
412
- type: "cf_agent_state",
413
722
  state: state,
723
+ type: MessageType.CF_AGENT_STATE
414
724
  }),
415
725
  source !== "server" ? [source.id] : []
416
726
  );
417
- return this.#tryCatch(() => {
418
- const { connection, request } = unstable_context.getStore() || {};
419
- return unstable_context.run(
420
- { agent: this, connection, request },
727
+ return this._tryCatch(() => {
728
+ const { connection, request, email } = agentContext.getStore() || {};
729
+ return agentContext.run(
730
+ { agent: this, connection, request, email },
421
731
  async () => {
732
+ this.observability?.emit(
733
+ {
734
+ displayMessage: "State updated",
735
+ id: nanoid(),
736
+ payload: {},
737
+ timestamp: Date.now(),
738
+ type: "state:update"
739
+ },
740
+ this.ctx
741
+ );
422
742
  return this.onStateUpdate(state, source);
423
743
  }
424
744
  );
@@ -430,7 +750,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
430
750
  * @param state New state to set
431
751
  */
432
752
  setState(state: State) {
433
- this.#setStateInternal(state, "server");
753
+ this._setStateInternal(state, "server");
434
754
  }
435
755
 
436
756
  /**
@@ -438,24 +758,90 @@ export class Agent<Env, State = unknown> extends Server<Env> {
438
758
  * @param state Updated state
439
759
  * @param source Source of the state update ("server" or a client connection)
440
760
  */
761
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
441
762
  onStateUpdate(state: State | undefined, source: Connection | "server") {
442
763
  // override this to handle state updates
443
764
  }
444
765
 
445
766
  /**
446
- * Called when the Agent receives an email
767
+ * Called when the Agent receives an email via routeAgentEmail()
768
+ * Override this method to handle incoming emails
447
769
  * @param email Email message to process
448
770
  */
449
- onEmail(email: ForwardableEmailMessage) {
450
- return unstable_context.run(
451
- { agent: this, connection: undefined, request: undefined },
771
+ async _onEmail(email: AgentEmail) {
772
+ // nb: we use this roundabout way of getting to onEmail
773
+ // because of https://github.com/cloudflare/workerd/issues/4499
774
+ return agentContext.run(
775
+ { agent: this, connection: undefined, request: undefined, email: email },
452
776
  async () => {
453
- console.error("onEmail not implemented");
777
+ if ("onEmail" in this && typeof this.onEmail === "function") {
778
+ return this._tryCatch(() =>
779
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
780
+ );
781
+ } else {
782
+ console.log("Received email from:", email.from, "to:", email.to);
783
+ console.log("Subject:", email.headers.get("subject"));
784
+ console.log(
785
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
786
+ );
787
+ }
454
788
  }
455
789
  );
456
790
  }
457
791
 
458
- async #tryCatch<T>(fn: () => T | Promise<T>) {
792
+ /**
793
+ * Reply to an email
794
+ * @param email The email to reply to
795
+ * @param options Options for the reply
796
+ * @returns void
797
+ */
798
+ async replyToEmail(
799
+ email: AgentEmail,
800
+ options: {
801
+ fromName: string;
802
+ subject?: string | undefined;
803
+ body: string;
804
+ contentType?: string;
805
+ headers?: Record<string, string>;
806
+ }
807
+ ): Promise<void> {
808
+ return this._tryCatch(async () => {
809
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
810
+ const agentId = this.name;
811
+
812
+ const { createMimeMessage } = await import("mimetext");
813
+ const msg = createMimeMessage();
814
+ msg.setSender({ addr: email.to, name: options.fromName });
815
+ msg.setRecipient(email.from);
816
+ msg.setSubject(
817
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
818
+ );
819
+ msg.addMessage({
820
+ contentType: options.contentType || "text/plain",
821
+ data: options.body
822
+ });
823
+
824
+ const domain = email.from.split("@")[1];
825
+ const messageId = `<${agentId}@${domain}>`;
826
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
827
+ msg.setHeader("Message-ID", messageId);
828
+ msg.setHeader("X-Agent-Name", agentName);
829
+ msg.setHeader("X-Agent-ID", agentId);
830
+
831
+ if (options.headers) {
832
+ for (const [key, value] of Object.entries(options.headers)) {
833
+ msg.setHeader(key, value);
834
+ }
835
+ }
836
+ await email.reply({
837
+ from: email.to,
838
+ raw: msg.asRaw(),
839
+ to: email.from
840
+ });
841
+ });
842
+ }
843
+
844
+ private async _tryCatch<T>(fn: () => T | Promise<T>) {
459
845
  try {
460
846
  return await fn();
461
847
  } catch (e) {
@@ -463,6 +849,68 @@ export class Agent<Env, State = unknown> extends Server<Env> {
463
849
  }
464
850
  }
465
851
 
852
+ /**
853
+ * Automatically wrap custom methods with agent context
854
+ * This ensures getCurrentAgent() works in all custom methods without decorators
855
+ */
856
+ private _autoWrapCustomMethods() {
857
+ // Collect all methods from base prototypes (Agent and Server)
858
+ const basePrototypes = [Agent.prototype, Server.prototype];
859
+ const baseMethods = new Set<string>();
860
+ for (const baseProto of basePrototypes) {
861
+ let proto = baseProto;
862
+ while (proto && proto !== Object.prototype) {
863
+ const methodNames = Object.getOwnPropertyNames(proto);
864
+ for (const methodName of methodNames) {
865
+ baseMethods.add(methodName);
866
+ }
867
+ proto = Object.getPrototypeOf(proto);
868
+ }
869
+ }
870
+ // Get all methods from the current instance's prototype chain
871
+ let proto = Object.getPrototypeOf(this);
872
+ let depth = 0;
873
+ while (proto && proto !== Object.prototype && depth < 10) {
874
+ const methodNames = Object.getOwnPropertyNames(proto);
875
+ for (const methodName of methodNames) {
876
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
877
+
878
+ // Skip if it's a private method, a base method, a getter, or not a function,
879
+ if (
880
+ baseMethods.has(methodName) ||
881
+ methodName.startsWith("_") ||
882
+ !descriptor ||
883
+ !!descriptor.get ||
884
+ typeof descriptor.value !== "function"
885
+ ) {
886
+ continue;
887
+ }
888
+
889
+ // Now, methodName is confirmed to be a custom method/function
890
+ // Wrap the custom method with context
891
+ const wrappedFunction = withAgentContext(
892
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
893
+ this[methodName as keyof this] as (...args: any[]) => any
894
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
895
+ ) as any;
896
+
897
+ // if the method is callable, copy the metadata from the original method
898
+ if (this._isCallable(methodName)) {
899
+ callableMetadata.set(
900
+ wrappedFunction,
901
+ callableMetadata.get(this[methodName as keyof this] as Function)!
902
+ );
903
+ }
904
+
905
+ // set the wrapped function on the prototype
906
+ this.constructor.prototype[methodName as keyof this] = wrappedFunction;
907
+ }
908
+
909
+ proto = Object.getPrototypeOf(proto);
910
+ depth++;
911
+ }
912
+ }
913
+
466
914
  override onError(
467
915
  connection: Connection,
468
916
  error: unknown
@@ -497,6 +945,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
497
945
  throw new Error("Not implemented");
498
946
  }
499
947
 
948
+ /**
949
+ * Queue a task to be executed in the future
950
+ * @param payload Payload to pass to the callback
951
+ * @param callback Name of the method to call
952
+ * @returns The ID of the queued task
953
+ */
954
+ async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
955
+ const id = nanoid(9);
956
+ if (typeof callback !== "string") {
957
+ throw new Error("Callback must be a string");
958
+ }
959
+
960
+ if (typeof this[callback] !== "function") {
961
+ throw new Error(`this.${callback} is not a function`);
962
+ }
963
+
964
+ this.sql`
965
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
966
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
967
+ `;
968
+
969
+ void this._flushQueue().catch((e) => {
970
+ console.error("Error flushing queue:", e);
971
+ });
972
+
973
+ return id;
974
+ }
975
+
976
+ private _flushingQueue = false;
977
+
978
+ private async _flushQueue() {
979
+ if (this._flushingQueue) {
980
+ return;
981
+ }
982
+ this._flushingQueue = true;
983
+ while (true) {
984
+ const result = this.sql<QueueItem<string>>`
985
+ SELECT * FROM cf_agents_queues
986
+ ORDER BY created_at ASC
987
+ `;
988
+
989
+ if (!result || result.length === 0) {
990
+ break;
991
+ }
992
+
993
+ for (const row of result || []) {
994
+ const callback = this[row.callback as keyof Agent<Env>];
995
+ if (!callback) {
996
+ console.error(`callback ${row.callback} not found`);
997
+ continue;
998
+ }
999
+ const { connection, request, email } = agentContext.getStore() || {};
1000
+ await agentContext.run(
1001
+ {
1002
+ agent: this,
1003
+ connection,
1004
+ request,
1005
+ email
1006
+ },
1007
+ async () => {
1008
+ // TODO: add retries and backoff
1009
+ await (
1010
+ callback as (
1011
+ payload: unknown,
1012
+ queueItem: QueueItem<string>
1013
+ ) => Promise<void>
1014
+ ).bind(this)(JSON.parse(row.payload as string), row);
1015
+ await this.dequeue(row.id);
1016
+ }
1017
+ );
1018
+ }
1019
+ }
1020
+ this._flushingQueue = false;
1021
+ }
1022
+
1023
+ /**
1024
+ * Dequeue a task by ID
1025
+ * @param id ID of the task to dequeue
1026
+ */
1027
+ async dequeue(id: string) {
1028
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
1029
+ }
1030
+
1031
+ /**
1032
+ * Dequeue all tasks
1033
+ */
1034
+ async dequeueAll() {
1035
+ this.sql`DELETE FROM cf_agents_queues`;
1036
+ }
1037
+
1038
+ /**
1039
+ * Dequeue all tasks by callback
1040
+ * @param callback Name of the callback to dequeue
1041
+ */
1042
+ async dequeueAllByCallback(callback: string) {
1043
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
1044
+ }
1045
+
1046
+ /**
1047
+ * Get a queued task by ID
1048
+ * @param id ID of the task to get
1049
+ * @returns The task or undefined if not found
1050
+ */
1051
+ async getQueue(id: string): Promise<QueueItem<string> | undefined> {
1052
+ const result = this.sql<QueueItem<string>>`
1053
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
1054
+ `;
1055
+ return result
1056
+ ? { ...result[0], payload: JSON.parse(result[0].payload) }
1057
+ : undefined;
1058
+ }
1059
+
1060
+ /**
1061
+ * Get all queues by key and value
1062
+ * @param key Key to filter by
1063
+ * @param value Value to filter by
1064
+ * @returns Array of matching QueueItem objects
1065
+ */
1066
+ async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
1067
+ const result = this.sql<QueueItem<string>>`
1068
+ SELECT * FROM cf_agents_queues
1069
+ `;
1070
+ return result.filter((row) => JSON.parse(row.payload)[key] === value);
1071
+ }
1072
+
500
1073
  /**
501
1074
  * Schedule a task to be executed in the future
502
1075
  * @template T Type of the payload data
@@ -512,6 +1085,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
512
1085
  ): Promise<Schedule<T>> {
513
1086
  const id = nanoid(9);
514
1087
 
1088
+ const emitScheduleCreate = (schedule: Schedule<T>) =>
1089
+ this.observability?.emit(
1090
+ {
1091
+ displayMessage: `Schedule ${schedule.id} created`,
1092
+ id: nanoid(),
1093
+ payload: {
1094
+ callback: callback as string,
1095
+ id: id
1096
+ },
1097
+ timestamp: Date.now(),
1098
+ type: "schedule:create"
1099
+ },
1100
+ this.ctx
1101
+ );
1102
+
515
1103
  if (typeof callback !== "string") {
516
1104
  throw new Error("Callback must be a string");
517
1105
  }
@@ -529,15 +1117,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
529
1117
  )}, 'scheduled', ${timestamp})
530
1118
  `;
531
1119
 
532
- await this.#scheduleNextAlarm();
1120
+ await this._scheduleNextAlarm();
533
1121
 
534
- return {
535
- id,
1122
+ const schedule: Schedule<T> = {
536
1123
  callback: callback,
1124
+ id,
537
1125
  payload: payload as T,
538
1126
  time: timestamp,
539
- type: "scheduled",
1127
+ type: "scheduled"
540
1128
  };
1129
+
1130
+ emitScheduleCreate(schedule);
1131
+
1132
+ return schedule;
541
1133
  }
542
1134
  if (typeof when === "number") {
543
1135
  const time = new Date(Date.now() + when * 1000);
@@ -550,16 +1142,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
550
1142
  )}, 'delayed', ${when}, ${timestamp})
551
1143
  `;
552
1144
 
553
- await this.#scheduleNextAlarm();
1145
+ await this._scheduleNextAlarm();
554
1146
 
555
- return {
556
- id,
1147
+ const schedule: Schedule<T> = {
557
1148
  callback: callback,
558
- payload: payload as T,
559
1149
  delayInSeconds: when,
1150
+ id,
1151
+ payload: payload as T,
560
1152
  time: timestamp,
561
- type: "delayed",
1153
+ type: "delayed"
562
1154
  };
1155
+
1156
+ emitScheduleCreate(schedule);
1157
+
1158
+ return schedule;
563
1159
  }
564
1160
  if (typeof when === "string") {
565
1161
  const nextExecutionTime = getNextCronTime(when);
@@ -572,16 +1168,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
572
1168
  )}, 'cron', ${when}, ${timestamp})
573
1169
  `;
574
1170
 
575
- await this.#scheduleNextAlarm();
1171
+ await this._scheduleNextAlarm();
576
1172
 
577
- return {
578
- id,
1173
+ const schedule: Schedule<T> = {
579
1174
  callback: callback,
580
- payload: payload as T,
581
1175
  cron: when,
1176
+ id,
1177
+ payload: payload as T,
582
1178
  time: timestamp,
583
- type: "cron",
1179
+ type: "cron"
584
1180
  };
1181
+
1182
+ emitScheduleCreate(schedule);
1183
+
1184
+ return schedule;
585
1185
  }
586
1186
  throw new Error("Invalid schedule type");
587
1187
  }
@@ -645,7 +1245,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
645
1245
  .toArray()
646
1246
  .map((row) => ({
647
1247
  ...row,
648
- payload: JSON.parse(row.payload as string) as T,
1248
+ payload: JSON.parse(row.payload as string) as T
649
1249
  })) as Schedule<T>[];
650
1250
 
651
1251
  return result;
@@ -657,18 +1257,34 @@ export class Agent<Env, State = unknown> extends Server<Env> {
657
1257
  * @returns true if the task was cancelled, false otherwise
658
1258
  */
659
1259
  async cancelSchedule(id: string): Promise<boolean> {
1260
+ const schedule = await this.getSchedule(id);
1261
+ if (schedule) {
1262
+ this.observability?.emit(
1263
+ {
1264
+ displayMessage: `Schedule ${id} cancelled`,
1265
+ id: nanoid(),
1266
+ payload: {
1267
+ callback: schedule.callback,
1268
+ id: schedule.id
1269
+ },
1270
+ timestamp: Date.now(),
1271
+ type: "schedule:cancel"
1272
+ },
1273
+ this.ctx
1274
+ );
1275
+ }
660
1276
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
661
1277
 
662
- await this.#scheduleNextAlarm();
1278
+ await this._scheduleNextAlarm();
663
1279
  return true;
664
1280
  }
665
1281
 
666
- async #scheduleNextAlarm() {
1282
+ private async _scheduleNextAlarm() {
667
1283
  // Find the next schedule that needs to be executed
668
1284
  const result = this.sql`
669
- SELECT time FROM cf_agents_schedules
1285
+ SELECT time FROM cf_agents_schedules
670
1286
  WHERE time > ${Math.floor(Date.now() / 1000)}
671
- ORDER BY time ASC
1287
+ ORDER BY time ASC
672
1288
  LIMIT 1
673
1289
  `;
674
1290
  if (!result) return;
@@ -680,10 +1296,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
680
1296
  }
681
1297
 
682
1298
  /**
683
- * Method called when an alarm fires
684
- * Executes any scheduled tasks that are due
1299
+ * Method called when an alarm fires.
1300
+ * Executes any scheduled tasks that are due.
1301
+ *
1302
+ * @remarks
1303
+ * To schedule a task, please use the `this.schedule` method instead.
1304
+ * See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
685
1305
  */
686
- async alarm() {
1306
+ public readonly alarm = async () => {
687
1307
  const now = Math.floor(Date.now() / 1000);
688
1308
 
689
1309
  // Get all schedules that should be executed now
@@ -691,46 +1311,67 @@ export class Agent<Env, State = unknown> extends Server<Env> {
691
1311
  SELECT * FROM cf_agents_schedules WHERE time <= ${now}
692
1312
  `;
693
1313
 
694
- for (const row of result || []) {
695
- const callback = this[row.callback as keyof Agent<Env>];
696
- if (!callback) {
697
- console.error(`callback ${row.callback} not found`);
698
- continue;
699
- }
700
- await unstable_context.run(
701
- { agent: this, connection: undefined, request: undefined },
702
- async () => {
703
- try {
704
- await (
705
- callback as (
706
- payload: unknown,
707
- schedule: Schedule<unknown>
708
- ) => Promise<void>
709
- ).bind(this)(JSON.parse(row.payload as string), row);
710
- } catch (e) {
711
- console.error(`error executing callback "${row.callback}"`, e);
712
- }
1314
+ if (result && Array.isArray(result)) {
1315
+ for (const row of result) {
1316
+ const callback = this[row.callback as keyof Agent<Env>];
1317
+ if (!callback) {
1318
+ console.error(`callback ${row.callback} not found`);
1319
+ continue;
713
1320
  }
714
- );
715
- if (row.type === "cron") {
716
- // Update next execution time for cron schedules
717
- const nextExecutionTime = getNextCronTime(row.cron);
718
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1321
+ await agentContext.run(
1322
+ {
1323
+ agent: this,
1324
+ connection: undefined,
1325
+ request: undefined,
1326
+ email: undefined
1327
+ },
1328
+ async () => {
1329
+ try {
1330
+ this.observability?.emit(
1331
+ {
1332
+ displayMessage: `Schedule ${row.id} executed`,
1333
+ id: nanoid(),
1334
+ payload: {
1335
+ callback: row.callback,
1336
+ id: row.id
1337
+ },
1338
+ timestamp: Date.now(),
1339
+ type: "schedule:execute"
1340
+ },
1341
+ this.ctx
1342
+ );
719
1343
 
720
- this.sql`
1344
+ await (
1345
+ callback as (
1346
+ payload: unknown,
1347
+ schedule: Schedule<unknown>
1348
+ ) => Promise<void>
1349
+ ).bind(this)(JSON.parse(row.payload as string), row);
1350
+ } catch (e) {
1351
+ console.error(`error executing callback "${row.callback}"`, e);
1352
+ }
1353
+ }
1354
+ );
1355
+ if (row.type === "cron") {
1356
+ // Update next execution time for cron schedules
1357
+ const nextExecutionTime = getNextCronTime(row.cron);
1358
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1359
+
1360
+ this.sql`
721
1361
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
722
1362
  `;
723
- } else {
724
- // Delete one-time schedules after execution
725
- this.sql`
1363
+ } else {
1364
+ // Delete one-time schedules after execution
1365
+ this.sql`
726
1366
  DELETE FROM cf_agents_schedules WHERE id = ${row.id}
727
1367
  `;
1368
+ }
728
1369
  }
729
1370
  }
730
1371
 
731
1372
  // Schedule the next alarm
732
- await this.#scheduleNextAlarm();
733
- }
1373
+ await this._scheduleNextAlarm();
1374
+ };
734
1375
 
735
1376
  /**
736
1377
  * Destroy the Agent, removing all state and scheduled tasks
@@ -739,22 +1380,230 @@ export class Agent<Env, State = unknown> extends Server<Env> {
739
1380
  // drop all tables
740
1381
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
741
1382
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1383
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1384
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
742
1385
 
743
1386
  // delete all alarms
744
1387
  await this.ctx.storage.deleteAlarm();
745
1388
  await this.ctx.storage.deleteAll();
1389
+ this.ctx.abort("destroyed"); // enforce that the agent is evicted
1390
+
1391
+ this.observability?.emit(
1392
+ {
1393
+ displayMessage: "Agent destroyed",
1394
+ id: nanoid(),
1395
+ payload: {},
1396
+ timestamp: Date.now(),
1397
+ type: "destroy"
1398
+ },
1399
+ this.ctx
1400
+ );
746
1401
  }
747
1402
 
748
1403
  /**
749
1404
  * Get all methods marked as callable on this Agent
750
1405
  * @returns A map of method names to their metadata
751
1406
  */
752
- #isCallable(method: string): boolean {
753
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
1407
+ private _isCallable(method: string): boolean {
754
1408
  return callableMetadata.has(this[method as keyof this] as Function);
755
1409
  }
1410
+
1411
+ /**
1412
+ * Connect to a new MCP Server
1413
+ *
1414
+ * @param serverName Name of the MCP server
1415
+ * @param url MCP Server SSE URL
1416
+ * @param callbackHost Base host for the agent, used for the redirect URI. If not provided, will be derived from the current request.
1417
+ * @param agentsPrefix agents routing prefix if not using `agents`
1418
+ * @param options MCP client and transport (header) options
1419
+ * @returns authUrl
1420
+ */
1421
+ async addMcpServer(
1422
+ serverName: string,
1423
+ url: string,
1424
+ callbackHost?: string,
1425
+ agentsPrefix = "agents",
1426
+ options?: {
1427
+ client?: ConstructorParameters<typeof Client>[1];
1428
+ transport?: {
1429
+ headers: HeadersInit;
1430
+ };
1431
+ }
1432
+ ): Promise<{ id: string; authUrl: string | undefined }> {
1433
+ // If callbackHost is not provided, derive it from the current request
1434
+ let resolvedCallbackHost = callbackHost;
1435
+ if (!resolvedCallbackHost) {
1436
+ const { request } = getCurrentAgent();
1437
+ if (!request) {
1438
+ throw new Error(
1439
+ "callbackHost is required when not called within a request context"
1440
+ );
1441
+ }
1442
+
1443
+ // Extract the origin from the request
1444
+ const requestUrl = new URL(request.url);
1445
+ resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
1446
+ }
1447
+
1448
+ const callbackUrl = `${resolvedCallbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
1449
+
1450
+ const result = await this._connectToMcpServerInternal(
1451
+ serverName,
1452
+ url,
1453
+ callbackUrl,
1454
+ options
1455
+ );
1456
+ this.sql`
1457
+ INSERT
1458
+ OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1459
+ VALUES (
1460
+ ${result.id},
1461
+ ${serverName},
1462
+ ${url},
1463
+ ${result.clientId ?? null},
1464
+ ${result.authUrl ?? null},
1465
+ ${callbackUrl},
1466
+ ${options ? JSON.stringify(options) : null}
1467
+ );
1468
+ `;
1469
+
1470
+ this.broadcast(
1471
+ JSON.stringify({
1472
+ mcp: this.getMcpServers(),
1473
+ type: MessageType.CF_AGENT_MCP_SERVERS
1474
+ })
1475
+ );
1476
+
1477
+ return result;
1478
+ }
1479
+
1480
+ async _connectToMcpServerInternal(
1481
+ _serverName: string,
1482
+ url: string,
1483
+ callbackUrl: string,
1484
+ // it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
1485
+ options?: {
1486
+ client?: ConstructorParameters<typeof Client>[1];
1487
+ /**
1488
+ * We don't expose the normal set of transport options because:
1489
+ * 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
1490
+ * 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
1491
+ *
1492
+ * This has the limitation that you can't override fetch, but I think headers should handle nearly all cases needed (i.e. non-standard bearer auth).
1493
+ */
1494
+ transport?: {
1495
+ headers?: HeadersInit;
1496
+ type?: TransportType;
1497
+ };
1498
+ },
1499
+ reconnect?: {
1500
+ id: string;
1501
+ oauthClientId?: string;
1502
+ }
1503
+ ): Promise<{
1504
+ id: string;
1505
+ authUrl: string | undefined;
1506
+ clientId: string | undefined;
1507
+ }> {
1508
+ const authProvider = new DurableObjectOAuthClientProvider(
1509
+ this.ctx.storage,
1510
+ this.name,
1511
+ callbackUrl
1512
+ );
1513
+
1514
+ if (reconnect) {
1515
+ authProvider.serverId = reconnect.id;
1516
+ if (reconnect.oauthClientId) {
1517
+ authProvider.clientId = reconnect.oauthClientId;
1518
+ }
1519
+ }
1520
+
1521
+ // allows passing through transport headers if necessary
1522
+ // this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
1523
+ let headerTransportOpts: SSEClientTransportOptions = {};
1524
+ if (options?.transport?.headers) {
1525
+ headerTransportOpts = {
1526
+ eventSourceInit: {
1527
+ fetch: (url, init) =>
1528
+ fetch(url, {
1529
+ ...init,
1530
+ headers: options?.transport?.headers
1531
+ })
1532
+ },
1533
+ requestInit: {
1534
+ headers: options?.transport?.headers
1535
+ }
1536
+ };
1537
+ }
1538
+
1539
+ // Use the transport type specified in options, or default to "auto"
1540
+ const transportType = options?.transport?.type || "auto";
1541
+
1542
+ const { id, authUrl, clientId } = await this.mcp.connect(url, {
1543
+ client: options?.client,
1544
+ reconnect,
1545
+ transport: {
1546
+ ...headerTransportOpts,
1547
+ authProvider,
1548
+ type: transportType
1549
+ }
1550
+ });
1551
+
1552
+ return {
1553
+ authUrl,
1554
+ clientId,
1555
+ id
1556
+ };
1557
+ }
1558
+
1559
+ async removeMcpServer(id: string) {
1560
+ this.mcp.closeConnection(id);
1561
+ this.mcp.unregisterCallbackUrl(id);
1562
+ this.sql`
1563
+ DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
1564
+ `;
1565
+ this.broadcast(
1566
+ JSON.stringify({
1567
+ mcp: this.getMcpServers(),
1568
+ type: MessageType.CF_AGENT_MCP_SERVERS
1569
+ })
1570
+ );
1571
+ }
1572
+
1573
+ getMcpServers(): MCPServersState {
1574
+ const mcpState: MCPServersState = {
1575
+ prompts: this.mcp.listPrompts(),
1576
+ resources: this.mcp.listResources(),
1577
+ servers: {},
1578
+ tools: this.mcp.listTools()
1579
+ };
1580
+
1581
+ const servers = this.sql<MCPServerRow>`
1582
+ SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1583
+ `;
1584
+
1585
+ if (servers && Array.isArray(servers) && servers.length > 0) {
1586
+ for (const server of servers) {
1587
+ const serverConn = this.mcp.mcpConnections[server.id];
1588
+ mcpState.servers[server.id] = {
1589
+ auth_url: server.auth_url,
1590
+ capabilities: serverConn?.serverCapabilities ?? null,
1591
+ instructions: serverConn?.instructions ?? null,
1592
+ name: server.name,
1593
+ server_url: server.server_url,
1594
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1595
+ state: serverConn?.connectionState ?? "authenticating"
1596
+ };
1597
+ }
1598
+ }
1599
+
1600
+ return mcpState;
1601
+ }
756
1602
  }
757
1603
 
1604
+ // A set of classes that have been wrapped with agent context
1605
+ const wrappedClasses = new Set<typeof Agent.prototype.constructor>();
1606
+
758
1607
  /**
759
1608
  * Namespace for creating Agent instances
760
1609
  * @template Agentic Type of the Agent class
@@ -792,17 +1641,17 @@ export async function routeAgentRequest<Env>(
792
1641
  const corsHeaders =
793
1642
  options?.cors === true
794
1643
  ? {
795
- "Access-Control-Allow-Origin": "*",
796
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
797
1644
  "Access-Control-Allow-Credentials": "true",
798
- "Access-Control-Max-Age": "86400",
1645
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1646
+ "Access-Control-Allow-Origin": "*",
1647
+ "Access-Control-Max-Age": "86400"
799
1648
  }
800
1649
  : options?.cors;
801
1650
 
802
1651
  if (request.method === "OPTIONS") {
803
1652
  if (corsHeaders) {
804
1653
  return new Response(null, {
805
- headers: corsHeaders,
1654
+ headers: corsHeaders
806
1655
  });
807
1656
  }
808
1657
  console.warn(
@@ -815,7 +1664,7 @@ export async function routeAgentRequest<Env>(
815
1664
  env as Record<string, unknown>,
816
1665
  {
817
1666
  prefix: "agents",
818
- ...(options as PartyServerOptions<Record<string, unknown>>),
1667
+ ...(options as PartyServerOptions<Record<string, unknown>>)
819
1668
  }
820
1669
  );
821
1670
 
@@ -828,24 +1677,238 @@ export async function routeAgentRequest<Env>(
828
1677
  response = new Response(response.body, {
829
1678
  headers: {
830
1679
  ...response.headers,
831
- ...corsHeaders,
832
- },
1680
+ ...corsHeaders
1681
+ }
833
1682
  });
834
1683
  }
835
1684
  return response;
836
1685
  }
837
1686
 
1687
+ export type EmailResolver<Env> = (
1688
+ email: ForwardableEmailMessage,
1689
+ env: Env
1690
+ ) => Promise<{
1691
+ agentName: string;
1692
+ agentId: string;
1693
+ } | null>;
1694
+
1695
+ /**
1696
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1697
+ * @returns A function that resolves the agent to route the email to
1698
+ */
1699
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1700
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1701
+ const messageId = email.headers.get("message-id");
1702
+ if (messageId) {
1703
+ const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
1704
+ if (messageIdMatch) {
1705
+ const [, agentId, domain] = messageIdMatch;
1706
+ const agentName = domain.split(".")[0];
1707
+ return { agentName, agentId };
1708
+ }
1709
+ }
1710
+
1711
+ const references = email.headers.get("references");
1712
+ if (references) {
1713
+ const referencesMatch = references.match(
1714
+ /<([A-Za-z0-9+/]{43}=)@([^>]+)>/
1715
+ );
1716
+ if (referencesMatch) {
1717
+ const [, base64Id, domain] = referencesMatch;
1718
+ const agentId = Buffer.from(base64Id, "base64").toString("hex");
1719
+ const agentName = domain.split(".")[0];
1720
+ return { agentName, agentId };
1721
+ }
1722
+ }
1723
+
1724
+ const agentName = email.headers.get("x-agent-name");
1725
+ const agentId = email.headers.get("x-agent-id");
1726
+ if (agentName && agentId) {
1727
+ return { agentName, agentId };
1728
+ }
1729
+
1730
+ return null;
1731
+ };
1732
+ }
1733
+
1734
+ /**
1735
+ * Create a resolver that uses the email address to determine the agent to route the email to
1736
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1737
+ * @returns A function that resolves the agent to route the email to
1738
+ */
1739
+ export function createAddressBasedEmailResolver<Env>(
1740
+ defaultAgentName: string
1741
+ ): EmailResolver<Env> {
1742
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1743
+ const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
1744
+ if (!emailMatch) {
1745
+ return null;
1746
+ }
1747
+
1748
+ const [, localPart, subAddress] = emailMatch;
1749
+
1750
+ if (subAddress) {
1751
+ return {
1752
+ agentName: localPart,
1753
+ agentId: subAddress
1754
+ };
1755
+ }
1756
+
1757
+ // Option 2: Use defaultAgentName namespace, localPart as agentId
1758
+ // Common for catch-all email routing to a single EmailAgent namespace
1759
+ return {
1760
+ agentName: defaultAgentName,
1761
+ agentId: localPart
1762
+ };
1763
+ };
1764
+ }
1765
+
1766
+ /**
1767
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1768
+ * @param agentName The name of the agent to route the email to
1769
+ * @param agentId The id of the agent to route the email to
1770
+ * @returns A function that resolves the agent to route the email to
1771
+ */
1772
+ export function createCatchAllEmailResolver<Env>(
1773
+ agentName: string,
1774
+ agentId: string
1775
+ ): EmailResolver<Env> {
1776
+ return async () => ({ agentName, agentId });
1777
+ }
1778
+
1779
+ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1780
+ resolver: EmailResolver<Env>;
1781
+ };
1782
+
1783
+ // Cache the agent namespace map for email routing
1784
+ // This maps both kebab-case and original names to namespaces
1785
+ const agentMapCache = new WeakMap<
1786
+ Record<string, unknown>,
1787
+ Record<string, unknown>
1788
+ >();
1789
+
838
1790
  /**
839
1791
  * Route an email to the appropriate Agent
840
- * @param email Email message to route
841
- * @param env Environment containing Agent bindings
842
- * @param options Routing options
1792
+ * @param email The email to route
1793
+ * @param env The environment containing the Agent bindings
1794
+ * @param options The options for routing the email
1795
+ * @returns A promise that resolves when the email has been routed
843
1796
  */
844
1797
  export async function routeAgentEmail<Env>(
845
1798
  email: ForwardableEmailMessage,
846
1799
  env: Env,
847
- options?: AgentOptions<Env>
848
- ): Promise<void> {}
1800
+ options: EmailRoutingOptions<Env>
1801
+ ): Promise<void> {
1802
+ const routingInfo = await options.resolver(email, env);
1803
+
1804
+ if (!routingInfo) {
1805
+ console.warn("No routing information found for email, dropping message");
1806
+ return;
1807
+ }
1808
+
1809
+ // Build a map that includes both original names and kebab-case versions
1810
+ if (!agentMapCache.has(env as Record<string, unknown>)) {
1811
+ const map: Record<string, unknown> = {};
1812
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1813
+ if (
1814
+ value &&
1815
+ typeof value === "object" &&
1816
+ "idFromName" in value &&
1817
+ typeof value.idFromName === "function"
1818
+ ) {
1819
+ // Add both the original name and kebab-case version
1820
+ map[key] = value;
1821
+ map[camelCaseToKebabCase(key)] = value;
1822
+ }
1823
+ }
1824
+ agentMapCache.set(env as Record<string, unknown>, map);
1825
+ }
1826
+
1827
+ const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
1828
+ const namespace = agentMap[routingInfo.agentName];
1829
+
1830
+ if (!namespace) {
1831
+ // Provide helpful error message listing available agents
1832
+ const availableAgents = Object.keys(agentMap)
1833
+ .filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
1834
+ .join(", ");
1835
+ throw new Error(
1836
+ `Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
1837
+ );
1838
+ }
1839
+
1840
+ const agent = await getAgentByName(
1841
+ namespace as unknown as AgentNamespace<Agent<Env>>,
1842
+ routingInfo.agentId
1843
+ );
1844
+
1845
+ // let's make a serialisable version of the email
1846
+ const serialisableEmail: AgentEmail = {
1847
+ getRaw: async () => {
1848
+ const reader = email.raw.getReader();
1849
+ const chunks: Uint8Array[] = [];
1850
+
1851
+ let done = false;
1852
+ while (!done) {
1853
+ const { value, done: readerDone } = await reader.read();
1854
+ done = readerDone;
1855
+ if (value) {
1856
+ chunks.push(value);
1857
+ }
1858
+ }
1859
+
1860
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1861
+ const combined = new Uint8Array(totalLength);
1862
+ let offset = 0;
1863
+ for (const chunk of chunks) {
1864
+ combined.set(chunk, offset);
1865
+ offset += chunk.length;
1866
+ }
1867
+
1868
+ return combined;
1869
+ },
1870
+ headers: email.headers,
1871
+ rawSize: email.rawSize,
1872
+ setReject: (reason: string) => {
1873
+ email.setReject(reason);
1874
+ },
1875
+ forward: (rcptTo: string, headers?: Headers) => {
1876
+ return email.forward(rcptTo, headers);
1877
+ },
1878
+ reply: (options: { from: string; to: string; raw: string }) => {
1879
+ return email.reply(
1880
+ new EmailMessage(options.from, options.to, options.raw)
1881
+ );
1882
+ },
1883
+ from: email.from,
1884
+ to: email.to
1885
+ };
1886
+
1887
+ await agent._onEmail(serialisableEmail);
1888
+ }
1889
+
1890
+ export type AgentEmail = {
1891
+ from: string;
1892
+ to: string;
1893
+ getRaw: () => Promise<Uint8Array>;
1894
+ headers: Headers;
1895
+ rawSize: number;
1896
+ setReject: (reason: string) => void;
1897
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1898
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1899
+ };
1900
+
1901
+ export type EmailSendOptions = {
1902
+ to: string;
1903
+ subject: string;
1904
+ body: string;
1905
+ contentType?: string;
1906
+ headers?: Record<string, string>;
1907
+ includeRoutingHeaders?: boolean;
1908
+ agentName?: string;
1909
+ agentId?: string;
1910
+ domain?: string;
1911
+ };
849
1912
 
850
1913
  /**
851
1914
  * Get or create an Agent by name
@@ -856,12 +1919,17 @@ export async function routeAgentEmail<Env>(
856
1919
  * @param options Options for Agent creation
857
1920
  * @returns Promise resolving to an Agent instance stub
858
1921
  */
859
- export function getAgentByName<Env, T extends Agent<Env>>(
1922
+ export async function getAgentByName<
1923
+ Env,
1924
+ T extends Agent<Env>,
1925
+ Props extends Record<string, unknown> = Record<string, unknown>
1926
+ >(
860
1927
  namespace: AgentNamespace<T>,
861
1928
  name: string,
862
1929
  options?: {
863
1930
  jurisdiction?: DurableObjectJurisdiction;
864
1931
  locationHint?: DurableObjectLocationHint;
1932
+ props?: Props;
865
1933
  }
866
1934
  ) {
867
1935
  return getServerByName<Env, T>(namespace, name, options);
@@ -871,13 +1939,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
871
1939
  * A wrapper for streaming responses in callable methods
872
1940
  */
873
1941
  export class StreamingResponse {
874
- #connection: Connection;
875
- #id: string;
876
- #closed = false;
1942
+ private _connection: Connection;
1943
+ private _id: string;
1944
+ private _closed = false;
877
1945
 
878
1946
  constructor(connection: Connection, id: string) {
879
- this.#connection = connection;
880
- this.#id = id;
1947
+ this._connection = connection;
1948
+ this._id = id;
881
1949
  }
882
1950
 
883
1951
  /**
@@ -885,17 +1953,17 @@ export class StreamingResponse {
885
1953
  * @param chunk The data to send
886
1954
  */
887
1955
  send(chunk: unknown) {
888
- if (this.#closed) {
1956
+ if (this._closed) {
889
1957
  throw new Error("StreamingResponse is already closed");
890
1958
  }
891
1959
  const response: RPCResponse = {
892
- type: "rpc",
893
- id: this.#id,
894
- success: true,
895
- result: chunk,
896
1960
  done: false,
1961
+ id: this._id,
1962
+ result: chunk,
1963
+ success: true,
1964
+ type: MessageType.RPC
897
1965
  };
898
- this.#connection.send(JSON.stringify(response));
1966
+ this._connection.send(JSON.stringify(response));
899
1967
  }
900
1968
 
901
1969
  /**
@@ -903,17 +1971,17 @@ export class StreamingResponse {
903
1971
  * @param finalChunk Optional final chunk of data to send
904
1972
  */
905
1973
  end(finalChunk?: unknown) {
906
- if (this.#closed) {
1974
+ if (this._closed) {
907
1975
  throw new Error("StreamingResponse is already closed");
908
1976
  }
909
- this.#closed = true;
1977
+ this._closed = true;
910
1978
  const response: RPCResponse = {
911
- type: "rpc",
912
- id: this.#id,
913
- success: true,
914
- result: finalChunk,
915
1979
  done: true,
1980
+ id: this._id,
1981
+ result: finalChunk,
1982
+ success: true,
1983
+ type: MessageType.RPC
916
1984
  };
917
- this.#connection.send(JSON.stringify(response));
1985
+ this._connection.send(JSON.stringify(response));
918
1986
  }
919
1987
  }