agents 0.0.0-2662748 → 0.0.0-2684ade

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 (50) hide show
  1. package/README.md +131 -25
  2. package/dist/ai-chat-agent.d.ts +12 -8
  3. package/dist/ai-chat-agent.js +166 -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 +63 -72
  9. package/dist/ai-react.js +161 -54
  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-OYJXQRRH.js → chunk-DS7BJNPH.js} +167 -34
  16. package/dist/chunk-DS7BJNPH.js.map +1 -0
  17. package/dist/{chunk-P3RZJ72N.js → chunk-EGCWEPQL.js} +639 -132
  18. package/dist/chunk-EGCWEPQL.js.map +1 -0
  19. package/dist/{chunk-BZXOAZUX.js → chunk-PVQZBKN7.js} +5 -5
  20. package/dist/chunk-PVQZBKN7.js.map +1 -0
  21. package/dist/{chunk-VCSB47AK.js → chunk-QEVM4BVL.js} +10 -10
  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-DgyzBU_8.d.ts +4601 -0
  26. package/dist/client.d.ts +2 -2
  27. package/dist/client.js +2 -1
  28. package/dist/index.d.ts +158 -21
  29. package/dist/index.js +11 -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.js +1 -1
  33. package/dist/mcp/index.d.ts +38 -10
  34. package/dist/mcp/index.js +233 -59
  35. package/dist/mcp/index.js.map +1 -1
  36. package/dist/observability/index.d.ts +46 -0
  37. package/dist/observability/index.js +11 -0
  38. package/dist/observability/index.js.map +1 -0
  39. package/dist/react.d.ts +12 -8
  40. package/dist/react.js +12 -10
  41. package/dist/react.js.map +1 -1
  42. package/dist/schedule.d.ts +6 -6
  43. package/dist/schedule.js +4 -4
  44. package/dist/schedule.js.map +1 -1
  45. package/package.json +83 -70
  46. package/src/index.ts +838 -169
  47. package/dist/chunk-BZXOAZUX.js.map +0 -1
  48. package/dist/chunk-OYJXQRRH.js.map +0 -1
  49. package/dist/chunk-P3RZJ72N.js.map +0 -1
  50. package/dist/chunk-VCSB47AK.js.map +0 -1
package/src/index.ts CHANGED
@@ -1,30 +1,32 @@
1
- import {
2
- Server,
3
- getServerByName,
4
- routePartykitRequest,
5
- type Connection,
6
- type ConnectionContext,
7
- type PartyServerOptions,
8
- type WSMessage,
9
- } from "partyserver";
10
-
11
- import { parseCronExpression } from "cron-schedule";
12
- import { nanoid } from "nanoid";
1
+ import type { env } from "cloudflare:workers";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
13
5
 
14
6
  import type {
15
7
  Prompt,
16
8
  Resource,
17
9
  ServerCapabilities,
18
- Tool,
10
+ Tool
19
11
  } from "@modelcontextprotocol/sdk/types.js";
20
- import { AsyncLocalStorage } from "node:async_hooks";
12
+ import { parseCronExpression } from "cron-schedule";
13
+ import { nanoid } from "nanoid";
14
+ import { EmailMessage } from "cloudflare:email";
15
+ import {
16
+ type Connection,
17
+ type ConnectionContext,
18
+ type PartyServerOptions,
19
+ Server,
20
+ type WSMessage,
21
+ getServerByName,
22
+ routePartykitRequest
23
+ } from "partyserver";
24
+ import { camelCaseToKebabCase } from "./client";
21
25
  import { MCPClientManager } from "./mcp/client";
26
+ // import type { MCPClientConnection } from "./mcp/client-connection";
22
27
  import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
23
-
24
- import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
25
- import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
26
-
27
- import { camelCaseToKebabCase } from "./client";
28
+ import { genericObservability, type Observability } from "./observability";
29
+ import { MessageType } from "./ai-types";
28
30
 
29
31
  export type { Connection, ConnectionContext, WSMessage } from "partyserver";
30
32
 
@@ -42,7 +44,7 @@ export type RPCRequest = {
42
44
  * State update message from client
43
45
  */
44
46
  export type StateUpdateMessage = {
45
- type: "cf_agent_state";
47
+ type: MessageType.CF_AGENT_STATE;
46
48
  state: unknown;
47
49
  };
48
50
 
@@ -50,7 +52,7 @@ export type StateUpdateMessage = {
50
52
  * RPC response message to client
51
53
  */
52
54
  export type RPCResponse = {
53
- type: "rpc";
55
+ type: MessageType.RPC;
54
56
  id: string;
55
57
  } & (
56
58
  | {
@@ -77,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
77
79
  typeof msg === "object" &&
78
80
  msg !== null &&
79
81
  "type" in msg &&
80
- msg.type === "rpc" &&
82
+ msg.type === MessageType.RPC &&
81
83
  "id" in msg &&
82
84
  typeof msg.id === "string" &&
83
85
  "method" in msg &&
@@ -95,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
95
97
  typeof msg === "object" &&
96
98
  msg !== null &&
97
99
  "type" in msg &&
98
- msg.type === "cf_agent_state" &&
100
+ msg.type === MessageType.CF_AGENT_STATE &&
99
101
  "state" in msg
100
102
  );
101
103
  }
@@ -119,6 +121,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
119
121
  export function unstable_callable(metadata: CallableMetadata = {}) {
120
122
  return function callableDecorator<This, Args extends unknown[], Return>(
121
123
  target: (this: This, ...args: Args) => Return,
124
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
122
125
  context: ClassMethodDecoratorContext
123
126
  ) {
124
127
  if (!callableMetadata.has(target)) {
@@ -129,6 +132,13 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
129
132
  };
130
133
  }
131
134
 
135
+ export type QueueItem<T = string> = {
136
+ id: string;
137
+ payload: T;
138
+ callback: keyof Agent<unknown>;
139
+ created_at: number;
140
+ };
141
+
132
142
  /**
133
143
  * Represents a scheduled task within an Agent
134
144
  * @template T Type of the payload data
@@ -174,7 +184,7 @@ function getNextCronTime(cron: string) {
174
184
  * MCP Server state update message from server -> Client
175
185
  */
176
186
  export type MCPServerMessage = {
177
- type: "cf_agent_mcp_servers";
187
+ type: MessageType.CF_AGENT_MCP_SERVERS;
178
188
  mcp: MCPServersState;
179
189
  };
180
190
 
@@ -218,23 +228,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
218
228
  const DEFAULT_STATE = {} as unknown;
219
229
 
220
230
  const agentContext = new AsyncLocalStorage<{
221
- agent: Agent<unknown>;
231
+ agent: Agent<unknown, unknown>;
222
232
  connection: Connection | undefined;
223
233
  request: Request | undefined;
234
+ email: AgentEmail | undefined;
224
235
  }>();
225
236
 
226
237
  export function getCurrentAgent<
227
- T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
238
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
228
239
  >(): {
229
240
  agent: T | undefined;
230
241
  connection: Connection | undefined;
231
- request: Request<unknown, CfProperties<unknown>> | undefined;
242
+ request: Request | undefined;
243
+ email: AgentEmail | undefined;
232
244
  } {
233
245
  const store = agentContext.getStore() as
234
246
  | {
235
247
  agent: T;
236
248
  connection: Connection | undefined;
237
- request: Request<unknown, CfProperties<unknown>> | undefined;
249
+ request: Request | undefined;
250
+ email: AgentEmail | undefined;
238
251
  }
239
252
  | undefined;
240
253
  if (!store) {
@@ -242,17 +255,37 @@ export function getCurrentAgent<
242
255
  agent: undefined,
243
256
  connection: undefined,
244
257
  request: undefined,
258
+ email: undefined
245
259
  };
246
260
  }
247
261
  return store;
248
262
  }
249
263
 
264
+ /**
265
+ * Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
266
+ * @param agent The agent instance
267
+ * @param method The method to wrap
268
+ * @returns A wrapped method that runs within the agent context
269
+ */
270
+
271
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
272
+ function withAgentContext<T extends (...args: any[]) => any>(
273
+ method: T
274
+ ): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
275
+ return function (...args: Parameters<T>): ReturnType<T> {
276
+ const { connection, request, email } = getCurrentAgent();
277
+ return agentContext.run({ agent: this, connection, request, email }, () => {
278
+ return method.apply(this, args);
279
+ });
280
+ };
281
+ }
282
+
250
283
  /**
251
284
  * Base class for creating Agent implementations
252
285
  * @template Env Environment type containing bindings
253
286
  * @template State State type to store within the Agent
254
287
  */
255
- export class Agent<Env, State = unknown> extends Server<Env> {
288
+ export class Agent<Env = typeof env, State = unknown> extends Server<Env> {
256
289
  private _state = DEFAULT_STATE as State;
257
290
 
258
291
  private _ParentClass: typeof Agent<Env, State> =
@@ -314,9 +347,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
314
347
  */
315
348
  static options = {
316
349
  /** Whether the Agent should hibernate when inactive */
317
- hibernate: true, // default to hibernate
350
+ hibernate: true // default to hibernate
318
351
  };
319
352
 
353
+ /**
354
+ * The observability implementation to use for the Agent
355
+ */
356
+ observability?: Observability = genericObservability;
357
+
320
358
  /**
321
359
  * Execute SQL queries against the Agent's database
322
360
  * @template T Type of the returned rows
@@ -346,6 +384,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
346
384
  constructor(ctx: AgentContext, env: Env) {
347
385
  super(ctx, env);
348
386
 
387
+ // Auto-wrap custom methods with agent context
388
+ this._autoWrapCustomMethods();
389
+
349
390
  this.sql`
350
391
  CREATE TABLE IF NOT EXISTS cf_agents_state (
351
392
  id TEXT PRIMARY KEY NOT NULL,
@@ -353,6 +394,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
353
394
  )
354
395
  `;
355
396
 
397
+ this.sql`
398
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
399
+ id TEXT PRIMARY KEY NOT NULL,
400
+ payload TEXT,
401
+ callback TEXT,
402
+ created_at INTEGER DEFAULT (unixepoch())
403
+ )
404
+ `;
405
+
356
406
  void this.ctx.blockConcurrencyWhile(async () => {
357
407
  return this._tryCatch(async () => {
358
408
  // Create alarms table if it doesn't exist
@@ -389,7 +439,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
389
439
  const _onRequest = this.onRequest.bind(this);
390
440
  this.onRequest = (request: Request) => {
391
441
  return agentContext.run(
392
- { agent: this, connection: undefined, request },
442
+ { agent: this, connection: undefined, request, email: undefined },
393
443
  async () => {
394
444
  if (this.mcp.isCallbackRequest(request)) {
395
445
  await this.mcp.handleCallbackRequest(request);
@@ -397,15 +447,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
397
447
  // after the MCP connection handshake, we can send updated mcp state
398
448
  this.broadcast(
399
449
  JSON.stringify({
400
- type: "cf_agent_mcp_servers",
401
450
  mcp: this.getMcpServers(),
451
+ type: MessageType.CF_AGENT_MCP_SERVERS
402
452
  })
403
453
  );
404
454
 
405
455
  // We probably should let the user configure this response/redirect, but this is fine for now.
406
456
  return new Response("<script>window.close();</script>", {
407
- status: 200,
408
457
  headers: { "content-type": "text/html" },
458
+ status: 200
409
459
  });
410
460
  }
411
461
 
@@ -417,7 +467,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
417
467
  const _onMessage = this.onMessage.bind(this);
418
468
  this.onMessage = async (connection: Connection, message: WSMessage) => {
419
469
  return agentContext.run(
420
- { agent: this, connection, request: undefined },
470
+ { agent: this, connection, request: undefined, email: undefined },
421
471
  async () => {
422
472
  if (typeof message !== "string") {
423
473
  return this._tryCatch(() => _onMessage(connection, message));
@@ -426,7 +476,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
426
476
  let parsed: unknown;
427
477
  try {
428
478
  parsed = JSON.parse(message);
429
- } catch (e) {
479
+ } catch (_e) {
430
480
  // silently fail and let the onMessage handler handle it
431
481
  return this._tryCatch(() => _onMessage(connection, message));
432
482
  }
@@ -461,22 +511,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
461
511
 
462
512
  // For regular methods, execute and send response
463
513
  const result = await methodFn.apply(this, args);
514
+
515
+ this.observability?.emit(
516
+ {
517
+ displayMessage: `RPC call to ${method}`,
518
+ id: nanoid(),
519
+ payload: {
520
+ method,
521
+ streaming: metadata?.streaming
522
+ },
523
+ timestamp: Date.now(),
524
+ type: "rpc"
525
+ },
526
+ this.ctx
527
+ );
528
+
464
529
  const response: RPCResponse = {
465
- type: "rpc",
530
+ done: true,
466
531
  id,
467
- success: true,
468
532
  result,
469
- done: true,
533
+ success: true,
534
+ type: MessageType.RPC
470
535
  };
471
536
  connection.send(JSON.stringify(response));
472
537
  } catch (e) {
473
538
  // Send error response
474
539
  const response: RPCResponse = {
475
- type: "rpc",
476
- id: parsed.id,
477
- success: false,
478
540
  error:
479
541
  e instanceof Error ? e.message : "Unknown error occurred",
542
+ id: parsed.id,
543
+ success: false,
544
+ type: MessageType.RPC
480
545
  };
481
546
  connection.send(JSON.stringify(response));
482
547
  console.error("RPC error:", e);
@@ -494,25 +559,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
494
559
  // TODO: This is a hack to ensure the state is sent after the connection is established
495
560
  // must fix this
496
561
  return agentContext.run(
497
- { agent: this, connection, request: ctx.request },
562
+ { agent: this, connection, request: ctx.request, email: undefined },
498
563
  async () => {
499
564
  setTimeout(() => {
500
565
  if (this.state) {
501
566
  connection.send(
502
567
  JSON.stringify({
503
- type: "cf_agent_state",
504
568
  state: this.state,
569
+ type: MessageType.CF_AGENT_STATE
505
570
  })
506
571
  );
507
572
  }
508
573
 
509
574
  connection.send(
510
575
  JSON.stringify({
511
- type: "cf_agent_mcp_servers",
512
576
  mcp: this.getMcpServers(),
577
+ type: MessageType.CF_AGENT_MCP_SERVERS
513
578
  })
514
579
  );
515
580
 
581
+ this.observability?.emit(
582
+ {
583
+ displayMessage: "Connection established",
584
+ id: nanoid(),
585
+ payload: {
586
+ connectionId: connection.id
587
+ },
588
+ timestamp: Date.now(),
589
+ type: "connect"
590
+ },
591
+ this.ctx
592
+ );
516
593
  return this._tryCatch(() => _onConnect(connection, ctx));
517
594
  }, 20);
518
595
  }
@@ -522,18 +599,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
522
599
  const _onStart = this.onStart.bind(this);
523
600
  this.onStart = async () => {
524
601
  return agentContext.run(
525
- { agent: this, connection: undefined, request: undefined },
602
+ {
603
+ agent: this,
604
+ connection: undefined,
605
+ request: undefined,
606
+ email: undefined
607
+ },
526
608
  async () => {
527
- const servers = this.sql<MCPServerRow>`
609
+ await this._tryCatch(() => {
610
+ const servers = this.sql<MCPServerRow>`
528
611
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
529
612
  `;
530
613
 
531
- // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
532
- await Promise.allSettled(
533
- servers
534
- .filter((server) => server.auth_url === null)
535
- .map((server) => {
536
- return this._connectToMcpServerInternal(
614
+ this.broadcast(
615
+ JSON.stringify({
616
+ mcp: this.getMcpServers(),
617
+ type: MessageType.CF_AGENT_MCP_SERVERS
618
+ })
619
+ );
620
+
621
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
622
+ if (servers && Array.isArray(servers) && servers.length > 0) {
623
+ servers.forEach((server) => {
624
+ this._connectToMcpServerInternal(
537
625
  server.name,
538
626
  server.server_url,
539
627
  server.callback_url,
@@ -542,20 +630,35 @@ export class Agent<Env, State = unknown> extends Server<Env> {
542
630
  : undefined,
543
631
  {
544
632
  id: server.id,
545
- oauthClientId: server.client_id ?? undefined,
633
+ oauthClientId: server.client_id ?? undefined
546
634
  }
547
- );
548
- })
549
- );
550
-
551
- this.broadcast(
552
- JSON.stringify({
553
- type: "cf_agent_mcp_servers",
554
- mcp: this.getMcpServers(),
555
- })
556
- );
557
-
558
- await this._tryCatch(() => _onStart());
635
+ )
636
+ .then(() => {
637
+ // Broadcast updated MCP servers state after each server connects
638
+ this.broadcast(
639
+ JSON.stringify({
640
+ mcp: this.getMcpServers(),
641
+ type: MessageType.CF_AGENT_MCP_SERVERS
642
+ })
643
+ );
644
+ })
645
+ .catch((error) => {
646
+ console.error(
647
+ `Error connecting to MCP server: ${server.name} (${server.server_url})`,
648
+ error
649
+ );
650
+ // Still broadcast even if connection fails, so clients know about the failure
651
+ this.broadcast(
652
+ JSON.stringify({
653
+ mcp: this.getMcpServers(),
654
+ type: MessageType.CF_AGENT_MCP_SERVERS
655
+ })
656
+ );
657
+ });
658
+ });
659
+ }
660
+ return _onStart();
661
+ });
559
662
  }
560
663
  );
561
664
  };
@@ -576,16 +679,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
576
679
  `;
577
680
  this.broadcast(
578
681
  JSON.stringify({
579
- type: "cf_agent_state",
580
682
  state: state,
683
+ type: MessageType.CF_AGENT_STATE
581
684
  }),
582
685
  source !== "server" ? [source.id] : []
583
686
  );
584
687
  return this._tryCatch(() => {
585
- const { connection, request } = agentContext.getStore() || {};
688
+ const { connection, request, email } = agentContext.getStore() || {};
586
689
  return agentContext.run(
587
- { agent: this, connection, request },
690
+ { agent: this, connection, request, email },
588
691
  async () => {
692
+ this.observability?.emit(
693
+ {
694
+ displayMessage: "State updated",
695
+ id: nanoid(),
696
+ payload: {},
697
+ timestamp: Date.now(),
698
+ type: "state:update"
699
+ },
700
+ this.ctx
701
+ );
589
702
  return this.onStateUpdate(state, source);
590
703
  }
591
704
  );
@@ -605,23 +718,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
605
718
  * @param state Updated state
606
719
  * @param source Source of the state update ("server" or a client connection)
607
720
  */
721
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
608
722
  onStateUpdate(state: State | undefined, source: Connection | "server") {
609
723
  // override this to handle state updates
610
724
  }
611
725
 
612
726
  /**
613
- * Called when the Agent receives an email
727
+ * Called when the Agent receives an email via routeAgentEmail()
728
+ * Override this method to handle incoming emails
614
729
  * @param email Email message to process
615
730
  */
616
- onEmail(email: ForwardableEmailMessage) {
731
+ async _onEmail(email: AgentEmail) {
732
+ // nb: we use this roundabout way of getting to onEmail
733
+ // because of https://github.com/cloudflare/workerd/issues/4499
617
734
  return agentContext.run(
618
- { agent: this, connection: undefined, request: undefined },
735
+ { agent: this, connection: undefined, request: undefined, email: email },
619
736
  async () => {
620
- console.error("onEmail not implemented");
737
+ if ("onEmail" in this && typeof this.onEmail === "function") {
738
+ return this._tryCatch(() =>
739
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
740
+ );
741
+ } else {
742
+ console.log("Received email from:", email.from, "to:", email.to);
743
+ console.log("Subject:", email.headers.get("subject"));
744
+ console.log(
745
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
746
+ );
747
+ }
621
748
  }
622
749
  );
623
750
  }
624
751
 
752
+ /**
753
+ * Reply to an email
754
+ * @param email The email to reply to
755
+ * @param options Options for the reply
756
+ * @returns void
757
+ */
758
+ async replyToEmail(
759
+ email: AgentEmail,
760
+ options: {
761
+ fromName: string;
762
+ subject?: string | undefined;
763
+ body: string;
764
+ contentType?: string;
765
+ headers?: Record<string, string>;
766
+ }
767
+ ): Promise<void> {
768
+ return this._tryCatch(async () => {
769
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
770
+ const agentId = this.name;
771
+
772
+ const { createMimeMessage } = await import("mimetext");
773
+ const msg = createMimeMessage();
774
+ msg.setSender({ addr: email.to, name: options.fromName });
775
+ msg.setRecipient(email.from);
776
+ msg.setSubject(
777
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
778
+ );
779
+ msg.addMessage({
780
+ contentType: options.contentType || "text/plain",
781
+ data: options.body
782
+ });
783
+
784
+ const domain = email.from.split("@")[1];
785
+ const messageId = `<${agentId}@${domain}>`;
786
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
787
+ msg.setHeader("Message-ID", messageId);
788
+ msg.setHeader("X-Agent-Name", agentName);
789
+ msg.setHeader("X-Agent-ID", agentId);
790
+
791
+ if (options.headers) {
792
+ for (const [key, value] of Object.entries(options.headers)) {
793
+ msg.setHeader(key, value);
794
+ }
795
+ }
796
+ await email.reply({
797
+ from: email.to,
798
+ raw: msg.asRaw(),
799
+ to: email.from
800
+ });
801
+ });
802
+ }
803
+
625
804
  private async _tryCatch<T>(fn: () => T | Promise<T>) {
626
805
  try {
627
806
  return await fn();
@@ -630,6 +809,72 @@ export class Agent<Env, State = unknown> extends Server<Env> {
630
809
  }
631
810
  }
632
811
 
812
+ /**
813
+ * Automatically wrap custom methods with agent context
814
+ * This ensures getCurrentAgent() works in all custom methods without decorators
815
+ */
816
+ private _autoWrapCustomMethods() {
817
+ // Collect all methods from base prototypes (Agent and Server)
818
+ const basePrototypes = [Agent.prototype, Server.prototype];
819
+ const baseMethods = new Set<string>();
820
+ for (const baseProto of basePrototypes) {
821
+ let proto = baseProto;
822
+ while (proto && proto !== Object.prototype) {
823
+ const methodNames = Object.getOwnPropertyNames(proto);
824
+ for (const methodName of methodNames) {
825
+ baseMethods.add(methodName);
826
+ }
827
+ proto = Object.getPrototypeOf(proto);
828
+ }
829
+ }
830
+ // Get all methods from the current instance's prototype chain
831
+ let proto = Object.getPrototypeOf(this);
832
+ let depth = 0;
833
+ while (proto && proto !== Object.prototype && depth < 10) {
834
+ const methodNames = Object.getOwnPropertyNames(proto);
835
+ for (const methodName of methodNames) {
836
+ // Skip if it's a private method or not a function
837
+ if (
838
+ baseMethods.has(methodName) ||
839
+ methodName.startsWith("_") ||
840
+ typeof this[methodName as keyof this] !== "function"
841
+ ) {
842
+ continue;
843
+ }
844
+ // If the method doesn't exist in base prototypes, it's a custom method
845
+ if (!baseMethods.has(methodName)) {
846
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
847
+ if (descriptor && typeof descriptor.value === "function") {
848
+ // Wrap the custom method with context
849
+
850
+ const wrappedFunction = withAgentContext(
851
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
852
+ this[methodName as keyof this] as (...args: any[]) => any
853
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
854
+ ) as any;
855
+
856
+ // if the method is callable, copy the metadata from the original method
857
+ if (this._isCallable(methodName)) {
858
+ callableMetadata.set(
859
+ wrappedFunction,
860
+ callableMetadata.get(
861
+ this[methodName as keyof this] as Function
862
+ )!
863
+ );
864
+ }
865
+
866
+ // set the wrapped function on the prototype
867
+ this.constructor.prototype[methodName as keyof this] =
868
+ wrappedFunction;
869
+ }
870
+ }
871
+ }
872
+
873
+ proto = Object.getPrototypeOf(proto);
874
+ depth++;
875
+ }
876
+ }
877
+
633
878
  override onError(
634
879
  connection: Connection,
635
880
  error: unknown
@@ -664,6 +909,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
664
909
  throw new Error("Not implemented");
665
910
  }
666
911
 
912
+ /**
913
+ * Queue a task to be executed in the future
914
+ * @param payload Payload to pass to the callback
915
+ * @param callback Name of the method to call
916
+ * @returns The ID of the queued task
917
+ */
918
+ async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
919
+ const id = nanoid(9);
920
+ if (typeof callback !== "string") {
921
+ throw new Error("Callback must be a string");
922
+ }
923
+
924
+ if (typeof this[callback] !== "function") {
925
+ throw new Error(`this.${callback} is not a function`);
926
+ }
927
+
928
+ this.sql`
929
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
930
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
931
+ `;
932
+
933
+ void this._flushQueue().catch((e) => {
934
+ console.error("Error flushing queue:", e);
935
+ });
936
+
937
+ return id;
938
+ }
939
+
940
+ private _flushingQueue = false;
941
+
942
+ private async _flushQueue() {
943
+ if (this._flushingQueue) {
944
+ return;
945
+ }
946
+ this._flushingQueue = true;
947
+ while (true) {
948
+ const result = this.sql<QueueItem<string>>`
949
+ SELECT * FROM cf_agents_queues
950
+ ORDER BY created_at ASC
951
+ `;
952
+
953
+ if (!result || result.length === 0) {
954
+ break;
955
+ }
956
+
957
+ for (const row of result || []) {
958
+ const callback = this[row.callback as keyof Agent<Env>];
959
+ if (!callback) {
960
+ console.error(`callback ${row.callback} not found`);
961
+ continue;
962
+ }
963
+ const { connection, request, email } = agentContext.getStore() || {};
964
+ await agentContext.run(
965
+ {
966
+ agent: this,
967
+ connection,
968
+ request,
969
+ email
970
+ },
971
+ async () => {
972
+ // TODO: add retries and backoff
973
+ await (
974
+ callback as (
975
+ payload: unknown,
976
+ queueItem: QueueItem<string>
977
+ ) => Promise<void>
978
+ ).bind(this)(JSON.parse(row.payload as string), row);
979
+ await this.dequeue(row.id);
980
+ }
981
+ );
982
+ }
983
+ }
984
+ this._flushingQueue = false;
985
+ }
986
+
987
+ /**
988
+ * Dequeue a task by ID
989
+ * @param id ID of the task to dequeue
990
+ */
991
+ async dequeue(id: string) {
992
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
993
+ }
994
+
995
+ /**
996
+ * Dequeue all tasks
997
+ */
998
+ async dequeueAll() {
999
+ this.sql`DELETE FROM cf_agents_queues`;
1000
+ }
1001
+
1002
+ /**
1003
+ * Dequeue all tasks by callback
1004
+ * @param callback Name of the callback to dequeue
1005
+ */
1006
+ async dequeueAllByCallback(callback: string) {
1007
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
1008
+ }
1009
+
1010
+ /**
1011
+ * Get a queued task by ID
1012
+ * @param id ID of the task to get
1013
+ * @returns The task or undefined if not found
1014
+ */
1015
+ async getQueue(id: string): Promise<QueueItem<string> | undefined> {
1016
+ const result = this.sql<QueueItem<string>>`
1017
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
1018
+ `;
1019
+ return result
1020
+ ? { ...result[0], payload: JSON.parse(result[0].payload) }
1021
+ : undefined;
1022
+ }
1023
+
1024
+ /**
1025
+ * Get all queues by key and value
1026
+ * @param key Key to filter by
1027
+ * @param value Value to filter by
1028
+ * @returns Array of matching QueueItem objects
1029
+ */
1030
+ async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
1031
+ const result = this.sql<QueueItem<string>>`
1032
+ SELECT * FROM cf_agents_queues
1033
+ `;
1034
+ return result.filter((row) => JSON.parse(row.payload)[key] === value);
1035
+ }
1036
+
667
1037
  /**
668
1038
  * Schedule a task to be executed in the future
669
1039
  * @template T Type of the payload data
@@ -679,6 +1049,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
679
1049
  ): Promise<Schedule<T>> {
680
1050
  const id = nanoid(9);
681
1051
 
1052
+ const emitScheduleCreate = (schedule: Schedule<T>) =>
1053
+ this.observability?.emit(
1054
+ {
1055
+ displayMessage: `Schedule ${schedule.id} created`,
1056
+ id: nanoid(),
1057
+ payload: {
1058
+ callback: callback as string,
1059
+ id: id
1060
+ },
1061
+ timestamp: Date.now(),
1062
+ type: "schedule:create"
1063
+ },
1064
+ this.ctx
1065
+ );
1066
+
682
1067
  if (typeof callback !== "string") {
683
1068
  throw new Error("Callback must be a string");
684
1069
  }
@@ -698,13 +1083,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
698
1083
 
699
1084
  await this._scheduleNextAlarm();
700
1085
 
701
- return {
702
- id,
1086
+ const schedule: Schedule<T> = {
703
1087
  callback: callback,
1088
+ id,
704
1089
  payload: payload as T,
705
1090
  time: timestamp,
706
- type: "scheduled",
1091
+ type: "scheduled"
707
1092
  };
1093
+
1094
+ emitScheduleCreate(schedule);
1095
+
1096
+ return schedule;
708
1097
  }
709
1098
  if (typeof when === "number") {
710
1099
  const time = new Date(Date.now() + when * 1000);
@@ -719,14 +1108,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
719
1108
 
720
1109
  await this._scheduleNextAlarm();
721
1110
 
722
- return {
723
- id,
1111
+ const schedule: Schedule<T> = {
724
1112
  callback: callback,
725
- payload: payload as T,
726
1113
  delayInSeconds: when,
1114
+ id,
1115
+ payload: payload as T,
727
1116
  time: timestamp,
728
- type: "delayed",
1117
+ type: "delayed"
729
1118
  };
1119
+
1120
+ emitScheduleCreate(schedule);
1121
+
1122
+ return schedule;
730
1123
  }
731
1124
  if (typeof when === "string") {
732
1125
  const nextExecutionTime = getNextCronTime(when);
@@ -741,14 +1134,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
741
1134
 
742
1135
  await this._scheduleNextAlarm();
743
1136
 
744
- return {
745
- id,
1137
+ const schedule: Schedule<T> = {
746
1138
  callback: callback,
747
- payload: payload as T,
748
1139
  cron: when,
1140
+ id,
1141
+ payload: payload as T,
749
1142
  time: timestamp,
750
- type: "cron",
1143
+ type: "cron"
751
1144
  };
1145
+
1146
+ emitScheduleCreate(schedule);
1147
+
1148
+ return schedule;
752
1149
  }
753
1150
  throw new Error("Invalid schedule type");
754
1151
  }
@@ -812,7 +1209,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
812
1209
  .toArray()
813
1210
  .map((row) => ({
814
1211
  ...row,
815
- payload: JSON.parse(row.payload as string) as T,
1212
+ payload: JSON.parse(row.payload as string) as T
816
1213
  })) as Schedule<T>[];
817
1214
 
818
1215
  return result;
@@ -824,6 +1221,22 @@ export class Agent<Env, State = unknown> extends Server<Env> {
824
1221
  * @returns true if the task was cancelled, false otherwise
825
1222
  */
826
1223
  async cancelSchedule(id: string): Promise<boolean> {
1224
+ const schedule = await this.getSchedule(id);
1225
+ if (schedule) {
1226
+ this.observability?.emit(
1227
+ {
1228
+ displayMessage: `Schedule ${id} cancelled`,
1229
+ id: nanoid(),
1230
+ payload: {
1231
+ callback: schedule.callback,
1232
+ id: schedule.id
1233
+ },
1234
+ timestamp: Date.now(),
1235
+ type: "schedule:cancel"
1236
+ },
1237
+ this.ctx
1238
+ );
1239
+ }
827
1240
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
828
1241
 
829
1242
  await this._scheduleNextAlarm();
@@ -833,9 +1246,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
833
1246
  private async _scheduleNextAlarm() {
834
1247
  // Find the next schedule that needs to be executed
835
1248
  const result = this.sql`
836
- SELECT time FROM cf_agents_schedules
1249
+ SELECT time FROM cf_agents_schedules
837
1250
  WHERE time > ${Math.floor(Date.now() / 1000)}
838
- ORDER BY time ASC
1251
+ ORDER BY time ASC
839
1252
  LIMIT 1
840
1253
  `;
841
1254
  if (!result) return;
@@ -862,40 +1275,61 @@ export class Agent<Env, State = unknown> extends Server<Env> {
862
1275
  SELECT * FROM cf_agents_schedules WHERE time <= ${now}
863
1276
  `;
864
1277
 
865
- for (const row of result || []) {
866
- const callback = this[row.callback as keyof Agent<Env>];
867
- if (!callback) {
868
- console.error(`callback ${row.callback} not found`);
869
- continue;
870
- }
871
- await agentContext.run(
872
- { agent: this, connection: undefined, request: undefined },
873
- async () => {
874
- try {
875
- await (
876
- callback as (
877
- payload: unknown,
878
- schedule: Schedule<unknown>
879
- ) => Promise<void>
880
- ).bind(this)(JSON.parse(row.payload as string), row);
881
- } catch (e) {
882
- console.error(`error executing callback "${row.callback}"`, e);
883
- }
1278
+ if (result && Array.isArray(result)) {
1279
+ for (const row of result) {
1280
+ const callback = this[row.callback as keyof Agent<Env>];
1281
+ if (!callback) {
1282
+ console.error(`callback ${row.callback} not found`);
1283
+ continue;
884
1284
  }
885
- );
886
- if (row.type === "cron") {
887
- // Update next execution time for cron schedules
888
- const nextExecutionTime = getNextCronTime(row.cron);
889
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1285
+ await agentContext.run(
1286
+ {
1287
+ agent: this,
1288
+ connection: undefined,
1289
+ request: undefined,
1290
+ email: undefined
1291
+ },
1292
+ async () => {
1293
+ try {
1294
+ this.observability?.emit(
1295
+ {
1296
+ displayMessage: `Schedule ${row.id} executed`,
1297
+ id: nanoid(),
1298
+ payload: {
1299
+ callback: row.callback,
1300
+ id: row.id
1301
+ },
1302
+ timestamp: Date.now(),
1303
+ type: "schedule:execute"
1304
+ },
1305
+ this.ctx
1306
+ );
890
1307
 
891
- this.sql`
1308
+ await (
1309
+ callback as (
1310
+ payload: unknown,
1311
+ schedule: Schedule<unknown>
1312
+ ) => Promise<void>
1313
+ ).bind(this)(JSON.parse(row.payload as string), row);
1314
+ } catch (e) {
1315
+ console.error(`error executing callback "${row.callback}"`, e);
1316
+ }
1317
+ }
1318
+ );
1319
+ if (row.type === "cron") {
1320
+ // Update next execution time for cron schedules
1321
+ const nextExecutionTime = getNextCronTime(row.cron);
1322
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1323
+
1324
+ this.sql`
892
1325
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
893
1326
  `;
894
- } else {
895
- // Delete one-time schedules after execution
896
- this.sql`
1327
+ } else {
1328
+ // Delete one-time schedules after execution
1329
+ this.sql`
897
1330
  DELETE FROM cf_agents_schedules WHERE id = ${row.id}
898
1331
  `;
1332
+ }
899
1333
  }
900
1334
  }
901
1335
 
@@ -911,10 +1345,23 @@ export class Agent<Env, State = unknown> extends Server<Env> {
911
1345
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
912
1346
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
913
1347
  this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1348
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
914
1349
 
915
1350
  // delete all alarms
916
1351
  await this.ctx.storage.deleteAlarm();
917
1352
  await this.ctx.storage.deleteAll();
1353
+ this.ctx.abort("destroyed"); // enforce that the agent is evicted
1354
+
1355
+ this.observability?.emit(
1356
+ {
1357
+ displayMessage: "Agent destroyed",
1358
+ id: nanoid(),
1359
+ payload: {},
1360
+ timestamp: Date.now(),
1361
+ type: "destroy"
1362
+ },
1363
+ this.ctx
1364
+ );
918
1365
  }
919
1366
 
920
1367
  /**
@@ -954,11 +1401,24 @@ export class Agent<Env, State = unknown> extends Server<Env> {
954
1401
  callbackUrl,
955
1402
  options
956
1403
  );
1404
+ this.sql`
1405
+ INSERT
1406
+ OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1407
+ VALUES (
1408
+ ${result.id},
1409
+ ${serverName},
1410
+ ${url},
1411
+ ${result.clientId ?? null},
1412
+ ${result.authUrl ?? null},
1413
+ ${callbackUrl},
1414
+ ${options ? JSON.stringify(options) : null}
1415
+ );
1416
+ `;
957
1417
 
958
1418
  this.broadcast(
959
1419
  JSON.stringify({
960
- type: "cf_agent_mcp_servers",
961
1420
  mcp: this.getMcpServers(),
1421
+ type: MessageType.CF_AGENT_MCP_SERVERS
962
1422
  })
963
1423
  );
964
1424
 
@@ -966,7 +1426,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
966
1426
  }
967
1427
 
968
1428
  async _connectToMcpServerInternal(
969
- serverName: string,
1429
+ _serverName: string,
970
1430
  url: string,
971
1431
  callbackUrl: string,
972
1432
  // it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
@@ -987,7 +1447,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
987
1447
  id: string;
988
1448
  oauthClientId?: string;
989
1449
  }
990
- ): Promise<{ id: string; authUrl: string | undefined }> {
1450
+ ): Promise<{
1451
+ id: string;
1452
+ authUrl: string | undefined;
1453
+ clientId: string | undefined;
1454
+ }> {
991
1455
  const authProvider = new DurableObjectOAuthClientProvider(
992
1456
  this.ctx.storage,
993
1457
  this.name,
@@ -1010,40 +1474,28 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1010
1474
  fetch: (url, init) =>
1011
1475
  fetch(url, {
1012
1476
  ...init,
1013
- headers: options?.transport?.headers,
1014
- }),
1477
+ headers: options?.transport?.headers
1478
+ })
1015
1479
  },
1016
1480
  requestInit: {
1017
- headers: options?.transport?.headers,
1018
- },
1481
+ headers: options?.transport?.headers
1482
+ }
1019
1483
  };
1020
1484
  }
1021
1485
 
1022
1486
  const { id, authUrl, clientId } = await this.mcp.connect(url, {
1487
+ client: options?.client,
1023
1488
  reconnect,
1024
1489
  transport: {
1025
1490
  ...headerTransportOpts,
1026
- authProvider,
1027
- },
1028
- client: options?.client,
1491
+ authProvider
1492
+ }
1029
1493
  });
1030
1494
 
1031
- this.sql`
1032
- INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1033
- VALUES (
1034
- ${id},
1035
- ${serverName},
1036
- ${url},
1037
- ${clientId ?? null},
1038
- ${authUrl ?? null},
1039
- ${callbackUrl},
1040
- ${options ? JSON.stringify(options) : null}
1041
- );
1042
- `;
1043
-
1044
1495
  return {
1045
- id,
1046
1496
  authUrl,
1497
+ clientId,
1498
+ id
1047
1499
  };
1048
1500
  }
1049
1501
 
@@ -1054,34 +1506,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1054
1506
  `;
1055
1507
  this.broadcast(
1056
1508
  JSON.stringify({
1057
- type: "cf_agent_mcp_servers",
1058
1509
  mcp: this.getMcpServers(),
1510
+ type: MessageType.CF_AGENT_MCP_SERVERS
1059
1511
  })
1060
1512
  );
1061
1513
  }
1062
1514
 
1063
1515
  getMcpServers(): MCPServersState {
1064
1516
  const mcpState: MCPServersState = {
1065
- servers: {},
1066
- tools: this.mcp.listTools(),
1067
1517
  prompts: this.mcp.listPrompts(),
1068
1518
  resources: this.mcp.listResources(),
1519
+ servers: {},
1520
+ tools: this.mcp.listTools()
1069
1521
  };
1070
1522
 
1071
1523
  const servers = this.sql<MCPServerRow>`
1072
1524
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1073
1525
  `;
1074
1526
 
1075
- for (const server of servers) {
1076
- mcpState.servers[server.id] = {
1077
- name: server.name,
1078
- server_url: server.server_url,
1079
- auth_url: server.auth_url,
1080
- state: this.mcp.mcpConnections[server.id].connectionState,
1081
- instructions: this.mcp.mcpConnections[server.id].instructions ?? null,
1082
- capabilities:
1083
- this.mcp.mcpConnections[server.id].serverCapabilities ?? null,
1084
- };
1527
+ if (servers && Array.isArray(servers) && servers.length > 0) {
1528
+ for (const server of servers) {
1529
+ const serverConn = this.mcp.mcpConnections[server.id];
1530
+ mcpState.servers[server.id] = {
1531
+ auth_url: server.auth_url,
1532
+ capabilities: serverConn?.serverCapabilities ?? null,
1533
+ instructions: serverConn?.instructions ?? null,
1534
+ name: server.name,
1535
+ server_url: server.server_url,
1536
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1537
+ state: serverConn?.connectionState ?? "authenticating"
1538
+ };
1539
+ }
1085
1540
  }
1086
1541
 
1087
1542
  return mcpState;
@@ -1125,17 +1580,17 @@ export async function routeAgentRequest<Env>(
1125
1580
  const corsHeaders =
1126
1581
  options?.cors === true
1127
1582
  ? {
1128
- "Access-Control-Allow-Origin": "*",
1129
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1130
1583
  "Access-Control-Allow-Credentials": "true",
1131
- "Access-Control-Max-Age": "86400",
1584
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1585
+ "Access-Control-Allow-Origin": "*",
1586
+ "Access-Control-Max-Age": "86400"
1132
1587
  }
1133
1588
  : options?.cors;
1134
1589
 
1135
1590
  if (request.method === "OPTIONS") {
1136
1591
  if (corsHeaders) {
1137
1592
  return new Response(null, {
1138
- headers: corsHeaders,
1593
+ headers: corsHeaders
1139
1594
  });
1140
1595
  }
1141
1596
  console.warn(
@@ -1148,7 +1603,7 @@ export async function routeAgentRequest<Env>(
1148
1603
  env as Record<string, unknown>,
1149
1604
  {
1150
1605
  prefix: "agents",
1151
- ...(options as PartyServerOptions<Record<string, unknown>>),
1606
+ ...(options as PartyServerOptions<Record<string, unknown>>)
1152
1607
  }
1153
1608
  );
1154
1609
 
@@ -1161,24 +1616,238 @@ export async function routeAgentRequest<Env>(
1161
1616
  response = new Response(response.body, {
1162
1617
  headers: {
1163
1618
  ...response.headers,
1164
- ...corsHeaders,
1165
- },
1619
+ ...corsHeaders
1620
+ }
1166
1621
  });
1167
1622
  }
1168
1623
  return response;
1169
1624
  }
1170
1625
 
1626
+ export type EmailResolver<Env> = (
1627
+ email: ForwardableEmailMessage,
1628
+ env: Env
1629
+ ) => Promise<{
1630
+ agentName: string;
1631
+ agentId: string;
1632
+ } | null>;
1633
+
1634
+ /**
1635
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1636
+ * @returns A function that resolves the agent to route the email to
1637
+ */
1638
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1639
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1640
+ const messageId = email.headers.get("message-id");
1641
+ if (messageId) {
1642
+ const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
1643
+ if (messageIdMatch) {
1644
+ const [, agentId, domain] = messageIdMatch;
1645
+ const agentName = domain.split(".")[0];
1646
+ return { agentName, agentId };
1647
+ }
1648
+ }
1649
+
1650
+ const references = email.headers.get("references");
1651
+ if (references) {
1652
+ const referencesMatch = references.match(
1653
+ /<([A-Za-z0-9+/]{43}=)@([^>]+)>/
1654
+ );
1655
+ if (referencesMatch) {
1656
+ const [, base64Id, domain] = referencesMatch;
1657
+ const agentId = Buffer.from(base64Id, "base64").toString("hex");
1658
+ const agentName = domain.split(".")[0];
1659
+ return { agentName, agentId };
1660
+ }
1661
+ }
1662
+
1663
+ const agentName = email.headers.get("x-agent-name");
1664
+ const agentId = email.headers.get("x-agent-id");
1665
+ if (agentName && agentId) {
1666
+ return { agentName, agentId };
1667
+ }
1668
+
1669
+ return null;
1670
+ };
1671
+ }
1672
+
1673
+ /**
1674
+ * Create a resolver that uses the email address to determine the agent to route the email to
1675
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1676
+ * @returns A function that resolves the agent to route the email to
1677
+ */
1678
+ export function createAddressBasedEmailResolver<Env>(
1679
+ defaultAgentName: string
1680
+ ): EmailResolver<Env> {
1681
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1682
+ const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
1683
+ if (!emailMatch) {
1684
+ return null;
1685
+ }
1686
+
1687
+ const [, localPart, subAddress] = emailMatch;
1688
+
1689
+ if (subAddress) {
1690
+ return {
1691
+ agentName: localPart,
1692
+ agentId: subAddress
1693
+ };
1694
+ }
1695
+
1696
+ // Option 2: Use defaultAgentName namespace, localPart as agentId
1697
+ // Common for catch-all email routing to a single EmailAgent namespace
1698
+ return {
1699
+ agentName: defaultAgentName,
1700
+ agentId: localPart
1701
+ };
1702
+ };
1703
+ }
1704
+
1705
+ /**
1706
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1707
+ * @param agentName The name of the agent to route the email to
1708
+ * @param agentId The id of the agent to route the email to
1709
+ * @returns A function that resolves the agent to route the email to
1710
+ */
1711
+ export function createCatchAllEmailResolver<Env>(
1712
+ agentName: string,
1713
+ agentId: string
1714
+ ): EmailResolver<Env> {
1715
+ return async () => ({ agentName, agentId });
1716
+ }
1717
+
1718
+ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1719
+ resolver: EmailResolver<Env>;
1720
+ };
1721
+
1722
+ // Cache the agent namespace map for email routing
1723
+ // This maps both kebab-case and original names to namespaces
1724
+ const agentMapCache = new WeakMap<
1725
+ Record<string, unknown>,
1726
+ Record<string, unknown>
1727
+ >();
1728
+
1171
1729
  /**
1172
1730
  * Route an email to the appropriate Agent
1173
- * @param email Email message to route
1174
- * @param env Environment containing Agent bindings
1175
- * @param options Routing options
1731
+ * @param email The email to route
1732
+ * @param env The environment containing the Agent bindings
1733
+ * @param options The options for routing the email
1734
+ * @returns A promise that resolves when the email has been routed
1176
1735
  */
1177
1736
  export async function routeAgentEmail<Env>(
1178
1737
  email: ForwardableEmailMessage,
1179
1738
  env: Env,
1180
- options?: AgentOptions<Env>
1181
- ): Promise<void> {}
1739
+ options: EmailRoutingOptions<Env>
1740
+ ): Promise<void> {
1741
+ const routingInfo = await options.resolver(email, env);
1742
+
1743
+ if (!routingInfo) {
1744
+ console.warn("No routing information found for email, dropping message");
1745
+ return;
1746
+ }
1747
+
1748
+ // Build a map that includes both original names and kebab-case versions
1749
+ if (!agentMapCache.has(env as Record<string, unknown>)) {
1750
+ const map: Record<string, unknown> = {};
1751
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1752
+ if (
1753
+ value &&
1754
+ typeof value === "object" &&
1755
+ "idFromName" in value &&
1756
+ typeof value.idFromName === "function"
1757
+ ) {
1758
+ // Add both the original name and kebab-case version
1759
+ map[key] = value;
1760
+ map[camelCaseToKebabCase(key)] = value;
1761
+ }
1762
+ }
1763
+ agentMapCache.set(env as Record<string, unknown>, map);
1764
+ }
1765
+
1766
+ const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
1767
+ const namespace = agentMap[routingInfo.agentName];
1768
+
1769
+ if (!namespace) {
1770
+ // Provide helpful error message listing available agents
1771
+ const availableAgents = Object.keys(agentMap)
1772
+ .filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
1773
+ .join(", ");
1774
+ throw new Error(
1775
+ `Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
1776
+ );
1777
+ }
1778
+
1779
+ const agent = await getAgentByName(
1780
+ namespace as unknown as AgentNamespace<Agent<Env>>,
1781
+ routingInfo.agentId
1782
+ );
1783
+
1784
+ // let's make a serialisable version of the email
1785
+ const serialisableEmail: AgentEmail = {
1786
+ getRaw: async () => {
1787
+ const reader = email.raw.getReader();
1788
+ const chunks: Uint8Array[] = [];
1789
+
1790
+ let done = false;
1791
+ while (!done) {
1792
+ const { value, done: readerDone } = await reader.read();
1793
+ done = readerDone;
1794
+ if (value) {
1795
+ chunks.push(value);
1796
+ }
1797
+ }
1798
+
1799
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1800
+ const combined = new Uint8Array(totalLength);
1801
+ let offset = 0;
1802
+ for (const chunk of chunks) {
1803
+ combined.set(chunk, offset);
1804
+ offset += chunk.length;
1805
+ }
1806
+
1807
+ return combined;
1808
+ },
1809
+ headers: email.headers,
1810
+ rawSize: email.rawSize,
1811
+ setReject: (reason: string) => {
1812
+ email.setReject(reason);
1813
+ },
1814
+ forward: (rcptTo: string, headers?: Headers) => {
1815
+ return email.forward(rcptTo, headers);
1816
+ },
1817
+ reply: (options: { from: string; to: string; raw: string }) => {
1818
+ return email.reply(
1819
+ new EmailMessage(options.from, options.to, options.raw)
1820
+ );
1821
+ },
1822
+ from: email.from,
1823
+ to: email.to
1824
+ };
1825
+
1826
+ await agent._onEmail(serialisableEmail);
1827
+ }
1828
+
1829
+ export type AgentEmail = {
1830
+ from: string;
1831
+ to: string;
1832
+ getRaw: () => Promise<Uint8Array>;
1833
+ headers: Headers;
1834
+ rawSize: number;
1835
+ setReject: (reason: string) => void;
1836
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1837
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1838
+ };
1839
+
1840
+ export type EmailSendOptions = {
1841
+ to: string;
1842
+ subject: string;
1843
+ body: string;
1844
+ contentType?: string;
1845
+ headers?: Record<string, string>;
1846
+ includeRoutingHeaders?: boolean;
1847
+ agentName?: string;
1848
+ agentId?: string;
1849
+ domain?: string;
1850
+ };
1182
1851
 
1183
1852
  /**
1184
1853
  * Get or create an Agent by name
@@ -1222,11 +1891,11 @@ export class StreamingResponse {
1222
1891
  throw new Error("StreamingResponse is already closed");
1223
1892
  }
1224
1893
  const response: RPCResponse = {
1225
- type: "rpc",
1894
+ done: false,
1226
1895
  id: this._id,
1227
- success: true,
1228
1896
  result: chunk,
1229
- done: false,
1897
+ success: true,
1898
+ type: MessageType.RPC
1230
1899
  };
1231
1900
  this._connection.send(JSON.stringify(response));
1232
1901
  }
@@ -1241,11 +1910,11 @@ export class StreamingResponse {
1241
1910
  }
1242
1911
  this._closed = true;
1243
1912
  const response: RPCResponse = {
1244
- type: "rpc",
1913
+ done: true,
1245
1914
  id: this._id,
1246
- success: true,
1247
1915
  result: finalChunk,
1248
- done: true,
1916
+ success: true,
1917
+ type: MessageType.RPC
1249
1918
  };
1250
1919
  this._connection.send(JSON.stringify(response));
1251
1920
  }