agents 0.0.0-8157d08 → 0.0.0-8234d41

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 +255 -27
  2. package/dist/ai-chat-agent.d.ts +12 -9
  3. package/dist/ai-chat-agent.js +150 -59
  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 +68 -71
  9. package/dist/ai-react.js +177 -37
  10. package/dist/ai-react.js.map +1 -1
  11. package/dist/ai-types.d.ts +37 -19
  12. package/dist/ai-types.js +6 -0
  13. package/dist/chunk-BER7KXUJ.js +18 -0
  14. package/dist/chunk-BER7KXUJ.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-JFRK72K3.js → chunk-M5SZRJTA.js} +519 -129
  18. package/dist/chunk-M5SZRJTA.js.map +1 -0
  19. package/dist/{chunk-E3LCYPCB.js → chunk-MH46VMM4.js} +163 -20
  20. package/dist/chunk-MH46VMM4.js.map +1 -0
  21. package/dist/{chunk-NKZZ66QY.js → chunk-QEVM4BVL.js} +5 -5
  22. package/dist/chunk-QEVM4BVL.js.map +1 -0
  23. package/dist/chunk-UJVEAURM.js +150 -0
  24. package/dist/chunk-UJVEAURM.js.map +1 -0
  25. package/dist/client-CvaJdLQA.d.ts +5015 -0
  26. package/dist/client.d.ts +2 -2
  27. package/dist/client.js +2 -1
  28. package/dist/index.d.ts +557 -24
  29. package/dist/index.js +13 -4
  30. package/dist/mcp/client.d.ts +9 -1053
  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 +64 -56
  35. package/dist/mcp/index.js +952 -637
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/observability/index.d.ts +46 -12
  38. package/dist/observability/index.js +5 -4
  39. package/dist/react.d.ts +10 -6
  40. package/dist/react.js +7 -5
  41. package/dist/react.js.map +1 -1
  42. package/dist/schedule.d.ts +83 -9
  43. package/dist/schedule.js +15 -2
  44. package/dist/schedule.js.map +1 -1
  45. package/package.json +21 -10
  46. package/src/index.ts +761 -173
  47. package/dist/chunk-767EASBA.js.map +0 -1
  48. package/dist/chunk-E3LCYPCB.js.map +0 -1
  49. package/dist/chunk-JFRK72K3.js.map +0 -1
  50. package/dist/chunk-NKZZ66QY.js.map +0 -1
  51. package/dist/index-CITGJflw.d.ts +0 -486
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,24 +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";
26
28
  import { genericObservability, type Observability } from "./observability";
29
+ import { MessageType } from "./ai-types";
27
30
 
28
31
  export type { Connection, ConnectionContext, WSMessage } from "partyserver";
29
32
 
@@ -41,7 +44,7 @@ export type RPCRequest = {
41
44
  * State update message from client
42
45
  */
43
46
  export type StateUpdateMessage = {
44
- type: "cf_agent_state";
47
+ type: MessageType.CF_AGENT_STATE;
45
48
  state: unknown;
46
49
  };
47
50
 
@@ -49,7 +52,7 @@ export type StateUpdateMessage = {
49
52
  * RPC response message to client
50
53
  */
51
54
  export type RPCResponse = {
52
- type: "rpc";
55
+ type: MessageType.RPC;
53
56
  id: string;
54
57
  } & (
55
58
  | {
@@ -76,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
76
79
  typeof msg === "object" &&
77
80
  msg !== null &&
78
81
  "type" in msg &&
79
- msg.type === "rpc" &&
82
+ msg.type === MessageType.RPC &&
80
83
  "id" in msg &&
81
84
  typeof msg.id === "string" &&
82
85
  "method" in msg &&
@@ -94,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
94
97
  typeof msg === "object" &&
95
98
  msg !== null &&
96
99
  "type" in msg &&
97
- msg.type === "cf_agent_state" &&
100
+ msg.type === MessageType.CF_AGENT_STATE &&
98
101
  "state" in msg
99
102
  );
100
103
  }
@@ -115,7 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
115
118
  * Decorator that marks a method as callable by clients
116
119
  * @param metadata Optional metadata about the callable method
117
120
  */
118
- export function unstable_callable(metadata: CallableMetadata = {}) {
121
+ export function callable(metadata: CallableMetadata = {}) {
119
122
  return function callableDecorator<This, Args extends unknown[], Return>(
120
123
  target: (this: This, ...args: Args) => Return,
121
124
  // biome-ignore lint/correctness/noUnusedFunctionParameters: later
@@ -129,6 +132,30 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
129
132
  };
130
133
  }
131
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
+
132
159
  /**
133
160
  * Represents a scheduled task within an Agent
134
161
  * @template T Type of the payload data
@@ -174,7 +201,7 @@ function getNextCronTime(cron: string) {
174
201
  * MCP Server state update message from server -> Client
175
202
  */
176
203
  export type MCPServerMessage = {
177
- type: "cf_agent_mcp_servers";
204
+ type: MessageType.CF_AGENT_MCP_SERVERS;
178
205
  mcp: MCPServersState;
179
206
  };
180
207
 
@@ -218,23 +245,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
218
245
  const DEFAULT_STATE = {} as unknown;
219
246
 
220
247
  const agentContext = new AsyncLocalStorage<{
221
- agent: Agent<unknown>;
248
+ agent: Agent<unknown, unknown>;
222
249
  connection: Connection | undefined;
223
250
  request: Request | undefined;
251
+ email: AgentEmail | undefined;
224
252
  }>();
225
253
 
226
254
  export function getCurrentAgent<
227
- T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
255
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
228
256
  >(): {
229
257
  agent: T | undefined;
230
258
  connection: Connection | undefined;
231
- request: Request<unknown, CfProperties<unknown>> | undefined;
259
+ request: Request | undefined;
260
+ email: AgentEmail | undefined;
232
261
  } {
233
262
  const store = agentContext.getStore() as
234
263
  | {
235
264
  agent: T;
236
265
  connection: Connection | undefined;
237
- request: Request<unknown, CfProperties<unknown>> | undefined;
266
+ request: Request | undefined;
267
+ email: AgentEmail | undefined;
238
268
  }
239
269
  | undefined;
240
270
  if (!store) {
@@ -242,17 +272,47 @@ export function getCurrentAgent<
242
272
  agent: undefined,
243
273
  connection: undefined,
244
274
  request: undefined,
275
+ email: undefined
245
276
  };
246
277
  }
247
278
  return store;
248
279
  }
249
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
+
250
306
  /**
251
307
  * Base class for creating Agent implementations
252
308
  * @template Env Environment type containing bindings
253
309
  * @template State State type to store within the Agent
254
310
  */
255
- export class Agent<Env, State = unknown> extends Server<Env> {
311
+ export class Agent<
312
+ Env = typeof env,
313
+ State = unknown,
314
+ Props extends Record<string, unknown> = Record<string, unknown>
315
+ > extends Server<Env, Props> {
256
316
  private _state = DEFAULT_STATE as State;
257
317
 
258
318
  private _ParentClass: typeof Agent<Env, State> =
@@ -314,7 +374,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
314
374
  */
315
375
  static options = {
316
376
  /** Whether the Agent should hibernate when inactive */
317
- hibernate: true, // default to hibernate
377
+ hibernate: true // default to hibernate
318
378
  };
319
379
 
320
380
  /**
@@ -351,6 +411,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
351
411
  constructor(ctx: AgentContext, env: Env) {
352
412
  super(ctx, env);
353
413
 
414
+ if (!wrappedClasses.has(this.constructor)) {
415
+ // Auto-wrap custom methods with agent context
416
+ this._autoWrapCustomMethods();
417
+ wrappedClasses.add(this.constructor);
418
+ }
419
+
354
420
  this.sql`
355
421
  CREATE TABLE IF NOT EXISTS cf_agents_state (
356
422
  id TEXT PRIMARY KEY NOT NULL,
@@ -358,6 +424,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
358
424
  )
359
425
  `;
360
426
 
427
+ this.sql`
428
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
429
+ id TEXT PRIMARY KEY NOT NULL,
430
+ payload TEXT,
431
+ callback TEXT,
432
+ created_at INTEGER DEFAULT (unixepoch())
433
+ )
434
+ `;
435
+
361
436
  void this.ctx.blockConcurrencyWhile(async () => {
362
437
  return this._tryCatch(async () => {
363
438
  // Create alarms table if it doesn't exist
@@ -394,7 +469,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
394
469
  const _onRequest = this.onRequest.bind(this);
395
470
  this.onRequest = (request: Request) => {
396
471
  return agentContext.run(
397
- { agent: this, connection: undefined, request },
472
+ { agent: this, connection: undefined, request, email: undefined },
398
473
  async () => {
399
474
  if (this.mcp.isCallbackRequest(request)) {
400
475
  await this.mcp.handleCallbackRequest(request);
@@ -403,14 +478,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
403
478
  this.broadcast(
404
479
  JSON.stringify({
405
480
  mcp: this.getMcpServers(),
406
- type: "cf_agent_mcp_servers",
481
+ type: MessageType.CF_AGENT_MCP_SERVERS
407
482
  })
408
483
  );
409
484
 
410
485
  // We probably should let the user configure this response/redirect, but this is fine for now.
411
486
  return new Response("<script>window.close();</script>", {
412
487
  headers: { "content-type": "text/html" },
413
- status: 200,
488
+ status: 200
414
489
  });
415
490
  }
416
491
 
@@ -422,7 +497,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
422
497
  const _onMessage = this.onMessage.bind(this);
423
498
  this.onMessage = async (connection: Connection, message: WSMessage) => {
424
499
  return agentContext.run(
425
- { agent: this, connection, request: undefined },
500
+ { agent: this, connection, request: undefined, email: undefined },
426
501
  async () => {
427
502
  if (typeof message !== "string") {
428
503
  return this._tryCatch(() => _onMessage(connection, message));
@@ -472,13 +547,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
472
547
  displayMessage: `RPC call to ${method}`,
473
548
  id: nanoid(),
474
549
  payload: {
475
- args,
476
550
  method,
477
- streaming: metadata?.streaming,
478
- success: true,
551
+ streaming: metadata?.streaming
479
552
  },
480
553
  timestamp: Date.now(),
481
- type: "rpc",
554
+ type: "rpc"
482
555
  },
483
556
  this.ctx
484
557
  );
@@ -488,7 +561,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
488
561
  id,
489
562
  result,
490
563
  success: true,
491
- type: "rpc",
564
+ type: MessageType.RPC
492
565
  };
493
566
  connection.send(JSON.stringify(response));
494
567
  } catch (e) {
@@ -498,7 +571,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
498
571
  e instanceof Error ? e.message : "Unknown error occurred",
499
572
  id: parsed.id,
500
573
  success: false,
501
- type: "rpc",
574
+ type: MessageType.RPC
502
575
  };
503
576
  connection.send(JSON.stringify(response));
504
577
  console.error("RPC error:", e);
@@ -516,77 +589,104 @@ export class Agent<Env, State = unknown> extends Server<Env> {
516
589
  // TODO: This is a hack to ensure the state is sent after the connection is established
517
590
  // must fix this
518
591
  return agentContext.run(
519
- { agent: this, connection, request: ctx.request },
520
- async () => {
521
- setTimeout(() => {
522
- if (this.state) {
523
- connection.send(
524
- JSON.stringify({
525
- state: this.state,
526
- type: "cf_agent_state",
527
- })
528
- );
529
- }
530
-
592
+ { agent: this, connection, request: ctx.request, email: undefined },
593
+ () => {
594
+ if (this.state) {
531
595
  connection.send(
532
596
  JSON.stringify({
533
- mcp: this.getMcpServers(),
534
- type: "cf_agent_mcp_servers",
597
+ state: this.state,
598
+ type: MessageType.CF_AGENT_STATE
535
599
  })
536
600
  );
601
+ }
537
602
 
538
- this.observability?.emit(
539
- {
540
- displayMessage: "Connection established",
541
- id: nanoid(),
542
- payload: {
543
- connectionId: connection.id,
544
- },
545
- timestamp: Date.now(),
546
- type: "connect",
603
+ connection.send(
604
+ JSON.stringify({
605
+ mcp: this.getMcpServers(),
606
+ type: MessageType.CF_AGENT_MCP_SERVERS
607
+ })
608
+ );
609
+
610
+ this.observability?.emit(
611
+ {
612
+ displayMessage: "Connection established",
613
+ id: nanoid(),
614
+ payload: {
615
+ connectionId: connection.id
547
616
  },
548
- this.ctx
549
- );
550
- return this._tryCatch(() => _onConnect(connection, ctx));
551
- }, 20);
617
+ timestamp: Date.now(),
618
+ type: "connect"
619
+ },
620
+ this.ctx
621
+ );
622
+ return this._tryCatch(() => _onConnect(connection, ctx));
552
623
  }
553
624
  );
554
625
  };
555
626
 
556
627
  const _onStart = this.onStart.bind(this);
557
- this.onStart = async () => {
628
+ this.onStart = async (props?: Props) => {
558
629
  return agentContext.run(
559
- { agent: this, connection: undefined, request: undefined },
630
+ {
631
+ agent: this,
632
+ connection: undefined,
633
+ request: undefined,
634
+ email: undefined
635
+ },
560
636
  async () => {
561
- const servers = this.sql<MCPServerRow>`
637
+ await this._tryCatch(() => {
638
+ const servers = this.sql<MCPServerRow>`
562
639
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
563
640
  `;
564
641
 
565
- // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
566
- Promise.allSettled(
567
- servers.map((server) => {
568
- return this._connectToMcpServerInternal(
569
- server.name,
570
- server.server_url,
571
- server.callback_url,
572
- server.server_options
573
- ? JSON.parse(server.server_options)
574
- : undefined,
575
- {
576
- id: server.id,
577
- oauthClientId: server.client_id ?? undefined,
578
- }
579
- );
580
- })
581
- ).then((_results) => {
582
642
  this.broadcast(
583
643
  JSON.stringify({
584
644
  mcp: this.getMcpServers(),
585
- type: "cf_agent_mcp_servers",
645
+ type: MessageType.CF_AGENT_MCP_SERVERS
586
646
  })
587
647
  );
648
+
649
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
650
+ if (servers && Array.isArray(servers) && servers.length > 0) {
651
+ servers.forEach((server) => {
652
+ this._connectToMcpServerInternal(
653
+ server.name,
654
+ server.server_url,
655
+ server.callback_url,
656
+ server.server_options
657
+ ? JSON.parse(server.server_options)
658
+ : undefined,
659
+ {
660
+ id: server.id,
661
+ oauthClientId: server.client_id ?? undefined
662
+ }
663
+ )
664
+ .then(() => {
665
+ // Broadcast updated MCP servers state after each server connects
666
+ this.broadcast(
667
+ JSON.stringify({
668
+ mcp: this.getMcpServers(),
669
+ type: MessageType.CF_AGENT_MCP_SERVERS
670
+ })
671
+ );
672
+ })
673
+ .catch((error) => {
674
+ console.error(
675
+ `Error connecting to MCP server: ${server.name} (${server.server_url})`,
676
+ error
677
+ );
678
+ // Still broadcast even if connection fails, so clients know about the failure
679
+ this.broadcast(
680
+ JSON.stringify({
681
+ mcp: this.getMcpServers(),
682
+ type: MessageType.CF_AGENT_MCP_SERVERS
683
+ })
684
+ );
685
+ });
686
+ });
687
+ }
688
+ return _onStart(props);
588
689
  });
589
- await this._tryCatch(() => _onStart());
590
690
  }
591
691
  );
592
692
  };
@@ -596,7 +696,6 @@ export class Agent<Env, State = unknown> extends Server<Env> {
596
696
  state: State,
597
697
  source: Connection | "server" = "server"
598
698
  ) {
599
- const previousState = this._state;
600
699
  this._state = state;
601
700
  this.sql`
602
701
  INSERT OR REPLACE INTO cf_agents_state (id, state)
@@ -609,25 +708,22 @@ export class Agent<Env, State = unknown> extends Server<Env> {
609
708
  this.broadcast(
610
709
  JSON.stringify({
611
710
  state: state,
612
- type: "cf_agent_state",
711
+ type: MessageType.CF_AGENT_STATE
613
712
  }),
614
713
  source !== "server" ? [source.id] : []
615
714
  );
616
715
  return this._tryCatch(() => {
617
- const { connection, request } = agentContext.getStore() || {};
716
+ const { connection, request, email } = agentContext.getStore() || {};
618
717
  return agentContext.run(
619
- { agent: this, connection, request },
718
+ { agent: this, connection, request, email },
620
719
  async () => {
621
720
  this.observability?.emit(
622
721
  {
623
722
  displayMessage: "State updated",
624
723
  id: nanoid(),
625
- payload: {
626
- previousState,
627
- state,
628
- },
724
+ payload: {},
629
725
  timestamp: Date.now(),
630
- type: "state:update",
726
+ type: "state:update"
631
727
  },
632
728
  this.ctx
633
729
  );
@@ -656,19 +752,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
656
752
  }
657
753
 
658
754
  /**
659
- * Called when the Agent receives an email
755
+ * Called when the Agent receives an email via routeAgentEmail()
756
+ * Override this method to handle incoming emails
660
757
  * @param email Email message to process
661
758
  */
662
- // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
663
- onEmail(email: ForwardableEmailMessage) {
759
+ async _onEmail(email: AgentEmail) {
760
+ // nb: we use this roundabout way of getting to onEmail
761
+ // because of https://github.com/cloudflare/workerd/issues/4499
664
762
  return agentContext.run(
665
- { agent: this, connection: undefined, request: undefined },
763
+ { agent: this, connection: undefined, request: undefined, email: email },
666
764
  async () => {
667
- console.error("onEmail not implemented");
765
+ if ("onEmail" in this && typeof this.onEmail === "function") {
766
+ return this._tryCatch(() =>
767
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
768
+ );
769
+ } else {
770
+ console.log("Received email from:", email.from, "to:", email.to);
771
+ console.log("Subject:", email.headers.get("subject"));
772
+ console.log(
773
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
774
+ );
775
+ }
668
776
  }
669
777
  );
670
778
  }
671
779
 
780
+ /**
781
+ * Reply to an email
782
+ * @param email The email to reply to
783
+ * @param options Options for the reply
784
+ * @returns void
785
+ */
786
+ async replyToEmail(
787
+ email: AgentEmail,
788
+ options: {
789
+ fromName: string;
790
+ subject?: string | undefined;
791
+ body: string;
792
+ contentType?: string;
793
+ headers?: Record<string, string>;
794
+ }
795
+ ): Promise<void> {
796
+ return this._tryCatch(async () => {
797
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
798
+ const agentId = this.name;
799
+
800
+ const { createMimeMessage } = await import("mimetext");
801
+ const msg = createMimeMessage();
802
+ msg.setSender({ addr: email.to, name: options.fromName });
803
+ msg.setRecipient(email.from);
804
+ msg.setSubject(
805
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
806
+ );
807
+ msg.addMessage({
808
+ contentType: options.contentType || "text/plain",
809
+ data: options.body
810
+ });
811
+
812
+ const domain = email.from.split("@")[1];
813
+ const messageId = `<${agentId}@${domain}>`;
814
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
815
+ msg.setHeader("Message-ID", messageId);
816
+ msg.setHeader("X-Agent-Name", agentName);
817
+ msg.setHeader("X-Agent-ID", agentId);
818
+
819
+ if (options.headers) {
820
+ for (const [key, value] of Object.entries(options.headers)) {
821
+ msg.setHeader(key, value);
822
+ }
823
+ }
824
+ await email.reply({
825
+ from: email.to,
826
+ raw: msg.asRaw(),
827
+ to: email.from
828
+ });
829
+ });
830
+ }
831
+
672
832
  private async _tryCatch<T>(fn: () => T | Promise<T>) {
673
833
  try {
674
834
  return await fn();
@@ -677,6 +837,68 @@ export class Agent<Env, State = unknown> extends Server<Env> {
677
837
  }
678
838
  }
679
839
 
840
+ /**
841
+ * Automatically wrap custom methods with agent context
842
+ * This ensures getCurrentAgent() works in all custom methods without decorators
843
+ */
844
+ private _autoWrapCustomMethods() {
845
+ // Collect all methods from base prototypes (Agent and Server)
846
+ const basePrototypes = [Agent.prototype, Server.prototype];
847
+ const baseMethods = new Set<string>();
848
+ for (const baseProto of basePrototypes) {
849
+ let proto = baseProto;
850
+ while (proto && proto !== Object.prototype) {
851
+ const methodNames = Object.getOwnPropertyNames(proto);
852
+ for (const methodName of methodNames) {
853
+ baseMethods.add(methodName);
854
+ }
855
+ proto = Object.getPrototypeOf(proto);
856
+ }
857
+ }
858
+ // Get all methods from the current instance's prototype chain
859
+ let proto = Object.getPrototypeOf(this);
860
+ let depth = 0;
861
+ while (proto && proto !== Object.prototype && depth < 10) {
862
+ const methodNames = Object.getOwnPropertyNames(proto);
863
+ for (const methodName of methodNames) {
864
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
865
+
866
+ // Skip if it's a private method, a base method, a getter, or not a function,
867
+ if (
868
+ baseMethods.has(methodName) ||
869
+ methodName.startsWith("_") ||
870
+ !descriptor ||
871
+ !!descriptor.get ||
872
+ typeof descriptor.value !== "function"
873
+ ) {
874
+ continue;
875
+ }
876
+
877
+ // Now, methodName is confirmed to be a custom method/function
878
+ // Wrap the custom method with context
879
+ const wrappedFunction = withAgentContext(
880
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
881
+ this[methodName as keyof this] as (...args: any[]) => any
882
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
883
+ ) as any;
884
+
885
+ // if the method is callable, copy the metadata from the original method
886
+ if (this._isCallable(methodName)) {
887
+ callableMetadata.set(
888
+ wrappedFunction,
889
+ callableMetadata.get(this[methodName as keyof this] as Function)!
890
+ );
891
+ }
892
+
893
+ // set the wrapped function on the prototype
894
+ this.constructor.prototype[methodName as keyof this] = wrappedFunction;
895
+ }
896
+
897
+ proto = Object.getPrototypeOf(proto);
898
+ depth++;
899
+ }
900
+ }
901
+
680
902
  override onError(
681
903
  connection: Connection,
682
904
  error: unknown
@@ -711,6 +933,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
711
933
  throw new Error("Not implemented");
712
934
  }
713
935
 
936
+ /**
937
+ * Queue a task to be executed in the future
938
+ * @param payload Payload to pass to the callback
939
+ * @param callback Name of the method to call
940
+ * @returns The ID of the queued task
941
+ */
942
+ async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
943
+ const id = nanoid(9);
944
+ if (typeof callback !== "string") {
945
+ throw new Error("Callback must be a string");
946
+ }
947
+
948
+ if (typeof this[callback] !== "function") {
949
+ throw new Error(`this.${callback} is not a function`);
950
+ }
951
+
952
+ this.sql`
953
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
954
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
955
+ `;
956
+
957
+ void this._flushQueue().catch((e) => {
958
+ console.error("Error flushing queue:", e);
959
+ });
960
+
961
+ return id;
962
+ }
963
+
964
+ private _flushingQueue = false;
965
+
966
+ private async _flushQueue() {
967
+ if (this._flushingQueue) {
968
+ return;
969
+ }
970
+ this._flushingQueue = true;
971
+ while (true) {
972
+ const result = this.sql<QueueItem<string>>`
973
+ SELECT * FROM cf_agents_queues
974
+ ORDER BY created_at ASC
975
+ `;
976
+
977
+ if (!result || result.length === 0) {
978
+ break;
979
+ }
980
+
981
+ for (const row of result || []) {
982
+ const callback = this[row.callback as keyof Agent<Env>];
983
+ if (!callback) {
984
+ console.error(`callback ${row.callback} not found`);
985
+ continue;
986
+ }
987
+ const { connection, request, email } = agentContext.getStore() || {};
988
+ await agentContext.run(
989
+ {
990
+ agent: this,
991
+ connection,
992
+ request,
993
+ email
994
+ },
995
+ async () => {
996
+ // TODO: add retries and backoff
997
+ await (
998
+ callback as (
999
+ payload: unknown,
1000
+ queueItem: QueueItem<string>
1001
+ ) => Promise<void>
1002
+ ).bind(this)(JSON.parse(row.payload as string), row);
1003
+ await this.dequeue(row.id);
1004
+ }
1005
+ );
1006
+ }
1007
+ }
1008
+ this._flushingQueue = false;
1009
+ }
1010
+
1011
+ /**
1012
+ * Dequeue a task by ID
1013
+ * @param id ID of the task to dequeue
1014
+ */
1015
+ async dequeue(id: string) {
1016
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
1017
+ }
1018
+
1019
+ /**
1020
+ * Dequeue all tasks
1021
+ */
1022
+ async dequeueAll() {
1023
+ this.sql`DELETE FROM cf_agents_queues`;
1024
+ }
1025
+
1026
+ /**
1027
+ * Dequeue all tasks by callback
1028
+ * @param callback Name of the callback to dequeue
1029
+ */
1030
+ async dequeueAllByCallback(callback: string) {
1031
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
1032
+ }
1033
+
1034
+ /**
1035
+ * Get a queued task by ID
1036
+ * @param id ID of the task to get
1037
+ * @returns The task or undefined if not found
1038
+ */
1039
+ async getQueue(id: string): Promise<QueueItem<string> | undefined> {
1040
+ const result = this.sql<QueueItem<string>>`
1041
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
1042
+ `;
1043
+ return result
1044
+ ? { ...result[0], payload: JSON.parse(result[0].payload) }
1045
+ : undefined;
1046
+ }
1047
+
1048
+ /**
1049
+ * Get all queues by key and value
1050
+ * @param key Key to filter by
1051
+ * @param value Value to filter by
1052
+ * @returns Array of matching QueueItem objects
1053
+ */
1054
+ async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
1055
+ const result = this.sql<QueueItem<string>>`
1056
+ SELECT * FROM cf_agents_queues
1057
+ `;
1058
+ return result.filter((row) => JSON.parse(row.payload)[key] === value);
1059
+ }
1060
+
714
1061
  /**
715
1062
  * Schedule a task to be executed in the future
716
1063
  * @template T Type of the payload data
@@ -731,9 +1078,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
731
1078
  {
732
1079
  displayMessage: `Schedule ${schedule.id} created`,
733
1080
  id: nanoid(),
734
- payload: schedule,
1081
+ payload: {
1082
+ callback: callback as string,
1083
+ id: id
1084
+ },
735
1085
  timestamp: Date.now(),
736
- type: "schedule:create",
1086
+ type: "schedule:create"
737
1087
  },
738
1088
  this.ctx
739
1089
  );
@@ -762,7 +1112,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
762
1112
  id,
763
1113
  payload: payload as T,
764
1114
  time: timestamp,
765
- type: "scheduled",
1115
+ type: "scheduled"
766
1116
  };
767
1117
 
768
1118
  emitScheduleCreate(schedule);
@@ -788,7 +1138,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
788
1138
  id,
789
1139
  payload: payload as T,
790
1140
  time: timestamp,
791
- type: "delayed",
1141
+ type: "delayed"
792
1142
  };
793
1143
 
794
1144
  emitScheduleCreate(schedule);
@@ -814,7 +1164,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
814
1164
  id,
815
1165
  payload: payload as T,
816
1166
  time: timestamp,
817
- type: "cron",
1167
+ type: "cron"
818
1168
  };
819
1169
 
820
1170
  emitScheduleCreate(schedule);
@@ -883,7 +1233,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
883
1233
  .toArray()
884
1234
  .map((row) => ({
885
1235
  ...row,
886
- payload: JSON.parse(row.payload as string) as T,
1236
+ payload: JSON.parse(row.payload as string) as T
887
1237
  })) as Schedule<T>[];
888
1238
 
889
1239
  return result;
@@ -901,9 +1251,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
901
1251
  {
902
1252
  displayMessage: `Schedule ${id} cancelled`,
903
1253
  id: nanoid(),
904
- payload: schedule,
1254
+ payload: {
1255
+ callback: schedule.callback,
1256
+ id: schedule.id
1257
+ },
905
1258
  timestamp: Date.now(),
906
- type: "schedule:cancel",
1259
+ type: "schedule:cancel"
907
1260
  },
908
1261
  this.ctx
909
1262
  );
@@ -917,9 +1270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
917
1270
  private async _scheduleNextAlarm() {
918
1271
  // Find the next schedule that needs to be executed
919
1272
  const result = this.sql`
920
- SELECT time FROM cf_agents_schedules
1273
+ SELECT time FROM cf_agents_schedules
921
1274
  WHERE time > ${Math.floor(Date.now() / 1000)}
922
- ORDER BY time ASC
1275
+ ORDER BY time ASC
923
1276
  LIMIT 1
924
1277
  `;
925
1278
  if (!result) return;
@@ -946,51 +1299,61 @@ export class Agent<Env, State = unknown> extends Server<Env> {
946
1299
  SELECT * FROM cf_agents_schedules WHERE time <= ${now}
947
1300
  `;
948
1301
 
949
- for (const row of result || []) {
950
- const callback = this[row.callback as keyof Agent<Env>];
951
- if (!callback) {
952
- console.error(`callback ${row.callback} not found`);
953
- continue;
954
- }
955
- await agentContext.run(
956
- { agent: this, connection: undefined, request: undefined },
957
- async () => {
958
- try {
959
- this.observability?.emit(
960
- {
961
- displayMessage: `Schedule ${row.id} executed`,
962
- id: nanoid(),
963
- payload: row,
964
- timestamp: Date.now(),
965
- type: "schedule:execute",
966
- },
967
- this.ctx
968
- );
1302
+ if (result && Array.isArray(result)) {
1303
+ for (const row of result) {
1304
+ const callback = this[row.callback as keyof Agent<Env>];
1305
+ if (!callback) {
1306
+ console.error(`callback ${row.callback} not found`);
1307
+ continue;
1308
+ }
1309
+ await agentContext.run(
1310
+ {
1311
+ agent: this,
1312
+ connection: undefined,
1313
+ request: undefined,
1314
+ email: undefined
1315
+ },
1316
+ async () => {
1317
+ try {
1318
+ this.observability?.emit(
1319
+ {
1320
+ displayMessage: `Schedule ${row.id} executed`,
1321
+ id: nanoid(),
1322
+ payload: {
1323
+ callback: row.callback,
1324
+ id: row.id
1325
+ },
1326
+ timestamp: Date.now(),
1327
+ type: "schedule:execute"
1328
+ },
1329
+ this.ctx
1330
+ );
969
1331
 
970
- await (
971
- callback as (
972
- payload: unknown,
973
- schedule: Schedule<unknown>
974
- ) => Promise<void>
975
- ).bind(this)(JSON.parse(row.payload as string), row);
976
- } catch (e) {
977
- console.error(`error executing callback "${row.callback}"`, e);
1332
+ await (
1333
+ callback as (
1334
+ payload: unknown,
1335
+ schedule: Schedule<unknown>
1336
+ ) => Promise<void>
1337
+ ).bind(this)(JSON.parse(row.payload as string), row);
1338
+ } catch (e) {
1339
+ console.error(`error executing callback "${row.callback}"`, e);
1340
+ }
978
1341
  }
979
- }
980
- );
981
- if (row.type === "cron") {
982
- // Update next execution time for cron schedules
983
- const nextExecutionTime = getNextCronTime(row.cron);
984
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1342
+ );
1343
+ if (row.type === "cron") {
1344
+ // Update next execution time for cron schedules
1345
+ const nextExecutionTime = getNextCronTime(row.cron);
1346
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
985
1347
 
986
- this.sql`
1348
+ this.sql`
987
1349
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
988
1350
  `;
989
- } else {
990
- // Delete one-time schedules after execution
991
- this.sql`
1351
+ } else {
1352
+ // Delete one-time schedules after execution
1353
+ this.sql`
992
1354
  DELETE FROM cf_agents_schedules WHERE id = ${row.id}
993
1355
  `;
1356
+ }
994
1357
  }
995
1358
  }
996
1359
 
@@ -1006,6 +1369,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1006
1369
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
1007
1370
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1008
1371
  this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1372
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
1009
1373
 
1010
1374
  // delete all alarms
1011
1375
  await this.ctx.storage.deleteAlarm();
@@ -1018,7 +1382,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1018
1382
  id: nanoid(),
1019
1383
  payload: {},
1020
1384
  timestamp: Date.now(),
1021
- type: "destroy",
1385
+ type: "destroy"
1022
1386
  },
1023
1387
  this.ctx
1024
1388
  );
@@ -1078,7 +1442,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1078
1442
  this.broadcast(
1079
1443
  JSON.stringify({
1080
1444
  mcp: this.getMcpServers(),
1081
- type: "cf_agent_mcp_servers",
1445
+ type: MessageType.CF_AGENT_MCP_SERVERS
1082
1446
  })
1083
1447
  );
1084
1448
 
@@ -1134,12 +1498,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1134
1498
  fetch: (url, init) =>
1135
1499
  fetch(url, {
1136
1500
  ...init,
1137
- headers: options?.transport?.headers,
1138
- }),
1501
+ headers: options?.transport?.headers
1502
+ })
1139
1503
  },
1140
1504
  requestInit: {
1141
- headers: options?.transport?.headers,
1142
- },
1505
+ headers: options?.transport?.headers
1506
+ }
1143
1507
  };
1144
1508
  }
1145
1509
 
@@ -1148,14 +1512,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1148
1512
  reconnect,
1149
1513
  transport: {
1150
1514
  ...headerTransportOpts,
1151
- authProvider,
1152
- },
1515
+ authProvider
1516
+ }
1153
1517
  });
1154
1518
 
1155
1519
  return {
1156
1520
  authUrl,
1157
1521
  clientId,
1158
- id,
1522
+ id
1159
1523
  };
1160
1524
  }
1161
1525
 
@@ -1167,7 +1531,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1167
1531
  this.broadcast(
1168
1532
  JSON.stringify({
1169
1533
  mcp: this.getMcpServers(),
1170
- type: "cf_agent_mcp_servers",
1534
+ type: MessageType.CF_AGENT_MCP_SERVERS
1171
1535
  })
1172
1536
  );
1173
1537
  }
@@ -1177,30 +1541,35 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1177
1541
  prompts: this.mcp.listPrompts(),
1178
1542
  resources: this.mcp.listResources(),
1179
1543
  servers: {},
1180
- tools: this.mcp.listTools(),
1544
+ tools: this.mcp.listTools()
1181
1545
  };
1182
1546
 
1183
1547
  const servers = this.sql<MCPServerRow>`
1184
1548
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1185
1549
  `;
1186
1550
 
1187
- for (const server of servers) {
1188
- const serverConn = this.mcp.mcpConnections[server.id];
1189
- mcpState.servers[server.id] = {
1190
- auth_url: server.auth_url,
1191
- capabilities: serverConn?.serverCapabilities ?? null,
1192
- instructions: serverConn?.instructions ?? null,
1193
- name: server.name,
1194
- server_url: server.server_url,
1195
- // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1196
- state: serverConn?.connectionState ?? "authenticating",
1197
- };
1551
+ if (servers && Array.isArray(servers) && servers.length > 0) {
1552
+ for (const server of servers) {
1553
+ const serverConn = this.mcp.mcpConnections[server.id];
1554
+ mcpState.servers[server.id] = {
1555
+ auth_url: server.auth_url,
1556
+ capabilities: serverConn?.serverCapabilities ?? null,
1557
+ instructions: serverConn?.instructions ?? null,
1558
+ name: server.name,
1559
+ server_url: server.server_url,
1560
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1561
+ state: serverConn?.connectionState ?? "authenticating"
1562
+ };
1563
+ }
1198
1564
  }
1199
1565
 
1200
1566
  return mcpState;
1201
1567
  }
1202
1568
  }
1203
1569
 
1570
+ // A set of classes that have been wrapped with agent context
1571
+ const wrappedClasses = new Set<typeof Agent.prototype.constructor>();
1572
+
1204
1573
  /**
1205
1574
  * Namespace for creating Agent instances
1206
1575
  * @template Agentic Type of the Agent class
@@ -1241,14 +1610,14 @@ export async function routeAgentRequest<Env>(
1241
1610
  "Access-Control-Allow-Credentials": "true",
1242
1611
  "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1243
1612
  "Access-Control-Allow-Origin": "*",
1244
- "Access-Control-Max-Age": "86400",
1613
+ "Access-Control-Max-Age": "86400"
1245
1614
  }
1246
1615
  : options?.cors;
1247
1616
 
1248
1617
  if (request.method === "OPTIONS") {
1249
1618
  if (corsHeaders) {
1250
1619
  return new Response(null, {
1251
- headers: corsHeaders,
1620
+ headers: corsHeaders
1252
1621
  });
1253
1622
  }
1254
1623
  console.warn(
@@ -1261,7 +1630,7 @@ export async function routeAgentRequest<Env>(
1261
1630
  env as Record<string, unknown>,
1262
1631
  {
1263
1632
  prefix: "agents",
1264
- ...(options as PartyServerOptions<Record<string, unknown>>),
1633
+ ...(options as PartyServerOptions<Record<string, unknown>>)
1265
1634
  }
1266
1635
  );
1267
1636
 
@@ -1274,24 +1643,238 @@ export async function routeAgentRequest<Env>(
1274
1643
  response = new Response(response.body, {
1275
1644
  headers: {
1276
1645
  ...response.headers,
1277
- ...corsHeaders,
1278
- },
1646
+ ...corsHeaders
1647
+ }
1279
1648
  });
1280
1649
  }
1281
1650
  return response;
1282
1651
  }
1283
1652
 
1653
+ export type EmailResolver<Env> = (
1654
+ email: ForwardableEmailMessage,
1655
+ env: Env
1656
+ ) => Promise<{
1657
+ agentName: string;
1658
+ agentId: string;
1659
+ } | null>;
1660
+
1661
+ /**
1662
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1663
+ * @returns A function that resolves the agent to route the email to
1664
+ */
1665
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1666
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1667
+ const messageId = email.headers.get("message-id");
1668
+ if (messageId) {
1669
+ const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
1670
+ if (messageIdMatch) {
1671
+ const [, agentId, domain] = messageIdMatch;
1672
+ const agentName = domain.split(".")[0];
1673
+ return { agentName, agentId };
1674
+ }
1675
+ }
1676
+
1677
+ const references = email.headers.get("references");
1678
+ if (references) {
1679
+ const referencesMatch = references.match(
1680
+ /<([A-Za-z0-9+/]{43}=)@([^>]+)>/
1681
+ );
1682
+ if (referencesMatch) {
1683
+ const [, base64Id, domain] = referencesMatch;
1684
+ const agentId = Buffer.from(base64Id, "base64").toString("hex");
1685
+ const agentName = domain.split(".")[0];
1686
+ return { agentName, agentId };
1687
+ }
1688
+ }
1689
+
1690
+ const agentName = email.headers.get("x-agent-name");
1691
+ const agentId = email.headers.get("x-agent-id");
1692
+ if (agentName && agentId) {
1693
+ return { agentName, agentId };
1694
+ }
1695
+
1696
+ return null;
1697
+ };
1698
+ }
1699
+
1700
+ /**
1701
+ * Create a resolver that uses the email address to determine the agent to route the email to
1702
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1703
+ * @returns A function that resolves the agent to route the email to
1704
+ */
1705
+ export function createAddressBasedEmailResolver<Env>(
1706
+ defaultAgentName: string
1707
+ ): EmailResolver<Env> {
1708
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1709
+ const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
1710
+ if (!emailMatch) {
1711
+ return null;
1712
+ }
1713
+
1714
+ const [, localPart, subAddress] = emailMatch;
1715
+
1716
+ if (subAddress) {
1717
+ return {
1718
+ agentName: localPart,
1719
+ agentId: subAddress
1720
+ };
1721
+ }
1722
+
1723
+ // Option 2: Use defaultAgentName namespace, localPart as agentId
1724
+ // Common for catch-all email routing to a single EmailAgent namespace
1725
+ return {
1726
+ agentName: defaultAgentName,
1727
+ agentId: localPart
1728
+ };
1729
+ };
1730
+ }
1731
+
1732
+ /**
1733
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1734
+ * @param agentName The name of the agent to route the email to
1735
+ * @param agentId The id of the agent to route the email to
1736
+ * @returns A function that resolves the agent to route the email to
1737
+ */
1738
+ export function createCatchAllEmailResolver<Env>(
1739
+ agentName: string,
1740
+ agentId: string
1741
+ ): EmailResolver<Env> {
1742
+ return async () => ({ agentName, agentId });
1743
+ }
1744
+
1745
+ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1746
+ resolver: EmailResolver<Env>;
1747
+ };
1748
+
1749
+ // Cache the agent namespace map for email routing
1750
+ // This maps both kebab-case and original names to namespaces
1751
+ const agentMapCache = new WeakMap<
1752
+ Record<string, unknown>,
1753
+ Record<string, unknown>
1754
+ >();
1755
+
1284
1756
  /**
1285
1757
  * Route an email to the appropriate Agent
1286
- * @param email Email message to route
1287
- * @param env Environment containing Agent bindings
1288
- * @param options Routing options
1758
+ * @param email The email to route
1759
+ * @param env The environment containing the Agent bindings
1760
+ * @param options The options for routing the email
1761
+ * @returns A promise that resolves when the email has been routed
1289
1762
  */
1290
1763
  export async function routeAgentEmail<Env>(
1291
- _email: ForwardableEmailMessage,
1292
- _env: Env,
1293
- _options?: AgentOptions<Env>
1294
- ): Promise<void> {}
1764
+ email: ForwardableEmailMessage,
1765
+ env: Env,
1766
+ options: EmailRoutingOptions<Env>
1767
+ ): Promise<void> {
1768
+ const routingInfo = await options.resolver(email, env);
1769
+
1770
+ if (!routingInfo) {
1771
+ console.warn("No routing information found for email, dropping message");
1772
+ return;
1773
+ }
1774
+
1775
+ // Build a map that includes both original names and kebab-case versions
1776
+ if (!agentMapCache.has(env as Record<string, unknown>)) {
1777
+ const map: Record<string, unknown> = {};
1778
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1779
+ if (
1780
+ value &&
1781
+ typeof value === "object" &&
1782
+ "idFromName" in value &&
1783
+ typeof value.idFromName === "function"
1784
+ ) {
1785
+ // Add both the original name and kebab-case version
1786
+ map[key] = value;
1787
+ map[camelCaseToKebabCase(key)] = value;
1788
+ }
1789
+ }
1790
+ agentMapCache.set(env as Record<string, unknown>, map);
1791
+ }
1792
+
1793
+ const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
1794
+ const namespace = agentMap[routingInfo.agentName];
1795
+
1796
+ if (!namespace) {
1797
+ // Provide helpful error message listing available agents
1798
+ const availableAgents = Object.keys(agentMap)
1799
+ .filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
1800
+ .join(", ");
1801
+ throw new Error(
1802
+ `Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
1803
+ );
1804
+ }
1805
+
1806
+ const agent = await getAgentByName(
1807
+ namespace as unknown as AgentNamespace<Agent<Env>>,
1808
+ routingInfo.agentId
1809
+ );
1810
+
1811
+ // let's make a serialisable version of the email
1812
+ const serialisableEmail: AgentEmail = {
1813
+ getRaw: async () => {
1814
+ const reader = email.raw.getReader();
1815
+ const chunks: Uint8Array[] = [];
1816
+
1817
+ let done = false;
1818
+ while (!done) {
1819
+ const { value, done: readerDone } = await reader.read();
1820
+ done = readerDone;
1821
+ if (value) {
1822
+ chunks.push(value);
1823
+ }
1824
+ }
1825
+
1826
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1827
+ const combined = new Uint8Array(totalLength);
1828
+ let offset = 0;
1829
+ for (const chunk of chunks) {
1830
+ combined.set(chunk, offset);
1831
+ offset += chunk.length;
1832
+ }
1833
+
1834
+ return combined;
1835
+ },
1836
+ headers: email.headers,
1837
+ rawSize: email.rawSize,
1838
+ setReject: (reason: string) => {
1839
+ email.setReject(reason);
1840
+ },
1841
+ forward: (rcptTo: string, headers?: Headers) => {
1842
+ return email.forward(rcptTo, headers);
1843
+ },
1844
+ reply: (options: { from: string; to: string; raw: string }) => {
1845
+ return email.reply(
1846
+ new EmailMessage(options.from, options.to, options.raw)
1847
+ );
1848
+ },
1849
+ from: email.from,
1850
+ to: email.to
1851
+ };
1852
+
1853
+ await agent._onEmail(serialisableEmail);
1854
+ }
1855
+
1856
+ export type AgentEmail = {
1857
+ from: string;
1858
+ to: string;
1859
+ getRaw: () => Promise<Uint8Array>;
1860
+ headers: Headers;
1861
+ rawSize: number;
1862
+ setReject: (reason: string) => void;
1863
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1864
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1865
+ };
1866
+
1867
+ export type EmailSendOptions = {
1868
+ to: string;
1869
+ subject: string;
1870
+ body: string;
1871
+ contentType?: string;
1872
+ headers?: Record<string, string>;
1873
+ includeRoutingHeaders?: boolean;
1874
+ agentName?: string;
1875
+ agentId?: string;
1876
+ domain?: string;
1877
+ };
1295
1878
 
1296
1879
  /**
1297
1880
  * Get or create an Agent by name
@@ -1302,12 +1885,17 @@ export async function routeAgentEmail<Env>(
1302
1885
  * @param options Options for Agent creation
1303
1886
  * @returns Promise resolving to an Agent instance stub
1304
1887
  */
1305
- export async function getAgentByName<Env, T extends Agent<Env>>(
1888
+ export async function getAgentByName<
1889
+ Env,
1890
+ T extends Agent<Env>,
1891
+ Props extends Record<string, unknown> = Record<string, unknown>
1892
+ >(
1306
1893
  namespace: AgentNamespace<T>,
1307
1894
  name: string,
1308
1895
  options?: {
1309
1896
  jurisdiction?: DurableObjectJurisdiction;
1310
1897
  locationHint?: DurableObjectLocationHint;
1898
+ props?: Props;
1311
1899
  }
1312
1900
  ) {
1313
1901
  return getServerByName<Env, T>(namespace, name, options);
@@ -1339,7 +1927,7 @@ export class StreamingResponse {
1339
1927
  id: this._id,
1340
1928
  result: chunk,
1341
1929
  success: true,
1342
- type: "rpc",
1930
+ type: MessageType.RPC
1343
1931
  };
1344
1932
  this._connection.send(JSON.stringify(response));
1345
1933
  }
@@ -1358,7 +1946,7 @@ export class StreamingResponse {
1358
1946
  id: this._id,
1359
1947
  result: finalChunk,
1360
1948
  success: true,
1361
- type: "rpc",
1949
+ type: MessageType.RPC
1362
1950
  };
1363
1951
  this._connection.send(JSON.stringify(response));
1364
1952
  }