agents 0.0.103 → 0.0.105
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-NKZZ66QY.js → chunk-KUH345EY.js} +1 -1
- package/dist/chunk-KUH345EY.js.map +1 -0
- package/dist/{chunk-JFRK72K3.js → chunk-MFNGQLFL.js} +433 -83
- package/dist/chunk-MFNGQLFL.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 +2 -1
- package/src/index.ts +643 -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,131 @@ 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 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
|
+
|
|
714
1019
|
/**
|
|
715
1020
|
* Schedule a task to be executed in the future
|
|
716
1021
|
* @template T Type of the payload data
|
|
@@ -733,7 +1038,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
733
1038
|
id: nanoid(),
|
|
734
1039
|
payload: schedule,
|
|
735
1040
|
timestamp: Date.now(),
|
|
736
|
-
type: "schedule:create"
|
|
1041
|
+
type: "schedule:create"
|
|
737
1042
|
},
|
|
738
1043
|
this.ctx
|
|
739
1044
|
);
|
|
@@ -762,7 +1067,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
762
1067
|
id,
|
|
763
1068
|
payload: payload as T,
|
|
764
1069
|
time: timestamp,
|
|
765
|
-
type: "scheduled"
|
|
1070
|
+
type: "scheduled"
|
|
766
1071
|
};
|
|
767
1072
|
|
|
768
1073
|
emitScheduleCreate(schedule);
|
|
@@ -788,7 +1093,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
788
1093
|
id,
|
|
789
1094
|
payload: payload as T,
|
|
790
1095
|
time: timestamp,
|
|
791
|
-
type: "delayed"
|
|
1096
|
+
type: "delayed"
|
|
792
1097
|
};
|
|
793
1098
|
|
|
794
1099
|
emitScheduleCreate(schedule);
|
|
@@ -814,7 +1119,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
814
1119
|
id,
|
|
815
1120
|
payload: payload as T,
|
|
816
1121
|
time: timestamp,
|
|
817
|
-
type: "cron"
|
|
1122
|
+
type: "cron"
|
|
818
1123
|
};
|
|
819
1124
|
|
|
820
1125
|
emitScheduleCreate(schedule);
|
|
@@ -883,7 +1188,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
883
1188
|
.toArray()
|
|
884
1189
|
.map((row) => ({
|
|
885
1190
|
...row,
|
|
886
|
-
payload: JSON.parse(row.payload as string) as T
|
|
1191
|
+
payload: JSON.parse(row.payload as string) as T
|
|
887
1192
|
})) as Schedule<T>[];
|
|
888
1193
|
|
|
889
1194
|
return result;
|
|
@@ -903,7 +1208,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
903
1208
|
id: nanoid(),
|
|
904
1209
|
payload: schedule,
|
|
905
1210
|
timestamp: Date.now(),
|
|
906
|
-
type: "schedule:cancel"
|
|
1211
|
+
type: "schedule:cancel"
|
|
907
1212
|
},
|
|
908
1213
|
this.ctx
|
|
909
1214
|
);
|
|
@@ -946,51 +1251,58 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
946
1251
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
947
1252
|
`;
|
|
948
1253
|
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
+
);
|
|
969
1280
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
+
}
|
|
978
1290
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
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);
|
|
985
1296
|
|
|
986
|
-
|
|
1297
|
+
this.sql`
|
|
987
1298
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
988
1299
|
`;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1300
|
+
} else {
|
|
1301
|
+
// Delete one-time schedules after execution
|
|
1302
|
+
this.sql`
|
|
992
1303
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
993
1304
|
`;
|
|
1305
|
+
}
|
|
994
1306
|
}
|
|
995
1307
|
}
|
|
996
1308
|
|
|
@@ -1006,6 +1318,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1006
1318
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1007
1319
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1008
1320
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1321
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1009
1322
|
|
|
1010
1323
|
// delete all alarms
|
|
1011
1324
|
await this.ctx.storage.deleteAlarm();
|
|
@@ -1018,7 +1331,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1018
1331
|
id: nanoid(),
|
|
1019
1332
|
payload: {},
|
|
1020
1333
|
timestamp: Date.now(),
|
|
1021
|
-
type: "destroy"
|
|
1334
|
+
type: "destroy"
|
|
1022
1335
|
},
|
|
1023
1336
|
this.ctx
|
|
1024
1337
|
);
|
|
@@ -1078,7 +1391,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1078
1391
|
this.broadcast(
|
|
1079
1392
|
JSON.stringify({
|
|
1080
1393
|
mcp: this.getMcpServers(),
|
|
1081
|
-
type: "cf_agent_mcp_servers"
|
|
1394
|
+
type: "cf_agent_mcp_servers"
|
|
1082
1395
|
})
|
|
1083
1396
|
);
|
|
1084
1397
|
|
|
@@ -1134,12 +1447,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1134
1447
|
fetch: (url, init) =>
|
|
1135
1448
|
fetch(url, {
|
|
1136
1449
|
...init,
|
|
1137
|
-
headers: options?.transport?.headers
|
|
1138
|
-
})
|
|
1450
|
+
headers: options?.transport?.headers
|
|
1451
|
+
})
|
|
1139
1452
|
},
|
|
1140
1453
|
requestInit: {
|
|
1141
|
-
headers: options?.transport?.headers
|
|
1142
|
-
}
|
|
1454
|
+
headers: options?.transport?.headers
|
|
1455
|
+
}
|
|
1143
1456
|
};
|
|
1144
1457
|
}
|
|
1145
1458
|
|
|
@@ -1148,14 +1461,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1148
1461
|
reconnect,
|
|
1149
1462
|
transport: {
|
|
1150
1463
|
...headerTransportOpts,
|
|
1151
|
-
authProvider
|
|
1152
|
-
}
|
|
1464
|
+
authProvider
|
|
1465
|
+
}
|
|
1153
1466
|
});
|
|
1154
1467
|
|
|
1155
1468
|
return {
|
|
1156
1469
|
authUrl,
|
|
1157
1470
|
clientId,
|
|
1158
|
-
id
|
|
1471
|
+
id
|
|
1159
1472
|
};
|
|
1160
1473
|
}
|
|
1161
1474
|
|
|
@@ -1167,7 +1480,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1167
1480
|
this.broadcast(
|
|
1168
1481
|
JSON.stringify({
|
|
1169
1482
|
mcp: this.getMcpServers(),
|
|
1170
|
-
type: "cf_agent_mcp_servers"
|
|
1483
|
+
type: "cf_agent_mcp_servers"
|
|
1171
1484
|
})
|
|
1172
1485
|
);
|
|
1173
1486
|
}
|
|
@@ -1177,24 +1490,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1177
1490
|
prompts: this.mcp.listPrompts(),
|
|
1178
1491
|
resources: this.mcp.listResources(),
|
|
1179
1492
|
servers: {},
|
|
1180
|
-
tools: this.mcp.listTools()
|
|
1493
|
+
tools: this.mcp.listTools()
|
|
1181
1494
|
};
|
|
1182
1495
|
|
|
1183
1496
|
const servers = this.sql<MCPServerRow>`
|
|
1184
1497
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1185
1498
|
`;
|
|
1186
1499
|
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
+
}
|
|
1198
1513
|
}
|
|
1199
1514
|
|
|
1200
1515
|
return mcpState;
|
|
@@ -1241,14 +1556,14 @@ export async function routeAgentRequest<Env>(
|
|
|
1241
1556
|
"Access-Control-Allow-Credentials": "true",
|
|
1242
1557
|
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1243
1558
|
"Access-Control-Allow-Origin": "*",
|
|
1244
|
-
"Access-Control-Max-Age": "86400"
|
|
1559
|
+
"Access-Control-Max-Age": "86400"
|
|
1245
1560
|
}
|
|
1246
1561
|
: options?.cors;
|
|
1247
1562
|
|
|
1248
1563
|
if (request.method === "OPTIONS") {
|
|
1249
1564
|
if (corsHeaders) {
|
|
1250
1565
|
return new Response(null, {
|
|
1251
|
-
headers: corsHeaders
|
|
1566
|
+
headers: corsHeaders
|
|
1252
1567
|
});
|
|
1253
1568
|
}
|
|
1254
1569
|
console.warn(
|
|
@@ -1261,7 +1576,7 @@ export async function routeAgentRequest<Env>(
|
|
|
1261
1576
|
env as Record<string, unknown>,
|
|
1262
1577
|
{
|
|
1263
1578
|
prefix: "agents",
|
|
1264
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1579
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1265
1580
|
}
|
|
1266
1581
|
);
|
|
1267
1582
|
|
|
@@ -1274,24 +1589,218 @@ export async function routeAgentRequest<Env>(
|
|
|
1274
1589
|
response = new Response(response.body, {
|
|
1275
1590
|
headers: {
|
|
1276
1591
|
...response.headers,
|
|
1277
|
-
...corsHeaders
|
|
1278
|
-
}
|
|
1592
|
+
...corsHeaders
|
|
1593
|
+
}
|
|
1279
1594
|
});
|
|
1280
1595
|
}
|
|
1281
1596
|
return response;
|
|
1282
1597
|
}
|
|
1283
1598
|
|
|
1599
|
+
export type EmailResolver<Env> = (
|
|
1600
|
+
email: ForwardableEmailMessage,
|
|
1601
|
+
env: Env
|
|
1602
|
+
) => Promise<{
|
|
1603
|
+
agentName: string;
|
|
1604
|
+
agentId: string;
|
|
1605
|
+
} | null>;
|
|
1606
|
+
|
|
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> {
|
|
1612
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1613
|
+
const messageId = email.headers.get("message-id");
|
|
1614
|
+
if (messageId) {
|
|
1615
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1616
|
+
if (messageIdMatch) {
|
|
1617
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1618
|
+
const agentName = domain.split(".")[0];
|
|
1619
|
+
return { agentName, agentId };
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const references = email.headers.get("references");
|
|
1624
|
+
if (references) {
|
|
1625
|
+
const referencesMatch = references.match(
|
|
1626
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1627
|
+
);
|
|
1628
|
+
if (referencesMatch) {
|
|
1629
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1630
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1631
|
+
const agentName = domain.split(".")[0];
|
|
1632
|
+
return { agentName, agentId };
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1637
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1638
|
+
if (agentName && agentId) {
|
|
1639
|
+
return { agentName, agentId };
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
return null;
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
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>(
|
|
1652
|
+
defaultAgentName: string
|
|
1653
|
+
): EmailResolver<Env> {
|
|
1654
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1655
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1656
|
+
if (!emailMatch) {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1661
|
+
|
|
1662
|
+
if (subAddress) {
|
|
1663
|
+
return {
|
|
1664
|
+
agentName: localPart,
|
|
1665
|
+
agentId: subAddress
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1670
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1671
|
+
return {
|
|
1672
|
+
agentName: defaultAgentName,
|
|
1673
|
+
agentId: localPart
|
|
1674
|
+
};
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
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>(
|
|
1685
|
+
agentName: string,
|
|
1686
|
+
agentId: string
|
|
1687
|
+
): EmailResolver<Env> {
|
|
1688
|
+
return async () => ({ agentName, agentId });
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1692
|
+
resolver: EmailResolver<Env>;
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1284
1695
|
/**
|
|
1285
1696
|
* Route an email to the appropriate Agent
|
|
1286
|
-
* @param email
|
|
1287
|
-
* @param env
|
|
1288
|
-
* @param options
|
|
1697
|
+
* @param email The email to route
|
|
1698
|
+
* @param env The environment containing the Agent bindings
|
|
1699
|
+
* @param options The options for routing the email
|
|
1700
|
+
* @returns A promise that resolves when the email has been routed
|
|
1289
1701
|
*/
|
|
1290
1702
|
export async function routeAgentEmail<Env>(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
): Promise<void> {
|
|
1703
|
+
email: ForwardableEmailMessage,
|
|
1704
|
+
env: Env,
|
|
1705
|
+
options: EmailRoutingOptions<Env>
|
|
1706
|
+
): Promise<void> {
|
|
1707
|
+
const routingInfo = await options.resolver(email, env);
|
|
1708
|
+
|
|
1709
|
+
if (!routingInfo) {
|
|
1710
|
+
console.warn("No routing information found for email, dropping message");
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const namespaceBinding = env[routingInfo.agentName as keyof Env];
|
|
1715
|
+
if (!namespaceBinding) {
|
|
1716
|
+
throw new Error(
|
|
1717
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment`
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Type guard to check if this is actually a DurableObjectNamespace (AgentNamespace)
|
|
1722
|
+
if (
|
|
1723
|
+
typeof namespaceBinding !== "object" ||
|
|
1724
|
+
!("idFromName" in namespaceBinding) ||
|
|
1725
|
+
typeof namespaceBinding.idFromName !== "function"
|
|
1726
|
+
) {
|
|
1727
|
+
throw new Error(
|
|
1728
|
+
`Environment binding '${routingInfo.agentName}' is not an AgentNamespace (found: ${typeof namespaceBinding})`
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// Safe cast after runtime validation
|
|
1733
|
+
const namespace = namespaceBinding as unknown as AgentNamespace<Agent<Env>>;
|
|
1734
|
+
|
|
1735
|
+
const agent = await getAgentByName(namespace, routingInfo.agentId);
|
|
1736
|
+
|
|
1737
|
+
// let's make a serialisable version of the email
|
|
1738
|
+
const serialisableEmail: AgentEmail = {
|
|
1739
|
+
getRaw: async () => {
|
|
1740
|
+
const reader = email.raw.getReader();
|
|
1741
|
+
const chunks: Uint8Array[] = [];
|
|
1742
|
+
|
|
1743
|
+
let done = false;
|
|
1744
|
+
while (!done) {
|
|
1745
|
+
const { value, done: readerDone } = await reader.read();
|
|
1746
|
+
done = readerDone;
|
|
1747
|
+
if (value) {
|
|
1748
|
+
chunks.push(value);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1753
|
+
const combined = new Uint8Array(totalLength);
|
|
1754
|
+
let offset = 0;
|
|
1755
|
+
for (const chunk of chunks) {
|
|
1756
|
+
combined.set(chunk, offset);
|
|
1757
|
+
offset += chunk.length;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
return combined;
|
|
1761
|
+
},
|
|
1762
|
+
headers: email.headers,
|
|
1763
|
+
rawSize: email.rawSize,
|
|
1764
|
+
setReject: (reason: string) => {
|
|
1765
|
+
email.setReject(reason);
|
|
1766
|
+
},
|
|
1767
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1768
|
+
return email.forward(rcptTo, headers);
|
|
1769
|
+
},
|
|
1770
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1771
|
+
return email.reply(
|
|
1772
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1773
|
+
);
|
|
1774
|
+
},
|
|
1775
|
+
from: email.from,
|
|
1776
|
+
to: email.to
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
await agent._onEmail(serialisableEmail);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
export type AgentEmail = {
|
|
1783
|
+
from: string;
|
|
1784
|
+
to: string;
|
|
1785
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1786
|
+
headers: Headers;
|
|
1787
|
+
rawSize: number;
|
|
1788
|
+
setReject: (reason: string) => void;
|
|
1789
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1790
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
export type EmailSendOptions = {
|
|
1794
|
+
to: string;
|
|
1795
|
+
subject: string;
|
|
1796
|
+
body: string;
|
|
1797
|
+
contentType?: string;
|
|
1798
|
+
headers?: Record<string, string>;
|
|
1799
|
+
includeRoutingHeaders?: boolean;
|
|
1800
|
+
agentName?: string;
|
|
1801
|
+
agentId?: string;
|
|
1802
|
+
domain?: string;
|
|
1803
|
+
};
|
|
1295
1804
|
|
|
1296
1805
|
/**
|
|
1297
1806
|
* Get or create an Agent by name
|
|
@@ -1339,7 +1848,7 @@ export class StreamingResponse {
|
|
|
1339
1848
|
id: this._id,
|
|
1340
1849
|
result: chunk,
|
|
1341
1850
|
success: true,
|
|
1342
|
-
type: "rpc"
|
|
1851
|
+
type: "rpc"
|
|
1343
1852
|
};
|
|
1344
1853
|
this._connection.send(JSON.stringify(response));
|
|
1345
1854
|
}
|
|
@@ -1358,7 +1867,7 @@ export class StreamingResponse {
|
|
|
1358
1867
|
id: this._id,
|
|
1359
1868
|
result: finalChunk,
|
|
1360
1869
|
success: true,
|
|
1361
|
-
type: "rpc"
|
|
1870
|
+
type: "rpc"
|
|
1362
1871
|
};
|
|
1363
1872
|
this._connection.send(JSON.stringify(response));
|
|
1364
1873
|
}
|