agents 0.0.0-1232c19 → 0.0.0-14616d3

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 (51) hide show
  1. package/README.md +131 -25
  2. package/dist/ai-chat-agent.d.ts +11 -7
  3. package/dist/ai-chat-agent.js +146 -47
  4. package/dist/ai-chat-agent.js.map +1 -1
  5. package/dist/ai-chat-v5-migration.d.ts +152 -0
  6. package/dist/ai-chat-v5-migration.js +19 -0
  7. package/dist/ai-chat-v5-migration.js.map +1 -0
  8. package/dist/ai-react.d.ts +61 -70
  9. package/dist/ai-react.js +144 -37
  10. package/dist/ai-react.js.map +1 -1
  11. package/dist/ai-types.d.ts +36 -19
  12. package/dist/ai-types.js +6 -0
  13. package/dist/chunk-AVYJQSLW.js +17 -0
  14. package/dist/chunk-AVYJQSLW.js.map +1 -0
  15. package/dist/{chunk-767EASBA.js → chunk-LL2AFX7V.js} +5 -2
  16. package/dist/chunk-LL2AFX7V.js.map +1 -0
  17. package/dist/{chunk-ZRRXJUAA.js → chunk-O4KIASSE.js} +632 -118
  18. package/dist/chunk-O4KIASSE.js.map +1 -0
  19. package/dist/{chunk-NKZZ66QY.js → chunk-QEVM4BVL.js} +5 -5
  20. package/dist/chunk-QEVM4BVL.js.map +1 -0
  21. package/dist/chunk-UJVEAURM.js +150 -0
  22. package/dist/chunk-UJVEAURM.js.map +1 -0
  23. package/dist/{chunk-E3LCYPCB.js → chunk-VYENMKFS.js} +163 -20
  24. package/dist/chunk-VYENMKFS.js.map +1 -0
  25. package/dist/client-CpU7236R.d.ts +4607 -0
  26. package/dist/client.d.ts +2 -2
  27. package/dist/client.js +2 -1
  28. package/dist/index.d.ts +164 -20
  29. package/dist/index.js +13 -4
  30. package/dist/mcp/client.d.ts +9 -781
  31. package/dist/mcp/client.js +1 -1
  32. package/dist/mcp/do-oauth-client-provider.d.ts +1 -0
  33. package/dist/mcp/do-oauth-client-provider.js +1 -1
  34. package/dist/mcp/index.d.ts +35 -7
  35. package/dist/mcp/index.js +191 -18
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/observability/index.d.ts +46 -0
  38. package/dist/observability/index.js +11 -0
  39. package/dist/observability/index.js.map +1 -0
  40. package/dist/react.d.ts +9 -5
  41. package/dist/react.js +7 -5
  42. package/dist/react.js.map +1 -1
  43. package/dist/schedule.d.ts +79 -5
  44. package/dist/schedule.js +15 -2
  45. package/dist/schedule.js.map +1 -1
  46. package/package.json +41 -28
  47. package/src/index.ts +841 -153
  48. package/dist/chunk-767EASBA.js.map +0 -1
  49. package/dist/chunk-E3LCYPCB.js.map +0 -1
  50. package/dist/chunk-NKZZ66QY.js.map +0 -1
  51. package/dist/chunk-ZRRXJUAA.js.map +0 -1
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { env } from "cloudflare:workers";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
4
  import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
@@ -6,23 +7,26 @@ import type {
6
7
  Prompt,
7
8
  Resource,
8
9
  ServerCapabilities,
9
- Tool,
10
+ Tool
10
11
  } from "@modelcontextprotocol/sdk/types.js";
11
12
  import { parseCronExpression } from "cron-schedule";
12
13
  import { nanoid } from "nanoid";
14
+ import { EmailMessage } from "cloudflare:email";
13
15
  import {
14
16
  type Connection,
15
17
  type ConnectionContext,
16
- getServerByName,
17
18
  type PartyServerOptions,
18
- routePartykitRequest,
19
19
  Server,
20
20
  type WSMessage,
21
+ getServerByName,
22
+ routePartykitRequest
21
23
  } from "partyserver";
22
24
  import { camelCaseToKebabCase } from "./client";
23
25
  import { MCPClientManager } from "./mcp/client";
24
26
  // import type { MCPClientConnection } from "./mcp/client-connection";
25
27
  import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
28
+ import { genericObservability, type Observability } from "./observability";
29
+ import { MessageType } from "./ai-types";
26
30
 
27
31
  export type { Connection, ConnectionContext, WSMessage } from "partyserver";
28
32
 
@@ -40,7 +44,7 @@ export type RPCRequest = {
40
44
  * State update message from client
41
45
  */
42
46
  export type StateUpdateMessage = {
43
- type: "cf_agent_state";
47
+ type: MessageType.CF_AGENT_STATE;
44
48
  state: unknown;
45
49
  };
46
50
 
@@ -48,7 +52,7 @@ export type StateUpdateMessage = {
48
52
  * RPC response message to client
49
53
  */
50
54
  export type RPCResponse = {
51
- type: "rpc";
55
+ type: MessageType.RPC;
52
56
  id: string;
53
57
  } & (
54
58
  | {
@@ -75,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
75
79
  typeof msg === "object" &&
76
80
  msg !== null &&
77
81
  "type" in msg &&
78
- msg.type === "rpc" &&
82
+ msg.type === MessageType.RPC &&
79
83
  "id" in msg &&
80
84
  typeof msg.id === "string" &&
81
85
  "method" in msg &&
@@ -93,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
93
97
  typeof msg === "object" &&
94
98
  msg !== null &&
95
99
  "type" in msg &&
96
- msg.type === "cf_agent_state" &&
100
+ msg.type === MessageType.CF_AGENT_STATE &&
97
101
  "state" in msg
98
102
  );
99
103
  }
@@ -114,7 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
114
118
  * Decorator that marks a method as callable by clients
115
119
  * @param metadata Optional metadata about the callable method
116
120
  */
117
- export function unstable_callable(metadata: CallableMetadata = {}) {
121
+ export function callable(metadata: CallableMetadata = {}) {
118
122
  return function callableDecorator<This, Args extends unknown[], Return>(
119
123
  target: (this: This, ...args: Args) => Return,
120
124
  // biome-ignore lint/correctness/noUnusedFunctionParameters: later
@@ -128,6 +132,30 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
128
132
  };
129
133
  }
130
134
 
135
+ let didWarnAboutUnstableCallable = false;
136
+
137
+ /**
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
141
+ */
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
+ };
158
+
131
159
  /**
132
160
  * Represents a scheduled task within an Agent
133
161
  * @template T Type of the payload data
@@ -173,7 +201,7 @@ function getNextCronTime(cron: string) {
173
201
  * MCP Server state update message from server -> Client
174
202
  */
175
203
  export type MCPServerMessage = {
176
- type: "cf_agent_mcp_servers";
204
+ type: MessageType.CF_AGENT_MCP_SERVERS;
177
205
  mcp: MCPServersState;
178
206
  };
179
207
 
@@ -217,23 +245,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
217
245
  const DEFAULT_STATE = {} as unknown;
218
246
 
219
247
  const agentContext = new AsyncLocalStorage<{
220
- agent: Agent<unknown>;
248
+ agent: Agent<unknown, unknown>;
221
249
  connection: Connection | undefined;
222
250
  request: Request | undefined;
251
+ email: AgentEmail | undefined;
223
252
  }>();
224
253
 
225
254
  export function getCurrentAgent<
226
- T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
255
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
227
256
  >(): {
228
257
  agent: T | undefined;
229
258
  connection: Connection | undefined;
230
- request: Request<unknown, CfProperties<unknown>> | undefined;
259
+ request: Request | undefined;
260
+ email: AgentEmail | undefined;
231
261
  } {
232
262
  const store = agentContext.getStore() as
233
263
  | {
234
264
  agent: T;
235
265
  connection: Connection | undefined;
236
- request: Request<unknown, CfProperties<unknown>> | undefined;
266
+ request: Request | undefined;
267
+ email: AgentEmail | undefined;
237
268
  }
238
269
  | undefined;
239
270
  if (!store) {
@@ -241,17 +272,43 @@ export function getCurrentAgent<
241
272
  agent: undefined,
242
273
  connection: undefined,
243
274
  request: undefined,
275
+ email: undefined
244
276
  };
245
277
  }
246
278
  return store;
247
279
  }
248
280
 
281
+ /**
282
+ * Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
283
+ * @param agent The agent instance
284
+ * @param method The method to wrap
285
+ * @returns A wrapped method that runs within the agent context
286
+ */
287
+
288
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
289
+ function withAgentContext<T extends (...args: any[]) => any>(
290
+ method: T
291
+ ): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
292
+ return function (...args: Parameters<T>): ReturnType<T> {
293
+ const { connection, request, email, agent } = getCurrentAgent();
294
+
295
+ if (agent === this) {
296
+ // already wrapped, so we can just call the method
297
+ return method.apply(this, args);
298
+ }
299
+ // not wrapped, so we need to wrap it
300
+ return agentContext.run({ agent: this, connection, request, email }, () => {
301
+ return method.apply(this, args);
302
+ });
303
+ };
304
+ }
305
+
249
306
  /**
250
307
  * Base class for creating Agent implementations
251
308
  * @template Env Environment type containing bindings
252
309
  * @template State State type to store within the Agent
253
310
  */
254
- export class Agent<Env, State = unknown> extends Server<Env> {
311
+ export class Agent<Env = typeof env, State = unknown> extends Server<Env> {
255
312
  private _state = DEFAULT_STATE as State;
256
313
 
257
314
  private _ParentClass: typeof Agent<Env, State> =
@@ -313,9 +370,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
313
370
  */
314
371
  static options = {
315
372
  /** Whether the Agent should hibernate when inactive */
316
- hibernate: true, // default to hibernate
373
+ hibernate: true // default to hibernate
317
374
  };
318
375
 
376
+ /**
377
+ * The observability implementation to use for the Agent
378
+ */
379
+ observability?: Observability = genericObservability;
380
+
319
381
  /**
320
382
  * Execute SQL queries against the Agent's database
321
383
  * @template T Type of the returned rows
@@ -345,6 +407,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
345
407
  constructor(ctx: AgentContext, env: Env) {
346
408
  super(ctx, env);
347
409
 
410
+ // Auto-wrap custom methods with agent context
411
+ this._autoWrapCustomMethods();
412
+
348
413
  this.sql`
349
414
  CREATE TABLE IF NOT EXISTS cf_agents_state (
350
415
  id TEXT PRIMARY KEY NOT NULL,
@@ -352,6 +417,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
352
417
  )
353
418
  `;
354
419
 
420
+ this.sql`
421
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
422
+ id TEXT PRIMARY KEY NOT NULL,
423
+ payload TEXT,
424
+ callback TEXT,
425
+ created_at INTEGER DEFAULT (unixepoch())
426
+ )
427
+ `;
428
+
355
429
  void this.ctx.blockConcurrencyWhile(async () => {
356
430
  return this._tryCatch(async () => {
357
431
  // Create alarms table if it doesn't exist
@@ -388,7 +462,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
388
462
  const _onRequest = this.onRequest.bind(this);
389
463
  this.onRequest = (request: Request) => {
390
464
  return agentContext.run(
391
- { agent: this, connection: undefined, request },
465
+ { agent: this, connection: undefined, request, email: undefined },
392
466
  async () => {
393
467
  if (this.mcp.isCallbackRequest(request)) {
394
468
  await this.mcp.handleCallbackRequest(request);
@@ -397,14 +471,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
397
471
  this.broadcast(
398
472
  JSON.stringify({
399
473
  mcp: this.getMcpServers(),
400
- type: "cf_agent_mcp_servers",
474
+ type: MessageType.CF_AGENT_MCP_SERVERS
401
475
  })
402
476
  );
403
477
 
404
478
  // We probably should let the user configure this response/redirect, but this is fine for now.
405
479
  return new Response("<script>window.close();</script>", {
406
480
  headers: { "content-type": "text/html" },
407
- status: 200,
481
+ status: 200
408
482
  });
409
483
  }
410
484
 
@@ -416,7 +490,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
416
490
  const _onMessage = this.onMessage.bind(this);
417
491
  this.onMessage = async (connection: Connection, message: WSMessage) => {
418
492
  return agentContext.run(
419
- { agent: this, connection, request: undefined },
493
+ { agent: this, connection, request: undefined, email: undefined },
420
494
  async () => {
421
495
  if (typeof message !== "string") {
422
496
  return this._tryCatch(() => _onMessage(connection, message));
@@ -460,12 +534,27 @@ export class Agent<Env, State = unknown> extends Server<Env> {
460
534
 
461
535
  // For regular methods, execute and send response
462
536
  const result = await methodFn.apply(this, args);
537
+
538
+ this.observability?.emit(
539
+ {
540
+ displayMessage: `RPC call to ${method}`,
541
+ id: nanoid(),
542
+ payload: {
543
+ method,
544
+ streaming: metadata?.streaming
545
+ },
546
+ timestamp: Date.now(),
547
+ type: "rpc"
548
+ },
549
+ this.ctx
550
+ );
551
+
463
552
  const response: RPCResponse = {
464
553
  done: true,
465
554
  id,
466
555
  result,
467
556
  success: true,
468
- type: "rpc",
557
+ type: MessageType.RPC
469
558
  };
470
559
  connection.send(JSON.stringify(response));
471
560
  } catch (e) {
@@ -475,7 +564,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
475
564
  e instanceof Error ? e.message : "Unknown error occurred",
476
565
  id: parsed.id,
477
566
  success: false,
478
- type: "rpc",
567
+ type: MessageType.RPC
479
568
  };
480
569
  connection.send(JSON.stringify(response));
481
570
  console.error("RPC error:", e);
@@ -493,27 +582,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
493
582
  // TODO: This is a hack to ensure the state is sent after the connection is established
494
583
  // must fix this
495
584
  return agentContext.run(
496
- { agent: this, connection, request: ctx.request },
497
- async () => {
498
- setTimeout(() => {
499
- if (this.state) {
500
- connection.send(
501
- JSON.stringify({
502
- state: this.state,
503
- type: "cf_agent_state",
504
- })
505
- );
506
- }
507
-
585
+ { agent: this, connection, request: ctx.request, email: undefined },
586
+ () => {
587
+ if (this.state) {
508
588
  connection.send(
509
589
  JSON.stringify({
510
- mcp: this.getMcpServers(),
511
- type: "cf_agent_mcp_servers",
590
+ state: this.state,
591
+ type: MessageType.CF_AGENT_STATE
512
592
  })
513
593
  );
594
+ }
595
+
596
+ connection.send(
597
+ JSON.stringify({
598
+ mcp: this.getMcpServers(),
599
+ type: MessageType.CF_AGENT_MCP_SERVERS
600
+ })
601
+ );
514
602
 
515
- return this._tryCatch(() => _onConnect(connection, ctx));
516
- }, 20);
603
+ this.observability?.emit(
604
+ {
605
+ displayMessage: "Connection established",
606
+ id: nanoid(),
607
+ payload: {
608
+ connectionId: connection.id
609
+ },
610
+ timestamp: Date.now(),
611
+ type: "connect"
612
+ },
613
+ this.ctx
614
+ );
615
+ return this._tryCatch(() => _onConnect(connection, ctx));
517
616
  }
518
617
  );
519
618
  };
@@ -521,18 +620,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
521
620
  const _onStart = this.onStart.bind(this);
522
621
  this.onStart = async () => {
523
622
  return agentContext.run(
524
- { agent: this, connection: undefined, request: undefined },
623
+ {
624
+ agent: this,
625
+ connection: undefined,
626
+ request: undefined,
627
+ email: undefined
628
+ },
525
629
  async () => {
526
- const servers = this.sql<MCPServerRow>`
630
+ await this._tryCatch(() => {
631
+ const servers = this.sql<MCPServerRow>`
527
632
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
528
633
  `;
529
634
 
530
- // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
531
- await Promise.allSettled(
532
- servers
533
- .filter((server) => server.auth_url === null)
534
- .map((server) => {
535
- return this._connectToMcpServerInternal(
635
+ this.broadcast(
636
+ JSON.stringify({
637
+ mcp: this.getMcpServers(),
638
+ type: MessageType.CF_AGENT_MCP_SERVERS
639
+ })
640
+ );
641
+
642
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
643
+ if (servers && Array.isArray(servers) && servers.length > 0) {
644
+ servers.forEach((server) => {
645
+ this._connectToMcpServerInternal(
536
646
  server.name,
537
647
  server.server_url,
538
648
  server.callback_url,
@@ -541,20 +651,35 @@ export class Agent<Env, State = unknown> extends Server<Env> {
541
651
  : undefined,
542
652
  {
543
653
  id: server.id,
544
- oauthClientId: server.client_id ?? undefined,
654
+ oauthClientId: server.client_id ?? undefined
545
655
  }
546
- );
547
- })
548
- );
549
-
550
- this.broadcast(
551
- JSON.stringify({
552
- mcp: this.getMcpServers(),
553
- type: "cf_agent_mcp_servers",
554
- })
555
- );
556
-
557
- await this._tryCatch(() => _onStart());
656
+ )
657
+ .then(() => {
658
+ // Broadcast updated MCP servers state after each server connects
659
+ this.broadcast(
660
+ JSON.stringify({
661
+ mcp: this.getMcpServers(),
662
+ type: MessageType.CF_AGENT_MCP_SERVERS
663
+ })
664
+ );
665
+ })
666
+ .catch((error) => {
667
+ console.error(
668
+ `Error connecting to MCP server: ${server.name} (${server.server_url})`,
669
+ error
670
+ );
671
+ // Still broadcast even if connection fails, so clients know about the failure
672
+ this.broadcast(
673
+ JSON.stringify({
674
+ mcp: this.getMcpServers(),
675
+ type: MessageType.CF_AGENT_MCP_SERVERS
676
+ })
677
+ );
678
+ });
679
+ });
680
+ }
681
+ return _onStart();
682
+ });
558
683
  }
559
684
  );
560
685
  };
@@ -576,15 +701,25 @@ export class Agent<Env, State = unknown> extends Server<Env> {
576
701
  this.broadcast(
577
702
  JSON.stringify({
578
703
  state: state,
579
- type: "cf_agent_state",
704
+ type: MessageType.CF_AGENT_STATE
580
705
  }),
581
706
  source !== "server" ? [source.id] : []
582
707
  );
583
708
  return this._tryCatch(() => {
584
- const { connection, request } = agentContext.getStore() || {};
709
+ const { connection, request, email } = agentContext.getStore() || {};
585
710
  return agentContext.run(
586
- { agent: this, connection, request },
711
+ { agent: this, connection, request, email },
587
712
  async () => {
713
+ this.observability?.emit(
714
+ {
715
+ displayMessage: "State updated",
716
+ id: nanoid(),
717
+ payload: {},
718
+ timestamp: Date.now(),
719
+ type: "state:update"
720
+ },
721
+ this.ctx
722
+ );
588
723
  return this.onStateUpdate(state, source);
589
724
  }
590
725
  );
@@ -610,19 +745,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
610
745
  }
611
746
 
612
747
  /**
613
- * Called when the Agent receives an email
748
+ * Called when the Agent receives an email via routeAgentEmail()
749
+ * Override this method to handle incoming emails
614
750
  * @param email Email message to process
615
751
  */
616
- // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
617
- onEmail(email: ForwardableEmailMessage) {
752
+ async _onEmail(email: AgentEmail) {
753
+ // nb: we use this roundabout way of getting to onEmail
754
+ // because of https://github.com/cloudflare/workerd/issues/4499
618
755
  return agentContext.run(
619
- { agent: this, connection: undefined, request: undefined },
756
+ { agent: this, connection: undefined, request: undefined, email: email },
620
757
  async () => {
621
- console.error("onEmail not implemented");
758
+ if ("onEmail" in this && typeof this.onEmail === "function") {
759
+ return this._tryCatch(() =>
760
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
761
+ );
762
+ } else {
763
+ console.log("Received email from:", email.from, "to:", email.to);
764
+ console.log("Subject:", email.headers.get("subject"));
765
+ console.log(
766
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
767
+ );
768
+ }
622
769
  }
623
770
  );
624
771
  }
625
772
 
773
+ /**
774
+ * Reply to an email
775
+ * @param email The email to reply to
776
+ * @param options Options for the reply
777
+ * @returns void
778
+ */
779
+ async replyToEmail(
780
+ email: AgentEmail,
781
+ options: {
782
+ fromName: string;
783
+ subject?: string | undefined;
784
+ body: string;
785
+ contentType?: string;
786
+ headers?: Record<string, string>;
787
+ }
788
+ ): Promise<void> {
789
+ return this._tryCatch(async () => {
790
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
791
+ const agentId = this.name;
792
+
793
+ const { createMimeMessage } = await import("mimetext");
794
+ const msg = createMimeMessage();
795
+ msg.setSender({ addr: email.to, name: options.fromName });
796
+ msg.setRecipient(email.from);
797
+ msg.setSubject(
798
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
799
+ );
800
+ msg.addMessage({
801
+ contentType: options.contentType || "text/plain",
802
+ data: options.body
803
+ });
804
+
805
+ const domain = email.from.split("@")[1];
806
+ const messageId = `<${agentId}@${domain}>`;
807
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
808
+ msg.setHeader("Message-ID", messageId);
809
+ msg.setHeader("X-Agent-Name", agentName);
810
+ msg.setHeader("X-Agent-ID", agentId);
811
+
812
+ if (options.headers) {
813
+ for (const [key, value] of Object.entries(options.headers)) {
814
+ msg.setHeader(key, value);
815
+ }
816
+ }
817
+ await email.reply({
818
+ from: email.to,
819
+ raw: msg.asRaw(),
820
+ to: email.from
821
+ });
822
+ });
823
+ }
824
+
626
825
  private async _tryCatch<T>(fn: () => T | Promise<T>) {
627
826
  try {
628
827
  return await fn();
@@ -631,6 +830,73 @@ export class Agent<Env, State = unknown> extends Server<Env> {
631
830
  }
632
831
  }
633
832
 
833
+ /**
834
+ * Automatically wrap custom methods with agent context
835
+ * This ensures getCurrentAgent() works in all custom methods without decorators
836
+ */
837
+ private _autoWrapCustomMethods() {
838
+ // Collect all methods from base prototypes (Agent and Server)
839
+ const basePrototypes = [Agent.prototype, Server.prototype];
840
+ const baseMethods = new Set<string>();
841
+ for (const baseProto of basePrototypes) {
842
+ let proto = baseProto;
843
+ while (proto && proto !== Object.prototype) {
844
+ const methodNames = Object.getOwnPropertyNames(proto);
845
+ for (const methodName of methodNames) {
846
+ baseMethods.add(methodName);
847
+ }
848
+ proto = Object.getPrototypeOf(proto);
849
+ }
850
+ }
851
+ // Get all methods from the current instance's prototype chain
852
+ let proto = Object.getPrototypeOf(this);
853
+ let depth = 0;
854
+ while (proto && proto !== Object.prototype && depth < 10) {
855
+ const methodNames = Object.getOwnPropertyNames(proto);
856
+ for (const methodName of methodNames) {
857
+ // Skip if it's a private method or not a function or a getter
858
+ if (
859
+ baseMethods.has(methodName) ||
860
+ methodName.startsWith("_") ||
861
+ typeof this[methodName as keyof this] !== "function" ||
862
+ !!Object.getOwnPropertyDescriptor(proto, methodName)?.get
863
+ ) {
864
+ continue;
865
+ }
866
+ // If the method doesn't exist in base prototypes, it's a custom method
867
+ if (!baseMethods.has(methodName)) {
868
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
869
+ if (descriptor && typeof descriptor.value === "function") {
870
+ // Wrap the custom method with context
871
+
872
+ const wrappedFunction = withAgentContext(
873
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
874
+ this[methodName as keyof this] as (...args: any[]) => any
875
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
876
+ ) as any;
877
+
878
+ // if the method is callable, copy the metadata from the original method
879
+ if (this._isCallable(methodName)) {
880
+ callableMetadata.set(
881
+ wrappedFunction,
882
+ callableMetadata.get(
883
+ this[methodName as keyof this] as Function
884
+ )!
885
+ );
886
+ }
887
+
888
+ // set the wrapped function on the prototype
889
+ this.constructor.prototype[methodName as keyof this] =
890
+ wrappedFunction;
891
+ }
892
+ }
893
+ }
894
+
895
+ proto = Object.getPrototypeOf(proto);
896
+ depth++;
897
+ }
898
+ }
899
+
634
900
  override onError(
635
901
  connection: Connection,
636
902
  error: unknown
@@ -665,6 +931,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
665
931
  throw new Error("Not implemented");
666
932
  }
667
933
 
934
+ /**
935
+ * Queue a task to be executed in the future
936
+ * @param payload Payload to pass to the callback
937
+ * @param callback Name of the method to call
938
+ * @returns The ID of the queued task
939
+ */
940
+ async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
941
+ const id = nanoid(9);
942
+ if (typeof callback !== "string") {
943
+ throw new Error("Callback must be a string");
944
+ }
945
+
946
+ if (typeof this[callback] !== "function") {
947
+ throw new Error(`this.${callback} is not a function`);
948
+ }
949
+
950
+ this.sql`
951
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
952
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
953
+ `;
954
+
955
+ void this._flushQueue().catch((e) => {
956
+ console.error("Error flushing queue:", e);
957
+ });
958
+
959
+ return id;
960
+ }
961
+
962
+ private _flushingQueue = false;
963
+
964
+ private async _flushQueue() {
965
+ if (this._flushingQueue) {
966
+ return;
967
+ }
968
+ this._flushingQueue = true;
969
+ while (true) {
970
+ const result = this.sql<QueueItem<string>>`
971
+ SELECT * FROM cf_agents_queues
972
+ ORDER BY created_at ASC
973
+ `;
974
+
975
+ if (!result || result.length === 0) {
976
+ break;
977
+ }
978
+
979
+ for (const row of result || []) {
980
+ const callback = this[row.callback as keyof Agent<Env>];
981
+ if (!callback) {
982
+ console.error(`callback ${row.callback} not found`);
983
+ continue;
984
+ }
985
+ const { connection, request, email } = agentContext.getStore() || {};
986
+ await agentContext.run(
987
+ {
988
+ agent: this,
989
+ connection,
990
+ request,
991
+ email
992
+ },
993
+ async () => {
994
+ // TODO: add retries and backoff
995
+ await (
996
+ callback as (
997
+ payload: unknown,
998
+ queueItem: QueueItem<string>
999
+ ) => Promise<void>
1000
+ ).bind(this)(JSON.parse(row.payload as string), row);
1001
+ await this.dequeue(row.id);
1002
+ }
1003
+ );
1004
+ }
1005
+ }
1006
+ this._flushingQueue = false;
1007
+ }
1008
+
1009
+ /**
1010
+ * Dequeue a task by ID
1011
+ * @param id ID of the task to dequeue
1012
+ */
1013
+ async dequeue(id: string) {
1014
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
1015
+ }
1016
+
1017
+ /**
1018
+ * Dequeue all tasks
1019
+ */
1020
+ async dequeueAll() {
1021
+ this.sql`DELETE FROM cf_agents_queues`;
1022
+ }
1023
+
1024
+ /**
1025
+ * Dequeue all tasks by callback
1026
+ * @param callback Name of the callback to dequeue
1027
+ */
1028
+ async dequeueAllByCallback(callback: string) {
1029
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
1030
+ }
1031
+
1032
+ /**
1033
+ * Get a queued task by ID
1034
+ * @param id ID of the task to get
1035
+ * @returns The task or undefined if not found
1036
+ */
1037
+ async getQueue(id: string): Promise<QueueItem<string> | undefined> {
1038
+ const result = this.sql<QueueItem<string>>`
1039
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
1040
+ `;
1041
+ return result
1042
+ ? { ...result[0], payload: JSON.parse(result[0].payload) }
1043
+ : undefined;
1044
+ }
1045
+
1046
+ /**
1047
+ * Get all queues by key and value
1048
+ * @param key Key to filter by
1049
+ * @param value Value to filter by
1050
+ * @returns Array of matching QueueItem objects
1051
+ */
1052
+ async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
1053
+ const result = this.sql<QueueItem<string>>`
1054
+ SELECT * FROM cf_agents_queues
1055
+ `;
1056
+ return result.filter((row) => JSON.parse(row.payload)[key] === value);
1057
+ }
1058
+
668
1059
  /**
669
1060
  * Schedule a task to be executed in the future
670
1061
  * @template T Type of the payload data
@@ -680,6 +1071,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
680
1071
  ): Promise<Schedule<T>> {
681
1072
  const id = nanoid(9);
682
1073
 
1074
+ const emitScheduleCreate = (schedule: Schedule<T>) =>
1075
+ this.observability?.emit(
1076
+ {
1077
+ displayMessage: `Schedule ${schedule.id} created`,
1078
+ id: nanoid(),
1079
+ payload: {
1080
+ callback: callback as string,
1081
+ id: id
1082
+ },
1083
+ timestamp: Date.now(),
1084
+ type: "schedule:create"
1085
+ },
1086
+ this.ctx
1087
+ );
1088
+
683
1089
  if (typeof callback !== "string") {
684
1090
  throw new Error("Callback must be a string");
685
1091
  }
@@ -699,13 +1105,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
699
1105
 
700
1106
  await this._scheduleNextAlarm();
701
1107
 
702
- return {
1108
+ const schedule: Schedule<T> = {
703
1109
  callback: callback,
704
1110
  id,
705
1111
  payload: payload as T,
706
1112
  time: timestamp,
707
- type: "scheduled",
1113
+ type: "scheduled"
708
1114
  };
1115
+
1116
+ emitScheduleCreate(schedule);
1117
+
1118
+ return schedule;
709
1119
  }
710
1120
  if (typeof when === "number") {
711
1121
  const time = new Date(Date.now() + when * 1000);
@@ -720,14 +1130,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
720
1130
 
721
1131
  await this._scheduleNextAlarm();
722
1132
 
723
- return {
1133
+ const schedule: Schedule<T> = {
724
1134
  callback: callback,
725
1135
  delayInSeconds: when,
726
1136
  id,
727
1137
  payload: payload as T,
728
1138
  time: timestamp,
729
- type: "delayed",
1139
+ type: "delayed"
730
1140
  };
1141
+
1142
+ emitScheduleCreate(schedule);
1143
+
1144
+ return schedule;
731
1145
  }
732
1146
  if (typeof when === "string") {
733
1147
  const nextExecutionTime = getNextCronTime(when);
@@ -742,14 +1156,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
742
1156
 
743
1157
  await this._scheduleNextAlarm();
744
1158
 
745
- return {
1159
+ const schedule: Schedule<T> = {
746
1160
  callback: callback,
747
1161
  cron: when,
748
1162
  id,
749
1163
  payload: payload as T,
750
1164
  time: timestamp,
751
- type: "cron",
1165
+ type: "cron"
752
1166
  };
1167
+
1168
+ emitScheduleCreate(schedule);
1169
+
1170
+ return schedule;
753
1171
  }
754
1172
  throw new Error("Invalid schedule type");
755
1173
  }
@@ -813,7 +1231,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
813
1231
  .toArray()
814
1232
  .map((row) => ({
815
1233
  ...row,
816
- payload: JSON.parse(row.payload as string) as T,
1234
+ payload: JSON.parse(row.payload as string) as T
817
1235
  })) as Schedule<T>[];
818
1236
 
819
1237
  return result;
@@ -825,6 +1243,22 @@ export class Agent<Env, State = unknown> extends Server<Env> {
825
1243
  * @returns true if the task was cancelled, false otherwise
826
1244
  */
827
1245
  async cancelSchedule(id: string): Promise<boolean> {
1246
+ const schedule = await this.getSchedule(id);
1247
+ if (schedule) {
1248
+ this.observability?.emit(
1249
+ {
1250
+ displayMessage: `Schedule ${id} cancelled`,
1251
+ id: nanoid(),
1252
+ payload: {
1253
+ callback: schedule.callback,
1254
+ id: schedule.id
1255
+ },
1256
+ timestamp: Date.now(),
1257
+ type: "schedule:cancel"
1258
+ },
1259
+ this.ctx
1260
+ );
1261
+ }
828
1262
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
829
1263
 
830
1264
  await this._scheduleNextAlarm();
@@ -834,9 +1268,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
834
1268
  private async _scheduleNextAlarm() {
835
1269
  // Find the next schedule that needs to be executed
836
1270
  const result = this.sql`
837
- SELECT time FROM cf_agents_schedules
1271
+ SELECT time FROM cf_agents_schedules
838
1272
  WHERE time > ${Math.floor(Date.now() / 1000)}
839
- ORDER BY time ASC
1273
+ ORDER BY time ASC
840
1274
  LIMIT 1
841
1275
  `;
842
1276
  if (!result) return;
@@ -863,40 +1297,61 @@ export class Agent<Env, State = unknown> extends Server<Env> {
863
1297
  SELECT * FROM cf_agents_schedules WHERE time <= ${now}
864
1298
  `;
865
1299
 
866
- for (const row of result || []) {
867
- const callback = this[row.callback as keyof Agent<Env>];
868
- if (!callback) {
869
- console.error(`callback ${row.callback} not found`);
870
- continue;
871
- }
872
- await agentContext.run(
873
- { agent: this, connection: undefined, request: undefined },
874
- async () => {
875
- try {
876
- await (
877
- callback as (
878
- payload: unknown,
879
- schedule: Schedule<unknown>
880
- ) => Promise<void>
881
- ).bind(this)(JSON.parse(row.payload as string), row);
882
- } catch (e) {
883
- console.error(`error executing callback "${row.callback}"`, e);
884
- }
1300
+ if (result && Array.isArray(result)) {
1301
+ for (const row of result) {
1302
+ const callback = this[row.callback as keyof Agent<Env>];
1303
+ if (!callback) {
1304
+ console.error(`callback ${row.callback} not found`);
1305
+ continue;
885
1306
  }
886
- );
887
- if (row.type === "cron") {
888
- // Update next execution time for cron schedules
889
- const nextExecutionTime = getNextCronTime(row.cron);
890
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1307
+ await agentContext.run(
1308
+ {
1309
+ agent: this,
1310
+ connection: undefined,
1311
+ request: undefined,
1312
+ email: undefined
1313
+ },
1314
+ async () => {
1315
+ try {
1316
+ this.observability?.emit(
1317
+ {
1318
+ displayMessage: `Schedule ${row.id} executed`,
1319
+ id: nanoid(),
1320
+ payload: {
1321
+ callback: row.callback,
1322
+ id: row.id
1323
+ },
1324
+ timestamp: Date.now(),
1325
+ type: "schedule:execute"
1326
+ },
1327
+ this.ctx
1328
+ );
891
1329
 
892
- this.sql`
1330
+ await (
1331
+ callback as (
1332
+ payload: unknown,
1333
+ schedule: Schedule<unknown>
1334
+ ) => Promise<void>
1335
+ ).bind(this)(JSON.parse(row.payload as string), row);
1336
+ } catch (e) {
1337
+ console.error(`error executing callback "${row.callback}"`, e);
1338
+ }
1339
+ }
1340
+ );
1341
+ if (row.type === "cron") {
1342
+ // Update next execution time for cron schedules
1343
+ const nextExecutionTime = getNextCronTime(row.cron);
1344
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1345
+
1346
+ this.sql`
893
1347
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
894
1348
  `;
895
- } else {
896
- // Delete one-time schedules after execution
897
- this.sql`
1349
+ } else {
1350
+ // Delete one-time schedules after execution
1351
+ this.sql`
898
1352
  DELETE FROM cf_agents_schedules WHERE id = ${row.id}
899
1353
  `;
1354
+ }
900
1355
  }
901
1356
  }
902
1357
 
@@ -912,11 +1367,23 @@ export class Agent<Env, State = unknown> extends Server<Env> {
912
1367
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
913
1368
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
914
1369
  this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1370
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
915
1371
 
916
1372
  // delete all alarms
917
1373
  await this.ctx.storage.deleteAlarm();
918
1374
  await this.ctx.storage.deleteAll();
919
1375
  this.ctx.abort("destroyed"); // enforce that the agent is evicted
1376
+
1377
+ this.observability?.emit(
1378
+ {
1379
+ displayMessage: "Agent destroyed",
1380
+ id: nanoid(),
1381
+ payload: {},
1382
+ timestamp: Date.now(),
1383
+ type: "destroy"
1384
+ },
1385
+ this.ctx
1386
+ );
920
1387
  }
921
1388
 
922
1389
  /**
@@ -956,11 +1423,24 @@ export class Agent<Env, State = unknown> extends Server<Env> {
956
1423
  callbackUrl,
957
1424
  options
958
1425
  );
1426
+ this.sql`
1427
+ INSERT
1428
+ OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1429
+ VALUES (
1430
+ ${result.id},
1431
+ ${serverName},
1432
+ ${url},
1433
+ ${result.clientId ?? null},
1434
+ ${result.authUrl ?? null},
1435
+ ${callbackUrl},
1436
+ ${options ? JSON.stringify(options) : null}
1437
+ );
1438
+ `;
959
1439
 
960
1440
  this.broadcast(
961
1441
  JSON.stringify({
962
1442
  mcp: this.getMcpServers(),
963
- type: "cf_agent_mcp_servers",
1443
+ type: MessageType.CF_AGENT_MCP_SERVERS
964
1444
  })
965
1445
  );
966
1446
 
@@ -968,7 +1448,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
968
1448
  }
969
1449
 
970
1450
  async _connectToMcpServerInternal(
971
- serverName: string,
1451
+ _serverName: string,
972
1452
  url: string,
973
1453
  callbackUrl: string,
974
1454
  // it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
@@ -989,7 +1469,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
989
1469
  id: string;
990
1470
  oauthClientId?: string;
991
1471
  }
992
- ): Promise<{ id: string; authUrl: string | undefined }> {
1472
+ ): Promise<{
1473
+ id: string;
1474
+ authUrl: string | undefined;
1475
+ clientId: string | undefined;
1476
+ }> {
993
1477
  const authProvider = new DurableObjectOAuthClientProvider(
994
1478
  this.ctx.storage,
995
1479
  this.name,
@@ -1012,12 +1496,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1012
1496
  fetch: (url, init) =>
1013
1497
  fetch(url, {
1014
1498
  ...init,
1015
- headers: options?.transport?.headers,
1016
- }),
1499
+ headers: options?.transport?.headers
1500
+ })
1017
1501
  },
1018
1502
  requestInit: {
1019
- headers: options?.transport?.headers,
1020
- },
1503
+ headers: options?.transport?.headers
1504
+ }
1021
1505
  };
1022
1506
  }
1023
1507
 
@@ -1026,26 +1510,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1026
1510
  reconnect,
1027
1511
  transport: {
1028
1512
  ...headerTransportOpts,
1029
- authProvider,
1030
- },
1513
+ authProvider
1514
+ }
1031
1515
  });
1032
1516
 
1033
- this.sql`
1034
- INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1035
- VALUES (
1036
- ${id},
1037
- ${serverName},
1038
- ${url},
1039
- ${clientId ?? null},
1040
- ${authUrl ?? null},
1041
- ${callbackUrl},
1042
- ${options ? JSON.stringify(options) : null}
1043
- );
1044
- `;
1045
-
1046
1517
  return {
1047
1518
  authUrl,
1048
- id,
1519
+ clientId,
1520
+ id
1049
1521
  };
1050
1522
  }
1051
1523
 
@@ -1057,7 +1529,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1057
1529
  this.broadcast(
1058
1530
  JSON.stringify({
1059
1531
  mcp: this.getMcpServers(),
1060
- type: "cf_agent_mcp_servers",
1532
+ type: MessageType.CF_AGENT_MCP_SERVERS
1061
1533
  })
1062
1534
  );
1063
1535
  }
@@ -1067,24 +1539,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1067
1539
  prompts: this.mcp.listPrompts(),
1068
1540
  resources: this.mcp.listResources(),
1069
1541
  servers: {},
1070
- tools: this.mcp.listTools(),
1542
+ tools: this.mcp.listTools()
1071
1543
  };
1072
1544
 
1073
1545
  const servers = this.sql<MCPServerRow>`
1074
1546
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1075
1547
  `;
1076
1548
 
1077
- for (const server of servers) {
1078
- const serverConn = this.mcp.mcpConnections[server.id];
1079
- mcpState.servers[server.id] = {
1080
- auth_url: server.auth_url,
1081
- capabilities: serverConn?.serverCapabilities ?? null,
1082
- instructions: serverConn?.instructions ?? null,
1083
- name: server.name,
1084
- server_url: server.server_url,
1085
- // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1086
- state: serverConn?.connectionState ?? "authenticating",
1087
- };
1549
+ if (servers && Array.isArray(servers) && servers.length > 0) {
1550
+ for (const server of servers) {
1551
+ const serverConn = this.mcp.mcpConnections[server.id];
1552
+ mcpState.servers[server.id] = {
1553
+ auth_url: server.auth_url,
1554
+ capabilities: serverConn?.serverCapabilities ?? null,
1555
+ instructions: serverConn?.instructions ?? null,
1556
+ name: server.name,
1557
+ server_url: server.server_url,
1558
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1559
+ state: serverConn?.connectionState ?? "authenticating"
1560
+ };
1561
+ }
1088
1562
  }
1089
1563
 
1090
1564
  return mcpState;
@@ -1131,14 +1605,14 @@ export async function routeAgentRequest<Env>(
1131
1605
  "Access-Control-Allow-Credentials": "true",
1132
1606
  "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1133
1607
  "Access-Control-Allow-Origin": "*",
1134
- "Access-Control-Max-Age": "86400",
1608
+ "Access-Control-Max-Age": "86400"
1135
1609
  }
1136
1610
  : options?.cors;
1137
1611
 
1138
1612
  if (request.method === "OPTIONS") {
1139
1613
  if (corsHeaders) {
1140
1614
  return new Response(null, {
1141
- headers: corsHeaders,
1615
+ headers: corsHeaders
1142
1616
  });
1143
1617
  }
1144
1618
  console.warn(
@@ -1151,7 +1625,7 @@ export async function routeAgentRequest<Env>(
1151
1625
  env as Record<string, unknown>,
1152
1626
  {
1153
1627
  prefix: "agents",
1154
- ...(options as PartyServerOptions<Record<string, unknown>>),
1628
+ ...(options as PartyServerOptions<Record<string, unknown>>)
1155
1629
  }
1156
1630
  );
1157
1631
 
@@ -1164,24 +1638,238 @@ export async function routeAgentRequest<Env>(
1164
1638
  response = new Response(response.body, {
1165
1639
  headers: {
1166
1640
  ...response.headers,
1167
- ...corsHeaders,
1168
- },
1641
+ ...corsHeaders
1642
+ }
1169
1643
  });
1170
1644
  }
1171
1645
  return response;
1172
1646
  }
1173
1647
 
1648
+ export type EmailResolver<Env> = (
1649
+ email: ForwardableEmailMessage,
1650
+ env: Env
1651
+ ) => Promise<{
1652
+ agentName: string;
1653
+ agentId: string;
1654
+ } | null>;
1655
+
1656
+ /**
1657
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1658
+ * @returns A function that resolves the agent to route the email to
1659
+ */
1660
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1661
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1662
+ const messageId = email.headers.get("message-id");
1663
+ if (messageId) {
1664
+ const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
1665
+ if (messageIdMatch) {
1666
+ const [, agentId, domain] = messageIdMatch;
1667
+ const agentName = domain.split(".")[0];
1668
+ return { agentName, agentId };
1669
+ }
1670
+ }
1671
+
1672
+ const references = email.headers.get("references");
1673
+ if (references) {
1674
+ const referencesMatch = references.match(
1675
+ /<([A-Za-z0-9+/]{43}=)@([^>]+)>/
1676
+ );
1677
+ if (referencesMatch) {
1678
+ const [, base64Id, domain] = referencesMatch;
1679
+ const agentId = Buffer.from(base64Id, "base64").toString("hex");
1680
+ const agentName = domain.split(".")[0];
1681
+ return { agentName, agentId };
1682
+ }
1683
+ }
1684
+
1685
+ const agentName = email.headers.get("x-agent-name");
1686
+ const agentId = email.headers.get("x-agent-id");
1687
+ if (agentName && agentId) {
1688
+ return { agentName, agentId };
1689
+ }
1690
+
1691
+ return null;
1692
+ };
1693
+ }
1694
+
1695
+ /**
1696
+ * Create a resolver that uses the email address to determine the agent to route the email to
1697
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1698
+ * @returns A function that resolves the agent to route the email to
1699
+ */
1700
+ export function createAddressBasedEmailResolver<Env>(
1701
+ defaultAgentName: string
1702
+ ): EmailResolver<Env> {
1703
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1704
+ const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
1705
+ if (!emailMatch) {
1706
+ return null;
1707
+ }
1708
+
1709
+ const [, localPart, subAddress] = emailMatch;
1710
+
1711
+ if (subAddress) {
1712
+ return {
1713
+ agentName: localPart,
1714
+ agentId: subAddress
1715
+ };
1716
+ }
1717
+
1718
+ // Option 2: Use defaultAgentName namespace, localPart as agentId
1719
+ // Common for catch-all email routing to a single EmailAgent namespace
1720
+ return {
1721
+ agentName: defaultAgentName,
1722
+ agentId: localPart
1723
+ };
1724
+ };
1725
+ }
1726
+
1727
+ /**
1728
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1729
+ * @param agentName The name of the agent to route the email to
1730
+ * @param agentId The id of the agent to route the email to
1731
+ * @returns A function that resolves the agent to route the email to
1732
+ */
1733
+ export function createCatchAllEmailResolver<Env>(
1734
+ agentName: string,
1735
+ agentId: string
1736
+ ): EmailResolver<Env> {
1737
+ return async () => ({ agentName, agentId });
1738
+ }
1739
+
1740
+ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1741
+ resolver: EmailResolver<Env>;
1742
+ };
1743
+
1744
+ // Cache the agent namespace map for email routing
1745
+ // This maps both kebab-case and original names to namespaces
1746
+ const agentMapCache = new WeakMap<
1747
+ Record<string, unknown>,
1748
+ Record<string, unknown>
1749
+ >();
1750
+
1174
1751
  /**
1175
1752
  * Route an email to the appropriate Agent
1176
- * @param email Email message to route
1177
- * @param env Environment containing Agent bindings
1178
- * @param options Routing options
1753
+ * @param email The email to route
1754
+ * @param env The environment containing the Agent bindings
1755
+ * @param options The options for routing the email
1756
+ * @returns A promise that resolves when the email has been routed
1179
1757
  */
1180
1758
  export async function routeAgentEmail<Env>(
1181
- _email: ForwardableEmailMessage,
1182
- _env: Env,
1183
- _options?: AgentOptions<Env>
1184
- ): Promise<void> {}
1759
+ email: ForwardableEmailMessage,
1760
+ env: Env,
1761
+ options: EmailRoutingOptions<Env>
1762
+ ): Promise<void> {
1763
+ const routingInfo = await options.resolver(email, env);
1764
+
1765
+ if (!routingInfo) {
1766
+ console.warn("No routing information found for email, dropping message");
1767
+ return;
1768
+ }
1769
+
1770
+ // Build a map that includes both original names and kebab-case versions
1771
+ if (!agentMapCache.has(env as Record<string, unknown>)) {
1772
+ const map: Record<string, unknown> = {};
1773
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1774
+ if (
1775
+ value &&
1776
+ typeof value === "object" &&
1777
+ "idFromName" in value &&
1778
+ typeof value.idFromName === "function"
1779
+ ) {
1780
+ // Add both the original name and kebab-case version
1781
+ map[key] = value;
1782
+ map[camelCaseToKebabCase(key)] = value;
1783
+ }
1784
+ }
1785
+ agentMapCache.set(env as Record<string, unknown>, map);
1786
+ }
1787
+
1788
+ const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
1789
+ const namespace = agentMap[routingInfo.agentName];
1790
+
1791
+ if (!namespace) {
1792
+ // Provide helpful error message listing available agents
1793
+ const availableAgents = Object.keys(agentMap)
1794
+ .filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
1795
+ .join(", ");
1796
+ throw new Error(
1797
+ `Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
1798
+ );
1799
+ }
1800
+
1801
+ const agent = await getAgentByName(
1802
+ namespace as unknown as AgentNamespace<Agent<Env>>,
1803
+ routingInfo.agentId
1804
+ );
1805
+
1806
+ // let's make a serialisable version of the email
1807
+ const serialisableEmail: AgentEmail = {
1808
+ getRaw: async () => {
1809
+ const reader = email.raw.getReader();
1810
+ const chunks: Uint8Array[] = [];
1811
+
1812
+ let done = false;
1813
+ while (!done) {
1814
+ const { value, done: readerDone } = await reader.read();
1815
+ done = readerDone;
1816
+ if (value) {
1817
+ chunks.push(value);
1818
+ }
1819
+ }
1820
+
1821
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1822
+ const combined = new Uint8Array(totalLength);
1823
+ let offset = 0;
1824
+ for (const chunk of chunks) {
1825
+ combined.set(chunk, offset);
1826
+ offset += chunk.length;
1827
+ }
1828
+
1829
+ return combined;
1830
+ },
1831
+ headers: email.headers,
1832
+ rawSize: email.rawSize,
1833
+ setReject: (reason: string) => {
1834
+ email.setReject(reason);
1835
+ },
1836
+ forward: (rcptTo: string, headers?: Headers) => {
1837
+ return email.forward(rcptTo, headers);
1838
+ },
1839
+ reply: (options: { from: string; to: string; raw: string }) => {
1840
+ return email.reply(
1841
+ new EmailMessage(options.from, options.to, options.raw)
1842
+ );
1843
+ },
1844
+ from: email.from,
1845
+ to: email.to
1846
+ };
1847
+
1848
+ await agent._onEmail(serialisableEmail);
1849
+ }
1850
+
1851
+ export type AgentEmail = {
1852
+ from: string;
1853
+ to: string;
1854
+ getRaw: () => Promise<Uint8Array>;
1855
+ headers: Headers;
1856
+ rawSize: number;
1857
+ setReject: (reason: string) => void;
1858
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1859
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1860
+ };
1861
+
1862
+ export type EmailSendOptions = {
1863
+ to: string;
1864
+ subject: string;
1865
+ body: string;
1866
+ contentType?: string;
1867
+ headers?: Record<string, string>;
1868
+ includeRoutingHeaders?: boolean;
1869
+ agentName?: string;
1870
+ agentId?: string;
1871
+ domain?: string;
1872
+ };
1185
1873
 
1186
1874
  /**
1187
1875
  * Get or create an Agent by name
@@ -1229,7 +1917,7 @@ export class StreamingResponse {
1229
1917
  id: this._id,
1230
1918
  result: chunk,
1231
1919
  success: true,
1232
- type: "rpc",
1920
+ type: MessageType.RPC
1233
1921
  };
1234
1922
  this._connection.send(JSON.stringify(response));
1235
1923
  }
@@ -1248,7 +1936,7 @@ export class StreamingResponse {
1248
1936
  id: this._id,
1249
1937
  result: finalChunk,
1250
1938
  success: true,
1251
- type: "rpc",
1939
+ type: MessageType.RPC
1252
1940
  };
1253
1941
  this._connection.send(JSON.stringify(response));
1254
1942
  }