agents 0.0.0-2801d35 → 0.0.0-2a6e66e

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