agents 0.0.0-ac74811 → 0.0.0-b123357
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 +128 -22
- package/dist/ai-chat-agent.d.ts +5 -3
- package/dist/ai-chat-agent.js +4 -4
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-react.d.ts +6 -4
- package/dist/ai-react.js.map +1 -1
- package/dist/{chunk-E3LCYPCB.js → chunk-HY7ZLHJB.js} +146 -17
- package/dist/chunk-HY7ZLHJB.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-JFRK72K3.js → chunk-OJFA7RKX.js} +445 -85
- package/dist/chunk-OJFA7RKX.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-DgyzBU_8.d.ts +4601 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/{index-CITGJflw.d.ts → index-BCJclX6q.d.ts} +147 -18
- package/dist/index.d.ts +17 -7
- package/dist/index.js +10 -4
- package/dist/mcp/client.d.ts +9 -1053
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/do-oauth-client-provider.js +1 -1
- package/dist/mcp/index.d.ts +24 -7
- package/dist/mcp/index.js +136 -11
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +5 -3
- package/dist/observability/index.js +4 -4
- package/dist/react.d.ts +8 -6
- package/dist/react.js.map +1 -1
- package/dist/schedule.d.ts +4 -4
- package/dist/schedule.js.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +667 -137
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { env } from "cloudflare:workers";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
4
|
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
@@ -6,18 +7,19 @@ import type {
|
|
|
6
7
|
Prompt,
|
|
7
8
|
Resource,
|
|
8
9
|
ServerCapabilities,
|
|
9
|
-
Tool
|
|
10
|
+
Tool
|
|
10
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
12
|
import { parseCronExpression } from "cron-schedule";
|
|
12
13
|
import { nanoid } from "nanoid";
|
|
14
|
+
import { EmailMessage } from "cloudflare:email";
|
|
13
15
|
import {
|
|
14
16
|
type Connection,
|
|
15
17
|
type ConnectionContext,
|
|
16
|
-
getServerByName,
|
|
17
18
|
type PartyServerOptions,
|
|
18
|
-
routePartykitRequest,
|
|
19
19
|
Server,
|
|
20
20
|
type WSMessage,
|
|
21
|
+
getServerByName,
|
|
22
|
+
routePartykitRequest
|
|
21
23
|
} from "partyserver";
|
|
22
24
|
import { camelCaseToKebabCase } from "./client";
|
|
23
25
|
import { MCPClientManager } from "./mcp/client";
|
|
@@ -129,6 +131,13 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
export type QueueItem<T = string> = {
|
|
135
|
+
id: string;
|
|
136
|
+
payload: T;
|
|
137
|
+
callback: keyof Agent<unknown>;
|
|
138
|
+
created_at: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
132
141
|
/**
|
|
133
142
|
* Represents a scheduled task within an Agent
|
|
134
143
|
* @template T Type of the payload data
|
|
@@ -218,23 +227,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
|
218
227
|
const DEFAULT_STATE = {} as unknown;
|
|
219
228
|
|
|
220
229
|
const agentContext = new AsyncLocalStorage<{
|
|
221
|
-
agent: Agent<unknown>;
|
|
230
|
+
agent: Agent<unknown, unknown>;
|
|
222
231
|
connection: Connection | undefined;
|
|
223
232
|
request: Request | undefined;
|
|
233
|
+
email: AgentEmail | undefined;
|
|
224
234
|
}>();
|
|
225
235
|
|
|
226
236
|
export function getCurrentAgent<
|
|
227
|
-
T extends Agent<unknown, unknown> = Agent<unknown, unknown
|
|
237
|
+
T extends Agent<unknown, unknown> = Agent<unknown, unknown>
|
|
228
238
|
>(): {
|
|
229
239
|
agent: T | undefined;
|
|
230
240
|
connection: Connection | undefined;
|
|
231
|
-
request: Request
|
|
241
|
+
request: Request | undefined;
|
|
242
|
+
email: AgentEmail | undefined;
|
|
232
243
|
} {
|
|
233
244
|
const store = agentContext.getStore() as
|
|
234
245
|
| {
|
|
235
246
|
agent: T;
|
|
236
247
|
connection: Connection | undefined;
|
|
237
|
-
request: Request
|
|
248
|
+
request: Request | undefined;
|
|
249
|
+
email: AgentEmail | undefined;
|
|
238
250
|
}
|
|
239
251
|
| undefined;
|
|
240
252
|
if (!store) {
|
|
@@ -242,17 +254,37 @@ export function getCurrentAgent<
|
|
|
242
254
|
agent: undefined,
|
|
243
255
|
connection: undefined,
|
|
244
256
|
request: undefined,
|
|
257
|
+
email: undefined
|
|
245
258
|
};
|
|
246
259
|
}
|
|
247
260
|
return store;
|
|
248
261
|
}
|
|
249
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
|
|
265
|
+
* @param agent The agent instance
|
|
266
|
+
* @param method The method to wrap
|
|
267
|
+
* @returns A wrapped method that runs within the agent context
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
271
|
+
function withAgentContext<T extends (...args: any[]) => any>(
|
|
272
|
+
method: T
|
|
273
|
+
): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
|
|
274
|
+
return function (...args: Parameters<T>): ReturnType<T> {
|
|
275
|
+
const { connection, request, email } = getCurrentAgent();
|
|
276
|
+
return agentContext.run({ agent: this, connection, request, email }, () => {
|
|
277
|
+
return method.apply(this, args);
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
250
282
|
/**
|
|
251
283
|
* Base class for creating Agent implementations
|
|
252
284
|
* @template Env Environment type containing bindings
|
|
253
285
|
* @template State State type to store within the Agent
|
|
254
286
|
*/
|
|
255
|
-
export class Agent<Env, State = unknown> extends Server<Env> {
|
|
287
|
+
export class Agent<Env = typeof env, State = unknown> extends Server<Env> {
|
|
256
288
|
private _state = DEFAULT_STATE as State;
|
|
257
289
|
|
|
258
290
|
private _ParentClass: typeof Agent<Env, State> =
|
|
@@ -314,7 +346,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
314
346
|
*/
|
|
315
347
|
static options = {
|
|
316
348
|
/** Whether the Agent should hibernate when inactive */
|
|
317
|
-
hibernate: true
|
|
349
|
+
hibernate: true // default to hibernate
|
|
318
350
|
};
|
|
319
351
|
|
|
320
352
|
/**
|
|
@@ -351,6 +383,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
351
383
|
constructor(ctx: AgentContext, env: Env) {
|
|
352
384
|
super(ctx, env);
|
|
353
385
|
|
|
386
|
+
// Auto-wrap custom methods with agent context
|
|
387
|
+
this._autoWrapCustomMethods();
|
|
388
|
+
|
|
354
389
|
this.sql`
|
|
355
390
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
356
391
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -358,6 +393,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
358
393
|
)
|
|
359
394
|
`;
|
|
360
395
|
|
|
396
|
+
this.sql`
|
|
397
|
+
CREATE TABLE IF NOT EXISTS cf_agents_queues (
|
|
398
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
399
|
+
payload TEXT,
|
|
400
|
+
callback TEXT,
|
|
401
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
402
|
+
)
|
|
403
|
+
`;
|
|
404
|
+
|
|
361
405
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
362
406
|
return this._tryCatch(async () => {
|
|
363
407
|
// Create alarms table if it doesn't exist
|
|
@@ -394,7 +438,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
394
438
|
const _onRequest = this.onRequest.bind(this);
|
|
395
439
|
this.onRequest = (request: Request) => {
|
|
396
440
|
return agentContext.run(
|
|
397
|
-
{ agent: this, connection: undefined, request },
|
|
441
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
398
442
|
async () => {
|
|
399
443
|
if (this.mcp.isCallbackRequest(request)) {
|
|
400
444
|
await this.mcp.handleCallbackRequest(request);
|
|
@@ -403,14 +447,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
403
447
|
this.broadcast(
|
|
404
448
|
JSON.stringify({
|
|
405
449
|
mcp: this.getMcpServers(),
|
|
406
|
-
type: "cf_agent_mcp_servers"
|
|
450
|
+
type: "cf_agent_mcp_servers"
|
|
407
451
|
})
|
|
408
452
|
);
|
|
409
453
|
|
|
410
454
|
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
411
455
|
return new Response("<script>window.close();</script>", {
|
|
412
456
|
headers: { "content-type": "text/html" },
|
|
413
|
-
status: 200
|
|
457
|
+
status: 200
|
|
414
458
|
});
|
|
415
459
|
}
|
|
416
460
|
|
|
@@ -422,7 +466,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
422
466
|
const _onMessage = this.onMessage.bind(this);
|
|
423
467
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
424
468
|
return agentContext.run(
|
|
425
|
-
{ agent: this, connection, request: undefined },
|
|
469
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
426
470
|
async () => {
|
|
427
471
|
if (typeof message !== "string") {
|
|
428
472
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
@@ -475,10 +519,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
475
519
|
args,
|
|
476
520
|
method,
|
|
477
521
|
streaming: metadata?.streaming,
|
|
478
|
-
success: true
|
|
522
|
+
success: true
|
|
479
523
|
},
|
|
480
524
|
timestamp: Date.now(),
|
|
481
|
-
type: "rpc"
|
|
525
|
+
type: "rpc"
|
|
482
526
|
},
|
|
483
527
|
this.ctx
|
|
484
528
|
);
|
|
@@ -488,7 +532,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
488
532
|
id,
|
|
489
533
|
result,
|
|
490
534
|
success: true,
|
|
491
|
-
type: "rpc"
|
|
535
|
+
type: "rpc"
|
|
492
536
|
};
|
|
493
537
|
connection.send(JSON.stringify(response));
|
|
494
538
|
} catch (e) {
|
|
@@ -498,7 +542,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
498
542
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
499
543
|
id: parsed.id,
|
|
500
544
|
success: false,
|
|
501
|
-
type: "rpc"
|
|
545
|
+
type: "rpc"
|
|
502
546
|
};
|
|
503
547
|
connection.send(JSON.stringify(response));
|
|
504
548
|
console.error("RPC error:", e);
|
|
@@ -516,14 +560,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
516
560
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
517
561
|
// must fix this
|
|
518
562
|
return agentContext.run(
|
|
519
|
-
{ agent: this, connection, request: ctx.request },
|
|
563
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
520
564
|
async () => {
|
|
521
565
|
setTimeout(() => {
|
|
522
566
|
if (this.state) {
|
|
523
567
|
connection.send(
|
|
524
568
|
JSON.stringify({
|
|
525
569
|
state: this.state,
|
|
526
|
-
type: "cf_agent_state"
|
|
570
|
+
type: "cf_agent_state"
|
|
527
571
|
})
|
|
528
572
|
);
|
|
529
573
|
}
|
|
@@ -531,7 +575,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
531
575
|
connection.send(
|
|
532
576
|
JSON.stringify({
|
|
533
577
|
mcp: this.getMcpServers(),
|
|
534
|
-
type: "cf_agent_mcp_servers"
|
|
578
|
+
type: "cf_agent_mcp_servers"
|
|
535
579
|
})
|
|
536
580
|
);
|
|
537
581
|
|
|
@@ -540,10 +584,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
540
584
|
displayMessage: "Connection established",
|
|
541
585
|
id: nanoid(),
|
|
542
586
|
payload: {
|
|
543
|
-
connectionId: connection.id
|
|
587
|
+
connectionId: connection.id
|
|
544
588
|
},
|
|
545
589
|
timestamp: Date.now(),
|
|
546
|
-
type: "connect"
|
|
590
|
+
type: "connect"
|
|
547
591
|
},
|
|
548
592
|
this.ctx
|
|
549
593
|
);
|
|
@@ -556,36 +600,43 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
556
600
|
const _onStart = this.onStart.bind(this);
|
|
557
601
|
this.onStart = async () => {
|
|
558
602
|
return agentContext.run(
|
|
559
|
-
{
|
|
603
|
+
{
|
|
604
|
+
agent: this,
|
|
605
|
+
connection: undefined,
|
|
606
|
+
request: undefined,
|
|
607
|
+
email: undefined
|
|
608
|
+
},
|
|
560
609
|
async () => {
|
|
561
610
|
const servers = this.sql<MCPServerRow>`
|
|
562
611
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
563
612
|
`;
|
|
564
613
|
|
|
565
614
|
// 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",
|
|
615
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
616
|
+
Promise.allSettled(
|
|
617
|
+
servers.map((server) => {
|
|
618
|
+
return this._connectToMcpServerInternal(
|
|
619
|
+
server.name,
|
|
620
|
+
server.server_url,
|
|
621
|
+
server.callback_url,
|
|
622
|
+
server.server_options
|
|
623
|
+
? JSON.parse(server.server_options)
|
|
624
|
+
: undefined,
|
|
625
|
+
{
|
|
626
|
+
id: server.id,
|
|
627
|
+
oauthClientId: server.client_id ?? undefined
|
|
628
|
+
}
|
|
629
|
+
);
|
|
586
630
|
})
|
|
587
|
-
)
|
|
588
|
-
|
|
631
|
+
).then((_results) => {
|
|
632
|
+
this.broadcast(
|
|
633
|
+
JSON.stringify({
|
|
634
|
+
mcp: this.getMcpServers(),
|
|
635
|
+
type: "cf_agent_mcp_servers"
|
|
636
|
+
})
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
589
640
|
await this._tryCatch(() => _onStart());
|
|
590
641
|
}
|
|
591
642
|
);
|
|
@@ -609,14 +660,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
609
660
|
this.broadcast(
|
|
610
661
|
JSON.stringify({
|
|
611
662
|
state: state,
|
|
612
|
-
type: "cf_agent_state"
|
|
663
|
+
type: "cf_agent_state"
|
|
613
664
|
}),
|
|
614
665
|
source !== "server" ? [source.id] : []
|
|
615
666
|
);
|
|
616
667
|
return this._tryCatch(() => {
|
|
617
|
-
const { connection, request } = agentContext.getStore() || {};
|
|
668
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
618
669
|
return agentContext.run(
|
|
619
|
-
{ agent: this, connection, request },
|
|
670
|
+
{ agent: this, connection, request, email },
|
|
620
671
|
async () => {
|
|
621
672
|
this.observability?.emit(
|
|
622
673
|
{
|
|
@@ -624,10 +675,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
624
675
|
id: nanoid(),
|
|
625
676
|
payload: {
|
|
626
677
|
previousState,
|
|
627
|
-
state
|
|
678
|
+
state
|
|
628
679
|
},
|
|
629
680
|
timestamp: Date.now(),
|
|
630
|
-
type: "state:update"
|
|
681
|
+
type: "state:update"
|
|
631
682
|
},
|
|
632
683
|
this.ctx
|
|
633
684
|
);
|
|
@@ -656,19 +707,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
656
707
|
}
|
|
657
708
|
|
|
658
709
|
/**
|
|
659
|
-
* Called when the Agent receives an email
|
|
710
|
+
* Called when the Agent receives an email via routeAgentEmail()
|
|
711
|
+
* Override this method to handle incoming emails
|
|
660
712
|
* @param email Email message to process
|
|
661
713
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
714
|
+
async _onEmail(email: AgentEmail) {
|
|
715
|
+
// nb: we use this roundabout way of getting to onEmail
|
|
716
|
+
// because of https://github.com/cloudflare/workerd/issues/4499
|
|
664
717
|
return agentContext.run(
|
|
665
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
718
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
666
719
|
async () => {
|
|
667
|
-
|
|
720
|
+
if ("onEmail" in this && typeof this.onEmail === "function") {
|
|
721
|
+
return this._tryCatch(() =>
|
|
722
|
+
(this.onEmail as (email: AgentEmail) => Promise<void>)(email)
|
|
723
|
+
);
|
|
724
|
+
} else {
|
|
725
|
+
console.log("Received email from:", email.from, "to:", email.to);
|
|
726
|
+
console.log("Subject:", email.headers.get("subject"));
|
|
727
|
+
console.log(
|
|
728
|
+
"Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
|
|
729
|
+
);
|
|
730
|
+
}
|
|
668
731
|
}
|
|
669
732
|
);
|
|
670
733
|
}
|
|
671
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Reply to an email
|
|
737
|
+
* @param email The email to reply to
|
|
738
|
+
* @param options Options for the reply
|
|
739
|
+
* @returns void
|
|
740
|
+
*/
|
|
741
|
+
async replyToEmail(
|
|
742
|
+
email: AgentEmail,
|
|
743
|
+
options: {
|
|
744
|
+
fromName: string;
|
|
745
|
+
subject?: string | undefined;
|
|
746
|
+
body: string;
|
|
747
|
+
contentType?: string;
|
|
748
|
+
headers?: Record<string, string>;
|
|
749
|
+
}
|
|
750
|
+
): Promise<void> {
|
|
751
|
+
return this._tryCatch(async () => {
|
|
752
|
+
const agentName = camelCaseToKebabCase(this._ParentClass.name);
|
|
753
|
+
const agentId = this.name;
|
|
754
|
+
|
|
755
|
+
const { createMimeMessage } = await import("mimetext");
|
|
756
|
+
const msg = createMimeMessage();
|
|
757
|
+
msg.setSender({ addr: email.to, name: options.fromName });
|
|
758
|
+
msg.setRecipient(email.from);
|
|
759
|
+
msg.setSubject(
|
|
760
|
+
options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
|
|
761
|
+
);
|
|
762
|
+
msg.addMessage({
|
|
763
|
+
contentType: options.contentType || "text/plain",
|
|
764
|
+
data: options.body
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const domain = email.from.split("@")[1];
|
|
768
|
+
const messageId = `<${agentId}@${domain}>`;
|
|
769
|
+
msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
|
|
770
|
+
msg.setHeader("Message-ID", messageId);
|
|
771
|
+
msg.setHeader("X-Agent-Name", agentName);
|
|
772
|
+
msg.setHeader("X-Agent-ID", agentId);
|
|
773
|
+
|
|
774
|
+
if (options.headers) {
|
|
775
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
776
|
+
msg.setHeader(key, value);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
await email.reply({
|
|
780
|
+
from: email.to,
|
|
781
|
+
raw: msg.asRaw(),
|
|
782
|
+
to: email.from
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
672
787
|
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
673
788
|
try {
|
|
674
789
|
return await fn();
|
|
@@ -677,6 +792,72 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
677
792
|
}
|
|
678
793
|
}
|
|
679
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Automatically wrap custom methods with agent context
|
|
797
|
+
* This ensures getCurrentAgent() works in all custom methods without decorators
|
|
798
|
+
*/
|
|
799
|
+
private _autoWrapCustomMethods() {
|
|
800
|
+
// Collect all methods from base prototypes (Agent and Server)
|
|
801
|
+
const basePrototypes = [Agent.prototype, Server.prototype];
|
|
802
|
+
const baseMethods = new Set<string>();
|
|
803
|
+
for (const baseProto of basePrototypes) {
|
|
804
|
+
let proto = baseProto;
|
|
805
|
+
while (proto && proto !== Object.prototype) {
|
|
806
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
807
|
+
for (const methodName of methodNames) {
|
|
808
|
+
baseMethods.add(methodName);
|
|
809
|
+
}
|
|
810
|
+
proto = Object.getPrototypeOf(proto);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Get all methods from the current instance's prototype chain
|
|
814
|
+
let proto = Object.getPrototypeOf(this);
|
|
815
|
+
let depth = 0;
|
|
816
|
+
while (proto && proto !== Object.prototype && depth < 10) {
|
|
817
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
818
|
+
for (const methodName of methodNames) {
|
|
819
|
+
// Skip if it's a private method or not a function
|
|
820
|
+
if (
|
|
821
|
+
baseMethods.has(methodName) ||
|
|
822
|
+
methodName.startsWith("_") ||
|
|
823
|
+
typeof this[methodName as keyof this] !== "function"
|
|
824
|
+
) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
// If the method doesn't exist in base prototypes, it's a custom method
|
|
828
|
+
if (!baseMethods.has(methodName)) {
|
|
829
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
|
|
830
|
+
if (descriptor && typeof descriptor.value === "function") {
|
|
831
|
+
// Wrap the custom method with context
|
|
832
|
+
|
|
833
|
+
const wrappedFunction = withAgentContext(
|
|
834
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
835
|
+
this[methodName as keyof this] as (...args: any[]) => any
|
|
836
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
837
|
+
) as any;
|
|
838
|
+
|
|
839
|
+
// if the method is callable, copy the metadata from the original method
|
|
840
|
+
if (this._isCallable(methodName)) {
|
|
841
|
+
callableMetadata.set(
|
|
842
|
+
wrappedFunction,
|
|
843
|
+
callableMetadata.get(
|
|
844
|
+
this[methodName as keyof this] as Function
|
|
845
|
+
)!
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// set the wrapped function on the prototype
|
|
850
|
+
this.constructor.prototype[methodName as keyof this] =
|
|
851
|
+
wrappedFunction;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
proto = Object.getPrototypeOf(proto);
|
|
857
|
+
depth++;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
680
861
|
override onError(
|
|
681
862
|
connection: Connection,
|
|
682
863
|
error: unknown
|
|
@@ -711,6 +892,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
711
892
|
throw new Error("Not implemented");
|
|
712
893
|
}
|
|
713
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Queue a task to be executed in the future
|
|
897
|
+
* @param payload Payload to pass to the callback
|
|
898
|
+
* @param callback Name of the method to call
|
|
899
|
+
* @returns The ID of the queued task
|
|
900
|
+
*/
|
|
901
|
+
async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
|
|
902
|
+
const id = nanoid(9);
|
|
903
|
+
if (typeof callback !== "string") {
|
|
904
|
+
throw new Error("Callback must be a string");
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (typeof this[callback] !== "function") {
|
|
908
|
+
throw new Error(`this.${callback} is not a function`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
this.sql`
|
|
912
|
+
INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
|
|
913
|
+
VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
|
|
914
|
+
`;
|
|
915
|
+
|
|
916
|
+
void this._flushQueue().catch((e) => {
|
|
917
|
+
console.error("Error flushing queue:", e);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
return id;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private _flushingQueue = false;
|
|
924
|
+
|
|
925
|
+
private async _flushQueue() {
|
|
926
|
+
if (this._flushingQueue) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this._flushingQueue = true;
|
|
930
|
+
while (true) {
|
|
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
|
+
await this.dequeue(row.id);
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
this._flushingQueue = false;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Dequeue a task by ID
|
|
972
|
+
* @param id ID of the task to dequeue
|
|
973
|
+
*/
|
|
974
|
+
async dequeue(id: string) {
|
|
975
|
+
this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Dequeue all tasks
|
|
980
|
+
*/
|
|
981
|
+
async dequeueAll() {
|
|
982
|
+
this.sql`DELETE FROM cf_agents_queues`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Dequeue all tasks by callback
|
|
987
|
+
* @param callback Name of the callback to dequeue
|
|
988
|
+
*/
|
|
989
|
+
async dequeueAllByCallback(callback: string) {
|
|
990
|
+
this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Get a queued task by ID
|
|
995
|
+
* @param id ID of the task to get
|
|
996
|
+
* @returns The task or undefined if not found
|
|
997
|
+
*/
|
|
998
|
+
async getQueue(id: string): Promise<QueueItem<string> | undefined> {
|
|
999
|
+
const result = this.sql<QueueItem<string>>`
|
|
1000
|
+
SELECT * FROM cf_agents_queues WHERE id = ${id}
|
|
1001
|
+
`;
|
|
1002
|
+
return result
|
|
1003
|
+
? { ...result[0], payload: JSON.parse(result[0].payload) }
|
|
1004
|
+
: undefined;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Get all queues by key and value
|
|
1009
|
+
* @param key Key to filter by
|
|
1010
|
+
* @param value Value to filter by
|
|
1011
|
+
* @returns Array of matching QueueItem objects
|
|
1012
|
+
*/
|
|
1013
|
+
async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
|
|
1014
|
+
const result = this.sql<QueueItem<string>>`
|
|
1015
|
+
SELECT * FROM cf_agents_queues
|
|
1016
|
+
`;
|
|
1017
|
+
return result.filter((row) => JSON.parse(row.payload)[key] === value);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
714
1020
|
/**
|
|
715
1021
|
* Schedule a task to be executed in the future
|
|
716
1022
|
* @template T Type of the payload data
|
|
@@ -733,7 +1039,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
733
1039
|
id: nanoid(),
|
|
734
1040
|
payload: schedule,
|
|
735
1041
|
timestamp: Date.now(),
|
|
736
|
-
type: "schedule:create"
|
|
1042
|
+
type: "schedule:create"
|
|
737
1043
|
},
|
|
738
1044
|
this.ctx
|
|
739
1045
|
);
|
|
@@ -762,7 +1068,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
762
1068
|
id,
|
|
763
1069
|
payload: payload as T,
|
|
764
1070
|
time: timestamp,
|
|
765
|
-
type: "scheduled"
|
|
1071
|
+
type: "scheduled"
|
|
766
1072
|
};
|
|
767
1073
|
|
|
768
1074
|
emitScheduleCreate(schedule);
|
|
@@ -788,7 +1094,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
788
1094
|
id,
|
|
789
1095
|
payload: payload as T,
|
|
790
1096
|
time: timestamp,
|
|
791
|
-
type: "delayed"
|
|
1097
|
+
type: "delayed"
|
|
792
1098
|
};
|
|
793
1099
|
|
|
794
1100
|
emitScheduleCreate(schedule);
|
|
@@ -814,7 +1120,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
814
1120
|
id,
|
|
815
1121
|
payload: payload as T,
|
|
816
1122
|
time: timestamp,
|
|
817
|
-
type: "cron"
|
|
1123
|
+
type: "cron"
|
|
818
1124
|
};
|
|
819
1125
|
|
|
820
1126
|
emitScheduleCreate(schedule);
|
|
@@ -883,7 +1189,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
883
1189
|
.toArray()
|
|
884
1190
|
.map((row) => ({
|
|
885
1191
|
...row,
|
|
886
|
-
payload: JSON.parse(row.payload as string) as T
|
|
1192
|
+
payload: JSON.parse(row.payload as string) as T
|
|
887
1193
|
})) as Schedule<T>[];
|
|
888
1194
|
|
|
889
1195
|
return result;
|
|
@@ -903,7 +1209,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
903
1209
|
id: nanoid(),
|
|
904
1210
|
payload: schedule,
|
|
905
1211
|
timestamp: Date.now(),
|
|
906
|
-
type: "schedule:cancel"
|
|
1212
|
+
type: "schedule:cancel"
|
|
907
1213
|
},
|
|
908
1214
|
this.ctx
|
|
909
1215
|
);
|
|
@@ -917,9 +1223,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
917
1223
|
private async _scheduleNextAlarm() {
|
|
918
1224
|
// Find the next schedule that needs to be executed
|
|
919
1225
|
const result = this.sql`
|
|
920
|
-
SELECT time FROM cf_agents_schedules
|
|
1226
|
+
SELECT time FROM cf_agents_schedules
|
|
921
1227
|
WHERE time > ${Math.floor(Date.now() / 1000)}
|
|
922
|
-
ORDER BY time ASC
|
|
1228
|
+
ORDER BY time ASC
|
|
923
1229
|
LIMIT 1
|
|
924
1230
|
`;
|
|
925
1231
|
if (!result) return;
|
|
@@ -946,51 +1252,58 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
946
1252
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
947
1253
|
`;
|
|
948
1254
|
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1255
|
+
if (result && Array.isArray(result)) {
|
|
1256
|
+
for (const row of result) {
|
|
1257
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
1258
|
+
if (!callback) {
|
|
1259
|
+
console.error(`callback ${row.callback} not found`);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
await agentContext.run(
|
|
1263
|
+
{
|
|
1264
|
+
agent: this,
|
|
1265
|
+
connection: undefined,
|
|
1266
|
+
request: undefined,
|
|
1267
|
+
email: undefined
|
|
1268
|
+
},
|
|
1269
|
+
async () => {
|
|
1270
|
+
try {
|
|
1271
|
+
this.observability?.emit(
|
|
1272
|
+
{
|
|
1273
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
1274
|
+
id: nanoid(),
|
|
1275
|
+
payload: row,
|
|
1276
|
+
timestamp: Date.now(),
|
|
1277
|
+
type: "schedule:execute"
|
|
1278
|
+
},
|
|
1279
|
+
this.ctx
|
|
1280
|
+
);
|
|
969
1281
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1282
|
+
await (
|
|
1283
|
+
callback as (
|
|
1284
|
+
payload: unknown,
|
|
1285
|
+
schedule: Schedule<unknown>
|
|
1286
|
+
) => Promise<void>
|
|
1287
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
console.error(`error executing callback "${row.callback}"`, e);
|
|
1290
|
+
}
|
|
978
1291
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
1292
|
+
);
|
|
1293
|
+
if (row.type === "cron") {
|
|
1294
|
+
// Update next execution time for cron schedules
|
|
1295
|
+
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1296
|
+
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
985
1297
|
|
|
986
|
-
|
|
1298
|
+
this.sql`
|
|
987
1299
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
988
1300
|
`;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1301
|
+
} else {
|
|
1302
|
+
// Delete one-time schedules after execution
|
|
1303
|
+
this.sql`
|
|
992
1304
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
993
1305
|
`;
|
|
1306
|
+
}
|
|
994
1307
|
}
|
|
995
1308
|
}
|
|
996
1309
|
|
|
@@ -1006,6 +1319,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1006
1319
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1007
1320
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1008
1321
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1322
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1009
1323
|
|
|
1010
1324
|
// delete all alarms
|
|
1011
1325
|
await this.ctx.storage.deleteAlarm();
|
|
@@ -1018,7 +1332,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1018
1332
|
id: nanoid(),
|
|
1019
1333
|
payload: {},
|
|
1020
1334
|
timestamp: Date.now(),
|
|
1021
|
-
type: "destroy"
|
|
1335
|
+
type: "destroy"
|
|
1022
1336
|
},
|
|
1023
1337
|
this.ctx
|
|
1024
1338
|
);
|
|
@@ -1078,7 +1392,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1078
1392
|
this.broadcast(
|
|
1079
1393
|
JSON.stringify({
|
|
1080
1394
|
mcp: this.getMcpServers(),
|
|
1081
|
-
type: "cf_agent_mcp_servers"
|
|
1395
|
+
type: "cf_agent_mcp_servers"
|
|
1082
1396
|
})
|
|
1083
1397
|
);
|
|
1084
1398
|
|
|
@@ -1134,12 +1448,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1134
1448
|
fetch: (url, init) =>
|
|
1135
1449
|
fetch(url, {
|
|
1136
1450
|
...init,
|
|
1137
|
-
headers: options?.transport?.headers
|
|
1138
|
-
})
|
|
1451
|
+
headers: options?.transport?.headers
|
|
1452
|
+
})
|
|
1139
1453
|
},
|
|
1140
1454
|
requestInit: {
|
|
1141
|
-
headers: options?.transport?.headers
|
|
1142
|
-
}
|
|
1455
|
+
headers: options?.transport?.headers
|
|
1456
|
+
}
|
|
1143
1457
|
};
|
|
1144
1458
|
}
|
|
1145
1459
|
|
|
@@ -1148,14 +1462,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1148
1462
|
reconnect,
|
|
1149
1463
|
transport: {
|
|
1150
1464
|
...headerTransportOpts,
|
|
1151
|
-
authProvider
|
|
1152
|
-
}
|
|
1465
|
+
authProvider
|
|
1466
|
+
}
|
|
1153
1467
|
});
|
|
1154
1468
|
|
|
1155
1469
|
return {
|
|
1156
1470
|
authUrl,
|
|
1157
1471
|
clientId,
|
|
1158
|
-
id
|
|
1472
|
+
id
|
|
1159
1473
|
};
|
|
1160
1474
|
}
|
|
1161
1475
|
|
|
@@ -1167,7 +1481,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1167
1481
|
this.broadcast(
|
|
1168
1482
|
JSON.stringify({
|
|
1169
1483
|
mcp: this.getMcpServers(),
|
|
1170
|
-
type: "cf_agent_mcp_servers"
|
|
1484
|
+
type: "cf_agent_mcp_servers"
|
|
1171
1485
|
})
|
|
1172
1486
|
);
|
|
1173
1487
|
}
|
|
@@ -1177,24 +1491,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1177
1491
|
prompts: this.mcp.listPrompts(),
|
|
1178
1492
|
resources: this.mcp.listResources(),
|
|
1179
1493
|
servers: {},
|
|
1180
|
-
tools: this.mcp.listTools()
|
|
1494
|
+
tools: this.mcp.listTools()
|
|
1181
1495
|
};
|
|
1182
1496
|
|
|
1183
1497
|
const servers = this.sql<MCPServerRow>`
|
|
1184
1498
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1185
1499
|
`;
|
|
1186
1500
|
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1501
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
1502
|
+
for (const server of servers) {
|
|
1503
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1504
|
+
mcpState.servers[server.id] = {
|
|
1505
|
+
auth_url: server.auth_url,
|
|
1506
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1507
|
+
instructions: serverConn?.instructions ?? null,
|
|
1508
|
+
name: server.name,
|
|
1509
|
+
server_url: server.server_url,
|
|
1510
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1511
|
+
state: serverConn?.connectionState ?? "authenticating"
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1198
1514
|
}
|
|
1199
1515
|
|
|
1200
1516
|
return mcpState;
|
|
@@ -1241,14 +1557,14 @@ export async function routeAgentRequest<Env>(
|
|
|
1241
1557
|
"Access-Control-Allow-Credentials": "true",
|
|
1242
1558
|
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1243
1559
|
"Access-Control-Allow-Origin": "*",
|
|
1244
|
-
"Access-Control-Max-Age": "86400"
|
|
1560
|
+
"Access-Control-Max-Age": "86400"
|
|
1245
1561
|
}
|
|
1246
1562
|
: options?.cors;
|
|
1247
1563
|
|
|
1248
1564
|
if (request.method === "OPTIONS") {
|
|
1249
1565
|
if (corsHeaders) {
|
|
1250
1566
|
return new Response(null, {
|
|
1251
|
-
headers: corsHeaders
|
|
1567
|
+
headers: corsHeaders
|
|
1252
1568
|
});
|
|
1253
1569
|
}
|
|
1254
1570
|
console.warn(
|
|
@@ -1261,7 +1577,7 @@ export async function routeAgentRequest<Env>(
|
|
|
1261
1577
|
env as Record<string, unknown>,
|
|
1262
1578
|
{
|
|
1263
1579
|
prefix: "agents",
|
|
1264
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1580
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1265
1581
|
}
|
|
1266
1582
|
);
|
|
1267
1583
|
|
|
@@ -1274,24 +1590,238 @@ export async function routeAgentRequest<Env>(
|
|
|
1274
1590
|
response = new Response(response.body, {
|
|
1275
1591
|
headers: {
|
|
1276
1592
|
...response.headers,
|
|
1277
|
-
...corsHeaders
|
|
1278
|
-
}
|
|
1593
|
+
...corsHeaders
|
|
1594
|
+
}
|
|
1279
1595
|
});
|
|
1280
1596
|
}
|
|
1281
1597
|
return response;
|
|
1282
1598
|
}
|
|
1283
1599
|
|
|
1600
|
+
export type EmailResolver<Env> = (
|
|
1601
|
+
email: ForwardableEmailMessage,
|
|
1602
|
+
env: Env
|
|
1603
|
+
) => Promise<{
|
|
1604
|
+
agentName: string;
|
|
1605
|
+
agentId: string;
|
|
1606
|
+
} | null>;
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Create a resolver that uses the message-id header to determine the agent to route the email to
|
|
1610
|
+
* @returns A function that resolves the agent to route the email to
|
|
1611
|
+
*/
|
|
1612
|
+
export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
|
|
1613
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1614
|
+
const messageId = email.headers.get("message-id");
|
|
1615
|
+
if (messageId) {
|
|
1616
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1617
|
+
if (messageIdMatch) {
|
|
1618
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1619
|
+
const agentName = domain.split(".")[0];
|
|
1620
|
+
return { agentName, agentId };
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const references = email.headers.get("references");
|
|
1625
|
+
if (references) {
|
|
1626
|
+
const referencesMatch = references.match(
|
|
1627
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1628
|
+
);
|
|
1629
|
+
if (referencesMatch) {
|
|
1630
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1631
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1632
|
+
const agentName = domain.split(".")[0];
|
|
1633
|
+
return { agentName, agentId };
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1638
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1639
|
+
if (agentName && agentId) {
|
|
1640
|
+
return { agentName, agentId };
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
return null;
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
1649
|
+
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
1650
|
+
* @returns A function that resolves the agent to route the email to
|
|
1651
|
+
*/
|
|
1652
|
+
export function createAddressBasedEmailResolver<Env>(
|
|
1653
|
+
defaultAgentName: string
|
|
1654
|
+
): EmailResolver<Env> {
|
|
1655
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1656
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1657
|
+
if (!emailMatch) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1662
|
+
|
|
1663
|
+
if (subAddress) {
|
|
1664
|
+
return {
|
|
1665
|
+
agentName: localPart,
|
|
1666
|
+
agentId: subAddress
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1671
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1672
|
+
return {
|
|
1673
|
+
agentName: defaultAgentName,
|
|
1674
|
+
agentId: localPart
|
|
1675
|
+
};
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
1681
|
+
* @param agentName The name of the agent to route the email to
|
|
1682
|
+
* @param agentId The id of the agent to route the email to
|
|
1683
|
+
* @returns A function that resolves the agent to route the email to
|
|
1684
|
+
*/
|
|
1685
|
+
export function createCatchAllEmailResolver<Env>(
|
|
1686
|
+
agentName: string,
|
|
1687
|
+
agentId: string
|
|
1688
|
+
): EmailResolver<Env> {
|
|
1689
|
+
return async () => ({ agentName, agentId });
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1693
|
+
resolver: EmailResolver<Env>;
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
// Cache the agent namespace map for email routing
|
|
1697
|
+
// This maps both kebab-case and original names to namespaces
|
|
1698
|
+
const agentMapCache = new WeakMap<
|
|
1699
|
+
Record<string, unknown>,
|
|
1700
|
+
Record<string, unknown>
|
|
1701
|
+
>();
|
|
1702
|
+
|
|
1284
1703
|
/**
|
|
1285
1704
|
* Route an email to the appropriate Agent
|
|
1286
|
-
* @param email
|
|
1287
|
-
* @param env
|
|
1288
|
-
* @param options
|
|
1705
|
+
* @param email The email to route
|
|
1706
|
+
* @param env The environment containing the Agent bindings
|
|
1707
|
+
* @param options The options for routing the email
|
|
1708
|
+
* @returns A promise that resolves when the email has been routed
|
|
1289
1709
|
*/
|
|
1290
1710
|
export async function routeAgentEmail<Env>(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
): Promise<void> {
|
|
1711
|
+
email: ForwardableEmailMessage,
|
|
1712
|
+
env: Env,
|
|
1713
|
+
options: EmailRoutingOptions<Env>
|
|
1714
|
+
): Promise<void> {
|
|
1715
|
+
const routingInfo = await options.resolver(email, env);
|
|
1716
|
+
|
|
1717
|
+
if (!routingInfo) {
|
|
1718
|
+
console.warn("No routing information found for email, dropping message");
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Build a map that includes both original names and kebab-case versions
|
|
1723
|
+
if (!agentMapCache.has(env as Record<string, unknown>)) {
|
|
1724
|
+
const map: Record<string, unknown> = {};
|
|
1725
|
+
for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
|
|
1726
|
+
if (
|
|
1727
|
+
value &&
|
|
1728
|
+
typeof value === "object" &&
|
|
1729
|
+
"idFromName" in value &&
|
|
1730
|
+
typeof value.idFromName === "function"
|
|
1731
|
+
) {
|
|
1732
|
+
// Add both the original name and kebab-case version
|
|
1733
|
+
map[key] = value;
|
|
1734
|
+
map[camelCaseToKebabCase(key)] = value;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
agentMapCache.set(env as Record<string, unknown>, map);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
|
|
1741
|
+
const namespace = agentMap[routingInfo.agentName];
|
|
1742
|
+
|
|
1743
|
+
if (!namespace) {
|
|
1744
|
+
// Provide helpful error message listing available agents
|
|
1745
|
+
const availableAgents = Object.keys(agentMap)
|
|
1746
|
+
.filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
|
|
1747
|
+
.join(", ");
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
const agent = await getAgentByName(
|
|
1754
|
+
namespace as unknown as AgentNamespace<Agent<Env>>,
|
|
1755
|
+
routingInfo.agentId
|
|
1756
|
+
);
|
|
1757
|
+
|
|
1758
|
+
// let's make a serialisable version of the email
|
|
1759
|
+
const serialisableEmail: AgentEmail = {
|
|
1760
|
+
getRaw: async () => {
|
|
1761
|
+
const reader = email.raw.getReader();
|
|
1762
|
+
const chunks: Uint8Array[] = [];
|
|
1763
|
+
|
|
1764
|
+
let done = false;
|
|
1765
|
+
while (!done) {
|
|
1766
|
+
const { value, done: readerDone } = await reader.read();
|
|
1767
|
+
done = readerDone;
|
|
1768
|
+
if (value) {
|
|
1769
|
+
chunks.push(value);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1774
|
+
const combined = new Uint8Array(totalLength);
|
|
1775
|
+
let offset = 0;
|
|
1776
|
+
for (const chunk of chunks) {
|
|
1777
|
+
combined.set(chunk, offset);
|
|
1778
|
+
offset += chunk.length;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return combined;
|
|
1782
|
+
},
|
|
1783
|
+
headers: email.headers,
|
|
1784
|
+
rawSize: email.rawSize,
|
|
1785
|
+
setReject: (reason: string) => {
|
|
1786
|
+
email.setReject(reason);
|
|
1787
|
+
},
|
|
1788
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1789
|
+
return email.forward(rcptTo, headers);
|
|
1790
|
+
},
|
|
1791
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1792
|
+
return email.reply(
|
|
1793
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1794
|
+
);
|
|
1795
|
+
},
|
|
1796
|
+
from: email.from,
|
|
1797
|
+
to: email.to
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1800
|
+
await agent._onEmail(serialisableEmail);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
export type AgentEmail = {
|
|
1804
|
+
from: string;
|
|
1805
|
+
to: string;
|
|
1806
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1807
|
+
headers: Headers;
|
|
1808
|
+
rawSize: number;
|
|
1809
|
+
setReject: (reason: string) => void;
|
|
1810
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1811
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
export type EmailSendOptions = {
|
|
1815
|
+
to: string;
|
|
1816
|
+
subject: string;
|
|
1817
|
+
body: string;
|
|
1818
|
+
contentType?: string;
|
|
1819
|
+
headers?: Record<string, string>;
|
|
1820
|
+
includeRoutingHeaders?: boolean;
|
|
1821
|
+
agentName?: string;
|
|
1822
|
+
agentId?: string;
|
|
1823
|
+
domain?: string;
|
|
1824
|
+
};
|
|
1295
1825
|
|
|
1296
1826
|
/**
|
|
1297
1827
|
* Get or create an Agent by name
|
|
@@ -1339,7 +1869,7 @@ export class StreamingResponse {
|
|
|
1339
1869
|
id: this._id,
|
|
1340
1870
|
result: chunk,
|
|
1341
1871
|
success: true,
|
|
1342
|
-
type: "rpc"
|
|
1872
|
+
type: "rpc"
|
|
1343
1873
|
};
|
|
1344
1874
|
this._connection.send(JSON.stringify(response));
|
|
1345
1875
|
}
|
|
@@ -1358,7 +1888,7 @@ export class StreamingResponse {
|
|
|
1358
1888
|
id: this._id,
|
|
1359
1889
|
result: finalChunk,
|
|
1360
1890
|
success: true,
|
|
1361
|
-
type: "rpc"
|
|
1891
|
+
type: "rpc"
|
|
1362
1892
|
};
|
|
1363
1893
|
this._connection.send(JSON.stringify(response));
|
|
1364
1894
|
}
|