agents 0.0.0-0ac89c6 → 0.0.0-0bb74b8

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 +22 -22
  2. package/dist/ai-chat-agent.d.ts +5 -4
  3. package/dist/ai-chat-agent.js +64 -26
  4. package/dist/ai-chat-agent.js.map +1 -1
  5. package/dist/ai-react.d.ts +10 -9
  6. package/dist/ai-react.js +27 -27
  7. package/dist/ai-react.js.map +1 -1
  8. package/dist/{chunk-QSGN3REV.js → chunk-KUH345EY.js} +8 -15
  9. package/dist/chunk-KUH345EY.js.map +1 -0
  10. package/dist/{chunk-4ARKO5R4.js → chunk-MGHXAF5T.js} +390 -89
  11. package/dist/chunk-MGHXAF5T.js.map +1 -0
  12. package/dist/{chunk-Y67CHZBI.js → chunk-MW5BQ2FW.js} +23 -18
  13. package/dist/chunk-MW5BQ2FW.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.d.ts +8 -2
  17. package/dist/client.js +1 -1
  18. package/dist/index-CEVsIbwa.d.ts +565 -0
  19. package/dist/index.d.ts +32 -398
  20. package/dist/index.js +10 -4
  21. package/dist/mcp/client.d.ts +281 -9
  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 +9 -9
  25. package/dist/mcp/index.js +55 -51
  26. package/dist/mcp/index.js.map +1 -1
  27. package/dist/observability/index.d.ts +12 -0
  28. package/dist/observability/index.js +10 -0
  29. package/dist/observability/index.js.map +1 -0
  30. package/dist/react.d.ts +56 -38
  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 +76 -72
  40. package/src/index.ts +517 -126
  41. package/dist/chunk-4ARKO5R4.js.map +0 -1
  42. package/dist/chunk-BZXOAZUX.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,34 +1,32 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
4
+
5
+ import type {
6
+ Prompt,
7
+ Resource,
8
+ ServerCapabilities,
9
+ Tool
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+ import { parseCronExpression } from "cron-schedule";
12
+ import { nanoid } from "nanoid";
13
+ import { EmailMessage } from "cloudflare:email";
1
14
  import {
2
- Server,
3
- routePartykitRequest,
4
- type PartyServerOptions,
5
- getServerByName,
6
15
  type Connection,
7
16
  type ConnectionContext,
17
+ type PartyServerOptions,
18
+ Server,
8
19
  type WSMessage,
20
+ getServerByName,
21
+ routePartykitRequest
9
22
  } from "partyserver";
10
-
11
- import { parseCronExpression } from "cron-schedule";
12
- import { nanoid } from "nanoid";
13
-
14
- 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";
20
- import type {
21
- Tool,
22
- Resource,
23
- Prompt,
24
- } from "@modelcontextprotocol/sdk/types.js";
25
-
26
- import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
27
- import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
28
-
29
23
  import { camelCaseToKebabCase } from "./client";
24
+ import { MCPClientManager } from "./mcp/client";
25
+ // import type { MCPClientConnection } from "./mcp/client-connection";
26
+ import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
27
+ import { genericObservability, type Observability } from "./observability";
30
28
 
31
- export type { Connection, WSMessage, ConnectionContext } from "partyserver";
29
+ export type { Connection, ConnectionContext, WSMessage } from "partyserver";
32
30
 
33
31
  /**
34
32
  * RPC request message from client
@@ -121,6 +119,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
121
119
  export function unstable_callable(metadata: CallableMetadata = {}) {
122
120
  return function callableDecorator<This, Args extends unknown[], Return>(
123
121
  target: (this: This, ...args: Args) => Return,
122
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
124
123
  context: ClassMethodDecoratorContext
125
124
  ) {
126
125
  if (!callableMetadata.has(target)) {
@@ -193,7 +192,12 @@ export type MCPServer = {
193
192
  name: string;
194
193
  server_url: string;
195
194
  auth_url: string | null;
195
+ // This state is specifically about the temporary process of getting a token (if needed).
196
+ // Scope outside of that can't be relied upon because when the DO sleeps, there's no way
197
+ // to communicate a change to a non-ready state.
196
198
  state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
199
+ instructions: string | null;
200
+ capabilities: ServerCapabilities | null;
197
201
  };
198
202
 
199
203
  /**
@@ -218,10 +222,11 @@ const agentContext = new AsyncLocalStorage<{
218
222
  agent: Agent<unknown>;
219
223
  connection: Connection | undefined;
220
224
  request: Request | undefined;
225
+ email: AgentEmail | undefined;
221
226
  }>();
222
227
 
223
228
  export function getCurrentAgent<
224
- T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
229
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
225
230
  >(): {
226
231
  agent: T | undefined;
227
232
  connection: Connection | undefined;
@@ -238,7 +243,7 @@ export function getCurrentAgent<
238
243
  return {
239
244
  agent: undefined,
240
245
  connection: undefined,
241
- request: undefined,
246
+ request: undefined
242
247
  };
243
248
  }
244
249
  return store;
@@ -311,9 +316,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
311
316
  */
312
317
  static options = {
313
318
  /** Whether the Agent should hibernate when inactive */
314
- hibernate: true, // default to hibernate
319
+ hibernate: true // default to hibernate
315
320
  };
316
321
 
322
+ /**
323
+ * The observability implementation to use for the Agent
324
+ */
325
+ observability?: Observability = genericObservability;
326
+
317
327
  /**
318
328
  * Execute SQL queries against the Agent's database
319
329
  * @template T Type of the returned rows
@@ -386,7 +396,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
386
396
  const _onRequest = this.onRequest.bind(this);
387
397
  this.onRequest = (request: Request) => {
388
398
  return agentContext.run(
389
- { agent: this, connection: undefined, request },
399
+ { agent: this, connection: undefined, request, email: undefined },
390
400
  async () => {
391
401
  if (this.mcp.isCallbackRequest(request)) {
392
402
  await this.mcp.handleCallbackRequest(request);
@@ -394,15 +404,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
394
404
  // after the MCP connection handshake, we can send updated mcp state
395
405
  this.broadcast(
396
406
  JSON.stringify({
397
- type: "cf_agent_mcp_servers",
398
- mcp: this._getMcpServerStateInternal(),
407
+ mcp: this.getMcpServers(),
408
+ type: "cf_agent_mcp_servers"
399
409
  })
400
410
  );
401
411
 
402
412
  // We probably should let the user configure this response/redirect, but this is fine for now.
403
413
  return new Response("<script>window.close();</script>", {
404
- status: 200,
405
414
  headers: { "content-type": "text/html" },
415
+ status: 200
406
416
  });
407
417
  }
408
418
 
@@ -414,7 +424,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
414
424
  const _onMessage = this.onMessage.bind(this);
415
425
  this.onMessage = async (connection: Connection, message: WSMessage) => {
416
426
  return agentContext.run(
417
- { agent: this, connection, request: undefined },
427
+ { agent: this, connection, request: undefined, email: undefined },
418
428
  async () => {
419
429
  if (typeof message !== "string") {
420
430
  return this._tryCatch(() => _onMessage(connection, message));
@@ -423,7 +433,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
423
433
  let parsed: unknown;
424
434
  try {
425
435
  parsed = JSON.parse(message);
426
- } catch (e) {
436
+ } catch (_e) {
427
437
  // silently fail and let the onMessage handler handle it
428
438
  return this._tryCatch(() => _onMessage(connection, message));
429
439
  }
@@ -458,22 +468,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
458
468
 
459
469
  // For regular methods, execute and send response
460
470
  const result = await methodFn.apply(this, args);
471
+
472
+ this.observability?.emit(
473
+ {
474
+ displayMessage: `RPC call to ${method}`,
475
+ id: nanoid(),
476
+ payload: {
477
+ args,
478
+ method,
479
+ streaming: metadata?.streaming,
480
+ success: true
481
+ },
482
+ timestamp: Date.now(),
483
+ type: "rpc"
484
+ },
485
+ this.ctx
486
+ );
487
+
461
488
  const response: RPCResponse = {
462
- type: "rpc",
489
+ done: true,
463
490
  id,
464
- success: true,
465
491
  result,
466
- done: true,
492
+ success: true,
493
+ type: "rpc"
467
494
  };
468
495
  connection.send(JSON.stringify(response));
469
496
  } catch (e) {
470
497
  // Send error response
471
498
  const response: RPCResponse = {
472
- type: "rpc",
473
- id: parsed.id,
474
- success: false,
475
499
  error:
476
500
  e instanceof Error ? e.message : "Unknown error occurred",
501
+ id: parsed.id,
502
+ success: false,
503
+ type: "rpc"
477
504
  };
478
505
  connection.send(JSON.stringify(response));
479
506
  console.error("RPC error:", e);
@@ -491,25 +518,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
491
518
  // TODO: This is a hack to ensure the state is sent after the connection is established
492
519
  // must fix this
493
520
  return agentContext.run(
494
- { agent: this, connection, request: ctx.request },
521
+ { agent: this, connection, request: ctx.request, email: undefined },
495
522
  async () => {
496
523
  setTimeout(() => {
497
524
  if (this.state) {
498
525
  connection.send(
499
526
  JSON.stringify({
500
- type: "cf_agent_state",
501
527
  state: this.state,
528
+ type: "cf_agent_state"
502
529
  })
503
530
  );
504
531
  }
505
532
 
506
533
  connection.send(
507
534
  JSON.stringify({
508
- type: "cf_agent_mcp_servers",
509
- mcp: this._getMcpServerStateInternal(),
535
+ mcp: this.getMcpServers(),
536
+ type: "cf_agent_mcp_servers"
510
537
  })
511
538
  );
512
539
 
540
+ this.observability?.emit(
541
+ {
542
+ displayMessage: "Connection established",
543
+ id: nanoid(),
544
+ payload: {
545
+ connectionId: connection.id
546
+ },
547
+ timestamp: Date.now(),
548
+ type: "connect"
549
+ },
550
+ this.ctx
551
+ );
513
552
  return this._tryCatch(() => _onConnect(connection, ctx));
514
553
  }, 20);
515
554
  }
@@ -519,14 +558,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
519
558
  const _onStart = this.onStart.bind(this);
520
559
  this.onStart = async () => {
521
560
  return agentContext.run(
522
- { agent: this, connection: undefined, request: undefined },
561
+ {
562
+ agent: this,
563
+ connection: undefined,
564
+ request: undefined,
565
+ email: undefined
566
+ },
523
567
  async () => {
524
568
  const servers = this.sql<MCPServerRow>`
525
569
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
526
570
  `;
527
571
 
528
- // from DO storage, reconnect to all servers using our saved auth information
529
- await Promise.allSettled(
572
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
573
+ Promise.allSettled(
530
574
  servers.map((server) => {
531
575
  return this._connectToMcpServerInternal(
532
576
  server.name,
@@ -537,19 +581,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
537
581
  : undefined,
538
582
  {
539
583
  id: server.id,
540
- oauthClientId: server.client_id ?? undefined,
584
+ oauthClientId: server.client_id ?? undefined
541
585
  }
542
586
  );
543
587
  })
544
- );
545
-
546
- this.broadcast(
547
- JSON.stringify({
548
- type: "cf_agent_mcp_servers",
549
- mcp: this._getMcpServerStateInternal(),
550
- })
551
- );
552
-
588
+ ).then((_results) => {
589
+ this.broadcast(
590
+ JSON.stringify({
591
+ mcp: this.getMcpServers(),
592
+ type: "cf_agent_mcp_servers"
593
+ })
594
+ );
595
+ });
553
596
  await this._tryCatch(() => _onStart());
554
597
  }
555
598
  );
@@ -560,6 +603,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
560
603
  state: State,
561
604
  source: Connection | "server" = "server"
562
605
  ) {
606
+ const previousState = this._state;
563
607
  this._state = state;
564
608
  this.sql`
565
609
  INSERT OR REPLACE INTO cf_agents_state (id, state)
@@ -571,16 +615,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
571
615
  `;
572
616
  this.broadcast(
573
617
  JSON.stringify({
574
- type: "cf_agent_state",
575
618
  state: state,
619
+ type: "cf_agent_state"
576
620
  }),
577
621
  source !== "server" ? [source.id] : []
578
622
  );
579
623
  return this._tryCatch(() => {
580
- const { connection, request } = agentContext.getStore() || {};
624
+ const { connection, request, email } = agentContext.getStore() || {};
581
625
  return agentContext.run(
582
- { agent: this, connection, request },
626
+ { agent: this, connection, request, email },
583
627
  async () => {
628
+ this.observability?.emit(
629
+ {
630
+ displayMessage: "State updated",
631
+ id: nanoid(),
632
+ payload: {
633
+ previousState,
634
+ state
635
+ },
636
+ timestamp: Date.now(),
637
+ type: "state:update"
638
+ },
639
+ this.ctx
640
+ );
584
641
  return this.onStateUpdate(state, source);
585
642
  }
586
643
  );
@@ -600,23 +657,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
600
657
  * @param state Updated state
601
658
  * @param source Source of the state update ("server" or a client connection)
602
659
  */
660
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
603
661
  onStateUpdate(state: State | undefined, source: Connection | "server") {
604
662
  // override this to handle state updates
605
663
  }
606
664
 
607
665
  /**
608
- * Called when the Agent receives an email
666
+ * Called when the Agent receives an email via routeAgentEmail()
667
+ * Override this method to handle incoming emails
609
668
  * @param email Email message to process
610
669
  */
611
- onEmail(email: ForwardableEmailMessage) {
670
+ async _onEmail(email: AgentEmail) {
671
+ // nb: we use this roundabout way of getting to onEmail
672
+ // because of https://github.com/cloudflare/workerd/issues/4499
612
673
  return agentContext.run(
613
- { agent: this, connection: undefined, request: undefined },
674
+ { agent: this, connection: undefined, request: undefined, email: email },
614
675
  async () => {
615
- console.error("onEmail not implemented");
676
+ if ("onEmail" in this && typeof this.onEmail === "function") {
677
+ return this._tryCatch(() =>
678
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
679
+ );
680
+ } else {
681
+ console.log("Received email from:", email.from, "to:", email.to);
682
+ console.log("Subject:", email.headers.get("subject"));
683
+ console.log(
684
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
685
+ );
686
+ }
616
687
  }
617
688
  );
618
689
  }
619
690
 
691
+ /**
692
+ * Reply to an email
693
+ * @param email The email to reply to
694
+ * @param options Options for the reply
695
+ * @returns void
696
+ */
697
+ async replyToEmail(
698
+ email: AgentEmail,
699
+ options: {
700
+ fromName: string;
701
+ subject?: string | undefined;
702
+ body: string;
703
+ contentType?: string;
704
+ headers?: Record<string, string>;
705
+ }
706
+ ): Promise<void> {
707
+ return this._tryCatch(async () => {
708
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
709
+ const agentId = this.name;
710
+
711
+ const { createMimeMessage } = await import("mimetext");
712
+ const msg = createMimeMessage();
713
+ msg.setSender({ addr: email.to, name: options.fromName });
714
+ msg.setRecipient(email.from);
715
+ msg.setSubject(
716
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
717
+ );
718
+ msg.addMessage({
719
+ contentType: options.contentType || "text/plain",
720
+ data: options.body
721
+ });
722
+
723
+ const domain = email.from.split("@")[1];
724
+ const messageId = `<${agentId}@${domain}>`;
725
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
726
+ msg.setHeader("Message-ID", messageId);
727
+ msg.setHeader("X-Agent-Name", agentName);
728
+ msg.setHeader("X-Agent-ID", agentId);
729
+
730
+ if (options.headers) {
731
+ for (const [key, value] of Object.entries(options.headers)) {
732
+ msg.setHeader(key, value);
733
+ }
734
+ }
735
+ await email.reply({
736
+ from: email.to,
737
+ raw: msg.asRaw(),
738
+ to: email.from
739
+ });
740
+ });
741
+ }
742
+
620
743
  private async _tryCatch<T>(fn: () => T | Promise<T>) {
621
744
  try {
622
745
  return await fn();
@@ -674,6 +797,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
674
797
  ): Promise<Schedule<T>> {
675
798
  const id = nanoid(9);
676
799
 
800
+ const emitScheduleCreate = (schedule: Schedule<T>) =>
801
+ this.observability?.emit(
802
+ {
803
+ displayMessage: `Schedule ${schedule.id} created`,
804
+ id: nanoid(),
805
+ payload: schedule,
806
+ timestamp: Date.now(),
807
+ type: "schedule:create"
808
+ },
809
+ this.ctx
810
+ );
811
+
677
812
  if (typeof callback !== "string") {
678
813
  throw new Error("Callback must be a string");
679
814
  }
@@ -693,13 +828,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
693
828
 
694
829
  await this._scheduleNextAlarm();
695
830
 
696
- return {
697
- id,
831
+ const schedule: Schedule<T> = {
698
832
  callback: callback,
833
+ id,
699
834
  payload: payload as T,
700
835
  time: timestamp,
701
- type: "scheduled",
836
+ type: "scheduled"
702
837
  };
838
+
839
+ emitScheduleCreate(schedule);
840
+
841
+ return schedule;
703
842
  }
704
843
  if (typeof when === "number") {
705
844
  const time = new Date(Date.now() + when * 1000);
@@ -714,14 +853,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
714
853
 
715
854
  await this._scheduleNextAlarm();
716
855
 
717
- return {
718
- id,
856
+ const schedule: Schedule<T> = {
719
857
  callback: callback,
720
- payload: payload as T,
721
858
  delayInSeconds: when,
859
+ id,
860
+ payload: payload as T,
722
861
  time: timestamp,
723
- type: "delayed",
862
+ type: "delayed"
724
863
  };
864
+
865
+ emitScheduleCreate(schedule);
866
+
867
+ return schedule;
725
868
  }
726
869
  if (typeof when === "string") {
727
870
  const nextExecutionTime = getNextCronTime(when);
@@ -736,14 +879,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
736
879
 
737
880
  await this._scheduleNextAlarm();
738
881
 
739
- return {
740
- id,
882
+ const schedule: Schedule<T> = {
741
883
  callback: callback,
742
- payload: payload as T,
743
884
  cron: when,
885
+ id,
886
+ payload: payload as T,
744
887
  time: timestamp,
745
- type: "cron",
888
+ type: "cron"
746
889
  };
890
+
891
+ emitScheduleCreate(schedule);
892
+
893
+ return schedule;
747
894
  }
748
895
  throw new Error("Invalid schedule type");
749
896
  }
@@ -807,7 +954,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
807
954
  .toArray()
808
955
  .map((row) => ({
809
956
  ...row,
810
- payload: JSON.parse(row.payload as string) as T,
957
+ payload: JSON.parse(row.payload as string) as T
811
958
  })) as Schedule<T>[];
812
959
 
813
960
  return result;
@@ -819,6 +966,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
819
966
  * @returns true if the task was cancelled, false otherwise
820
967
  */
821
968
  async cancelSchedule(id: string): Promise<boolean> {
969
+ const schedule = await this.getSchedule(id);
970
+ if (schedule) {
971
+ this.observability?.emit(
972
+ {
973
+ displayMessage: `Schedule ${id} cancelled`,
974
+ id: nanoid(),
975
+ payload: schedule,
976
+ timestamp: Date.now(),
977
+ type: "schedule:cancel"
978
+ },
979
+ this.ctx
980
+ );
981
+ }
822
982
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
823
983
 
824
984
  await this._scheduleNextAlarm();
@@ -864,9 +1024,25 @@ export class Agent<Env, State = unknown> extends Server<Env> {
864
1024
  continue;
865
1025
  }
866
1026
  await agentContext.run(
867
- { agent: this, connection: undefined, request: undefined },
1027
+ {
1028
+ agent: this,
1029
+ connection: undefined,
1030
+ request: undefined,
1031
+ email: undefined
1032
+ },
868
1033
  async () => {
869
1034
  try {
1035
+ this.observability?.emit(
1036
+ {
1037
+ displayMessage: `Schedule ${row.id} executed`,
1038
+ id: nanoid(),
1039
+ payload: row,
1040
+ timestamp: Date.now(),
1041
+ type: "schedule:execute"
1042
+ },
1043
+ this.ctx
1044
+ );
1045
+
870
1046
  await (
871
1047
  callback as (
872
1048
  payload: unknown,
@@ -910,6 +1086,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
910
1086
  // delete all alarms
911
1087
  await this.ctx.storage.deleteAlarm();
912
1088
  await this.ctx.storage.deleteAll();
1089
+ this.ctx.abort("destroyed"); // enforce that the agent is evicted
1090
+
1091
+ this.observability?.emit(
1092
+ {
1093
+ displayMessage: "Agent destroyed",
1094
+ id: nanoid(),
1095
+ payload: {},
1096
+ timestamp: Date.now(),
1097
+ type: "destroy"
1098
+ },
1099
+ this.ctx
1100
+ );
913
1101
  }
914
1102
 
915
1103
  /**
@@ -949,11 +1137,24 @@ export class Agent<Env, State = unknown> extends Server<Env> {
949
1137
  callbackUrl,
950
1138
  options
951
1139
  );
1140
+ this.sql`
1141
+ INSERT
1142
+ OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1143
+ VALUES (
1144
+ ${result.id},
1145
+ ${serverName},
1146
+ ${url},
1147
+ ${result.clientId ?? null},
1148
+ ${result.authUrl ?? null},
1149
+ ${callbackUrl},
1150
+ ${options ? JSON.stringify(options) : null}
1151
+ );
1152
+ `;
952
1153
 
953
1154
  this.broadcast(
954
1155
  JSON.stringify({
955
- type: "cf_agent_mcp_servers",
956
- mcp: this._getMcpServerStateInternal(),
1156
+ mcp: this.getMcpServers(),
1157
+ type: "cf_agent_mcp_servers"
957
1158
  })
958
1159
  );
959
1160
 
@@ -961,7 +1162,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
961
1162
  }
962
1163
 
963
1164
  async _connectToMcpServerInternal(
964
- serverName: string,
1165
+ _serverName: string,
965
1166
  url: string,
966
1167
  callbackUrl: string,
967
1168
  // it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
@@ -982,7 +1183,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
982
1183
  id: string;
983
1184
  oauthClientId?: string;
984
1185
  }
985
- ): Promise<{ id: string; authUrl: string | undefined }> {
1186
+ ): Promise<{
1187
+ id: string;
1188
+ authUrl: string | undefined;
1189
+ clientId: string | undefined;
1190
+ }> {
986
1191
  const authProvider = new DurableObjectOAuthClientProvider(
987
1192
  this.ctx.storage,
988
1193
  this.name,
@@ -1005,40 +1210,28 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1005
1210
  fetch: (url, init) =>
1006
1211
  fetch(url, {
1007
1212
  ...init,
1008
- headers: options?.transport?.headers,
1009
- }),
1213
+ headers: options?.transport?.headers
1214
+ })
1010
1215
  },
1011
1216
  requestInit: {
1012
- headers: options?.transport?.headers,
1013
- },
1217
+ headers: options?.transport?.headers
1218
+ }
1014
1219
  };
1015
1220
  }
1016
1221
 
1017
1222
  const { id, authUrl, clientId } = await this.mcp.connect(url, {
1223
+ client: options?.client,
1018
1224
  reconnect,
1019
1225
  transport: {
1020
1226
  ...headerTransportOpts,
1021
- authProvider,
1022
- },
1023
- client: options?.client,
1227
+ authProvider
1228
+ }
1024
1229
  });
1025
1230
 
1026
- this.sql`
1027
- INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1028
- VALUES (
1029
- ${id},
1030
- ${serverName},
1031
- ${url},
1032
- ${clientId ?? null},
1033
- ${authUrl ?? null},
1034
- ${callbackUrl},
1035
- ${options ? JSON.stringify(options) : null}
1036
- );
1037
- `;
1038
-
1039
1231
  return {
1040
- id,
1041
1232
  authUrl,
1233
+ clientId,
1234
+ id
1042
1235
  };
1043
1236
  }
1044
1237
 
@@ -1049,18 +1242,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1049
1242
  `;
1050
1243
  this.broadcast(
1051
1244
  JSON.stringify({
1052
- type: "cf_agent_mcp_servers",
1053
- mcp: this._getMcpServerStateInternal(),
1245
+ mcp: this.getMcpServers(),
1246
+ type: "cf_agent_mcp_servers"
1054
1247
  })
1055
1248
  );
1056
1249
  }
1057
1250
 
1058
- private _getMcpServerStateInternal(): MCPServersState {
1251
+ getMcpServers(): MCPServersState {
1059
1252
  const mcpState: MCPServersState = {
1060
- servers: {},
1061
- tools: this.mcp.listTools(),
1062
1253
  prompts: this.mcp.listPrompts(),
1063
1254
  resources: this.mcp.listResources(),
1255
+ servers: {},
1256
+ tools: this.mcp.listTools()
1064
1257
  };
1065
1258
 
1066
1259
  const servers = this.sql<MCPServerRow>`
@@ -1068,11 +1261,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1068
1261
  `;
1069
1262
 
1070
1263
  for (const server of servers) {
1264
+ const serverConn = this.mcp.mcpConnections[server.id];
1071
1265
  mcpState.servers[server.id] = {
1266
+ auth_url: server.auth_url,
1267
+ capabilities: serverConn?.serverCapabilities ?? null,
1268
+ instructions: serverConn?.instructions ?? null,
1072
1269
  name: server.name,
1073
1270
  server_url: server.server_url,
1074
- auth_url: server.auth_url,
1075
- state: this.mcp.mcpConnections[server.id].connectionState,
1271
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1272
+ state: serverConn?.connectionState ?? "authenticating"
1076
1273
  };
1077
1274
  }
1078
1275
 
@@ -1117,17 +1314,17 @@ export async function routeAgentRequest<Env>(
1117
1314
  const corsHeaders =
1118
1315
  options?.cors === true
1119
1316
  ? {
1120
- "Access-Control-Allow-Origin": "*",
1121
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1122
1317
  "Access-Control-Allow-Credentials": "true",
1123
- "Access-Control-Max-Age": "86400",
1318
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1319
+ "Access-Control-Allow-Origin": "*",
1320
+ "Access-Control-Max-Age": "86400"
1124
1321
  }
1125
1322
  : options?.cors;
1126
1323
 
1127
1324
  if (request.method === "OPTIONS") {
1128
1325
  if (corsHeaders) {
1129
1326
  return new Response(null, {
1130
- headers: corsHeaders,
1327
+ headers: corsHeaders
1131
1328
  });
1132
1329
  }
1133
1330
  console.warn(
@@ -1140,7 +1337,7 @@ export async function routeAgentRequest<Env>(
1140
1337
  env as Record<string, unknown>,
1141
1338
  {
1142
1339
  prefix: "agents",
1143
- ...(options as PartyServerOptions<Record<string, unknown>>),
1340
+ ...(options as PartyServerOptions<Record<string, unknown>>)
1144
1341
  }
1145
1342
  );
1146
1343
 
@@ -1153,24 +1350,218 @@ export async function routeAgentRequest<Env>(
1153
1350
  response = new Response(response.body, {
1154
1351
  headers: {
1155
1352
  ...response.headers,
1156
- ...corsHeaders,
1157
- },
1353
+ ...corsHeaders
1354
+ }
1158
1355
  });
1159
1356
  }
1160
1357
  return response;
1161
1358
  }
1162
1359
 
1360
+ export type EmailResolver<Env> = (
1361
+ email: ForwardableEmailMessage,
1362
+ env: Env
1363
+ ) => Promise<{
1364
+ agentName: string;
1365
+ agentId: string;
1366
+ } | null>;
1367
+
1368
+ /**
1369
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1370
+ * @returns A function that resolves the agent to route the email to
1371
+ */
1372
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1373
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1374
+ const messageId = email.headers.get("message-id");
1375
+ if (messageId) {
1376
+ const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
1377
+ if (messageIdMatch) {
1378
+ const [, agentId, domain] = messageIdMatch;
1379
+ const agentName = domain.split(".")[0];
1380
+ return { agentName, agentId };
1381
+ }
1382
+ }
1383
+
1384
+ const references = email.headers.get("references");
1385
+ if (references) {
1386
+ const referencesMatch = references.match(
1387
+ /<([A-Za-z0-9+/]{43}=)@([^>]+)>/
1388
+ );
1389
+ if (referencesMatch) {
1390
+ const [, base64Id, domain] = referencesMatch;
1391
+ const agentId = Buffer.from(base64Id, "base64").toString("hex");
1392
+ const agentName = domain.split(".")[0];
1393
+ return { agentName, agentId };
1394
+ }
1395
+ }
1396
+
1397
+ const agentName = email.headers.get("x-agent-name");
1398
+ const agentId = email.headers.get("x-agent-id");
1399
+ if (agentName && agentId) {
1400
+ return { agentName, agentId };
1401
+ }
1402
+
1403
+ return null;
1404
+ };
1405
+ }
1406
+
1407
+ /**
1408
+ * Create a resolver that uses the email address to determine the agent to route the email to
1409
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1410
+ * @returns A function that resolves the agent to route the email to
1411
+ */
1412
+ export function createAddressBasedEmailResolver<Env>(
1413
+ defaultAgentName: string
1414
+ ): EmailResolver<Env> {
1415
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1416
+ const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
1417
+ if (!emailMatch) {
1418
+ return null;
1419
+ }
1420
+
1421
+ const [, localPart, subAddress] = emailMatch;
1422
+
1423
+ if (subAddress) {
1424
+ return {
1425
+ agentName: localPart,
1426
+ agentId: subAddress
1427
+ };
1428
+ }
1429
+
1430
+ // Option 2: Use defaultAgentName namespace, localPart as agentId
1431
+ // Common for catch-all email routing to a single EmailAgent namespace
1432
+ return {
1433
+ agentName: defaultAgentName,
1434
+ agentId: localPart
1435
+ };
1436
+ };
1437
+ }
1438
+
1439
+ /**
1440
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1441
+ * @param agentName The name of the agent to route the email to
1442
+ * @param agentId The id of the agent to route the email to
1443
+ * @returns A function that resolves the agent to route the email to
1444
+ */
1445
+ export function createCatchAllEmailResolver<Env>(
1446
+ agentName: string,
1447
+ agentId: string
1448
+ ): EmailResolver<Env> {
1449
+ return async () => ({ agentName, agentId });
1450
+ }
1451
+
1452
+ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1453
+ resolver: EmailResolver<Env>;
1454
+ };
1455
+
1163
1456
  /**
1164
1457
  * Route an email to the appropriate Agent
1165
- * @param email Email message to route
1166
- * @param env Environment containing Agent bindings
1167
- * @param options Routing options
1458
+ * @param email The email to route
1459
+ * @param env The environment containing the Agent bindings
1460
+ * @param options The options for routing the email
1461
+ * @returns A promise that resolves when the email has been routed
1168
1462
  */
1169
1463
  export async function routeAgentEmail<Env>(
1170
1464
  email: ForwardableEmailMessage,
1171
1465
  env: Env,
1172
- options?: AgentOptions<Env>
1173
- ): Promise<void> {}
1466
+ options: EmailRoutingOptions<Env>
1467
+ ): Promise<void> {
1468
+ const routingInfo = await options.resolver(email, env);
1469
+
1470
+ if (!routingInfo) {
1471
+ console.warn("No routing information found for email, dropping message");
1472
+ return;
1473
+ }
1474
+
1475
+ const namespaceBinding = env[routingInfo.agentName as keyof Env];
1476
+ if (!namespaceBinding) {
1477
+ throw new Error(
1478
+ `Agent namespace '${routingInfo.agentName}' not found in environment`
1479
+ );
1480
+ }
1481
+
1482
+ // Type guard to check if this is actually a DurableObjectNamespace (AgentNamespace)
1483
+ if (
1484
+ typeof namespaceBinding !== "object" ||
1485
+ !("idFromName" in namespaceBinding) ||
1486
+ typeof namespaceBinding.idFromName !== "function"
1487
+ ) {
1488
+ throw new Error(
1489
+ `Environment binding '${routingInfo.agentName}' is not an AgentNamespace (found: ${typeof namespaceBinding})`
1490
+ );
1491
+ }
1492
+
1493
+ // Safe cast after runtime validation
1494
+ const namespace = namespaceBinding as unknown as AgentNamespace<Agent<Env>>;
1495
+
1496
+ const agent = await getAgentByName(namespace, routingInfo.agentId);
1497
+
1498
+ // let's make a serialisable version of the email
1499
+ const serialisableEmail: AgentEmail = {
1500
+ getRaw: async () => {
1501
+ const reader = email.raw.getReader();
1502
+ const chunks: Uint8Array[] = [];
1503
+
1504
+ let done = false;
1505
+ while (!done) {
1506
+ const { value, done: readerDone } = await reader.read();
1507
+ done = readerDone;
1508
+ if (value) {
1509
+ chunks.push(value);
1510
+ }
1511
+ }
1512
+
1513
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1514
+ const combined = new Uint8Array(totalLength);
1515
+ let offset = 0;
1516
+ for (const chunk of chunks) {
1517
+ combined.set(chunk, offset);
1518
+ offset += chunk.length;
1519
+ }
1520
+
1521
+ return combined;
1522
+ },
1523
+ headers: email.headers,
1524
+ rawSize: email.rawSize,
1525
+ setReject: (reason: string) => {
1526
+ email.setReject(reason);
1527
+ },
1528
+ forward: (rcptTo: string, headers?: Headers) => {
1529
+ return email.forward(rcptTo, headers);
1530
+ },
1531
+ reply: (options: { from: string; to: string; raw: string }) => {
1532
+ return email.reply(
1533
+ new EmailMessage(options.from, options.to, options.raw)
1534
+ );
1535
+ },
1536
+ from: email.from,
1537
+ to: email.to
1538
+ };
1539
+
1540
+ await agent._onEmail(serialisableEmail);
1541
+ }
1542
+
1543
+ export type AgentEmail = {
1544
+ from: string;
1545
+ to: string;
1546
+ getRaw: () => Promise<Uint8Array>;
1547
+ headers: Headers;
1548
+ rawSize: number;
1549
+ setReject: (reason: string) => void;
1550
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1551
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1552
+ };
1553
+
1554
+ export type EmailSendOptions = {
1555
+ to: string;
1556
+ subject: string;
1557
+ body: string;
1558
+ contentType?: string;
1559
+ headers?: Record<string, string>;
1560
+ includeRoutingHeaders?: boolean;
1561
+ agentName?: string;
1562
+ agentId?: string;
1563
+ domain?: string;
1564
+ };
1174
1565
 
1175
1566
  /**
1176
1567
  * Get or create an Agent by name
@@ -1214,11 +1605,11 @@ export class StreamingResponse {
1214
1605
  throw new Error("StreamingResponse is already closed");
1215
1606
  }
1216
1607
  const response: RPCResponse = {
1217
- type: "rpc",
1608
+ done: false,
1218
1609
  id: this._id,
1219
- success: true,
1220
1610
  result: chunk,
1221
- done: false,
1611
+ success: true,
1612
+ type: "rpc"
1222
1613
  };
1223
1614
  this._connection.send(JSON.stringify(response));
1224
1615
  }
@@ -1233,11 +1624,11 @@ export class StreamingResponse {
1233
1624
  }
1234
1625
  this._closed = true;
1235
1626
  const response: RPCResponse = {
1236
- type: "rpc",
1627
+ done: true,
1237
1628
  id: this._id,
1238
- success: true,
1239
1629
  result: finalChunk,
1240
- done: true,
1630
+ success: true,
1631
+ type: "rpc"
1241
1632
  };
1242
1633
  this._connection.send(JSON.stringify(response));
1243
1634
  }