agents 0.0.0-d40512c → 0.0.0-d72c6a2
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 +234 -6
- package/dist/ai-chat-agent.d.ts +12 -9
- package/dist/ai-chat-agent.js +150 -59
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-chat-v5-migration.d.ts +152 -0
- package/dist/ai-chat-v5-migration.js +19 -0
- package/dist/ai-chat-v5-migration.js.map +1 -0
- package/dist/ai-react.d.ts +62 -71
- package/dist/ai-react.js +144 -37
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +36 -19
- package/dist/ai-types.js +6 -0
- package/dist/chunk-AVYJQSLW.js +17 -0
- package/dist/chunk-AVYJQSLW.js.map +1 -0
- package/dist/{chunk-PVQZBKN7.js → chunk-LL2AFX7V.js} +5 -2
- package/dist/chunk-LL2AFX7V.js.map +1 -0
- package/dist/{chunk-4RBEYCWK.js → chunk-MH46VMM4.js} +163 -20
- package/dist/chunk-MH46VMM4.js.map +1 -0
- package/dist/{chunk-KUH345EY.js → chunk-QEVM4BVL.js} +5 -5
- package/dist/chunk-QEVM4BVL.js.map +1 -0
- package/dist/chunk-UJVEAURM.js +150 -0
- package/dist/chunk-UJVEAURM.js.map +1 -0
- package/dist/{chunk-LU2RSO54.js → chunk-YDUDMOL6.js} +515 -129
- package/dist/chunk-YDUDMOL6.js.map +1 -0
- package/dist/client-CvaJdLQA.d.ts +5015 -0
- package/dist/client.js +2 -1
- package/dist/index.d.ts +557 -24
- package/dist/index.js +13 -4
- package/dist/mcp/client.d.ts +9 -1053
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/do-oauth-client-provider.d.ts +1 -0
- package/dist/mcp/do-oauth-client-provider.js +1 -1
- package/dist/mcp/index.d.ts +68 -55
- package/dist/mcp/index.js +854 -608
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +46 -12
- package/dist/observability/index.js +5 -4
- package/dist/react.d.ts +7 -3
- package/dist/react.js +7 -5
- package/dist/react.js.map +1 -1
- package/dist/schedule.d.ts +83 -9
- package/dist/schedule.js +15 -2
- package/dist/schedule.js.map +1 -1
- package/package.json +19 -8
- package/src/index.ts +731 -149
- package/dist/chunk-4RBEYCWK.js.map +0 -1
- package/dist/chunk-KUH345EY.js.map +0 -1
- package/dist/chunk-LU2RSO54.js.map +0 -1
- package/dist/chunk-PVQZBKN7.js.map +0 -1
- package/dist/index-CITGJflw.d.ts +0 -486
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";
|
|
@@ -10,20 +11,22 @@ import type {
|
|
|
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
|
-
type WSMessage
|
|
20
|
+
type WSMessage,
|
|
21
|
+
getServerByName,
|
|
22
|
+
routePartykitRequest
|
|
21
23
|
} from "partyserver";
|
|
22
24
|
import { camelCaseToKebabCase } from "./client";
|
|
23
25
|
import { MCPClientManager } from "./mcp/client";
|
|
24
26
|
// import type { MCPClientConnection } from "./mcp/client-connection";
|
|
25
27
|
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
|
|
26
28
|
import { genericObservability, type Observability } from "./observability";
|
|
29
|
+
import { MessageType } from "./ai-types";
|
|
27
30
|
|
|
28
31
|
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
|
|
29
32
|
|
|
@@ -41,7 +44,7 @@ export type RPCRequest = {
|
|
|
41
44
|
* State update message from client
|
|
42
45
|
*/
|
|
43
46
|
export type StateUpdateMessage = {
|
|
44
|
-
type:
|
|
47
|
+
type: MessageType.CF_AGENT_STATE;
|
|
45
48
|
state: unknown;
|
|
46
49
|
};
|
|
47
50
|
|
|
@@ -49,7 +52,7 @@ export type StateUpdateMessage = {
|
|
|
49
52
|
* RPC response message to client
|
|
50
53
|
*/
|
|
51
54
|
export type RPCResponse = {
|
|
52
|
-
type:
|
|
55
|
+
type: MessageType.RPC;
|
|
53
56
|
id: string;
|
|
54
57
|
} & (
|
|
55
58
|
| {
|
|
@@ -76,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
|
|
|
76
79
|
typeof msg === "object" &&
|
|
77
80
|
msg !== null &&
|
|
78
81
|
"type" in msg &&
|
|
79
|
-
msg.type ===
|
|
82
|
+
msg.type === MessageType.RPC &&
|
|
80
83
|
"id" in msg &&
|
|
81
84
|
typeof msg.id === "string" &&
|
|
82
85
|
"method" in msg &&
|
|
@@ -94,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
|
|
|
94
97
|
typeof msg === "object" &&
|
|
95
98
|
msg !== null &&
|
|
96
99
|
"type" in msg &&
|
|
97
|
-
msg.type ===
|
|
100
|
+
msg.type === MessageType.CF_AGENT_STATE &&
|
|
98
101
|
"state" in msg
|
|
99
102
|
);
|
|
100
103
|
}
|
|
@@ -115,7 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
|
|
|
115
118
|
* Decorator that marks a method as callable by clients
|
|
116
119
|
* @param metadata Optional metadata about the callable method
|
|
117
120
|
*/
|
|
118
|
-
export function
|
|
121
|
+
export function callable(metadata: CallableMetadata = {}) {
|
|
119
122
|
return function callableDecorator<This, Args extends unknown[], Return>(
|
|
120
123
|
target: (this: This, ...args: Args) => Return,
|
|
121
124
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: later
|
|
@@ -129,6 +132,30 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
let didWarnAboutUnstableCallable = false;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Decorator that marks a method as callable by clients
|
|
139
|
+
* @deprecated this has been renamed to callable, and unstable_callable will be removed in the next major version
|
|
140
|
+
* @param metadata Optional metadata about the callable method
|
|
141
|
+
*/
|
|
142
|
+
export const unstable_callable = (metadata: CallableMetadata = {}) => {
|
|
143
|
+
if (!didWarnAboutUnstableCallable) {
|
|
144
|
+
didWarnAboutUnstableCallable = true;
|
|
145
|
+
console.warn(
|
|
146
|
+
"unstable_callable is deprecated, use callable instead. unstable_callable will be removed in the next major version."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
callable(metadata);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type QueueItem<T = string> = {
|
|
153
|
+
id: string;
|
|
154
|
+
payload: T;
|
|
155
|
+
callback: keyof Agent<unknown>;
|
|
156
|
+
created_at: number;
|
|
157
|
+
};
|
|
158
|
+
|
|
132
159
|
/**
|
|
133
160
|
* Represents a scheduled task within an Agent
|
|
134
161
|
* @template T Type of the payload data
|
|
@@ -174,7 +201,7 @@ function getNextCronTime(cron: string) {
|
|
|
174
201
|
* MCP Server state update message from server -> Client
|
|
175
202
|
*/
|
|
176
203
|
export type MCPServerMessage = {
|
|
177
|
-
type:
|
|
204
|
+
type: MessageType.CF_AGENT_MCP_SERVERS;
|
|
178
205
|
mcp: MCPServersState;
|
|
179
206
|
};
|
|
180
207
|
|
|
@@ -218,9 +245,10 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
|
218
245
|
const DEFAULT_STATE = {} as unknown;
|
|
219
246
|
|
|
220
247
|
const agentContext = new AsyncLocalStorage<{
|
|
221
|
-
agent: Agent<unknown>;
|
|
248
|
+
agent: Agent<unknown, unknown>;
|
|
222
249
|
connection: Connection | undefined;
|
|
223
250
|
request: Request | undefined;
|
|
251
|
+
email: AgentEmail | undefined;
|
|
224
252
|
}>();
|
|
225
253
|
|
|
226
254
|
export function getCurrentAgent<
|
|
@@ -228,31 +256,63 @@ export function getCurrentAgent<
|
|
|
228
256
|
>(): {
|
|
229
257
|
agent: T | undefined;
|
|
230
258
|
connection: Connection | undefined;
|
|
231
|
-
request: Request
|
|
259
|
+
request: Request | undefined;
|
|
260
|
+
email: AgentEmail | undefined;
|
|
232
261
|
} {
|
|
233
262
|
const store = agentContext.getStore() as
|
|
234
263
|
| {
|
|
235
264
|
agent: T;
|
|
236
265
|
connection: Connection | undefined;
|
|
237
|
-
request: Request
|
|
266
|
+
request: Request | undefined;
|
|
267
|
+
email: AgentEmail | undefined;
|
|
238
268
|
}
|
|
239
269
|
| undefined;
|
|
240
270
|
if (!store) {
|
|
241
271
|
return {
|
|
242
272
|
agent: undefined,
|
|
243
273
|
connection: undefined,
|
|
244
|
-
request: undefined
|
|
274
|
+
request: undefined,
|
|
275
|
+
email: undefined
|
|
245
276
|
};
|
|
246
277
|
}
|
|
247
278
|
return store;
|
|
248
279
|
}
|
|
249
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
|
|
283
|
+
* @param agent The agent instance
|
|
284
|
+
* @param method The method to wrap
|
|
285
|
+
* @returns A wrapped method that runs within the agent context
|
|
286
|
+
*/
|
|
287
|
+
|
|
288
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
289
|
+
function withAgentContext<T extends (...args: any[]) => any>(
|
|
290
|
+
method: T
|
|
291
|
+
): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
|
|
292
|
+
return function (...args: Parameters<T>): ReturnType<T> {
|
|
293
|
+
const { connection, request, email, agent } = getCurrentAgent();
|
|
294
|
+
|
|
295
|
+
if (agent === this) {
|
|
296
|
+
// already wrapped, so we can just call the method
|
|
297
|
+
return method.apply(this, args);
|
|
298
|
+
}
|
|
299
|
+
// not wrapped, so we need to wrap it
|
|
300
|
+
return agentContext.run({ agent: this, connection, request, email }, () => {
|
|
301
|
+
return method.apply(this, args);
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
250
306
|
/**
|
|
251
307
|
* Base class for creating Agent implementations
|
|
252
308
|
* @template Env Environment type containing bindings
|
|
253
309
|
* @template State State type to store within the Agent
|
|
254
310
|
*/
|
|
255
|
-
export class Agent<
|
|
311
|
+
export class Agent<
|
|
312
|
+
Env = typeof env,
|
|
313
|
+
State = unknown,
|
|
314
|
+
Props extends Record<string, unknown> = Record<string, unknown>
|
|
315
|
+
> extends Server<Env, Props> {
|
|
256
316
|
private _state = DEFAULT_STATE as State;
|
|
257
317
|
|
|
258
318
|
private _ParentClass: typeof Agent<Env, State> =
|
|
@@ -351,6 +411,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
351
411
|
constructor(ctx: AgentContext, env: Env) {
|
|
352
412
|
super(ctx, env);
|
|
353
413
|
|
|
414
|
+
// Auto-wrap custom methods with agent context
|
|
415
|
+
this._autoWrapCustomMethods();
|
|
416
|
+
|
|
354
417
|
this.sql`
|
|
355
418
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
356
419
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -358,6 +421,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
358
421
|
)
|
|
359
422
|
`;
|
|
360
423
|
|
|
424
|
+
this.sql`
|
|
425
|
+
CREATE TABLE IF NOT EXISTS cf_agents_queues (
|
|
426
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
427
|
+
payload TEXT,
|
|
428
|
+
callback TEXT,
|
|
429
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
430
|
+
)
|
|
431
|
+
`;
|
|
432
|
+
|
|
361
433
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
362
434
|
return this._tryCatch(async () => {
|
|
363
435
|
// Create alarms table if it doesn't exist
|
|
@@ -394,7 +466,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
394
466
|
const _onRequest = this.onRequest.bind(this);
|
|
395
467
|
this.onRequest = (request: Request) => {
|
|
396
468
|
return agentContext.run(
|
|
397
|
-
{ agent: this, connection: undefined, request },
|
|
469
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
398
470
|
async () => {
|
|
399
471
|
if (this.mcp.isCallbackRequest(request)) {
|
|
400
472
|
await this.mcp.handleCallbackRequest(request);
|
|
@@ -403,7 +475,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
403
475
|
this.broadcast(
|
|
404
476
|
JSON.stringify({
|
|
405
477
|
mcp: this.getMcpServers(),
|
|
406
|
-
type:
|
|
478
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
407
479
|
})
|
|
408
480
|
);
|
|
409
481
|
|
|
@@ -422,7 +494,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
422
494
|
const _onMessage = this.onMessage.bind(this);
|
|
423
495
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
424
496
|
return agentContext.run(
|
|
425
|
-
{ agent: this, connection, request: undefined },
|
|
497
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
426
498
|
async () => {
|
|
427
499
|
if (typeof message !== "string") {
|
|
428
500
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
@@ -472,10 +544,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
472
544
|
displayMessage: `RPC call to ${method}`,
|
|
473
545
|
id: nanoid(),
|
|
474
546
|
payload: {
|
|
475
|
-
args,
|
|
476
547
|
method,
|
|
477
|
-
streaming: metadata?.streaming
|
|
478
|
-
success: true
|
|
548
|
+
streaming: metadata?.streaming
|
|
479
549
|
},
|
|
480
550
|
timestamp: Date.now(),
|
|
481
551
|
type: "rpc"
|
|
@@ -488,7 +558,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
488
558
|
id,
|
|
489
559
|
result,
|
|
490
560
|
success: true,
|
|
491
|
-
type:
|
|
561
|
+
type: MessageType.RPC
|
|
492
562
|
};
|
|
493
563
|
connection.send(JSON.stringify(response));
|
|
494
564
|
} catch (e) {
|
|
@@ -498,7 +568,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
498
568
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
499
569
|
id: parsed.id,
|
|
500
570
|
success: false,
|
|
501
|
-
type:
|
|
571
|
+
type: MessageType.RPC
|
|
502
572
|
};
|
|
503
573
|
connection.send(JSON.stringify(response));
|
|
504
574
|
console.error("RPC error:", e);
|
|
@@ -516,77 +586,104 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
516
586
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
517
587
|
// must fix this
|
|
518
588
|
return agentContext.run(
|
|
519
|
-
{ agent: this, connection, request: ctx.request },
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (this.state) {
|
|
523
|
-
connection.send(
|
|
524
|
-
JSON.stringify({
|
|
525
|
-
state: this.state,
|
|
526
|
-
type: "cf_agent_state"
|
|
527
|
-
})
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
589
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
590
|
+
() => {
|
|
591
|
+
if (this.state) {
|
|
531
592
|
connection.send(
|
|
532
593
|
JSON.stringify({
|
|
533
|
-
|
|
534
|
-
type:
|
|
594
|
+
state: this.state,
|
|
595
|
+
type: MessageType.CF_AGENT_STATE
|
|
535
596
|
})
|
|
536
597
|
);
|
|
598
|
+
}
|
|
537
599
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
600
|
+
connection.send(
|
|
601
|
+
JSON.stringify({
|
|
602
|
+
mcp: this.getMcpServers(),
|
|
603
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
this.observability?.emit(
|
|
608
|
+
{
|
|
609
|
+
displayMessage: "Connection established",
|
|
610
|
+
id: nanoid(),
|
|
611
|
+
payload: {
|
|
612
|
+
connectionId: connection.id
|
|
547
613
|
},
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
614
|
+
timestamp: Date.now(),
|
|
615
|
+
type: "connect"
|
|
616
|
+
},
|
|
617
|
+
this.ctx
|
|
618
|
+
);
|
|
619
|
+
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
552
620
|
}
|
|
553
621
|
);
|
|
554
622
|
};
|
|
555
623
|
|
|
556
624
|
const _onStart = this.onStart.bind(this);
|
|
557
|
-
this.onStart = async () => {
|
|
625
|
+
this.onStart = async (props?: Props) => {
|
|
558
626
|
return agentContext.run(
|
|
559
|
-
{
|
|
627
|
+
{
|
|
628
|
+
agent: this,
|
|
629
|
+
connection: undefined,
|
|
630
|
+
request: undefined,
|
|
631
|
+
email: undefined
|
|
632
|
+
},
|
|
560
633
|
async () => {
|
|
561
|
-
|
|
634
|
+
await this._tryCatch(() => {
|
|
635
|
+
const servers = this.sql<MCPServerRow>`
|
|
562
636
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
563
637
|
`;
|
|
564
638
|
|
|
565
|
-
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
566
|
-
Promise.allSettled(
|
|
567
|
-
servers.map((server) => {
|
|
568
|
-
return this._connectToMcpServerInternal(
|
|
569
|
-
server.name,
|
|
570
|
-
server.server_url,
|
|
571
|
-
server.callback_url,
|
|
572
|
-
server.server_options
|
|
573
|
-
? JSON.parse(server.server_options)
|
|
574
|
-
: undefined,
|
|
575
|
-
{
|
|
576
|
-
id: server.id,
|
|
577
|
-
oauthClientId: server.client_id ?? undefined
|
|
578
|
-
}
|
|
579
|
-
);
|
|
580
|
-
})
|
|
581
|
-
).then((_results) => {
|
|
582
639
|
this.broadcast(
|
|
583
640
|
JSON.stringify({
|
|
584
641
|
mcp: this.getMcpServers(),
|
|
585
|
-
type:
|
|
642
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
586
643
|
})
|
|
587
644
|
);
|
|
645
|
+
|
|
646
|
+
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
647
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
648
|
+
servers.forEach((server) => {
|
|
649
|
+
this._connectToMcpServerInternal(
|
|
650
|
+
server.name,
|
|
651
|
+
server.server_url,
|
|
652
|
+
server.callback_url,
|
|
653
|
+
server.server_options
|
|
654
|
+
? JSON.parse(server.server_options)
|
|
655
|
+
: undefined,
|
|
656
|
+
{
|
|
657
|
+
id: server.id,
|
|
658
|
+
oauthClientId: server.client_id ?? undefined
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
.then(() => {
|
|
662
|
+
// Broadcast updated MCP servers state after each server connects
|
|
663
|
+
this.broadcast(
|
|
664
|
+
JSON.stringify({
|
|
665
|
+
mcp: this.getMcpServers(),
|
|
666
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
667
|
+
})
|
|
668
|
+
);
|
|
669
|
+
})
|
|
670
|
+
.catch((error) => {
|
|
671
|
+
console.error(
|
|
672
|
+
`Error connecting to MCP server: ${server.name} (${server.server_url})`,
|
|
673
|
+
error
|
|
674
|
+
);
|
|
675
|
+
// Still broadcast even if connection fails, so clients know about the failure
|
|
676
|
+
this.broadcast(
|
|
677
|
+
JSON.stringify({
|
|
678
|
+
mcp: this.getMcpServers(),
|
|
679
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return _onStart(props);
|
|
588
686
|
});
|
|
589
|
-
await this._tryCatch(() => _onStart());
|
|
590
687
|
}
|
|
591
688
|
);
|
|
592
689
|
};
|
|
@@ -596,7 +693,6 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
596
693
|
state: State,
|
|
597
694
|
source: Connection | "server" = "server"
|
|
598
695
|
) {
|
|
599
|
-
const previousState = this._state;
|
|
600
696
|
this._state = state;
|
|
601
697
|
this.sql`
|
|
602
698
|
INSERT OR REPLACE INTO cf_agents_state (id, state)
|
|
@@ -609,23 +705,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
609
705
|
this.broadcast(
|
|
610
706
|
JSON.stringify({
|
|
611
707
|
state: state,
|
|
612
|
-
type:
|
|
708
|
+
type: MessageType.CF_AGENT_STATE
|
|
613
709
|
}),
|
|
614
710
|
source !== "server" ? [source.id] : []
|
|
615
711
|
);
|
|
616
712
|
return this._tryCatch(() => {
|
|
617
|
-
const { connection, request } = agentContext.getStore() || {};
|
|
713
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
618
714
|
return agentContext.run(
|
|
619
|
-
{ agent: this, connection, request },
|
|
715
|
+
{ agent: this, connection, request, email },
|
|
620
716
|
async () => {
|
|
621
717
|
this.observability?.emit(
|
|
622
718
|
{
|
|
623
719
|
displayMessage: "State updated",
|
|
624
720
|
id: nanoid(),
|
|
625
|
-
payload: {
|
|
626
|
-
previousState,
|
|
627
|
-
state
|
|
628
|
-
},
|
|
721
|
+
payload: {},
|
|
629
722
|
timestamp: Date.now(),
|
|
630
723
|
type: "state:update"
|
|
631
724
|
},
|
|
@@ -656,19 +749,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
656
749
|
}
|
|
657
750
|
|
|
658
751
|
/**
|
|
659
|
-
* Called when the Agent receives an email
|
|
752
|
+
* Called when the Agent receives an email via routeAgentEmail()
|
|
753
|
+
* Override this method to handle incoming emails
|
|
660
754
|
* @param email Email message to process
|
|
661
755
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
756
|
+
async _onEmail(email: AgentEmail) {
|
|
757
|
+
// nb: we use this roundabout way of getting to onEmail
|
|
758
|
+
// because of https://github.com/cloudflare/workerd/issues/4499
|
|
664
759
|
return agentContext.run(
|
|
665
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
760
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
666
761
|
async () => {
|
|
667
|
-
|
|
762
|
+
if ("onEmail" in this && typeof this.onEmail === "function") {
|
|
763
|
+
return this._tryCatch(() =>
|
|
764
|
+
(this.onEmail as (email: AgentEmail) => Promise<void>)(email)
|
|
765
|
+
);
|
|
766
|
+
} else {
|
|
767
|
+
console.log("Received email from:", email.from, "to:", email.to);
|
|
768
|
+
console.log("Subject:", email.headers.get("subject"));
|
|
769
|
+
console.log(
|
|
770
|
+
"Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
668
773
|
}
|
|
669
774
|
);
|
|
670
775
|
}
|
|
671
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Reply to an email
|
|
779
|
+
* @param email The email to reply to
|
|
780
|
+
* @param options Options for the reply
|
|
781
|
+
* @returns void
|
|
782
|
+
*/
|
|
783
|
+
async replyToEmail(
|
|
784
|
+
email: AgentEmail,
|
|
785
|
+
options: {
|
|
786
|
+
fromName: string;
|
|
787
|
+
subject?: string | undefined;
|
|
788
|
+
body: string;
|
|
789
|
+
contentType?: string;
|
|
790
|
+
headers?: Record<string, string>;
|
|
791
|
+
}
|
|
792
|
+
): Promise<void> {
|
|
793
|
+
return this._tryCatch(async () => {
|
|
794
|
+
const agentName = camelCaseToKebabCase(this._ParentClass.name);
|
|
795
|
+
const agentId = this.name;
|
|
796
|
+
|
|
797
|
+
const { createMimeMessage } = await import("mimetext");
|
|
798
|
+
const msg = createMimeMessage();
|
|
799
|
+
msg.setSender({ addr: email.to, name: options.fromName });
|
|
800
|
+
msg.setRecipient(email.from);
|
|
801
|
+
msg.setSubject(
|
|
802
|
+
options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
|
|
803
|
+
);
|
|
804
|
+
msg.addMessage({
|
|
805
|
+
contentType: options.contentType || "text/plain",
|
|
806
|
+
data: options.body
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const domain = email.from.split("@")[1];
|
|
810
|
+
const messageId = `<${agentId}@${domain}>`;
|
|
811
|
+
msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
|
|
812
|
+
msg.setHeader("Message-ID", messageId);
|
|
813
|
+
msg.setHeader("X-Agent-Name", agentName);
|
|
814
|
+
msg.setHeader("X-Agent-ID", agentId);
|
|
815
|
+
|
|
816
|
+
if (options.headers) {
|
|
817
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
818
|
+
msg.setHeader(key, value);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
await email.reply({
|
|
822
|
+
from: email.to,
|
|
823
|
+
raw: msg.asRaw(),
|
|
824
|
+
to: email.from
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
672
829
|
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
673
830
|
try {
|
|
674
831
|
return await fn();
|
|
@@ -677,6 +834,68 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
677
834
|
}
|
|
678
835
|
}
|
|
679
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Automatically wrap custom methods with agent context
|
|
839
|
+
* This ensures getCurrentAgent() works in all custom methods without decorators
|
|
840
|
+
*/
|
|
841
|
+
private _autoWrapCustomMethods() {
|
|
842
|
+
// Collect all methods from base prototypes (Agent and Server)
|
|
843
|
+
const basePrototypes = [Agent.prototype, Server.prototype];
|
|
844
|
+
const baseMethods = new Set<string>();
|
|
845
|
+
for (const baseProto of basePrototypes) {
|
|
846
|
+
let proto = baseProto;
|
|
847
|
+
while (proto && proto !== Object.prototype) {
|
|
848
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
849
|
+
for (const methodName of methodNames) {
|
|
850
|
+
baseMethods.add(methodName);
|
|
851
|
+
}
|
|
852
|
+
proto = Object.getPrototypeOf(proto);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// Get all methods from the current instance's prototype chain
|
|
856
|
+
let proto = Object.getPrototypeOf(this);
|
|
857
|
+
let depth = 0;
|
|
858
|
+
while (proto && proto !== Object.prototype && depth < 10) {
|
|
859
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
860
|
+
for (const methodName of methodNames) {
|
|
861
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
|
|
862
|
+
|
|
863
|
+
// Skip if it's a private method, a base method, a getter, or not a function,
|
|
864
|
+
if (
|
|
865
|
+
baseMethods.has(methodName) ||
|
|
866
|
+
methodName.startsWith("_") ||
|
|
867
|
+
!descriptor ||
|
|
868
|
+
!!descriptor.get ||
|
|
869
|
+
typeof descriptor.value !== "function"
|
|
870
|
+
) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Now, methodName is confirmed to be a custom method/function
|
|
875
|
+
// Wrap the custom method with context
|
|
876
|
+
const wrappedFunction = withAgentContext(
|
|
877
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
878
|
+
this[methodName as keyof this] as (...args: any[]) => any
|
|
879
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
880
|
+
) as any;
|
|
881
|
+
|
|
882
|
+
// if the method is callable, copy the metadata from the original method
|
|
883
|
+
if (this._isCallable(methodName)) {
|
|
884
|
+
callableMetadata.set(
|
|
885
|
+
wrappedFunction,
|
|
886
|
+
callableMetadata.get(this[methodName as keyof this] as Function)!
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// set the wrapped function on the prototype
|
|
891
|
+
this.constructor.prototype[methodName as keyof this] = wrappedFunction;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
proto = Object.getPrototypeOf(proto);
|
|
895
|
+
depth++;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
680
899
|
override onError(
|
|
681
900
|
connection: Connection,
|
|
682
901
|
error: unknown
|
|
@@ -711,6 +930,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
711
930
|
throw new Error("Not implemented");
|
|
712
931
|
}
|
|
713
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Queue a task to be executed in the future
|
|
935
|
+
* @param payload Payload to pass to the callback
|
|
936
|
+
* @param callback Name of the method to call
|
|
937
|
+
* @returns The ID of the queued task
|
|
938
|
+
*/
|
|
939
|
+
async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
|
|
940
|
+
const id = nanoid(9);
|
|
941
|
+
if (typeof callback !== "string") {
|
|
942
|
+
throw new Error("Callback must be a string");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (typeof this[callback] !== "function") {
|
|
946
|
+
throw new Error(`this.${callback} is not a function`);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
this.sql`
|
|
950
|
+
INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
|
|
951
|
+
VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
|
|
952
|
+
`;
|
|
953
|
+
|
|
954
|
+
void this._flushQueue().catch((e) => {
|
|
955
|
+
console.error("Error flushing queue:", e);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
return id;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private _flushingQueue = false;
|
|
962
|
+
|
|
963
|
+
private async _flushQueue() {
|
|
964
|
+
if (this._flushingQueue) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
this._flushingQueue = true;
|
|
968
|
+
while (true) {
|
|
969
|
+
const result = this.sql<QueueItem<string>>`
|
|
970
|
+
SELECT * FROM cf_agents_queues
|
|
971
|
+
ORDER BY created_at ASC
|
|
972
|
+
`;
|
|
973
|
+
|
|
974
|
+
if (!result || result.length === 0) {
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
for (const row of result || []) {
|
|
979
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
980
|
+
if (!callback) {
|
|
981
|
+
console.error(`callback ${row.callback} not found`);
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
985
|
+
await agentContext.run(
|
|
986
|
+
{
|
|
987
|
+
agent: this,
|
|
988
|
+
connection,
|
|
989
|
+
request,
|
|
990
|
+
email
|
|
991
|
+
},
|
|
992
|
+
async () => {
|
|
993
|
+
// TODO: add retries and backoff
|
|
994
|
+
await (
|
|
995
|
+
callback as (
|
|
996
|
+
payload: unknown,
|
|
997
|
+
queueItem: QueueItem<string>
|
|
998
|
+
) => Promise<void>
|
|
999
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1000
|
+
await this.dequeue(row.id);
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
this._flushingQueue = false;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Dequeue a task by ID
|
|
1010
|
+
* @param id ID of the task to dequeue
|
|
1011
|
+
*/
|
|
1012
|
+
async dequeue(id: string) {
|
|
1013
|
+
this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Dequeue all tasks
|
|
1018
|
+
*/
|
|
1019
|
+
async dequeueAll() {
|
|
1020
|
+
this.sql`DELETE FROM cf_agents_queues`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Dequeue all tasks by callback
|
|
1025
|
+
* @param callback Name of the callback to dequeue
|
|
1026
|
+
*/
|
|
1027
|
+
async dequeueAllByCallback(callback: string) {
|
|
1028
|
+
this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Get a queued task by ID
|
|
1033
|
+
* @param id ID of the task to get
|
|
1034
|
+
* @returns The task or undefined if not found
|
|
1035
|
+
*/
|
|
1036
|
+
async getQueue(id: string): Promise<QueueItem<string> | undefined> {
|
|
1037
|
+
const result = this.sql<QueueItem<string>>`
|
|
1038
|
+
SELECT * FROM cf_agents_queues WHERE id = ${id}
|
|
1039
|
+
`;
|
|
1040
|
+
return result
|
|
1041
|
+
? { ...result[0], payload: JSON.parse(result[0].payload) }
|
|
1042
|
+
: undefined;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Get all queues by key and value
|
|
1047
|
+
* @param key Key to filter by
|
|
1048
|
+
* @param value Value to filter by
|
|
1049
|
+
* @returns Array of matching QueueItem objects
|
|
1050
|
+
*/
|
|
1051
|
+
async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
|
|
1052
|
+
const result = this.sql<QueueItem<string>>`
|
|
1053
|
+
SELECT * FROM cf_agents_queues
|
|
1054
|
+
`;
|
|
1055
|
+
return result.filter((row) => JSON.parse(row.payload)[key] === value);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
714
1058
|
/**
|
|
715
1059
|
* Schedule a task to be executed in the future
|
|
716
1060
|
* @template T Type of the payload data
|
|
@@ -731,7 +1075,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
731
1075
|
{
|
|
732
1076
|
displayMessage: `Schedule ${schedule.id} created`,
|
|
733
1077
|
id: nanoid(),
|
|
734
|
-
payload:
|
|
1078
|
+
payload: {
|
|
1079
|
+
callback: callback as string,
|
|
1080
|
+
id: id
|
|
1081
|
+
},
|
|
735
1082
|
timestamp: Date.now(),
|
|
736
1083
|
type: "schedule:create"
|
|
737
1084
|
},
|
|
@@ -901,7 +1248,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
901
1248
|
{
|
|
902
1249
|
displayMessage: `Schedule ${id} cancelled`,
|
|
903
1250
|
id: nanoid(),
|
|
904
|
-
payload:
|
|
1251
|
+
payload: {
|
|
1252
|
+
callback: schedule.callback,
|
|
1253
|
+
id: schedule.id
|
|
1254
|
+
},
|
|
905
1255
|
timestamp: Date.now(),
|
|
906
1256
|
type: "schedule:cancel"
|
|
907
1257
|
},
|
|
@@ -917,9 +1267,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
917
1267
|
private async _scheduleNextAlarm() {
|
|
918
1268
|
// Find the next schedule that needs to be executed
|
|
919
1269
|
const result = this.sql`
|
|
920
|
-
SELECT time FROM cf_agents_schedules
|
|
1270
|
+
SELECT time FROM cf_agents_schedules
|
|
921
1271
|
WHERE time > ${Math.floor(Date.now() / 1000)}
|
|
922
|
-
ORDER BY time ASC
|
|
1272
|
+
ORDER BY time ASC
|
|
923
1273
|
LIMIT 1
|
|
924
1274
|
`;
|
|
925
1275
|
if (!result) return;
|
|
@@ -946,51 +1296,61 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
946
1296
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
947
1297
|
`;
|
|
948
1298
|
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1299
|
+
if (result && Array.isArray(result)) {
|
|
1300
|
+
for (const row of result) {
|
|
1301
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
1302
|
+
if (!callback) {
|
|
1303
|
+
console.error(`callback ${row.callback} not found`);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
await agentContext.run(
|
|
1307
|
+
{
|
|
1308
|
+
agent: this,
|
|
1309
|
+
connection: undefined,
|
|
1310
|
+
request: undefined,
|
|
1311
|
+
email: undefined
|
|
1312
|
+
},
|
|
1313
|
+
async () => {
|
|
1314
|
+
try {
|
|
1315
|
+
this.observability?.emit(
|
|
1316
|
+
{
|
|
1317
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
1318
|
+
id: nanoid(),
|
|
1319
|
+
payload: {
|
|
1320
|
+
callback: row.callback,
|
|
1321
|
+
id: row.id
|
|
1322
|
+
},
|
|
1323
|
+
timestamp: Date.now(),
|
|
1324
|
+
type: "schedule:execute"
|
|
1325
|
+
},
|
|
1326
|
+
this.ctx
|
|
1327
|
+
);
|
|
969
1328
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1329
|
+
await (
|
|
1330
|
+
callback as (
|
|
1331
|
+
payload: unknown,
|
|
1332
|
+
schedule: Schedule<unknown>
|
|
1333
|
+
) => Promise<void>
|
|
1334
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1335
|
+
} catch (e) {
|
|
1336
|
+
console.error(`error executing callback "${row.callback}"`, e);
|
|
1337
|
+
}
|
|
978
1338
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
1339
|
+
);
|
|
1340
|
+
if (row.type === "cron") {
|
|
1341
|
+
// Update next execution time for cron schedules
|
|
1342
|
+
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1343
|
+
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
985
1344
|
|
|
986
|
-
|
|
1345
|
+
this.sql`
|
|
987
1346
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
988
1347
|
`;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1348
|
+
} else {
|
|
1349
|
+
// Delete one-time schedules after execution
|
|
1350
|
+
this.sql`
|
|
992
1351
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
993
1352
|
`;
|
|
1353
|
+
}
|
|
994
1354
|
}
|
|
995
1355
|
}
|
|
996
1356
|
|
|
@@ -1006,6 +1366,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1006
1366
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1007
1367
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1008
1368
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1369
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1009
1370
|
|
|
1010
1371
|
// delete all alarms
|
|
1011
1372
|
await this.ctx.storage.deleteAlarm();
|
|
@@ -1078,7 +1439,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1078
1439
|
this.broadcast(
|
|
1079
1440
|
JSON.stringify({
|
|
1080
1441
|
mcp: this.getMcpServers(),
|
|
1081
|
-
type:
|
|
1442
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1082
1443
|
})
|
|
1083
1444
|
);
|
|
1084
1445
|
|
|
@@ -1167,7 +1528,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1167
1528
|
this.broadcast(
|
|
1168
1529
|
JSON.stringify({
|
|
1169
1530
|
mcp: this.getMcpServers(),
|
|
1170
|
-
type:
|
|
1531
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1171
1532
|
})
|
|
1172
1533
|
);
|
|
1173
1534
|
}
|
|
@@ -1184,17 +1545,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1184
1545
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1185
1546
|
`;
|
|
1186
1547
|
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1548
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
1549
|
+
for (const server of servers) {
|
|
1550
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1551
|
+
mcpState.servers[server.id] = {
|
|
1552
|
+
auth_url: server.auth_url,
|
|
1553
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1554
|
+
instructions: serverConn?.instructions ?? null,
|
|
1555
|
+
name: server.name,
|
|
1556
|
+
server_url: server.server_url,
|
|
1557
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1558
|
+
state: serverConn?.connectionState ?? "authenticating"
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1198
1561
|
}
|
|
1199
1562
|
|
|
1200
1563
|
return mcpState;
|
|
@@ -1281,17 +1644,231 @@ export async function routeAgentRequest<Env>(
|
|
|
1281
1644
|
return response;
|
|
1282
1645
|
}
|
|
1283
1646
|
|
|
1647
|
+
export type EmailResolver<Env> = (
|
|
1648
|
+
email: ForwardableEmailMessage,
|
|
1649
|
+
env: Env
|
|
1650
|
+
) => Promise<{
|
|
1651
|
+
agentName: string;
|
|
1652
|
+
agentId: string;
|
|
1653
|
+
} | null>;
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Create a resolver that uses the message-id header to determine the agent to route the email to
|
|
1657
|
+
* @returns A function that resolves the agent to route the email to
|
|
1658
|
+
*/
|
|
1659
|
+
export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
|
|
1660
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1661
|
+
const messageId = email.headers.get("message-id");
|
|
1662
|
+
if (messageId) {
|
|
1663
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1664
|
+
if (messageIdMatch) {
|
|
1665
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1666
|
+
const agentName = domain.split(".")[0];
|
|
1667
|
+
return { agentName, agentId };
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const references = email.headers.get("references");
|
|
1672
|
+
if (references) {
|
|
1673
|
+
const referencesMatch = references.match(
|
|
1674
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1675
|
+
);
|
|
1676
|
+
if (referencesMatch) {
|
|
1677
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1678
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1679
|
+
const agentName = domain.split(".")[0];
|
|
1680
|
+
return { agentName, agentId };
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1685
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1686
|
+
if (agentName && agentId) {
|
|
1687
|
+
return { agentName, agentId };
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return null;
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
1696
|
+
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
1697
|
+
* @returns A function that resolves the agent to route the email to
|
|
1698
|
+
*/
|
|
1699
|
+
export function createAddressBasedEmailResolver<Env>(
|
|
1700
|
+
defaultAgentName: string
|
|
1701
|
+
): EmailResolver<Env> {
|
|
1702
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1703
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1704
|
+
if (!emailMatch) {
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1709
|
+
|
|
1710
|
+
if (subAddress) {
|
|
1711
|
+
return {
|
|
1712
|
+
agentName: localPart,
|
|
1713
|
+
agentId: subAddress
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1718
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1719
|
+
return {
|
|
1720
|
+
agentName: defaultAgentName,
|
|
1721
|
+
agentId: localPart
|
|
1722
|
+
};
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
1728
|
+
* @param agentName The name of the agent to route the email to
|
|
1729
|
+
* @param agentId The id of the agent to route the email to
|
|
1730
|
+
* @returns A function that resolves the agent to route the email to
|
|
1731
|
+
*/
|
|
1732
|
+
export function createCatchAllEmailResolver<Env>(
|
|
1733
|
+
agentName: string,
|
|
1734
|
+
agentId: string
|
|
1735
|
+
): EmailResolver<Env> {
|
|
1736
|
+
return async () => ({ agentName, agentId });
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1740
|
+
resolver: EmailResolver<Env>;
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
// Cache the agent namespace map for email routing
|
|
1744
|
+
// This maps both kebab-case and original names to namespaces
|
|
1745
|
+
const agentMapCache = new WeakMap<
|
|
1746
|
+
Record<string, unknown>,
|
|
1747
|
+
Record<string, unknown>
|
|
1748
|
+
>();
|
|
1749
|
+
|
|
1284
1750
|
/**
|
|
1285
1751
|
* Route an email to the appropriate Agent
|
|
1286
|
-
* @param email
|
|
1287
|
-
* @param env
|
|
1288
|
-
* @param options
|
|
1752
|
+
* @param email The email to route
|
|
1753
|
+
* @param env The environment containing the Agent bindings
|
|
1754
|
+
* @param options The options for routing the email
|
|
1755
|
+
* @returns A promise that resolves when the email has been routed
|
|
1289
1756
|
*/
|
|
1290
1757
|
export async function routeAgentEmail<Env>(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
): Promise<void> {
|
|
1758
|
+
email: ForwardableEmailMessage,
|
|
1759
|
+
env: Env,
|
|
1760
|
+
options: EmailRoutingOptions<Env>
|
|
1761
|
+
): Promise<void> {
|
|
1762
|
+
const routingInfo = await options.resolver(email, env);
|
|
1763
|
+
|
|
1764
|
+
if (!routingInfo) {
|
|
1765
|
+
console.warn("No routing information found for email, dropping message");
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Build a map that includes both original names and kebab-case versions
|
|
1770
|
+
if (!agentMapCache.has(env as Record<string, unknown>)) {
|
|
1771
|
+
const map: Record<string, unknown> = {};
|
|
1772
|
+
for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
|
|
1773
|
+
if (
|
|
1774
|
+
value &&
|
|
1775
|
+
typeof value === "object" &&
|
|
1776
|
+
"idFromName" in value &&
|
|
1777
|
+
typeof value.idFromName === "function"
|
|
1778
|
+
) {
|
|
1779
|
+
// Add both the original name and kebab-case version
|
|
1780
|
+
map[key] = value;
|
|
1781
|
+
map[camelCaseToKebabCase(key)] = value;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
agentMapCache.set(env as Record<string, unknown>, map);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
|
|
1788
|
+
const namespace = agentMap[routingInfo.agentName];
|
|
1789
|
+
|
|
1790
|
+
if (!namespace) {
|
|
1791
|
+
// Provide helpful error message listing available agents
|
|
1792
|
+
const availableAgents = Object.keys(agentMap)
|
|
1793
|
+
.filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
|
|
1794
|
+
.join(", ");
|
|
1795
|
+
throw new Error(
|
|
1796
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const agent = await getAgentByName(
|
|
1801
|
+
namespace as unknown as AgentNamespace<Agent<Env>>,
|
|
1802
|
+
routingInfo.agentId
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
// let's make a serialisable version of the email
|
|
1806
|
+
const serialisableEmail: AgentEmail = {
|
|
1807
|
+
getRaw: async () => {
|
|
1808
|
+
const reader = email.raw.getReader();
|
|
1809
|
+
const chunks: Uint8Array[] = [];
|
|
1810
|
+
|
|
1811
|
+
let done = false;
|
|
1812
|
+
while (!done) {
|
|
1813
|
+
const { value, done: readerDone } = await reader.read();
|
|
1814
|
+
done = readerDone;
|
|
1815
|
+
if (value) {
|
|
1816
|
+
chunks.push(value);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1821
|
+
const combined = new Uint8Array(totalLength);
|
|
1822
|
+
let offset = 0;
|
|
1823
|
+
for (const chunk of chunks) {
|
|
1824
|
+
combined.set(chunk, offset);
|
|
1825
|
+
offset += chunk.length;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return combined;
|
|
1829
|
+
},
|
|
1830
|
+
headers: email.headers,
|
|
1831
|
+
rawSize: email.rawSize,
|
|
1832
|
+
setReject: (reason: string) => {
|
|
1833
|
+
email.setReject(reason);
|
|
1834
|
+
},
|
|
1835
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1836
|
+
return email.forward(rcptTo, headers);
|
|
1837
|
+
},
|
|
1838
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1839
|
+
return email.reply(
|
|
1840
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1841
|
+
);
|
|
1842
|
+
},
|
|
1843
|
+
from: email.from,
|
|
1844
|
+
to: email.to
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
await agent._onEmail(serialisableEmail);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
export type AgentEmail = {
|
|
1851
|
+
from: string;
|
|
1852
|
+
to: string;
|
|
1853
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1854
|
+
headers: Headers;
|
|
1855
|
+
rawSize: number;
|
|
1856
|
+
setReject: (reason: string) => void;
|
|
1857
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1858
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
export type EmailSendOptions = {
|
|
1862
|
+
to: string;
|
|
1863
|
+
subject: string;
|
|
1864
|
+
body: string;
|
|
1865
|
+
contentType?: string;
|
|
1866
|
+
headers?: Record<string, string>;
|
|
1867
|
+
includeRoutingHeaders?: boolean;
|
|
1868
|
+
agentName?: string;
|
|
1869
|
+
agentId?: string;
|
|
1870
|
+
domain?: string;
|
|
1871
|
+
};
|
|
1295
1872
|
|
|
1296
1873
|
/**
|
|
1297
1874
|
* Get or create an Agent by name
|
|
@@ -1302,12 +1879,17 @@ export async function routeAgentEmail<Env>(
|
|
|
1302
1879
|
* @param options Options for Agent creation
|
|
1303
1880
|
* @returns Promise resolving to an Agent instance stub
|
|
1304
1881
|
*/
|
|
1305
|
-
export async function getAgentByName<
|
|
1882
|
+
export async function getAgentByName<
|
|
1883
|
+
Env,
|
|
1884
|
+
T extends Agent<Env>,
|
|
1885
|
+
Props extends Record<string, unknown> = Record<string, unknown>
|
|
1886
|
+
>(
|
|
1306
1887
|
namespace: AgentNamespace<T>,
|
|
1307
1888
|
name: string,
|
|
1308
1889
|
options?: {
|
|
1309
1890
|
jurisdiction?: DurableObjectJurisdiction;
|
|
1310
1891
|
locationHint?: DurableObjectLocationHint;
|
|
1892
|
+
props?: Props;
|
|
1311
1893
|
}
|
|
1312
1894
|
) {
|
|
1313
1895
|
return getServerByName<Env, T>(namespace, name, options);
|
|
@@ -1339,7 +1921,7 @@ export class StreamingResponse {
|
|
|
1339
1921
|
id: this._id,
|
|
1340
1922
|
result: chunk,
|
|
1341
1923
|
success: true,
|
|
1342
|
-
type:
|
|
1924
|
+
type: MessageType.RPC
|
|
1343
1925
|
};
|
|
1344
1926
|
this._connection.send(JSON.stringify(response));
|
|
1345
1927
|
}
|
|
@@ -1358,7 +1940,7 @@ export class StreamingResponse {
|
|
|
1358
1940
|
id: this._id,
|
|
1359
1941
|
result: finalChunk,
|
|
1360
1942
|
success: true,
|
|
1361
|
-
type:
|
|
1943
|
+
type: MessageType.RPC
|
|
1362
1944
|
};
|
|
1363
1945
|
this._connection.send(JSON.stringify(response));
|
|
1364
1946
|
}
|