agents 0.0.102 → 0.0.104
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/README.md +22 -22
- package/dist/ai-chat-agent.d.ts +1 -1
- package/dist/ai-chat-agent.js +4 -4
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-react.d.ts +2 -2
- package/dist/ai-react.js.map +1 -1
- package/dist/{chunk-JFRK72K3.js → chunk-5YIRLLUX.js} +437 -83
- package/dist/chunk-5YIRLLUX.js.map +1 -0
- package/dist/{chunk-NKZZ66QY.js → chunk-KUH345EY.js} +1 -1
- package/dist/chunk-KUH345EY.js.map +1 -0
- package/dist/{chunk-E3LCYPCB.js → chunk-MW5BQ2FW.js} +1 -1
- package/dist/chunk-MW5BQ2FW.js.map +1 -0
- package/dist/{chunk-767EASBA.js → chunk-PVQZBKN7.js} +1 -1
- package/dist/chunk-PVQZBKN7.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/{index-CITGJflw.d.ts → index-BIJvkfYt.d.ts} +144 -16
- package/dist/index.d.ts +13 -5
- package/dist/index.js +10 -4
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/do-oauth-client-provider.js +1 -1
- package/dist/mcp/index.d.ts +3 -3
- package/dist/mcp/index.js +6 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js.map +1 -1
- package/dist/schedule.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +647 -134
- package/dist/chunk-767EASBA.js.map +0 -1
- package/dist/chunk-E3LCYPCB.js.map +0 -1
- package/dist/chunk-JFRK72K3.js.map +0 -1
- package/dist/chunk-NKZZ66QY.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -6,18 +6,19 @@ 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";
|
|
13
|
+
import { EmailMessage } from "cloudflare:email";
|
|
13
14
|
import {
|
|
14
15
|
type Connection,
|
|
15
16
|
type ConnectionContext,
|
|
16
|
-
getServerByName,
|
|
17
17
|
type PartyServerOptions,
|
|
18
|
-
routePartykitRequest,
|
|
19
18
|
Server,
|
|
20
19
|
type WSMessage,
|
|
20
|
+
getServerByName,
|
|
21
|
+
routePartykitRequest
|
|
21
22
|
} from "partyserver";
|
|
22
23
|
import { camelCaseToKebabCase } from "./client";
|
|
23
24
|
import { MCPClientManager } from "./mcp/client";
|
|
@@ -129,6 +130,13 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
export type QueueItem<T = string> = {
|
|
134
|
+
id: string;
|
|
135
|
+
payload: T;
|
|
136
|
+
callback: keyof Agent<unknown>;
|
|
137
|
+
created_at: number;
|
|
138
|
+
};
|
|
139
|
+
|
|
132
140
|
/**
|
|
133
141
|
* Represents a scheduled task within an Agent
|
|
134
142
|
* @template T Type of the payload data
|
|
@@ -218,23 +226,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
|
218
226
|
const DEFAULT_STATE = {} as unknown;
|
|
219
227
|
|
|
220
228
|
const agentContext = new AsyncLocalStorage<{
|
|
221
|
-
agent: Agent<unknown>;
|
|
229
|
+
agent: Agent<unknown, unknown>;
|
|
222
230
|
connection: Connection | undefined;
|
|
223
231
|
request: Request | undefined;
|
|
232
|
+
email: AgentEmail | undefined;
|
|
224
233
|
}>();
|
|
225
234
|
|
|
226
235
|
export function getCurrentAgent<
|
|
227
|
-
T extends Agent<unknown, unknown> = Agent<unknown, unknown
|
|
236
|
+
T extends Agent<unknown, unknown> = Agent<unknown, unknown>
|
|
228
237
|
>(): {
|
|
229
238
|
agent: T | undefined;
|
|
230
239
|
connection: Connection | undefined;
|
|
231
|
-
request: Request
|
|
240
|
+
request: Request | undefined;
|
|
241
|
+
email: AgentEmail | undefined;
|
|
232
242
|
} {
|
|
233
243
|
const store = agentContext.getStore() as
|
|
234
244
|
| {
|
|
235
245
|
agent: T;
|
|
236
246
|
connection: Connection | undefined;
|
|
237
|
-
request: Request
|
|
247
|
+
request: Request | undefined;
|
|
248
|
+
email: AgentEmail | undefined;
|
|
238
249
|
}
|
|
239
250
|
| undefined;
|
|
240
251
|
if (!store) {
|
|
@@ -242,11 +253,31 @@ export function getCurrentAgent<
|
|
|
242
253
|
agent: undefined,
|
|
243
254
|
connection: undefined,
|
|
244
255
|
request: undefined,
|
|
256
|
+
email: undefined
|
|
245
257
|
};
|
|
246
258
|
}
|
|
247
259
|
return store;
|
|
248
260
|
}
|
|
249
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
|
+
|
|
250
281
|
/**
|
|
251
282
|
* Base class for creating Agent implementations
|
|
252
283
|
* @template Env Environment type containing bindings
|
|
@@ -314,7 +345,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
314
345
|
*/
|
|
315
346
|
static options = {
|
|
316
347
|
/** Whether the Agent should hibernate when inactive */
|
|
317
|
-
hibernate: true
|
|
348
|
+
hibernate: true // default to hibernate
|
|
318
349
|
};
|
|
319
350
|
|
|
320
351
|
/**
|
|
@@ -351,6 +382,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
351
382
|
constructor(ctx: AgentContext, env: Env) {
|
|
352
383
|
super(ctx, env);
|
|
353
384
|
|
|
385
|
+
// Auto-wrap custom methods with agent context
|
|
386
|
+
this._autoWrapCustomMethods();
|
|
387
|
+
|
|
354
388
|
this.sql`
|
|
355
389
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
356
390
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -358,6 +392,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
358
392
|
)
|
|
359
393
|
`;
|
|
360
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
|
+
|
|
361
404
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
362
405
|
return this._tryCatch(async () => {
|
|
363
406
|
// Create alarms table if it doesn't exist
|
|
@@ -394,7 +437,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
394
437
|
const _onRequest = this.onRequest.bind(this);
|
|
395
438
|
this.onRequest = (request: Request) => {
|
|
396
439
|
return agentContext.run(
|
|
397
|
-
{ agent: this, connection: undefined, request },
|
|
440
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
398
441
|
async () => {
|
|
399
442
|
if (this.mcp.isCallbackRequest(request)) {
|
|
400
443
|
await this.mcp.handleCallbackRequest(request);
|
|
@@ -403,14 +446,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
403
446
|
this.broadcast(
|
|
404
447
|
JSON.stringify({
|
|
405
448
|
mcp: this.getMcpServers(),
|
|
406
|
-
type: "cf_agent_mcp_servers"
|
|
449
|
+
type: "cf_agent_mcp_servers"
|
|
407
450
|
})
|
|
408
451
|
);
|
|
409
452
|
|
|
410
453
|
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
411
454
|
return new Response("<script>window.close();</script>", {
|
|
412
455
|
headers: { "content-type": "text/html" },
|
|
413
|
-
status: 200
|
|
456
|
+
status: 200
|
|
414
457
|
});
|
|
415
458
|
}
|
|
416
459
|
|
|
@@ -422,7 +465,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
422
465
|
const _onMessage = this.onMessage.bind(this);
|
|
423
466
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
424
467
|
return agentContext.run(
|
|
425
|
-
{ agent: this, connection, request: undefined },
|
|
468
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
426
469
|
async () => {
|
|
427
470
|
if (typeof message !== "string") {
|
|
428
471
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
@@ -475,10 +518,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
475
518
|
args,
|
|
476
519
|
method,
|
|
477
520
|
streaming: metadata?.streaming,
|
|
478
|
-
success: true
|
|
521
|
+
success: true
|
|
479
522
|
},
|
|
480
523
|
timestamp: Date.now(),
|
|
481
|
-
type: "rpc"
|
|
524
|
+
type: "rpc"
|
|
482
525
|
},
|
|
483
526
|
this.ctx
|
|
484
527
|
);
|
|
@@ -488,7 +531,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
488
531
|
id,
|
|
489
532
|
result,
|
|
490
533
|
success: true,
|
|
491
|
-
type: "rpc"
|
|
534
|
+
type: "rpc"
|
|
492
535
|
};
|
|
493
536
|
connection.send(JSON.stringify(response));
|
|
494
537
|
} catch (e) {
|
|
@@ -498,7 +541,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
498
541
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
499
542
|
id: parsed.id,
|
|
500
543
|
success: false,
|
|
501
|
-
type: "rpc"
|
|
544
|
+
type: "rpc"
|
|
502
545
|
};
|
|
503
546
|
connection.send(JSON.stringify(response));
|
|
504
547
|
console.error("RPC error:", e);
|
|
@@ -516,14 +559,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
516
559
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
517
560
|
// must fix this
|
|
518
561
|
return agentContext.run(
|
|
519
|
-
{ agent: this, connection, request: ctx.request },
|
|
562
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
520
563
|
async () => {
|
|
521
564
|
setTimeout(() => {
|
|
522
565
|
if (this.state) {
|
|
523
566
|
connection.send(
|
|
524
567
|
JSON.stringify({
|
|
525
568
|
state: this.state,
|
|
526
|
-
type: "cf_agent_state"
|
|
569
|
+
type: "cf_agent_state"
|
|
527
570
|
})
|
|
528
571
|
);
|
|
529
572
|
}
|
|
@@ -531,7 +574,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
531
574
|
connection.send(
|
|
532
575
|
JSON.stringify({
|
|
533
576
|
mcp: this.getMcpServers(),
|
|
534
|
-
type: "cf_agent_mcp_servers"
|
|
577
|
+
type: "cf_agent_mcp_servers"
|
|
535
578
|
})
|
|
536
579
|
);
|
|
537
580
|
|
|
@@ -540,10 +583,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
540
583
|
displayMessage: "Connection established",
|
|
541
584
|
id: nanoid(),
|
|
542
585
|
payload: {
|
|
543
|
-
connectionId: connection.id
|
|
586
|
+
connectionId: connection.id
|
|
544
587
|
},
|
|
545
588
|
timestamp: Date.now(),
|
|
546
|
-
type: "connect"
|
|
589
|
+
type: "connect"
|
|
547
590
|
},
|
|
548
591
|
this.ctx
|
|
549
592
|
);
|
|
@@ -556,36 +599,43 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
556
599
|
const _onStart = this.onStart.bind(this);
|
|
557
600
|
this.onStart = async () => {
|
|
558
601
|
return agentContext.run(
|
|
559
|
-
{
|
|
602
|
+
{
|
|
603
|
+
agent: this,
|
|
604
|
+
connection: undefined,
|
|
605
|
+
request: undefined,
|
|
606
|
+
email: undefined
|
|
607
|
+
},
|
|
560
608
|
async () => {
|
|
561
609
|
const servers = this.sql<MCPServerRow>`
|
|
562
610
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
563
611
|
`;
|
|
564
612
|
|
|
565
613
|
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
).then((_results) => {
|
|
582
|
-
this.broadcast(
|
|
583
|
-
JSON.stringify({
|
|
584
|
-
mcp: this.getMcpServers(),
|
|
585
|
-
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
|
+
);
|
|
586
629
|
})
|
|
587
|
-
)
|
|
588
|
-
|
|
630
|
+
).then((_results) => {
|
|
631
|
+
this.broadcast(
|
|
632
|
+
JSON.stringify({
|
|
633
|
+
mcp: this.getMcpServers(),
|
|
634
|
+
type: "cf_agent_mcp_servers"
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
589
639
|
await this._tryCatch(() => _onStart());
|
|
590
640
|
}
|
|
591
641
|
);
|
|
@@ -609,14 +659,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
609
659
|
this.broadcast(
|
|
610
660
|
JSON.stringify({
|
|
611
661
|
state: state,
|
|
612
|
-
type: "cf_agent_state"
|
|
662
|
+
type: "cf_agent_state"
|
|
613
663
|
}),
|
|
614
664
|
source !== "server" ? [source.id] : []
|
|
615
665
|
);
|
|
616
666
|
return this._tryCatch(() => {
|
|
617
|
-
const { connection, request } = agentContext.getStore() || {};
|
|
667
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
618
668
|
return agentContext.run(
|
|
619
|
-
{ agent: this, connection, request },
|
|
669
|
+
{ agent: this, connection, request, email },
|
|
620
670
|
async () => {
|
|
621
671
|
this.observability?.emit(
|
|
622
672
|
{
|
|
@@ -624,10 +674,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
624
674
|
id: nanoid(),
|
|
625
675
|
payload: {
|
|
626
676
|
previousState,
|
|
627
|
-
state
|
|
677
|
+
state
|
|
628
678
|
},
|
|
629
679
|
timestamp: Date.now(),
|
|
630
|
-
type: "state:update"
|
|
680
|
+
type: "state:update"
|
|
631
681
|
},
|
|
632
682
|
this.ctx
|
|
633
683
|
);
|
|
@@ -656,19 +706,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
656
706
|
}
|
|
657
707
|
|
|
658
708
|
/**
|
|
659
|
-
* Called when the Agent receives an email
|
|
709
|
+
* Called when the Agent receives an email via routeAgentEmail()
|
|
710
|
+
* Override this method to handle incoming emails
|
|
660
711
|
* @param email Email message to process
|
|
661
712
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
664
716
|
return agentContext.run(
|
|
665
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
717
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
666
718
|
async () => {
|
|
667
|
-
|
|
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
|
+
}
|
|
668
730
|
}
|
|
669
731
|
);
|
|
670
732
|
}
|
|
671
733
|
|
|
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
|
+
}
|
|
749
|
+
): Promise<void> {
|
|
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
|
+
});
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
672
786
|
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
673
787
|
try {
|
|
674
788
|
return await fn();
|
|
@@ -677,6 +791,72 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
677
791
|
}
|
|
678
792
|
}
|
|
679
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
|
+
|
|
680
860
|
override onError(
|
|
681
861
|
connection: Connection,
|
|
682
862
|
error: unknown
|
|
@@ -711,6 +891,135 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
711
891
|
throw new Error("Not implemented");
|
|
712
892
|
}
|
|
713
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 executed: string[] = [];
|
|
931
|
+
const result = this.sql<QueueItem<string>>`
|
|
932
|
+
SELECT * FROM cf_agents_queues
|
|
933
|
+
ORDER BY created_at ASC
|
|
934
|
+
`;
|
|
935
|
+
|
|
936
|
+
if (!result || result.length === 0) {
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
for (const row of result || []) {
|
|
941
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
942
|
+
if (!callback) {
|
|
943
|
+
console.error(`callback ${row.callback} not found`);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
947
|
+
await agentContext.run(
|
|
948
|
+
{
|
|
949
|
+
agent: this,
|
|
950
|
+
connection,
|
|
951
|
+
request,
|
|
952
|
+
email
|
|
953
|
+
},
|
|
954
|
+
async () => {
|
|
955
|
+
// TODO: add retries and backoff
|
|
956
|
+
await (
|
|
957
|
+
callback as (
|
|
958
|
+
payload: unknown,
|
|
959
|
+
queueItem: QueueItem<string>
|
|
960
|
+
) => Promise<void>
|
|
961
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
962
|
+
executed.push(row.id);
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
for (const id of executed) {
|
|
967
|
+
await this.dequeue(id);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
this._flushingQueue = false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Dequeue a task by ID
|
|
975
|
+
* @param id ID of the task to dequeue
|
|
976
|
+
*/
|
|
977
|
+
async dequeue(id: string) {
|
|
978
|
+
this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Dequeue all tasks
|
|
983
|
+
*/
|
|
984
|
+
async dequeueAll() {
|
|
985
|
+
this.sql`DELETE FROM cf_agents_queues`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Dequeue all tasks by callback
|
|
990
|
+
* @param callback Name of the callback to dequeue
|
|
991
|
+
*/
|
|
992
|
+
async dequeueAllByCallback(callback: string) {
|
|
993
|
+
this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Get a queued task by ID
|
|
998
|
+
* @param id ID of the task to get
|
|
999
|
+
* @returns The task or undefined if not found
|
|
1000
|
+
*/
|
|
1001
|
+
async getQueue(id: string): Promise<QueueItem<string> | undefined> {
|
|
1002
|
+
const result = this.sql<QueueItem<string>>`
|
|
1003
|
+
SELECT * FROM cf_agents_queues WHERE id = ${id}
|
|
1004
|
+
`;
|
|
1005
|
+
return result
|
|
1006
|
+
? { ...result[0], payload: JSON.parse(result[0].payload) }
|
|
1007
|
+
: undefined;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Get all queues by key and value
|
|
1012
|
+
* @param key Key to filter by
|
|
1013
|
+
* @param value Value to filter by
|
|
1014
|
+
* @returns Array of matching QueueItem objects
|
|
1015
|
+
*/
|
|
1016
|
+
async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
|
|
1017
|
+
const result = this.sql<QueueItem<string>>`
|
|
1018
|
+
SELECT * FROM cf_agents_queues
|
|
1019
|
+
`;
|
|
1020
|
+
return result.filter((row) => JSON.parse(row.payload)[key] === value);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
714
1023
|
/**
|
|
715
1024
|
* Schedule a task to be executed in the future
|
|
716
1025
|
* @template T Type of the payload data
|
|
@@ -733,7 +1042,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
733
1042
|
id: nanoid(),
|
|
734
1043
|
payload: schedule,
|
|
735
1044
|
timestamp: Date.now(),
|
|
736
|
-
type: "schedule:create"
|
|
1045
|
+
type: "schedule:create"
|
|
737
1046
|
},
|
|
738
1047
|
this.ctx
|
|
739
1048
|
);
|
|
@@ -762,7 +1071,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
762
1071
|
id,
|
|
763
1072
|
payload: payload as T,
|
|
764
1073
|
time: timestamp,
|
|
765
|
-
type: "scheduled"
|
|
1074
|
+
type: "scheduled"
|
|
766
1075
|
};
|
|
767
1076
|
|
|
768
1077
|
emitScheduleCreate(schedule);
|
|
@@ -788,7 +1097,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
788
1097
|
id,
|
|
789
1098
|
payload: payload as T,
|
|
790
1099
|
time: timestamp,
|
|
791
|
-
type: "delayed"
|
|
1100
|
+
type: "delayed"
|
|
792
1101
|
};
|
|
793
1102
|
|
|
794
1103
|
emitScheduleCreate(schedule);
|
|
@@ -814,7 +1123,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
814
1123
|
id,
|
|
815
1124
|
payload: payload as T,
|
|
816
1125
|
time: timestamp,
|
|
817
|
-
type: "cron"
|
|
1126
|
+
type: "cron"
|
|
818
1127
|
};
|
|
819
1128
|
|
|
820
1129
|
emitScheduleCreate(schedule);
|
|
@@ -883,7 +1192,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
883
1192
|
.toArray()
|
|
884
1193
|
.map((row) => ({
|
|
885
1194
|
...row,
|
|
886
|
-
payload: JSON.parse(row.payload as string) as T
|
|
1195
|
+
payload: JSON.parse(row.payload as string) as T
|
|
887
1196
|
})) as Schedule<T>[];
|
|
888
1197
|
|
|
889
1198
|
return result;
|
|
@@ -903,7 +1212,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
903
1212
|
id: nanoid(),
|
|
904
1213
|
payload: schedule,
|
|
905
1214
|
timestamp: Date.now(),
|
|
906
|
-
type: "schedule:cancel"
|
|
1215
|
+
type: "schedule:cancel"
|
|
907
1216
|
},
|
|
908
1217
|
this.ctx
|
|
909
1218
|
);
|
|
@@ -946,51 +1255,58 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
946
1255
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
947
1256
|
`;
|
|
948
1257
|
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1258
|
+
if (result && Array.isArray(result)) {
|
|
1259
|
+
for (const row of result) {
|
|
1260
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
1261
|
+
if (!callback) {
|
|
1262
|
+
console.error(`callback ${row.callback} not found`);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
await agentContext.run(
|
|
1266
|
+
{
|
|
1267
|
+
agent: this,
|
|
1268
|
+
connection: undefined,
|
|
1269
|
+
request: undefined,
|
|
1270
|
+
email: undefined
|
|
1271
|
+
},
|
|
1272
|
+
async () => {
|
|
1273
|
+
try {
|
|
1274
|
+
this.observability?.emit(
|
|
1275
|
+
{
|
|
1276
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
1277
|
+
id: nanoid(),
|
|
1278
|
+
payload: row,
|
|
1279
|
+
timestamp: Date.now(),
|
|
1280
|
+
type: "schedule:execute"
|
|
1281
|
+
},
|
|
1282
|
+
this.ctx
|
|
1283
|
+
);
|
|
969
1284
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1285
|
+
await (
|
|
1286
|
+
callback as (
|
|
1287
|
+
payload: unknown,
|
|
1288
|
+
schedule: Schedule<unknown>
|
|
1289
|
+
) => Promise<void>
|
|
1290
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
console.error(`error executing callback "${row.callback}"`, e);
|
|
1293
|
+
}
|
|
978
1294
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
1295
|
+
);
|
|
1296
|
+
if (row.type === "cron") {
|
|
1297
|
+
// Update next execution time for cron schedules
|
|
1298
|
+
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1299
|
+
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
985
1300
|
|
|
986
|
-
|
|
1301
|
+
this.sql`
|
|
987
1302
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
988
1303
|
`;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1304
|
+
} else {
|
|
1305
|
+
// Delete one-time schedules after execution
|
|
1306
|
+
this.sql`
|
|
992
1307
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
993
1308
|
`;
|
|
1309
|
+
}
|
|
994
1310
|
}
|
|
995
1311
|
}
|
|
996
1312
|
|
|
@@ -1006,6 +1322,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1006
1322
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1007
1323
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1008
1324
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1325
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1009
1326
|
|
|
1010
1327
|
// delete all alarms
|
|
1011
1328
|
await this.ctx.storage.deleteAlarm();
|
|
@@ -1018,7 +1335,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1018
1335
|
id: nanoid(),
|
|
1019
1336
|
payload: {},
|
|
1020
1337
|
timestamp: Date.now(),
|
|
1021
|
-
type: "destroy"
|
|
1338
|
+
type: "destroy"
|
|
1022
1339
|
},
|
|
1023
1340
|
this.ctx
|
|
1024
1341
|
);
|
|
@@ -1078,7 +1395,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1078
1395
|
this.broadcast(
|
|
1079
1396
|
JSON.stringify({
|
|
1080
1397
|
mcp: this.getMcpServers(),
|
|
1081
|
-
type: "cf_agent_mcp_servers"
|
|
1398
|
+
type: "cf_agent_mcp_servers"
|
|
1082
1399
|
})
|
|
1083
1400
|
);
|
|
1084
1401
|
|
|
@@ -1134,12 +1451,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1134
1451
|
fetch: (url, init) =>
|
|
1135
1452
|
fetch(url, {
|
|
1136
1453
|
...init,
|
|
1137
|
-
headers: options?.transport?.headers
|
|
1138
|
-
})
|
|
1454
|
+
headers: options?.transport?.headers
|
|
1455
|
+
})
|
|
1139
1456
|
},
|
|
1140
1457
|
requestInit: {
|
|
1141
|
-
headers: options?.transport?.headers
|
|
1142
|
-
}
|
|
1458
|
+
headers: options?.transport?.headers
|
|
1459
|
+
}
|
|
1143
1460
|
};
|
|
1144
1461
|
}
|
|
1145
1462
|
|
|
@@ -1148,14 +1465,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1148
1465
|
reconnect,
|
|
1149
1466
|
transport: {
|
|
1150
1467
|
...headerTransportOpts,
|
|
1151
|
-
authProvider
|
|
1152
|
-
}
|
|
1468
|
+
authProvider
|
|
1469
|
+
}
|
|
1153
1470
|
});
|
|
1154
1471
|
|
|
1155
1472
|
return {
|
|
1156
1473
|
authUrl,
|
|
1157
1474
|
clientId,
|
|
1158
|
-
id
|
|
1475
|
+
id
|
|
1159
1476
|
};
|
|
1160
1477
|
}
|
|
1161
1478
|
|
|
@@ -1167,7 +1484,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1167
1484
|
this.broadcast(
|
|
1168
1485
|
JSON.stringify({
|
|
1169
1486
|
mcp: this.getMcpServers(),
|
|
1170
|
-
type: "cf_agent_mcp_servers"
|
|
1487
|
+
type: "cf_agent_mcp_servers"
|
|
1171
1488
|
})
|
|
1172
1489
|
);
|
|
1173
1490
|
}
|
|
@@ -1177,24 +1494,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1177
1494
|
prompts: this.mcp.listPrompts(),
|
|
1178
1495
|
resources: this.mcp.listResources(),
|
|
1179
1496
|
servers: {},
|
|
1180
|
-
tools: this.mcp.listTools()
|
|
1497
|
+
tools: this.mcp.listTools()
|
|
1181
1498
|
};
|
|
1182
1499
|
|
|
1183
1500
|
const servers = this.sql<MCPServerRow>`
|
|
1184
1501
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1185
1502
|
`;
|
|
1186
1503
|
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1504
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
1505
|
+
for (const server of servers) {
|
|
1506
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1507
|
+
mcpState.servers[server.id] = {
|
|
1508
|
+
auth_url: server.auth_url,
|
|
1509
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1510
|
+
instructions: serverConn?.instructions ?? null,
|
|
1511
|
+
name: server.name,
|
|
1512
|
+
server_url: server.server_url,
|
|
1513
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1514
|
+
state: serverConn?.connectionState ?? "authenticating"
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1198
1517
|
}
|
|
1199
1518
|
|
|
1200
1519
|
return mcpState;
|
|
@@ -1241,14 +1560,14 @@ export async function routeAgentRequest<Env>(
|
|
|
1241
1560
|
"Access-Control-Allow-Credentials": "true",
|
|
1242
1561
|
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1243
1562
|
"Access-Control-Allow-Origin": "*",
|
|
1244
|
-
"Access-Control-Max-Age": "86400"
|
|
1563
|
+
"Access-Control-Max-Age": "86400"
|
|
1245
1564
|
}
|
|
1246
1565
|
: options?.cors;
|
|
1247
1566
|
|
|
1248
1567
|
if (request.method === "OPTIONS") {
|
|
1249
1568
|
if (corsHeaders) {
|
|
1250
1569
|
return new Response(null, {
|
|
1251
|
-
headers: corsHeaders
|
|
1570
|
+
headers: corsHeaders
|
|
1252
1571
|
});
|
|
1253
1572
|
}
|
|
1254
1573
|
console.warn(
|
|
@@ -1261,7 +1580,7 @@ export async function routeAgentRequest<Env>(
|
|
|
1261
1580
|
env as Record<string, unknown>,
|
|
1262
1581
|
{
|
|
1263
1582
|
prefix: "agents",
|
|
1264
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1583
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1265
1584
|
}
|
|
1266
1585
|
);
|
|
1267
1586
|
|
|
@@ -1274,24 +1593,218 @@ export async function routeAgentRequest<Env>(
|
|
|
1274
1593
|
response = new Response(response.body, {
|
|
1275
1594
|
headers: {
|
|
1276
1595
|
...response.headers,
|
|
1277
|
-
...corsHeaders
|
|
1278
|
-
}
|
|
1596
|
+
...corsHeaders
|
|
1597
|
+
}
|
|
1279
1598
|
});
|
|
1280
1599
|
}
|
|
1281
1600
|
return response;
|
|
1282
1601
|
}
|
|
1283
1602
|
|
|
1603
|
+
export type EmailResolver<Env> = (
|
|
1604
|
+
email: ForwardableEmailMessage,
|
|
1605
|
+
env: Env
|
|
1606
|
+
) => Promise<{
|
|
1607
|
+
agentName: string;
|
|
1608
|
+
agentId: string;
|
|
1609
|
+
} | null>;
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Create a resolver that uses the message-id header to determine the agent to route the email to
|
|
1613
|
+
* @returns A function that resolves the agent to route the email to
|
|
1614
|
+
*/
|
|
1615
|
+
export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
|
|
1616
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1617
|
+
const messageId = email.headers.get("message-id");
|
|
1618
|
+
if (messageId) {
|
|
1619
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1620
|
+
if (messageIdMatch) {
|
|
1621
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1622
|
+
const agentName = domain.split(".")[0];
|
|
1623
|
+
return { agentName, agentId };
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const references = email.headers.get("references");
|
|
1628
|
+
if (references) {
|
|
1629
|
+
const referencesMatch = references.match(
|
|
1630
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1631
|
+
);
|
|
1632
|
+
if (referencesMatch) {
|
|
1633
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1634
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1635
|
+
const agentName = domain.split(".")[0];
|
|
1636
|
+
return { agentName, agentId };
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1641
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1642
|
+
if (agentName && agentId) {
|
|
1643
|
+
return { agentName, agentId };
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return null;
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
1652
|
+
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
1653
|
+
* @returns A function that resolves the agent to route the email to
|
|
1654
|
+
*/
|
|
1655
|
+
export function createAddressBasedEmailResolver<Env>(
|
|
1656
|
+
defaultAgentName: string
|
|
1657
|
+
): EmailResolver<Env> {
|
|
1658
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1659
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1660
|
+
if (!emailMatch) {
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1665
|
+
|
|
1666
|
+
if (subAddress) {
|
|
1667
|
+
return {
|
|
1668
|
+
agentName: localPart,
|
|
1669
|
+
agentId: subAddress
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1674
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1675
|
+
return {
|
|
1676
|
+
agentName: defaultAgentName,
|
|
1677
|
+
agentId: localPart
|
|
1678
|
+
};
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
1684
|
+
* @param agentName The name of the agent to route the email to
|
|
1685
|
+
* @param agentId The id of the agent to route the email to
|
|
1686
|
+
* @returns A function that resolves the agent to route the email to
|
|
1687
|
+
*/
|
|
1688
|
+
export function createCatchAllEmailResolver<Env>(
|
|
1689
|
+
agentName: string,
|
|
1690
|
+
agentId: string
|
|
1691
|
+
): EmailResolver<Env> {
|
|
1692
|
+
return async () => ({ agentName, agentId });
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1696
|
+
resolver: EmailResolver<Env>;
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1284
1699
|
/**
|
|
1285
1700
|
* Route an email to the appropriate Agent
|
|
1286
|
-
* @param email
|
|
1287
|
-
* @param env
|
|
1288
|
-
* @param options
|
|
1701
|
+
* @param email The email to route
|
|
1702
|
+
* @param env The environment containing the Agent bindings
|
|
1703
|
+
* @param options The options for routing the email
|
|
1704
|
+
* @returns A promise that resolves when the email has been routed
|
|
1289
1705
|
*/
|
|
1290
1706
|
export async function routeAgentEmail<Env>(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
): Promise<void> {
|
|
1707
|
+
email: ForwardableEmailMessage,
|
|
1708
|
+
env: Env,
|
|
1709
|
+
options: EmailRoutingOptions<Env>
|
|
1710
|
+
): Promise<void> {
|
|
1711
|
+
const routingInfo = await options.resolver(email, env);
|
|
1712
|
+
|
|
1713
|
+
if (!routingInfo) {
|
|
1714
|
+
console.warn("No routing information found for email, dropping message");
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const namespaceBinding = env[routingInfo.agentName as keyof Env];
|
|
1719
|
+
if (!namespaceBinding) {
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// Type guard to check if this is actually a DurableObjectNamespace (AgentNamespace)
|
|
1726
|
+
if (
|
|
1727
|
+
typeof namespaceBinding !== "object" ||
|
|
1728
|
+
!("idFromName" in namespaceBinding) ||
|
|
1729
|
+
typeof namespaceBinding.idFromName !== "function"
|
|
1730
|
+
) {
|
|
1731
|
+
throw new Error(
|
|
1732
|
+
`Environment binding '${routingInfo.agentName}' is not an AgentNamespace (found: ${typeof namespaceBinding})`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Safe cast after runtime validation
|
|
1737
|
+
const namespace = namespaceBinding as unknown as AgentNamespace<Agent<Env>>;
|
|
1738
|
+
|
|
1739
|
+
const agent = await getAgentByName(namespace, routingInfo.agentId);
|
|
1740
|
+
|
|
1741
|
+
// let's make a serialisable version of the email
|
|
1742
|
+
const serialisableEmail: AgentEmail = {
|
|
1743
|
+
getRaw: async () => {
|
|
1744
|
+
const reader = email.raw.getReader();
|
|
1745
|
+
const chunks: Uint8Array[] = [];
|
|
1746
|
+
|
|
1747
|
+
let done = false;
|
|
1748
|
+
while (!done) {
|
|
1749
|
+
const { value, done: readerDone } = await reader.read();
|
|
1750
|
+
done = readerDone;
|
|
1751
|
+
if (value) {
|
|
1752
|
+
chunks.push(value);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1757
|
+
const combined = new Uint8Array(totalLength);
|
|
1758
|
+
let offset = 0;
|
|
1759
|
+
for (const chunk of chunks) {
|
|
1760
|
+
combined.set(chunk, offset);
|
|
1761
|
+
offset += chunk.length;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
return combined;
|
|
1765
|
+
},
|
|
1766
|
+
headers: email.headers,
|
|
1767
|
+
rawSize: email.rawSize,
|
|
1768
|
+
setReject: (reason: string) => {
|
|
1769
|
+
email.setReject(reason);
|
|
1770
|
+
},
|
|
1771
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1772
|
+
return email.forward(rcptTo, headers);
|
|
1773
|
+
},
|
|
1774
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1775
|
+
return email.reply(
|
|
1776
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1777
|
+
);
|
|
1778
|
+
},
|
|
1779
|
+
from: email.from,
|
|
1780
|
+
to: email.to
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
await agent._onEmail(serialisableEmail);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
export type AgentEmail = {
|
|
1787
|
+
from: string;
|
|
1788
|
+
to: string;
|
|
1789
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1790
|
+
headers: Headers;
|
|
1791
|
+
rawSize: number;
|
|
1792
|
+
setReject: (reason: string) => void;
|
|
1793
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1794
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
export type EmailSendOptions = {
|
|
1798
|
+
to: string;
|
|
1799
|
+
subject: string;
|
|
1800
|
+
body: string;
|
|
1801
|
+
contentType?: string;
|
|
1802
|
+
headers?: Record<string, string>;
|
|
1803
|
+
includeRoutingHeaders?: boolean;
|
|
1804
|
+
agentName?: string;
|
|
1805
|
+
agentId?: string;
|
|
1806
|
+
domain?: string;
|
|
1807
|
+
};
|
|
1295
1808
|
|
|
1296
1809
|
/**
|
|
1297
1810
|
* Get or create an Agent by name
|
|
@@ -1339,7 +1852,7 @@ export class StreamingResponse {
|
|
|
1339
1852
|
id: this._id,
|
|
1340
1853
|
result: chunk,
|
|
1341
1854
|
success: true,
|
|
1342
|
-
type: "rpc"
|
|
1855
|
+
type: "rpc"
|
|
1343
1856
|
};
|
|
1344
1857
|
this._connection.send(JSON.stringify(response));
|
|
1345
1858
|
}
|
|
@@ -1358,7 +1871,7 @@ export class StreamingResponse {
|
|
|
1358
1871
|
id: this._id,
|
|
1359
1872
|
result: finalChunk,
|
|
1360
1873
|
success: true,
|
|
1361
|
-
type: "rpc"
|
|
1874
|
+
type: "rpc"
|
|
1362
1875
|
};
|
|
1363
1876
|
this._connection.send(JSON.stringify(response));
|
|
1364
1877
|
}
|