agents 0.0.0-569680f → 0.0.0-579b228

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.
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  Prompt,
7
7
  Resource,
8
8
  ServerCapabilities,
9
- Tool,
9
+ Tool
10
10
  } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { parseCronExpression } from "cron-schedule";
12
12
  import { nanoid } from "nanoid";
@@ -18,7 +18,7 @@ import {
18
18
  Server,
19
19
  type WSMessage,
20
20
  getServerByName,
21
- routePartykitRequest,
21
+ routePartykitRequest
22
22
  } from "partyserver";
23
23
  import { camelCaseToKebabCase } from "./client";
24
24
  import { MCPClientManager } from "./mcp/client";
@@ -130,6 +130,13 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
130
130
  };
131
131
  }
132
132
 
133
+ export type QueueItem<T = string> = {
134
+ id: string;
135
+ payload: T;
136
+ callback: keyof Agent<unknown>;
137
+ created_at: number;
138
+ };
139
+
133
140
  /**
134
141
  * Represents a scheduled task within an Agent
135
142
  * @template T Type of the payload data
@@ -219,23 +226,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
219
226
  const DEFAULT_STATE = {} as unknown;
220
227
 
221
228
  const agentContext = new AsyncLocalStorage<{
222
- agent: Agent<unknown>;
229
+ agent: Agent<unknown, unknown>;
223
230
  connection: Connection | undefined;
224
231
  request: Request | undefined;
232
+ email: AgentEmail | undefined;
225
233
  }>();
226
234
 
227
235
  export function getCurrentAgent<
228
- T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
236
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
229
237
  >(): {
230
238
  agent: T | undefined;
231
239
  connection: Connection | undefined;
232
- request: Request<unknown, CfProperties<unknown>> | undefined;
240
+ request: Request | undefined;
241
+ email: AgentEmail | undefined;
233
242
  } {
234
243
  const store = agentContext.getStore() as
235
244
  | {
236
245
  agent: T;
237
246
  connection: Connection | undefined;
238
- request: Request<unknown, CfProperties<unknown>> | undefined;
247
+ request: Request | undefined;
248
+ email: AgentEmail | undefined;
239
249
  }
240
250
  | undefined;
241
251
  if (!store) {
@@ -243,11 +253,31 @@ export function getCurrentAgent<
243
253
  agent: undefined,
244
254
  connection: undefined,
245
255
  request: undefined,
256
+ email: undefined
246
257
  };
247
258
  }
248
259
  return store;
249
260
  }
250
261
 
262
+ /**
263
+ * Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
264
+ * @param agent The agent instance
265
+ * @param method The method to wrap
266
+ * @returns A wrapped method that runs within the agent context
267
+ */
268
+
269
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
270
+ function withAgentContext<T extends (...args: any[]) => any>(
271
+ method: T
272
+ ): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
273
+ return function (...args: Parameters<T>): ReturnType<T> {
274
+ const { connection, request, email } = getCurrentAgent();
275
+ return agentContext.run({ agent: this, connection, request, email }, () => {
276
+ return method.apply(this, args);
277
+ });
278
+ };
279
+ }
280
+
251
281
  /**
252
282
  * Base class for creating Agent implementations
253
283
  * @template Env Environment type containing bindings
@@ -315,7 +345,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
315
345
  */
316
346
  static options = {
317
347
  /** Whether the Agent should hibernate when inactive */
318
- hibernate: true, // default to hibernate
348
+ hibernate: true // default to hibernate
319
349
  };
320
350
 
321
351
  /**
@@ -352,6 +382,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
352
382
  constructor(ctx: AgentContext, env: Env) {
353
383
  super(ctx, env);
354
384
 
385
+ // Auto-wrap custom methods with agent context
386
+ this._autoWrapCustomMethods();
387
+
355
388
  this.sql`
356
389
  CREATE TABLE IF NOT EXISTS cf_agents_state (
357
390
  id TEXT PRIMARY KEY NOT NULL,
@@ -359,6 +392,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
359
392
  )
360
393
  `;
361
394
 
395
+ this.sql`
396
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
397
+ id TEXT PRIMARY KEY NOT NULL,
398
+ payload TEXT,
399
+ callback TEXT,
400
+ created_at INTEGER DEFAULT (unixepoch())
401
+ )
402
+ `;
403
+
362
404
  void this.ctx.blockConcurrencyWhile(async () => {
363
405
  return this._tryCatch(async () => {
364
406
  // Create alarms table if it doesn't exist
@@ -395,7 +437,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
395
437
  const _onRequest = this.onRequest.bind(this);
396
438
  this.onRequest = (request: Request) => {
397
439
  return agentContext.run(
398
- { agent: this, connection: undefined, request },
440
+ { agent: this, connection: undefined, request, email: undefined },
399
441
  async () => {
400
442
  if (this.mcp.isCallbackRequest(request)) {
401
443
  await this.mcp.handleCallbackRequest(request);
@@ -404,14 +446,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
404
446
  this.broadcast(
405
447
  JSON.stringify({
406
448
  mcp: this.getMcpServers(),
407
- type: "cf_agent_mcp_servers",
449
+ type: "cf_agent_mcp_servers"
408
450
  })
409
451
  );
410
452
 
411
453
  // We probably should let the user configure this response/redirect, but this is fine for now.
412
454
  return new Response("<script>window.close();</script>", {
413
455
  headers: { "content-type": "text/html" },
414
- status: 200,
456
+ status: 200
415
457
  });
416
458
  }
417
459
 
@@ -423,7 +465,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
423
465
  const _onMessage = this.onMessage.bind(this);
424
466
  this.onMessage = async (connection: Connection, message: WSMessage) => {
425
467
  return agentContext.run(
426
- { agent: this, connection, request: undefined },
468
+ { agent: this, connection, request: undefined, email: undefined },
427
469
  async () => {
428
470
  if (typeof message !== "string") {
429
471
  return this._tryCatch(() => _onMessage(connection, message));
@@ -476,10 +518,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
476
518
  args,
477
519
  method,
478
520
  streaming: metadata?.streaming,
479
- success: true,
521
+ success: true
480
522
  },
481
523
  timestamp: Date.now(),
482
- type: "rpc",
524
+ type: "rpc"
483
525
  },
484
526
  this.ctx
485
527
  );
@@ -489,7 +531,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
489
531
  id,
490
532
  result,
491
533
  success: true,
492
- type: "rpc",
534
+ type: "rpc"
493
535
  };
494
536
  connection.send(JSON.stringify(response));
495
537
  } catch (e) {
@@ -499,7 +541,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
499
541
  e instanceof Error ? e.message : "Unknown error occurred",
500
542
  id: parsed.id,
501
543
  success: false,
502
- type: "rpc",
544
+ type: "rpc"
503
545
  };
504
546
  connection.send(JSON.stringify(response));
505
547
  console.error("RPC error:", e);
@@ -517,14 +559,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
517
559
  // TODO: This is a hack to ensure the state is sent after the connection is established
518
560
  // must fix this
519
561
  return agentContext.run(
520
- { agent: this, connection, request: ctx.request },
562
+ { agent: this, connection, request: ctx.request, email: undefined },
521
563
  async () => {
522
564
  setTimeout(() => {
523
565
  if (this.state) {
524
566
  connection.send(
525
567
  JSON.stringify({
526
568
  state: this.state,
527
- type: "cf_agent_state",
569
+ type: "cf_agent_state"
528
570
  })
529
571
  );
530
572
  }
@@ -532,7 +574,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
532
574
  connection.send(
533
575
  JSON.stringify({
534
576
  mcp: this.getMcpServers(),
535
- type: "cf_agent_mcp_servers",
577
+ type: "cf_agent_mcp_servers"
536
578
  })
537
579
  );
538
580
 
@@ -541,10 +583,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
541
583
  displayMessage: "Connection established",
542
584
  id: nanoid(),
543
585
  payload: {
544
- connectionId: connection.id,
586
+ connectionId: connection.id
545
587
  },
546
588
  timestamp: Date.now(),
547
- type: "connect",
589
+ type: "connect"
548
590
  },
549
591
  this.ctx
550
592
  );
@@ -557,36 +599,43 @@ export class Agent<Env, State = unknown> extends Server<Env> {
557
599
  const _onStart = this.onStart.bind(this);
558
600
  this.onStart = async () => {
559
601
  return agentContext.run(
560
- { agent: this, connection: undefined, request: undefined },
602
+ {
603
+ agent: this,
604
+ connection: undefined,
605
+ request: undefined,
606
+ email: undefined
607
+ },
561
608
  async () => {
562
609
  const servers = this.sql<MCPServerRow>`
563
610
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
564
611
  `;
565
612
 
566
613
  // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
567
- Promise.allSettled(
568
- servers.map((server) => {
569
- return this._connectToMcpServerInternal(
570
- server.name,
571
- server.server_url,
572
- server.callback_url,
573
- server.server_options
574
- ? JSON.parse(server.server_options)
575
- : undefined,
576
- {
577
- id: server.id,
578
- oauthClientId: server.client_id ?? undefined,
579
- }
580
- );
581
- })
582
- ).then((_results) => {
583
- this.broadcast(
584
- JSON.stringify({
585
- mcp: this.getMcpServers(),
586
- type: "cf_agent_mcp_servers",
614
+ if (servers && Array.isArray(servers) && servers.length > 0) {
615
+ Promise.allSettled(
616
+ servers.map((server) => {
617
+ return this._connectToMcpServerInternal(
618
+ server.name,
619
+ server.server_url,
620
+ server.callback_url,
621
+ server.server_options
622
+ ? JSON.parse(server.server_options)
623
+ : undefined,
624
+ {
625
+ id: server.id,
626
+ oauthClientId: server.client_id ?? undefined
627
+ }
628
+ );
587
629
  })
588
- );
589
- });
630
+ ).then((_results) => {
631
+ this.broadcast(
632
+ JSON.stringify({
633
+ mcp: this.getMcpServers(),
634
+ type: "cf_agent_mcp_servers"
635
+ })
636
+ );
637
+ });
638
+ }
590
639
  await this._tryCatch(() => _onStart());
591
640
  }
592
641
  );
@@ -610,14 +659,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
610
659
  this.broadcast(
611
660
  JSON.stringify({
612
661
  state: state,
613
- type: "cf_agent_state",
662
+ type: "cf_agent_state"
614
663
  }),
615
664
  source !== "server" ? [source.id] : []
616
665
  );
617
666
  return this._tryCatch(() => {
618
- const { connection, request } = agentContext.getStore() || {};
667
+ const { connection, request, email } = agentContext.getStore() || {};
619
668
  return agentContext.run(
620
- { agent: this, connection, request },
669
+ { agent: this, connection, request, email },
621
670
  async () => {
622
671
  this.observability?.emit(
623
672
  {
@@ -625,10 +674,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
625
674
  id: nanoid(),
626
675
  payload: {
627
676
  previousState,
628
- state,
677
+ state
629
678
  },
630
679
  timestamp: Date.now(),
631
- type: "state:update",
680
+ type: "state:update"
632
681
  },
633
682
  this.ctx
634
683
  );
@@ -657,35 +706,80 @@ export class Agent<Env, State = unknown> extends Server<Env> {
657
706
  }
658
707
 
659
708
  /**
660
- * Called when the Agent receives an email
709
+ * Called when the Agent receives an email via routeAgentEmail()
661
710
  * Override this method to handle incoming emails
662
711
  * @param email Email message to process
663
712
  */
664
- async onEmail(email: ForwardableEmailMessage) {
713
+ async _onEmail(email: AgentEmail) {
714
+ // nb: we use this roundabout way of getting to onEmail
715
+ // because of https://github.com/cloudflare/workerd/issues/4499
665
716
  return agentContext.run(
666
- { agent: this, connection: undefined, request: undefined },
717
+ { agent: this, connection: undefined, request: undefined, email: email },
667
718
  async () => {
668
- console.log("Received email from:", email.from, "to:", email.to);
669
- console.log("Subject:", email.headers.get("subject"));
670
- console.log("Override onEmail() in your agent to process emails");
719
+ if ("onEmail" in this && typeof this.onEmail === "function") {
720
+ return this._tryCatch(() =>
721
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
722
+ );
723
+ } else {
724
+ console.log("Received email from:", email.from, "to:", email.to);
725
+ console.log("Subject:", email.headers.get("subject"));
726
+ console.log(
727
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
728
+ );
729
+ }
671
730
  }
672
731
  );
673
732
  }
674
733
 
675
- async sendEmail(
676
- emailBinding: SendEmail,
677
- from: string,
678
- fromName: string,
679
- options: Omit<EmailSendOptions, "agentName" | "agentId">
734
+ /**
735
+ * Reply to an email
736
+ * @param email The email to reply to
737
+ * @param options Options for the reply
738
+ * @returns void
739
+ */
740
+ async replyToEmail(
741
+ email: AgentEmail,
742
+ options: {
743
+ fromName: string;
744
+ subject?: string | undefined;
745
+ body: string;
746
+ contentType?: string;
747
+ headers?: Record<string, string>;
748
+ }
680
749
  ): Promise<void> {
681
- const agentName = camelCaseToKebabCase(this._ParentClass.name);
682
- const agentId = this.name;
683
-
684
- return sendEmailWithRouting(emailBinding, from, fromName, {
685
- ...options,
686
- agentName,
687
- agentId,
688
- includeRoutingHeaders: true,
750
+ return this._tryCatch(async () => {
751
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
752
+ const agentId = this.name;
753
+
754
+ const { createMimeMessage } = await import("mimetext");
755
+ const msg = createMimeMessage();
756
+ msg.setSender({ addr: email.to, name: options.fromName });
757
+ msg.setRecipient(email.from);
758
+ msg.setSubject(
759
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
760
+ );
761
+ msg.addMessage({
762
+ contentType: options.contentType || "text/plain",
763
+ data: options.body
764
+ });
765
+
766
+ const domain = email.from.split("@")[1];
767
+ const messageId = `<${agentId}@${domain}>`;
768
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
769
+ msg.setHeader("Message-ID", messageId);
770
+ msg.setHeader("X-Agent-Name", agentName);
771
+ msg.setHeader("X-Agent-ID", agentId);
772
+
773
+ if (options.headers) {
774
+ for (const [key, value] of Object.entries(options.headers)) {
775
+ msg.setHeader(key, value);
776
+ }
777
+ }
778
+ await email.reply({
779
+ from: email.to,
780
+ raw: msg.asRaw(),
781
+ to: email.from
782
+ });
689
783
  });
690
784
  }
691
785
 
@@ -697,6 +791,72 @@ export class Agent<Env, State = unknown> extends Server<Env> {
697
791
  }
698
792
  }
699
793
 
794
+ /**
795
+ * Automatically wrap custom methods with agent context
796
+ * This ensures getCurrentAgent() works in all custom methods without decorators
797
+ */
798
+ private _autoWrapCustomMethods() {
799
+ // Collect all methods from base prototypes (Agent and Server)
800
+ const basePrototypes = [Agent.prototype, Server.prototype];
801
+ const baseMethods = new Set<string>();
802
+ for (const baseProto of basePrototypes) {
803
+ let proto = baseProto;
804
+ while (proto && proto !== Object.prototype) {
805
+ const methodNames = Object.getOwnPropertyNames(proto);
806
+ for (const methodName of methodNames) {
807
+ baseMethods.add(methodName);
808
+ }
809
+ proto = Object.getPrototypeOf(proto);
810
+ }
811
+ }
812
+ // Get all methods from the current instance's prototype chain
813
+ let proto = Object.getPrototypeOf(this);
814
+ let depth = 0;
815
+ while (proto && proto !== Object.prototype && depth < 10) {
816
+ const methodNames = Object.getOwnPropertyNames(proto);
817
+ for (const methodName of methodNames) {
818
+ // Skip if it's a private method or not a function
819
+ if (
820
+ baseMethods.has(methodName) ||
821
+ methodName.startsWith("_") ||
822
+ typeof this[methodName as keyof this] !== "function"
823
+ ) {
824
+ continue;
825
+ }
826
+ // If the method doesn't exist in base prototypes, it's a custom method
827
+ if (!baseMethods.has(methodName)) {
828
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
829
+ if (descriptor && typeof descriptor.value === "function") {
830
+ // Wrap the custom method with context
831
+
832
+ const wrappedFunction = withAgentContext(
833
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
834
+ this[methodName as keyof this] as (...args: any[]) => any
835
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
836
+ ) as any;
837
+
838
+ // if the method is callable, copy the metadata from the original method
839
+ if (this._isCallable(methodName)) {
840
+ callableMetadata.set(
841
+ wrappedFunction,
842
+ callableMetadata.get(
843
+ this[methodName as keyof this] as Function
844
+ )!
845
+ );
846
+ }
847
+
848
+ // set the wrapped function on the prototype
849
+ this.constructor.prototype[methodName as keyof this] =
850
+ wrappedFunction;
851
+ }
852
+ }
853
+ }
854
+
855
+ proto = Object.getPrototypeOf(proto);
856
+ depth++;
857
+ }
858
+ }
859
+
700
860
  override onError(
701
861
  connection: Connection,
702
862
  error: unknown
@@ -731,6 +891,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
731
891
  throw new Error("Not implemented");
732
892
  }
733
893
 
894
+ /**
895
+ * Queue a task to be executed in the future
896
+ * @param payload Payload to pass to the callback
897
+ * @param callback Name of the method to call
898
+ * @returns The ID of the queued task
899
+ */
900
+ async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
901
+ const id = nanoid(9);
902
+ if (typeof callback !== "string") {
903
+ throw new Error("Callback must be a string");
904
+ }
905
+
906
+ if (typeof this[callback] !== "function") {
907
+ throw new Error(`this.${callback} is not a function`);
908
+ }
909
+
910
+ this.sql`
911
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
912
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
913
+ `;
914
+
915
+ void this._flushQueue().catch((e) => {
916
+ console.error("Error flushing queue:", e);
917
+ });
918
+
919
+ return id;
920
+ }
921
+
922
+ private _flushingQueue = false;
923
+
924
+ private async _flushQueue() {
925
+ if (this._flushingQueue) {
926
+ return;
927
+ }
928
+ this._flushingQueue = true;
929
+ while (true) {
930
+ const result = this.sql<QueueItem<string>>`
931
+ SELECT * FROM cf_agents_queues
932
+ ORDER BY created_at ASC
933
+ `;
934
+
935
+ if (!result || result.length === 0) {
936
+ break;
937
+ }
938
+
939
+ for (const row of result || []) {
940
+ const callback = this[row.callback as keyof Agent<Env>];
941
+ if (!callback) {
942
+ console.error(`callback ${row.callback} not found`);
943
+ continue;
944
+ }
945
+ const { connection, request, email } = agentContext.getStore() || {};
946
+ await agentContext.run(
947
+ {
948
+ agent: this,
949
+ connection,
950
+ request,
951
+ email
952
+ },
953
+ async () => {
954
+ // TODO: add retries and backoff
955
+ await (
956
+ callback as (
957
+ payload: unknown,
958
+ queueItem: QueueItem<string>
959
+ ) => Promise<void>
960
+ ).bind(this)(JSON.parse(row.payload as string), row);
961
+ await this.dequeue(row.id);
962
+ }
963
+ );
964
+ }
965
+ }
966
+ this._flushingQueue = false;
967
+ }
968
+
969
+ /**
970
+ * Dequeue a task by ID
971
+ * @param id ID of the task to dequeue
972
+ */
973
+ async dequeue(id: string) {
974
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
975
+ }
976
+
977
+ /**
978
+ * Dequeue all tasks
979
+ */
980
+ async dequeueAll() {
981
+ this.sql`DELETE FROM cf_agents_queues`;
982
+ }
983
+
984
+ /**
985
+ * Dequeue all tasks by callback
986
+ * @param callback Name of the callback to dequeue
987
+ */
988
+ async dequeueAllByCallback(callback: string) {
989
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
990
+ }
991
+
992
+ /**
993
+ * Get a queued task by ID
994
+ * @param id ID of the task to get
995
+ * @returns The task or undefined if not found
996
+ */
997
+ async getQueue(id: string): Promise<QueueItem<string> | undefined> {
998
+ const result = this.sql<QueueItem<string>>`
999
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
1000
+ `;
1001
+ return result
1002
+ ? { ...result[0], payload: JSON.parse(result[0].payload) }
1003
+ : undefined;
1004
+ }
1005
+
1006
+ /**
1007
+ * Get all queues by key and value
1008
+ * @param key Key to filter by
1009
+ * @param value Value to filter by
1010
+ * @returns Array of matching QueueItem objects
1011
+ */
1012
+ async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
1013
+ const result = this.sql<QueueItem<string>>`
1014
+ SELECT * FROM cf_agents_queues
1015
+ `;
1016
+ return result.filter((row) => JSON.parse(row.payload)[key] === value);
1017
+ }
1018
+
734
1019
  /**
735
1020
  * Schedule a task to be executed in the future
736
1021
  * @template T Type of the payload data
@@ -753,7 +1038,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
753
1038
  id: nanoid(),
754
1039
  payload: schedule,
755
1040
  timestamp: Date.now(),
756
- type: "schedule:create",
1041
+ type: "schedule:create"
757
1042
  },
758
1043
  this.ctx
759
1044
  );
@@ -782,7 +1067,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
782
1067
  id,
783
1068
  payload: payload as T,
784
1069
  time: timestamp,
785
- type: "scheduled",
1070
+ type: "scheduled"
786
1071
  };
787
1072
 
788
1073
  emitScheduleCreate(schedule);
@@ -808,7 +1093,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
808
1093
  id,
809
1094
  payload: payload as T,
810
1095
  time: timestamp,
811
- type: "delayed",
1096
+ type: "delayed"
812
1097
  };
813
1098
 
814
1099
  emitScheduleCreate(schedule);
@@ -834,7 +1119,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
834
1119
  id,
835
1120
  payload: payload as T,
836
1121
  time: timestamp,
837
- type: "cron",
1122
+ type: "cron"
838
1123
  };
839
1124
 
840
1125
  emitScheduleCreate(schedule);
@@ -903,7 +1188,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
903
1188
  .toArray()
904
1189
  .map((row) => ({
905
1190
  ...row,
906
- payload: JSON.parse(row.payload as string) as T,
1191
+ payload: JSON.parse(row.payload as string) as T
907
1192
  })) as Schedule<T>[];
908
1193
 
909
1194
  return result;
@@ -923,7 +1208,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
923
1208
  id: nanoid(),
924
1209
  payload: schedule,
925
1210
  timestamp: Date.now(),
926
- type: "schedule:cancel",
1211
+ type: "schedule:cancel"
927
1212
  },
928
1213
  this.ctx
929
1214
  );
@@ -937,9 +1222,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
937
1222
  private async _scheduleNextAlarm() {
938
1223
  // Find the next schedule that needs to be executed
939
1224
  const result = this.sql`
940
- SELECT time FROM cf_agents_schedules
1225
+ SELECT time FROM cf_agents_schedules
941
1226
  WHERE time > ${Math.floor(Date.now() / 1000)}
942
- ORDER BY time ASC
1227
+ ORDER BY time ASC
943
1228
  LIMIT 1
944
1229
  `;
945
1230
  if (!result) return;
@@ -966,51 +1251,58 @@ export class Agent<Env, State = unknown> extends Server<Env> {
966
1251
  SELECT * FROM cf_agents_schedules WHERE time <= ${now}
967
1252
  `;
968
1253
 
969
- for (const row of result || []) {
970
- const callback = this[row.callback as keyof Agent<Env>];
971
- if (!callback) {
972
- console.error(`callback ${row.callback} not found`);
973
- continue;
974
- }
975
- await agentContext.run(
976
- { agent: this, connection: undefined, request: undefined },
977
- async () => {
978
- try {
979
- this.observability?.emit(
980
- {
981
- displayMessage: `Schedule ${row.id} executed`,
982
- id: nanoid(),
983
- payload: row,
984
- timestamp: Date.now(),
985
- type: "schedule:execute",
986
- },
987
- this.ctx
988
- );
1254
+ if (result && Array.isArray(result)) {
1255
+ for (const row of result) {
1256
+ const callback = this[row.callback as keyof Agent<Env>];
1257
+ if (!callback) {
1258
+ console.error(`callback ${row.callback} not found`);
1259
+ continue;
1260
+ }
1261
+ await agentContext.run(
1262
+ {
1263
+ agent: this,
1264
+ connection: undefined,
1265
+ request: undefined,
1266
+ email: undefined
1267
+ },
1268
+ async () => {
1269
+ try {
1270
+ this.observability?.emit(
1271
+ {
1272
+ displayMessage: `Schedule ${row.id} executed`,
1273
+ id: nanoid(),
1274
+ payload: row,
1275
+ timestamp: Date.now(),
1276
+ type: "schedule:execute"
1277
+ },
1278
+ this.ctx
1279
+ );
989
1280
 
990
- await (
991
- callback as (
992
- payload: unknown,
993
- schedule: Schedule<unknown>
994
- ) => Promise<void>
995
- ).bind(this)(JSON.parse(row.payload as string), row);
996
- } catch (e) {
997
- console.error(`error executing callback "${row.callback}"`, e);
1281
+ await (
1282
+ callback as (
1283
+ payload: unknown,
1284
+ schedule: Schedule<unknown>
1285
+ ) => Promise<void>
1286
+ ).bind(this)(JSON.parse(row.payload as string), row);
1287
+ } catch (e) {
1288
+ console.error(`error executing callback "${row.callback}"`, e);
1289
+ }
998
1290
  }
999
- }
1000
- );
1001
- if (row.type === "cron") {
1002
- // Update next execution time for cron schedules
1003
- const nextExecutionTime = getNextCronTime(row.cron);
1004
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1291
+ );
1292
+ if (row.type === "cron") {
1293
+ // Update next execution time for cron schedules
1294
+ const nextExecutionTime = getNextCronTime(row.cron);
1295
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1005
1296
 
1006
- this.sql`
1297
+ this.sql`
1007
1298
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
1008
1299
  `;
1009
- } else {
1010
- // Delete one-time schedules after execution
1011
- this.sql`
1300
+ } else {
1301
+ // Delete one-time schedules after execution
1302
+ this.sql`
1012
1303
  DELETE FROM cf_agents_schedules WHERE id = ${row.id}
1013
1304
  `;
1305
+ }
1014
1306
  }
1015
1307
  }
1016
1308
 
@@ -1026,6 +1318,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1026
1318
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
1027
1319
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1028
1320
  this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1321
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
1029
1322
 
1030
1323
  // delete all alarms
1031
1324
  await this.ctx.storage.deleteAlarm();
@@ -1038,7 +1331,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1038
1331
  id: nanoid(),
1039
1332
  payload: {},
1040
1333
  timestamp: Date.now(),
1041
- type: "destroy",
1334
+ type: "destroy"
1042
1335
  },
1043
1336
  this.ctx
1044
1337
  );
@@ -1098,7 +1391,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1098
1391
  this.broadcast(
1099
1392
  JSON.stringify({
1100
1393
  mcp: this.getMcpServers(),
1101
- type: "cf_agent_mcp_servers",
1394
+ type: "cf_agent_mcp_servers"
1102
1395
  })
1103
1396
  );
1104
1397
 
@@ -1154,12 +1447,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1154
1447
  fetch: (url, init) =>
1155
1448
  fetch(url, {
1156
1449
  ...init,
1157
- headers: options?.transport?.headers,
1158
- }),
1450
+ headers: options?.transport?.headers
1451
+ })
1159
1452
  },
1160
1453
  requestInit: {
1161
- headers: options?.transport?.headers,
1162
- },
1454
+ headers: options?.transport?.headers
1455
+ }
1163
1456
  };
1164
1457
  }
1165
1458
 
@@ -1168,14 +1461,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1168
1461
  reconnect,
1169
1462
  transport: {
1170
1463
  ...headerTransportOpts,
1171
- authProvider,
1172
- },
1464
+ authProvider
1465
+ }
1173
1466
  });
1174
1467
 
1175
1468
  return {
1176
1469
  authUrl,
1177
1470
  clientId,
1178
- id,
1471
+ id
1179
1472
  };
1180
1473
  }
1181
1474
 
@@ -1187,7 +1480,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1187
1480
  this.broadcast(
1188
1481
  JSON.stringify({
1189
1482
  mcp: this.getMcpServers(),
1190
- type: "cf_agent_mcp_servers",
1483
+ type: "cf_agent_mcp_servers"
1191
1484
  })
1192
1485
  );
1193
1486
  }
@@ -1197,24 +1490,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
1197
1490
  prompts: this.mcp.listPrompts(),
1198
1491
  resources: this.mcp.listResources(),
1199
1492
  servers: {},
1200
- tools: this.mcp.listTools(),
1493
+ tools: this.mcp.listTools()
1201
1494
  };
1202
1495
 
1203
1496
  const servers = this.sql<MCPServerRow>`
1204
1497
  SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1205
1498
  `;
1206
1499
 
1207
- for (const server of servers) {
1208
- const serverConn = this.mcp.mcpConnections[server.id];
1209
- mcpState.servers[server.id] = {
1210
- auth_url: server.auth_url,
1211
- capabilities: serverConn?.serverCapabilities ?? null,
1212
- instructions: serverConn?.instructions ?? null,
1213
- name: server.name,
1214
- server_url: server.server_url,
1215
- // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1216
- state: serverConn?.connectionState ?? "authenticating",
1217
- };
1500
+ if (servers && Array.isArray(servers) && servers.length > 0) {
1501
+ for (const server of servers) {
1502
+ const serverConn = this.mcp.mcpConnections[server.id];
1503
+ mcpState.servers[server.id] = {
1504
+ auth_url: server.auth_url,
1505
+ capabilities: serverConn?.serverCapabilities ?? null,
1506
+ instructions: serverConn?.instructions ?? null,
1507
+ name: server.name,
1508
+ server_url: server.server_url,
1509
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1510
+ state: serverConn?.connectionState ?? "authenticating"
1511
+ };
1512
+ }
1218
1513
  }
1219
1514
 
1220
1515
  return mcpState;
@@ -1261,14 +1556,14 @@ export async function routeAgentRequest<Env>(
1261
1556
  "Access-Control-Allow-Credentials": "true",
1262
1557
  "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1263
1558
  "Access-Control-Allow-Origin": "*",
1264
- "Access-Control-Max-Age": "86400",
1559
+ "Access-Control-Max-Age": "86400"
1265
1560
  }
1266
1561
  : options?.cors;
1267
1562
 
1268
1563
  if (request.method === "OPTIONS") {
1269
1564
  if (corsHeaders) {
1270
1565
  return new Response(null, {
1271
- headers: corsHeaders,
1566
+ headers: corsHeaders
1272
1567
  });
1273
1568
  }
1274
1569
  console.warn(
@@ -1281,7 +1576,7 @@ export async function routeAgentRequest<Env>(
1281
1576
  env as Record<string, unknown>,
1282
1577
  {
1283
1578
  prefix: "agents",
1284
- ...(options as PartyServerOptions<Record<string, unknown>>),
1579
+ ...(options as PartyServerOptions<Record<string, unknown>>)
1285
1580
  }
1286
1581
  );
1287
1582
 
@@ -1294,8 +1589,8 @@ export async function routeAgentRequest<Env>(
1294
1589
  response = new Response(response.body, {
1295
1590
  headers: {
1296
1591
  ...response.headers,
1297
- ...corsHeaders,
1298
- },
1592
+ ...corsHeaders
1593
+ }
1299
1594
  });
1300
1595
  }
1301
1596
  return response;
@@ -1309,7 +1604,11 @@ export type EmailResolver<Env> = (
1309
1604
  agentId: string;
1310
1605
  } | null>;
1311
1606
 
1312
- export function createHeaderBasedResolver<Env>(): EmailResolver<Env> {
1607
+ /**
1608
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1609
+ * @returns A function that resolves the agent to route the email to
1610
+ */
1611
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1313
1612
  return async (email: ForwardableEmailMessage, _env: Env) => {
1314
1613
  const messageId = email.headers.get("message-id");
1315
1614
  if (messageId) {
@@ -1344,7 +1643,12 @@ export function createHeaderBasedResolver<Env>(): EmailResolver<Env> {
1344
1643
  };
1345
1644
  }
1346
1645
 
1347
- export function createAddressBasedResolver<Env>(
1646
+ /**
1647
+ * Create a resolver that uses the email address to determine the agent to route the email to
1648
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1649
+ * @returns A function that resolves the agent to route the email to
1650
+ */
1651
+ export function createAddressBasedEmailResolver<Env>(
1348
1652
  defaultAgentName: string
1349
1653
  ): EmailResolver<Env> {
1350
1654
  return async (email: ForwardableEmailMessage, _env: Env) => {
@@ -1358,7 +1662,7 @@ export function createAddressBasedResolver<Env>(
1358
1662
  if (subAddress) {
1359
1663
  return {
1360
1664
  agentName: localPart,
1361
- agentId: subAddress,
1665
+ agentId: subAddress
1362
1666
  };
1363
1667
  }
1364
1668
 
@@ -1366,12 +1670,18 @@ export function createAddressBasedResolver<Env>(
1366
1670
  // Common for catch-all email routing to a single EmailAgent namespace
1367
1671
  return {
1368
1672
  agentName: defaultAgentName,
1369
- agentId: localPart,
1673
+ agentId: localPart
1370
1674
  };
1371
1675
  };
1372
1676
  }
1373
1677
 
1374
- export function createCatchAllResolver<Env>(
1678
+ /**
1679
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1680
+ * @param agentName The name of the agent to route the email to
1681
+ * @param agentId The id of the agent to route the email to
1682
+ * @returns A function that resolves the agent to route the email to
1683
+ */
1684
+ export function createCatchAllEmailResolver<Env>(
1375
1685
  agentName: string,
1376
1686
  agentId: string
1377
1687
  ): EmailResolver<Env> {
@@ -1382,6 +1692,20 @@ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1382
1692
  resolver: EmailResolver<Env>;
1383
1693
  };
1384
1694
 
1695
+ // Cache the agent namespace map for email routing
1696
+ // This maps both kebab-case and original names to namespaces
1697
+ const agentMapCache = new WeakMap<
1698
+ Record<string, unknown>,
1699
+ Record<string, unknown>
1700
+ >();
1701
+
1702
+ /**
1703
+ * Route an email to the appropriate Agent
1704
+ * @param email The email to route
1705
+ * @param env The environment containing the Agent bindings
1706
+ * @param options The options for routing the email
1707
+ * @returns A promise that resolves when the email has been routed
1708
+ */
1385
1709
  export async function routeAgentEmail<Env>(
1386
1710
  email: ForwardableEmailMessage,
1387
1711
  env: Env,
@@ -1394,33 +1718,98 @@ export async function routeAgentEmail<Env>(
1394
1718
  return;
1395
1719
  }
1396
1720
 
1397
- const namespaceBinding = env[routingInfo.agentName as keyof Env];
1398
- if (!namespaceBinding) {
1399
- console.error(
1400
- `Agent namespace '${routingInfo.agentName}' not found in environment`
1401
- );
1402
- return;
1721
+ // Build a map that includes both original names and kebab-case versions
1722
+ if (!agentMapCache.has(env as Record<string, unknown>)) {
1723
+ const map: Record<string, unknown> = {};
1724
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1725
+ if (
1726
+ value &&
1727
+ typeof value === "object" &&
1728
+ "idFromName" in value &&
1729
+ typeof value.idFromName === "function"
1730
+ ) {
1731
+ // Add both the original name and kebab-case version
1732
+ map[key] = value;
1733
+ map[camelCaseToKebabCase(key)] = value;
1734
+ }
1735
+ }
1736
+ agentMapCache.set(env as Record<string, unknown>, map);
1403
1737
  }
1404
1738
 
1405
- // Type guard to check if this is actually a DurableObjectNamespace (AgentNamespace)
1406
- if (
1407
- typeof namespaceBinding !== "object" ||
1408
- !("idFromName" in namespaceBinding) ||
1409
- typeof namespaceBinding.idFromName !== "function"
1410
- ) {
1411
- console.error(
1412
- `Environment binding '${routingInfo.agentName}' is not an AgentNamespace (found: ${typeof namespaceBinding})`
1739
+ const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
1740
+ const namespace = agentMap[routingInfo.agentName];
1741
+
1742
+ if (!namespace) {
1743
+ // Provide helpful error message listing available agents
1744
+ const availableAgents = Object.keys(agentMap)
1745
+ .filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
1746
+ .join(", ");
1747
+ throw new Error(
1748
+ `Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
1413
1749
  );
1414
- return;
1415
1750
  }
1416
1751
 
1417
- // Safe cast after runtime validation
1418
- const namespace = namespaceBinding as unknown as AgentNamespace<Agent<Env>>;
1752
+ const agent = await getAgentByName(
1753
+ namespace as unknown as AgentNamespace<Agent<Env>>,
1754
+ routingInfo.agentId
1755
+ );
1756
+
1757
+ // let's make a serialisable version of the email
1758
+ const serialisableEmail: AgentEmail = {
1759
+ getRaw: async () => {
1760
+ const reader = email.raw.getReader();
1761
+ const chunks: Uint8Array[] = [];
1762
+
1763
+ let done = false;
1764
+ while (!done) {
1765
+ const { value, done: readerDone } = await reader.read();
1766
+ done = readerDone;
1767
+ if (value) {
1768
+ chunks.push(value);
1769
+ }
1770
+ }
1771
+
1772
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1773
+ const combined = new Uint8Array(totalLength);
1774
+ let offset = 0;
1775
+ for (const chunk of chunks) {
1776
+ combined.set(chunk, offset);
1777
+ offset += chunk.length;
1778
+ }
1419
1779
 
1420
- const agent = await getAgentByName(namespace, routingInfo.agentId);
1421
- await agent.onEmail(email);
1780
+ return combined;
1781
+ },
1782
+ headers: email.headers,
1783
+ rawSize: email.rawSize,
1784
+ setReject: (reason: string) => {
1785
+ email.setReject(reason);
1786
+ },
1787
+ forward: (rcptTo: string, headers?: Headers) => {
1788
+ return email.forward(rcptTo, headers);
1789
+ },
1790
+ reply: (options: { from: string; to: string; raw: string }) => {
1791
+ return email.reply(
1792
+ new EmailMessage(options.from, options.to, options.raw)
1793
+ );
1794
+ },
1795
+ from: email.from,
1796
+ to: email.to
1797
+ };
1798
+
1799
+ await agent._onEmail(serialisableEmail);
1422
1800
  }
1423
1801
 
1802
+ export type AgentEmail = {
1803
+ from: string;
1804
+ to: string;
1805
+ getRaw: () => Promise<Uint8Array>;
1806
+ headers: Headers;
1807
+ rawSize: number;
1808
+ setReject: (reason: string) => void;
1809
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1810
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1811
+ };
1812
+
1424
1813
  export type EmailSendOptions = {
1425
1814
  to: string;
1426
1815
  subject: string;
@@ -1433,39 +1822,6 @@ export type EmailSendOptions = {
1433
1822
  domain?: string;
1434
1823
  };
1435
1824
 
1436
- export async function sendEmailWithRouting(
1437
- emailBinding: SendEmail,
1438
- from: string,
1439
- fromName: string,
1440
- options: EmailSendOptions
1441
- ): Promise<void> {
1442
- const { createMimeMessage } = await import("mimetext");
1443
- const msg = createMimeMessage();
1444
- msg.setSender({ addr: from, name: fromName });
1445
- msg.setRecipient(options.to);
1446
- msg.setSubject(options.subject);
1447
- msg.addMessage({
1448
- contentType: options.contentType || "text/plain",
1449
- data: options.body,
1450
- });
1451
-
1452
- if (options.includeRoutingHeaders && options.agentName && options.agentId) {
1453
- const domain = options.domain || from.split("@")[1];
1454
- const messageId = `<${options.agentId}@${domain}>`;
1455
- msg.setHeader("Message-ID", messageId);
1456
- msg.setHeader("X-Agent-Name", options.agentName);
1457
- msg.setHeader("X-Agent-ID", options.agentId);
1458
- }
1459
-
1460
- if (options.headers) {
1461
- for (const [key, value] of Object.entries(options.headers)) {
1462
- msg.setHeader(key, value);
1463
- }
1464
- }
1465
-
1466
- await emailBinding.send(new EmailMessage(from, options.to, msg.asRaw()));
1467
- }
1468
-
1469
1825
  /**
1470
1826
  * Get or create an Agent by name
1471
1827
  * @template Env Environment type containing bindings
@@ -1512,7 +1868,7 @@ export class StreamingResponse {
1512
1868
  id: this._id,
1513
1869
  result: chunk,
1514
1870
  success: true,
1515
- type: "rpc",
1871
+ type: "rpc"
1516
1872
  };
1517
1873
  this._connection.send(JSON.stringify(response));
1518
1874
  }
@@ -1531,7 +1887,7 @@ export class StreamingResponse {
1531
1887
  id: this._id,
1532
1888
  result: finalChunk,
1533
1889
  success: true,
1534
- type: "rpc",
1890
+ type: "rpc"
1535
1891
  };
1536
1892
  this._connection.send(JSON.stringify(response));
1537
1893
  }