agents 0.0.0-eeb70e2 → 0.0.0-f31397c
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 +4 -3
- package/dist/ai-chat-agent.js +64 -26
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-react.d.ts +9 -9
- package/dist/ai-react.js +27 -27
- package/dist/ai-react.js.map +1 -1
- package/dist/{chunk-VCSB47AK.js → chunk-KUH345EY.js} +8 -8
- package/dist/chunk-KUH345EY.js.map +1 -0
- package/dist/{chunk-P3RZJ72N.js → chunk-MFNGQLFL.js} +602 -125
- package/dist/chunk-MFNGQLFL.js.map +1 -0
- package/dist/{chunk-OYJXQRRH.js → chunk-MW5BQ2FW.js} +22 -18
- package/dist/chunk-MW5BQ2FW.js.map +1 -0
- package/dist/{chunk-BZXOAZUX.js → chunk-PVQZBKN7.js} +5 -5
- package/dist/chunk-PVQZBKN7.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/index-BIJvkfYt.d.ts +614 -0
- package/dist/index.d.ts +33 -405
- package/dist/index.js +10 -4
- package/dist/mcp/client.d.ts +281 -9
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/do-oauth-client-provider.js +1 -1
- package/dist/mcp/index.d.ts +9 -9
- package/dist/mcp/index.js +50 -49
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +12 -0
- package/dist/observability/index.js +10 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/react.d.ts +8 -8
- package/dist/react.js +7 -7
- package/dist/react.js.map +1 -1
- package/dist/schedule.d.ts +6 -6
- package/dist/schedule.js +4 -4
- package/dist/schedule.js.map +1 -1
- package/package.json +76 -71
- package/src/index.ts +777 -155
- package/dist/chunk-BZXOAZUX.js.map +0 -1
- package/dist/chunk-OYJXQRRH.js.map +0 -1
- package/dist/chunk-P3RZJ72N.js.map +0 -1
- package/dist/chunk-VCSB47AK.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
routePartykitRequest,
|
|
5
|
-
type Connection,
|
|
6
|
-
type ConnectionContext,
|
|
7
|
-
type PartyServerOptions,
|
|
8
|
-
type WSMessage,
|
|
9
|
-
} from "partyserver";
|
|
10
|
-
|
|
11
|
-
import { parseCronExpression } from "cron-schedule";
|
|
12
|
-
import { nanoid } from "nanoid";
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
13
4
|
|
|
14
5
|
import type {
|
|
15
6
|
Prompt,
|
|
16
7
|
Resource,
|
|
17
8
|
ServerCapabilities,
|
|
18
|
-
Tool
|
|
9
|
+
Tool
|
|
19
10
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
-
import {
|
|
11
|
+
import { parseCronExpression } from "cron-schedule";
|
|
12
|
+
import { nanoid } from "nanoid";
|
|
13
|
+
import { EmailMessage } from "cloudflare:email";
|
|
14
|
+
import {
|
|
15
|
+
type Connection,
|
|
16
|
+
type ConnectionContext,
|
|
17
|
+
type PartyServerOptions,
|
|
18
|
+
Server,
|
|
19
|
+
type WSMessage,
|
|
20
|
+
getServerByName,
|
|
21
|
+
routePartykitRequest
|
|
22
|
+
} from "partyserver";
|
|
23
|
+
import { camelCaseToKebabCase } from "./client";
|
|
21
24
|
import { MCPClientManager } from "./mcp/client";
|
|
25
|
+
// import type { MCPClientConnection } from "./mcp/client-connection";
|
|
22
26
|
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
|
|
23
|
-
|
|
24
|
-
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
25
|
-
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
26
|
-
|
|
27
|
-
import { camelCaseToKebabCase } from "./client";
|
|
27
|
+
import { genericObservability, type Observability } from "./observability";
|
|
28
28
|
|
|
29
29
|
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
|
|
30
30
|
|
|
@@ -119,6 +119,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
|
|
|
119
119
|
export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
120
120
|
return function callableDecorator<This, Args extends unknown[], Return>(
|
|
121
121
|
target: (this: This, ...args: Args) => Return,
|
|
122
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: later
|
|
122
123
|
context: ClassMethodDecoratorContext
|
|
123
124
|
) {
|
|
124
125
|
if (!callableMetadata.has(target)) {
|
|
@@ -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,9 +345,14 @@ 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
|
|
|
351
|
+
/**
|
|
352
|
+
* The observability implementation to use for the Agent
|
|
353
|
+
*/
|
|
354
|
+
observability?: Observability = genericObservability;
|
|
355
|
+
|
|
320
356
|
/**
|
|
321
357
|
* Execute SQL queries against the Agent's database
|
|
322
358
|
* @template T Type of the returned rows
|
|
@@ -346,6 +382,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
346
382
|
constructor(ctx: AgentContext, env: Env) {
|
|
347
383
|
super(ctx, env);
|
|
348
384
|
|
|
385
|
+
// Auto-wrap custom methods with agent context
|
|
386
|
+
this._autoWrapCustomMethods();
|
|
387
|
+
|
|
349
388
|
this.sql`
|
|
350
389
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
351
390
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -353,6 +392,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
353
392
|
)
|
|
354
393
|
`;
|
|
355
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
|
+
|
|
356
404
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
357
405
|
return this._tryCatch(async () => {
|
|
358
406
|
// Create alarms table if it doesn't exist
|
|
@@ -389,7 +437,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
389
437
|
const _onRequest = this.onRequest.bind(this);
|
|
390
438
|
this.onRequest = (request: Request) => {
|
|
391
439
|
return agentContext.run(
|
|
392
|
-
{ agent: this, connection: undefined, request },
|
|
440
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
393
441
|
async () => {
|
|
394
442
|
if (this.mcp.isCallbackRequest(request)) {
|
|
395
443
|
await this.mcp.handleCallbackRequest(request);
|
|
@@ -397,15 +445,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
397
445
|
// after the MCP connection handshake, we can send updated mcp state
|
|
398
446
|
this.broadcast(
|
|
399
447
|
JSON.stringify({
|
|
400
|
-
type: "cf_agent_mcp_servers",
|
|
401
448
|
mcp: this.getMcpServers(),
|
|
449
|
+
type: "cf_agent_mcp_servers"
|
|
402
450
|
})
|
|
403
451
|
);
|
|
404
452
|
|
|
405
453
|
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
406
454
|
return new Response("<script>window.close();</script>", {
|
|
407
|
-
status: 200,
|
|
408
455
|
headers: { "content-type": "text/html" },
|
|
456
|
+
status: 200
|
|
409
457
|
});
|
|
410
458
|
}
|
|
411
459
|
|
|
@@ -417,7 +465,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
417
465
|
const _onMessage = this.onMessage.bind(this);
|
|
418
466
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
419
467
|
return agentContext.run(
|
|
420
|
-
{ agent: this, connection, request: undefined },
|
|
468
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
421
469
|
async () => {
|
|
422
470
|
if (typeof message !== "string") {
|
|
423
471
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
@@ -426,7 +474,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
426
474
|
let parsed: unknown;
|
|
427
475
|
try {
|
|
428
476
|
parsed = JSON.parse(message);
|
|
429
|
-
} catch (
|
|
477
|
+
} catch (_e) {
|
|
430
478
|
// silently fail and let the onMessage handler handle it
|
|
431
479
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
432
480
|
}
|
|
@@ -461,22 +509,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
461
509
|
|
|
462
510
|
// For regular methods, execute and send response
|
|
463
511
|
const result = await methodFn.apply(this, args);
|
|
512
|
+
|
|
513
|
+
this.observability?.emit(
|
|
514
|
+
{
|
|
515
|
+
displayMessage: `RPC call to ${method}`,
|
|
516
|
+
id: nanoid(),
|
|
517
|
+
payload: {
|
|
518
|
+
args,
|
|
519
|
+
method,
|
|
520
|
+
streaming: metadata?.streaming,
|
|
521
|
+
success: true
|
|
522
|
+
},
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
type: "rpc"
|
|
525
|
+
},
|
|
526
|
+
this.ctx
|
|
527
|
+
);
|
|
528
|
+
|
|
464
529
|
const response: RPCResponse = {
|
|
465
|
-
|
|
530
|
+
done: true,
|
|
466
531
|
id,
|
|
467
|
-
success: true,
|
|
468
532
|
result,
|
|
469
|
-
|
|
533
|
+
success: true,
|
|
534
|
+
type: "rpc"
|
|
470
535
|
};
|
|
471
536
|
connection.send(JSON.stringify(response));
|
|
472
537
|
} catch (e) {
|
|
473
538
|
// Send error response
|
|
474
539
|
const response: RPCResponse = {
|
|
475
|
-
type: "rpc",
|
|
476
|
-
id: parsed.id,
|
|
477
|
-
success: false,
|
|
478
540
|
error:
|
|
479
541
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
542
|
+
id: parsed.id,
|
|
543
|
+
success: false,
|
|
544
|
+
type: "rpc"
|
|
480
545
|
};
|
|
481
546
|
connection.send(JSON.stringify(response));
|
|
482
547
|
console.error("RPC error:", e);
|
|
@@ -494,25 +559,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
494
559
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
495
560
|
// must fix this
|
|
496
561
|
return agentContext.run(
|
|
497
|
-
{ agent: this, connection, request: ctx.request },
|
|
562
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
498
563
|
async () => {
|
|
499
564
|
setTimeout(() => {
|
|
500
565
|
if (this.state) {
|
|
501
566
|
connection.send(
|
|
502
567
|
JSON.stringify({
|
|
503
|
-
type: "cf_agent_state",
|
|
504
568
|
state: this.state,
|
|
569
|
+
type: "cf_agent_state"
|
|
505
570
|
})
|
|
506
571
|
);
|
|
507
572
|
}
|
|
508
573
|
|
|
509
574
|
connection.send(
|
|
510
575
|
JSON.stringify({
|
|
511
|
-
type: "cf_agent_mcp_servers",
|
|
512
576
|
mcp: this.getMcpServers(),
|
|
577
|
+
type: "cf_agent_mcp_servers"
|
|
513
578
|
})
|
|
514
579
|
);
|
|
515
580
|
|
|
581
|
+
this.observability?.emit(
|
|
582
|
+
{
|
|
583
|
+
displayMessage: "Connection established",
|
|
584
|
+
id: nanoid(),
|
|
585
|
+
payload: {
|
|
586
|
+
connectionId: connection.id
|
|
587
|
+
},
|
|
588
|
+
timestamp: Date.now(),
|
|
589
|
+
type: "connect"
|
|
590
|
+
},
|
|
591
|
+
this.ctx
|
|
592
|
+
);
|
|
516
593
|
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
517
594
|
}, 20);
|
|
518
595
|
}
|
|
@@ -522,17 +599,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
522
599
|
const _onStart = this.onStart.bind(this);
|
|
523
600
|
this.onStart = async () => {
|
|
524
601
|
return agentContext.run(
|
|
525
|
-
{
|
|
602
|
+
{
|
|
603
|
+
agent: this,
|
|
604
|
+
connection: undefined,
|
|
605
|
+
request: undefined,
|
|
606
|
+
email: undefined
|
|
607
|
+
},
|
|
526
608
|
async () => {
|
|
527
609
|
const servers = this.sql<MCPServerRow>`
|
|
528
610
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
529
611
|
`;
|
|
530
612
|
|
|
531
613
|
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
.
|
|
535
|
-
.map((server) => {
|
|
614
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
615
|
+
Promise.allSettled(
|
|
616
|
+
servers.map((server) => {
|
|
536
617
|
return this._connectToMcpServerInternal(
|
|
537
618
|
server.name,
|
|
538
619
|
server.server_url,
|
|
@@ -542,19 +623,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
542
623
|
: undefined,
|
|
543
624
|
{
|
|
544
625
|
id: server.id,
|
|
545
|
-
oauthClientId: server.client_id ?? undefined
|
|
626
|
+
oauthClientId: server.client_id ?? undefined
|
|
546
627
|
}
|
|
547
628
|
);
|
|
548
629
|
})
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
630
|
+
).then((_results) => {
|
|
631
|
+
this.broadcast(
|
|
632
|
+
JSON.stringify({
|
|
633
|
+
mcp: this.getMcpServers(),
|
|
634
|
+
type: "cf_agent_mcp_servers"
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
558
639
|
await this._tryCatch(() => _onStart());
|
|
559
640
|
}
|
|
560
641
|
);
|
|
@@ -565,6 +646,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
565
646
|
state: State,
|
|
566
647
|
source: Connection | "server" = "server"
|
|
567
648
|
) {
|
|
649
|
+
const previousState = this._state;
|
|
568
650
|
this._state = state;
|
|
569
651
|
this.sql`
|
|
570
652
|
INSERT OR REPLACE INTO cf_agents_state (id, state)
|
|
@@ -576,16 +658,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
576
658
|
`;
|
|
577
659
|
this.broadcast(
|
|
578
660
|
JSON.stringify({
|
|
579
|
-
type: "cf_agent_state",
|
|
580
661
|
state: state,
|
|
662
|
+
type: "cf_agent_state"
|
|
581
663
|
}),
|
|
582
664
|
source !== "server" ? [source.id] : []
|
|
583
665
|
);
|
|
584
666
|
return this._tryCatch(() => {
|
|
585
|
-
const { connection, request } = agentContext.getStore() || {};
|
|
667
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
586
668
|
return agentContext.run(
|
|
587
|
-
{ agent: this, connection, request },
|
|
669
|
+
{ agent: this, connection, request, email },
|
|
588
670
|
async () => {
|
|
671
|
+
this.observability?.emit(
|
|
672
|
+
{
|
|
673
|
+
displayMessage: "State updated",
|
|
674
|
+
id: nanoid(),
|
|
675
|
+
payload: {
|
|
676
|
+
previousState,
|
|
677
|
+
state
|
|
678
|
+
},
|
|
679
|
+
timestamp: Date.now(),
|
|
680
|
+
type: "state:update"
|
|
681
|
+
},
|
|
682
|
+
this.ctx
|
|
683
|
+
);
|
|
589
684
|
return this.onStateUpdate(state, source);
|
|
590
685
|
}
|
|
591
686
|
);
|
|
@@ -605,23 +700,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
605
700
|
* @param state Updated state
|
|
606
701
|
* @param source Source of the state update ("server" or a client connection)
|
|
607
702
|
*/
|
|
703
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
|
|
608
704
|
onStateUpdate(state: State | undefined, source: Connection | "server") {
|
|
609
705
|
// override this to handle state updates
|
|
610
706
|
}
|
|
611
707
|
|
|
612
708
|
/**
|
|
613
|
-
* 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
|
|
614
711
|
* @param email Email message to process
|
|
615
712
|
*/
|
|
616
|
-
|
|
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
|
|
617
716
|
return agentContext.run(
|
|
618
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
717
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
619
718
|
async () => {
|
|
620
|
-
|
|
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
|
+
}
|
|
621
730
|
}
|
|
622
731
|
);
|
|
623
732
|
}
|
|
624
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
|
+
|
|
625
786
|
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
626
787
|
try {
|
|
627
788
|
return await fn();
|
|
@@ -630,6 +791,72 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
630
791
|
}
|
|
631
792
|
}
|
|
632
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
|
+
|
|
633
860
|
override onError(
|
|
634
861
|
connection: Connection,
|
|
635
862
|
error: unknown
|
|
@@ -664,6 +891,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
664
891
|
throw new Error("Not implemented");
|
|
665
892
|
}
|
|
666
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
|
+
|
|
667
1019
|
/**
|
|
668
1020
|
* Schedule a task to be executed in the future
|
|
669
1021
|
* @template T Type of the payload data
|
|
@@ -679,6 +1031,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
679
1031
|
): Promise<Schedule<T>> {
|
|
680
1032
|
const id = nanoid(9);
|
|
681
1033
|
|
|
1034
|
+
const emitScheduleCreate = (schedule: Schedule<T>) =>
|
|
1035
|
+
this.observability?.emit(
|
|
1036
|
+
{
|
|
1037
|
+
displayMessage: `Schedule ${schedule.id} created`,
|
|
1038
|
+
id: nanoid(),
|
|
1039
|
+
payload: schedule,
|
|
1040
|
+
timestamp: Date.now(),
|
|
1041
|
+
type: "schedule:create"
|
|
1042
|
+
},
|
|
1043
|
+
this.ctx
|
|
1044
|
+
);
|
|
1045
|
+
|
|
682
1046
|
if (typeof callback !== "string") {
|
|
683
1047
|
throw new Error("Callback must be a string");
|
|
684
1048
|
}
|
|
@@ -698,13 +1062,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
698
1062
|
|
|
699
1063
|
await this._scheduleNextAlarm();
|
|
700
1064
|
|
|
701
|
-
|
|
702
|
-
id,
|
|
1065
|
+
const schedule: Schedule<T> = {
|
|
703
1066
|
callback: callback,
|
|
1067
|
+
id,
|
|
704
1068
|
payload: payload as T,
|
|
705
1069
|
time: timestamp,
|
|
706
|
-
type: "scheduled"
|
|
1070
|
+
type: "scheduled"
|
|
707
1071
|
};
|
|
1072
|
+
|
|
1073
|
+
emitScheduleCreate(schedule);
|
|
1074
|
+
|
|
1075
|
+
return schedule;
|
|
708
1076
|
}
|
|
709
1077
|
if (typeof when === "number") {
|
|
710
1078
|
const time = new Date(Date.now() + when * 1000);
|
|
@@ -719,14 +1087,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
719
1087
|
|
|
720
1088
|
await this._scheduleNextAlarm();
|
|
721
1089
|
|
|
722
|
-
|
|
723
|
-
id,
|
|
1090
|
+
const schedule: Schedule<T> = {
|
|
724
1091
|
callback: callback,
|
|
725
|
-
payload: payload as T,
|
|
726
1092
|
delayInSeconds: when,
|
|
1093
|
+
id,
|
|
1094
|
+
payload: payload as T,
|
|
727
1095
|
time: timestamp,
|
|
728
|
-
type: "delayed"
|
|
1096
|
+
type: "delayed"
|
|
729
1097
|
};
|
|
1098
|
+
|
|
1099
|
+
emitScheduleCreate(schedule);
|
|
1100
|
+
|
|
1101
|
+
return schedule;
|
|
730
1102
|
}
|
|
731
1103
|
if (typeof when === "string") {
|
|
732
1104
|
const nextExecutionTime = getNextCronTime(when);
|
|
@@ -741,14 +1113,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
741
1113
|
|
|
742
1114
|
await this._scheduleNextAlarm();
|
|
743
1115
|
|
|
744
|
-
|
|
745
|
-
id,
|
|
1116
|
+
const schedule: Schedule<T> = {
|
|
746
1117
|
callback: callback,
|
|
747
|
-
payload: payload as T,
|
|
748
1118
|
cron: when,
|
|
1119
|
+
id,
|
|
1120
|
+
payload: payload as T,
|
|
749
1121
|
time: timestamp,
|
|
750
|
-
type: "cron"
|
|
1122
|
+
type: "cron"
|
|
751
1123
|
};
|
|
1124
|
+
|
|
1125
|
+
emitScheduleCreate(schedule);
|
|
1126
|
+
|
|
1127
|
+
return schedule;
|
|
752
1128
|
}
|
|
753
1129
|
throw new Error("Invalid schedule type");
|
|
754
1130
|
}
|
|
@@ -812,7 +1188,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
812
1188
|
.toArray()
|
|
813
1189
|
.map((row) => ({
|
|
814
1190
|
...row,
|
|
815
|
-
payload: JSON.parse(row.payload as string) as T
|
|
1191
|
+
payload: JSON.parse(row.payload as string) as T
|
|
816
1192
|
})) as Schedule<T>[];
|
|
817
1193
|
|
|
818
1194
|
return result;
|
|
@@ -824,6 +1200,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
824
1200
|
* @returns true if the task was cancelled, false otherwise
|
|
825
1201
|
*/
|
|
826
1202
|
async cancelSchedule(id: string): Promise<boolean> {
|
|
1203
|
+
const schedule = await this.getSchedule(id);
|
|
1204
|
+
if (schedule) {
|
|
1205
|
+
this.observability?.emit(
|
|
1206
|
+
{
|
|
1207
|
+
displayMessage: `Schedule ${id} cancelled`,
|
|
1208
|
+
id: nanoid(),
|
|
1209
|
+
payload: schedule,
|
|
1210
|
+
timestamp: Date.now(),
|
|
1211
|
+
type: "schedule:cancel"
|
|
1212
|
+
},
|
|
1213
|
+
this.ctx
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
827
1216
|
this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
|
|
828
1217
|
|
|
829
1218
|
await this._scheduleNextAlarm();
|
|
@@ -862,40 +1251,58 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
862
1251
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
863
1252
|
`;
|
|
864
1253
|
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
await agentContext.run(
|
|
872
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
873
|
-
async () => {
|
|
874
|
-
try {
|
|
875
|
-
await (
|
|
876
|
-
callback as (
|
|
877
|
-
payload: unknown,
|
|
878
|
-
schedule: Schedule<unknown>
|
|
879
|
-
) => Promise<void>
|
|
880
|
-
).bind(this)(JSON.parse(row.payload as string), row);
|
|
881
|
-
} catch (e) {
|
|
882
|
-
console.error(`error executing callback "${row.callback}"`, e);
|
|
883
|
-
}
|
|
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;
|
|
884
1260
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
);
|
|
890
1280
|
|
|
891
|
-
|
|
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
|
+
}
|
|
1290
|
+
}
|
|
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);
|
|
1296
|
+
|
|
1297
|
+
this.sql`
|
|
892
1298
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
893
1299
|
`;
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1300
|
+
} else {
|
|
1301
|
+
// Delete one-time schedules after execution
|
|
1302
|
+
this.sql`
|
|
897
1303
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
898
1304
|
`;
|
|
1305
|
+
}
|
|
899
1306
|
}
|
|
900
1307
|
}
|
|
901
1308
|
|
|
@@ -911,10 +1318,23 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
911
1318
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
912
1319
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
913
1320
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1321
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
914
1322
|
|
|
915
1323
|
// delete all alarms
|
|
916
1324
|
await this.ctx.storage.deleteAlarm();
|
|
917
1325
|
await this.ctx.storage.deleteAll();
|
|
1326
|
+
this.ctx.abort("destroyed"); // enforce that the agent is evicted
|
|
1327
|
+
|
|
1328
|
+
this.observability?.emit(
|
|
1329
|
+
{
|
|
1330
|
+
displayMessage: "Agent destroyed",
|
|
1331
|
+
id: nanoid(),
|
|
1332
|
+
payload: {},
|
|
1333
|
+
timestamp: Date.now(),
|
|
1334
|
+
type: "destroy"
|
|
1335
|
+
},
|
|
1336
|
+
this.ctx
|
|
1337
|
+
);
|
|
918
1338
|
}
|
|
919
1339
|
|
|
920
1340
|
/**
|
|
@@ -954,11 +1374,24 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
954
1374
|
callbackUrl,
|
|
955
1375
|
options
|
|
956
1376
|
);
|
|
1377
|
+
this.sql`
|
|
1378
|
+
INSERT
|
|
1379
|
+
OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1380
|
+
VALUES (
|
|
1381
|
+
${result.id},
|
|
1382
|
+
${serverName},
|
|
1383
|
+
${url},
|
|
1384
|
+
${result.clientId ?? null},
|
|
1385
|
+
${result.authUrl ?? null},
|
|
1386
|
+
${callbackUrl},
|
|
1387
|
+
${options ? JSON.stringify(options) : null}
|
|
1388
|
+
);
|
|
1389
|
+
`;
|
|
957
1390
|
|
|
958
1391
|
this.broadcast(
|
|
959
1392
|
JSON.stringify({
|
|
960
|
-
type: "cf_agent_mcp_servers",
|
|
961
1393
|
mcp: this.getMcpServers(),
|
|
1394
|
+
type: "cf_agent_mcp_servers"
|
|
962
1395
|
})
|
|
963
1396
|
);
|
|
964
1397
|
|
|
@@ -966,7 +1399,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
966
1399
|
}
|
|
967
1400
|
|
|
968
1401
|
async _connectToMcpServerInternal(
|
|
969
|
-
|
|
1402
|
+
_serverName: string,
|
|
970
1403
|
url: string,
|
|
971
1404
|
callbackUrl: string,
|
|
972
1405
|
// it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
|
|
@@ -987,7 +1420,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
987
1420
|
id: string;
|
|
988
1421
|
oauthClientId?: string;
|
|
989
1422
|
}
|
|
990
|
-
): Promise<{
|
|
1423
|
+
): Promise<{
|
|
1424
|
+
id: string;
|
|
1425
|
+
authUrl: string | undefined;
|
|
1426
|
+
clientId: string | undefined;
|
|
1427
|
+
}> {
|
|
991
1428
|
const authProvider = new DurableObjectOAuthClientProvider(
|
|
992
1429
|
this.ctx.storage,
|
|
993
1430
|
this.name,
|
|
@@ -1010,40 +1447,28 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1010
1447
|
fetch: (url, init) =>
|
|
1011
1448
|
fetch(url, {
|
|
1012
1449
|
...init,
|
|
1013
|
-
headers: options?.transport?.headers
|
|
1014
|
-
})
|
|
1450
|
+
headers: options?.transport?.headers
|
|
1451
|
+
})
|
|
1015
1452
|
},
|
|
1016
1453
|
requestInit: {
|
|
1017
|
-
headers: options?.transport?.headers
|
|
1018
|
-
}
|
|
1454
|
+
headers: options?.transport?.headers
|
|
1455
|
+
}
|
|
1019
1456
|
};
|
|
1020
1457
|
}
|
|
1021
1458
|
|
|
1022
1459
|
const { id, authUrl, clientId } = await this.mcp.connect(url, {
|
|
1460
|
+
client: options?.client,
|
|
1023
1461
|
reconnect,
|
|
1024
1462
|
transport: {
|
|
1025
1463
|
...headerTransportOpts,
|
|
1026
|
-
authProvider
|
|
1027
|
-
}
|
|
1028
|
-
client: options?.client,
|
|
1464
|
+
authProvider
|
|
1465
|
+
}
|
|
1029
1466
|
});
|
|
1030
1467
|
|
|
1031
|
-
this.sql`
|
|
1032
|
-
INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1033
|
-
VALUES (
|
|
1034
|
-
${id},
|
|
1035
|
-
${serverName},
|
|
1036
|
-
${url},
|
|
1037
|
-
${clientId ?? null},
|
|
1038
|
-
${authUrl ?? null},
|
|
1039
|
-
${callbackUrl},
|
|
1040
|
-
${options ? JSON.stringify(options) : null}
|
|
1041
|
-
);
|
|
1042
|
-
`;
|
|
1043
|
-
|
|
1044
1468
|
return {
|
|
1045
|
-
id,
|
|
1046
1469
|
authUrl,
|
|
1470
|
+
clientId,
|
|
1471
|
+
id
|
|
1047
1472
|
};
|
|
1048
1473
|
}
|
|
1049
1474
|
|
|
@@ -1054,34 +1479,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1054
1479
|
`;
|
|
1055
1480
|
this.broadcast(
|
|
1056
1481
|
JSON.stringify({
|
|
1057
|
-
type: "cf_agent_mcp_servers",
|
|
1058
1482
|
mcp: this.getMcpServers(),
|
|
1483
|
+
type: "cf_agent_mcp_servers"
|
|
1059
1484
|
})
|
|
1060
1485
|
);
|
|
1061
1486
|
}
|
|
1062
1487
|
|
|
1063
1488
|
getMcpServers(): MCPServersState {
|
|
1064
1489
|
const mcpState: MCPServersState = {
|
|
1065
|
-
servers: {},
|
|
1066
|
-
tools: this.mcp.listTools(),
|
|
1067
1490
|
prompts: this.mcp.listPrompts(),
|
|
1068
1491
|
resources: this.mcp.listResources(),
|
|
1492
|
+
servers: {},
|
|
1493
|
+
tools: this.mcp.listTools()
|
|
1069
1494
|
};
|
|
1070
1495
|
|
|
1071
1496
|
const servers = this.sql<MCPServerRow>`
|
|
1072
1497
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1073
1498
|
`;
|
|
1074
1499
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
+
}
|
|
1085
1513
|
}
|
|
1086
1514
|
|
|
1087
1515
|
return mcpState;
|
|
@@ -1125,17 +1553,17 @@ export async function routeAgentRequest<Env>(
|
|
|
1125
1553
|
const corsHeaders =
|
|
1126
1554
|
options?.cors === true
|
|
1127
1555
|
? {
|
|
1128
|
-
"Access-Control-Allow-Origin": "*",
|
|
1129
|
-
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1130
1556
|
"Access-Control-Allow-Credentials": "true",
|
|
1131
|
-
"Access-Control-
|
|
1557
|
+
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1558
|
+
"Access-Control-Allow-Origin": "*",
|
|
1559
|
+
"Access-Control-Max-Age": "86400"
|
|
1132
1560
|
}
|
|
1133
1561
|
: options?.cors;
|
|
1134
1562
|
|
|
1135
1563
|
if (request.method === "OPTIONS") {
|
|
1136
1564
|
if (corsHeaders) {
|
|
1137
1565
|
return new Response(null, {
|
|
1138
|
-
headers: corsHeaders
|
|
1566
|
+
headers: corsHeaders
|
|
1139
1567
|
});
|
|
1140
1568
|
}
|
|
1141
1569
|
console.warn(
|
|
@@ -1148,7 +1576,7 @@ export async function routeAgentRequest<Env>(
|
|
|
1148
1576
|
env as Record<string, unknown>,
|
|
1149
1577
|
{
|
|
1150
1578
|
prefix: "agents",
|
|
1151
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1579
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1152
1580
|
}
|
|
1153
1581
|
);
|
|
1154
1582
|
|
|
@@ -1161,24 +1589,218 @@ export async function routeAgentRequest<Env>(
|
|
|
1161
1589
|
response = new Response(response.body, {
|
|
1162
1590
|
headers: {
|
|
1163
1591
|
...response.headers,
|
|
1164
|
-
...corsHeaders
|
|
1165
|
-
}
|
|
1592
|
+
...corsHeaders
|
|
1593
|
+
}
|
|
1166
1594
|
});
|
|
1167
1595
|
}
|
|
1168
1596
|
return response;
|
|
1169
1597
|
}
|
|
1170
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
|
+
|
|
1171
1695
|
/**
|
|
1172
1696
|
* Route an email to the appropriate Agent
|
|
1173
|
-
* @param email
|
|
1174
|
-
* @param env
|
|
1175
|
-
* @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
|
|
1176
1701
|
*/
|
|
1177
1702
|
export async function routeAgentEmail<Env>(
|
|
1178
1703
|
email: ForwardableEmailMessage,
|
|
1179
1704
|
env: Env,
|
|
1180
|
-
options
|
|
1181
|
-
): Promise<void> {
|
|
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
|
+
};
|
|
1182
1804
|
|
|
1183
1805
|
/**
|
|
1184
1806
|
* Get or create an Agent by name
|
|
@@ -1222,11 +1844,11 @@ export class StreamingResponse {
|
|
|
1222
1844
|
throw new Error("StreamingResponse is already closed");
|
|
1223
1845
|
}
|
|
1224
1846
|
const response: RPCResponse = {
|
|
1225
|
-
|
|
1847
|
+
done: false,
|
|
1226
1848
|
id: this._id,
|
|
1227
|
-
success: true,
|
|
1228
1849
|
result: chunk,
|
|
1229
|
-
|
|
1850
|
+
success: true,
|
|
1851
|
+
type: "rpc"
|
|
1230
1852
|
};
|
|
1231
1853
|
this._connection.send(JSON.stringify(response));
|
|
1232
1854
|
}
|
|
@@ -1241,11 +1863,11 @@ export class StreamingResponse {
|
|
|
1241
1863
|
}
|
|
1242
1864
|
this._closed = true;
|
|
1243
1865
|
const response: RPCResponse = {
|
|
1244
|
-
|
|
1866
|
+
done: true,
|
|
1245
1867
|
id: this._id,
|
|
1246
|
-
success: true,
|
|
1247
1868
|
result: finalChunk,
|
|
1248
|
-
|
|
1869
|
+
success: true,
|
|
1870
|
+
type: "rpc"
|
|
1249
1871
|
};
|
|
1250
1872
|
this._connection.send(JSON.stringify(response));
|
|
1251
1873
|
}
|