agents 0.0.0-8157d08 → 0.0.0-8234d41
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 +255 -27
- 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 +68 -71
- package/dist/ai-react.js +177 -37
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +37 -19
- package/dist/ai-types.js +6 -0
- package/dist/chunk-BER7KXUJ.js +18 -0
- package/dist/chunk-BER7KXUJ.js.map +1 -0
- package/dist/{chunk-767EASBA.js → chunk-LL2AFX7V.js} +5 -2
- package/dist/chunk-LL2AFX7V.js.map +1 -0
- package/dist/{chunk-JFRK72K3.js → chunk-M5SZRJTA.js} +519 -129
- package/dist/chunk-M5SZRJTA.js.map +1 -0
- package/dist/{chunk-E3LCYPCB.js → chunk-MH46VMM4.js} +163 -20
- package/dist/chunk-MH46VMM4.js.map +1 -0
- package/dist/{chunk-NKZZ66QY.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/client-CvaJdLQA.d.ts +5015 -0
- package/dist/client.d.ts +2 -2
- 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 +64 -56
- package/dist/mcp/index.js +952 -637
- 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 +10 -6
- 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 +21 -10
- package/src/index.ts +761 -173
- package/dist/chunk-767EASBA.js.map +0 -1
- package/dist/chunk-E3LCYPCB.js.map +0 -1
- package/dist/chunk-JFRK72K3.js.map +0 -1
- package/dist/chunk-NKZZ66QY.js.map +0 -1
- package/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";
|
|
@@ -6,24 +7,26 @@ import type {
|
|
|
6
7
|
Prompt,
|
|
7
8
|
Resource,
|
|
8
9
|
ServerCapabilities,
|
|
9
|
-
Tool
|
|
10
|
+
Tool
|
|
10
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
12
|
import { parseCronExpression } from "cron-schedule";
|
|
12
13
|
import { nanoid } from "nanoid";
|
|
14
|
+
import { EmailMessage } from "cloudflare:email";
|
|
13
15
|
import {
|
|
14
16
|
type Connection,
|
|
15
17
|
type ConnectionContext,
|
|
16
|
-
getServerByName,
|
|
17
18
|
type PartyServerOptions,
|
|
18
|
-
routePartykitRequest,
|
|
19
19
|
Server,
|
|
20
20
|
type WSMessage,
|
|
21
|
+
getServerByName,
|
|
22
|
+
routePartykitRequest
|
|
21
23
|
} from "partyserver";
|
|
22
24
|
import { camelCaseToKebabCase } from "./client";
|
|
23
25
|
import { MCPClientManager } from "./mcp/client";
|
|
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,23 +245,26 @@ 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<
|
|
227
|
-
T extends Agent<unknown, unknown> = Agent<unknown, unknown
|
|
255
|
+
T extends Agent<unknown, unknown> = Agent<unknown, unknown>
|
|
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) {
|
|
@@ -242,17 +272,47 @@ export function getCurrentAgent<
|
|
|
242
272
|
agent: undefined,
|
|
243
273
|
connection: undefined,
|
|
244
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> =
|
|
@@ -314,7 +374,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
314
374
|
*/
|
|
315
375
|
static options = {
|
|
316
376
|
/** Whether the Agent should hibernate when inactive */
|
|
317
|
-
hibernate: true
|
|
377
|
+
hibernate: true // default to hibernate
|
|
318
378
|
};
|
|
319
379
|
|
|
320
380
|
/**
|
|
@@ -351,6 +411,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
351
411
|
constructor(ctx: AgentContext, env: Env) {
|
|
352
412
|
super(ctx, env);
|
|
353
413
|
|
|
414
|
+
if (!wrappedClasses.has(this.constructor)) {
|
|
415
|
+
// Auto-wrap custom methods with agent context
|
|
416
|
+
this._autoWrapCustomMethods();
|
|
417
|
+
wrappedClasses.add(this.constructor);
|
|
418
|
+
}
|
|
419
|
+
|
|
354
420
|
this.sql`
|
|
355
421
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
356
422
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -358,6 +424,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
358
424
|
)
|
|
359
425
|
`;
|
|
360
426
|
|
|
427
|
+
this.sql`
|
|
428
|
+
CREATE TABLE IF NOT EXISTS cf_agents_queues (
|
|
429
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
430
|
+
payload TEXT,
|
|
431
|
+
callback TEXT,
|
|
432
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
433
|
+
)
|
|
434
|
+
`;
|
|
435
|
+
|
|
361
436
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
362
437
|
return this._tryCatch(async () => {
|
|
363
438
|
// Create alarms table if it doesn't exist
|
|
@@ -394,7 +469,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
394
469
|
const _onRequest = this.onRequest.bind(this);
|
|
395
470
|
this.onRequest = (request: Request) => {
|
|
396
471
|
return agentContext.run(
|
|
397
|
-
{ agent: this, connection: undefined, request },
|
|
472
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
398
473
|
async () => {
|
|
399
474
|
if (this.mcp.isCallbackRequest(request)) {
|
|
400
475
|
await this.mcp.handleCallbackRequest(request);
|
|
@@ -403,14 +478,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
403
478
|
this.broadcast(
|
|
404
479
|
JSON.stringify({
|
|
405
480
|
mcp: this.getMcpServers(),
|
|
406
|
-
type:
|
|
481
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
407
482
|
})
|
|
408
483
|
);
|
|
409
484
|
|
|
410
485
|
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
411
486
|
return new Response("<script>window.close();</script>", {
|
|
412
487
|
headers: { "content-type": "text/html" },
|
|
413
|
-
status: 200
|
|
488
|
+
status: 200
|
|
414
489
|
});
|
|
415
490
|
}
|
|
416
491
|
|
|
@@ -422,7 +497,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
422
497
|
const _onMessage = this.onMessage.bind(this);
|
|
423
498
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
424
499
|
return agentContext.run(
|
|
425
|
-
{ agent: this, connection, request: undefined },
|
|
500
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
426
501
|
async () => {
|
|
427
502
|
if (typeof message !== "string") {
|
|
428
503
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
@@ -472,13 +547,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
472
547
|
displayMessage: `RPC call to ${method}`,
|
|
473
548
|
id: nanoid(),
|
|
474
549
|
payload: {
|
|
475
|
-
args,
|
|
476
550
|
method,
|
|
477
|
-
streaming: metadata?.streaming
|
|
478
|
-
success: true,
|
|
551
|
+
streaming: metadata?.streaming
|
|
479
552
|
},
|
|
480
553
|
timestamp: Date.now(),
|
|
481
|
-
type: "rpc"
|
|
554
|
+
type: "rpc"
|
|
482
555
|
},
|
|
483
556
|
this.ctx
|
|
484
557
|
);
|
|
@@ -488,7 +561,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
488
561
|
id,
|
|
489
562
|
result,
|
|
490
563
|
success: true,
|
|
491
|
-
type:
|
|
564
|
+
type: MessageType.RPC
|
|
492
565
|
};
|
|
493
566
|
connection.send(JSON.stringify(response));
|
|
494
567
|
} catch (e) {
|
|
@@ -498,7 +571,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
498
571
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
499
572
|
id: parsed.id,
|
|
500
573
|
success: false,
|
|
501
|
-
type:
|
|
574
|
+
type: MessageType.RPC
|
|
502
575
|
};
|
|
503
576
|
connection.send(JSON.stringify(response));
|
|
504
577
|
console.error("RPC error:", e);
|
|
@@ -516,77 +589,104 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
516
589
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
517
590
|
// must fix this
|
|
518
591
|
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
|
-
|
|
592
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
593
|
+
() => {
|
|
594
|
+
if (this.state) {
|
|
531
595
|
connection.send(
|
|
532
596
|
JSON.stringify({
|
|
533
|
-
|
|
534
|
-
type:
|
|
597
|
+
state: this.state,
|
|
598
|
+
type: MessageType.CF_AGENT_STATE
|
|
535
599
|
})
|
|
536
600
|
);
|
|
601
|
+
}
|
|
537
602
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
603
|
+
connection.send(
|
|
604
|
+
JSON.stringify({
|
|
605
|
+
mcp: this.getMcpServers(),
|
|
606
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
607
|
+
})
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
this.observability?.emit(
|
|
611
|
+
{
|
|
612
|
+
displayMessage: "Connection established",
|
|
613
|
+
id: nanoid(),
|
|
614
|
+
payload: {
|
|
615
|
+
connectionId: connection.id
|
|
547
616
|
},
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
617
|
+
timestamp: Date.now(),
|
|
618
|
+
type: "connect"
|
|
619
|
+
},
|
|
620
|
+
this.ctx
|
|
621
|
+
);
|
|
622
|
+
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
552
623
|
}
|
|
553
624
|
);
|
|
554
625
|
};
|
|
555
626
|
|
|
556
627
|
const _onStart = this.onStart.bind(this);
|
|
557
|
-
this.onStart = async () => {
|
|
628
|
+
this.onStart = async (props?: Props) => {
|
|
558
629
|
return agentContext.run(
|
|
559
|
-
{
|
|
630
|
+
{
|
|
631
|
+
agent: this,
|
|
632
|
+
connection: undefined,
|
|
633
|
+
request: undefined,
|
|
634
|
+
email: undefined
|
|
635
|
+
},
|
|
560
636
|
async () => {
|
|
561
|
-
|
|
637
|
+
await this._tryCatch(() => {
|
|
638
|
+
const servers = this.sql<MCPServerRow>`
|
|
562
639
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
563
640
|
`;
|
|
564
641
|
|
|
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
642
|
this.broadcast(
|
|
583
643
|
JSON.stringify({
|
|
584
644
|
mcp: this.getMcpServers(),
|
|
585
|
-
type:
|
|
645
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
586
646
|
})
|
|
587
647
|
);
|
|
648
|
+
|
|
649
|
+
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
650
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
651
|
+
servers.forEach((server) => {
|
|
652
|
+
this._connectToMcpServerInternal(
|
|
653
|
+
server.name,
|
|
654
|
+
server.server_url,
|
|
655
|
+
server.callback_url,
|
|
656
|
+
server.server_options
|
|
657
|
+
? JSON.parse(server.server_options)
|
|
658
|
+
: undefined,
|
|
659
|
+
{
|
|
660
|
+
id: server.id,
|
|
661
|
+
oauthClientId: server.client_id ?? undefined
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
.then(() => {
|
|
665
|
+
// Broadcast updated MCP servers state after each server connects
|
|
666
|
+
this.broadcast(
|
|
667
|
+
JSON.stringify({
|
|
668
|
+
mcp: this.getMcpServers(),
|
|
669
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
670
|
+
})
|
|
671
|
+
);
|
|
672
|
+
})
|
|
673
|
+
.catch((error) => {
|
|
674
|
+
console.error(
|
|
675
|
+
`Error connecting to MCP server: ${server.name} (${server.server_url})`,
|
|
676
|
+
error
|
|
677
|
+
);
|
|
678
|
+
// Still broadcast even if connection fails, so clients know about the failure
|
|
679
|
+
this.broadcast(
|
|
680
|
+
JSON.stringify({
|
|
681
|
+
mcp: this.getMcpServers(),
|
|
682
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return _onStart(props);
|
|
588
689
|
});
|
|
589
|
-
await this._tryCatch(() => _onStart());
|
|
590
690
|
}
|
|
591
691
|
);
|
|
592
692
|
};
|
|
@@ -596,7 +696,6 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
596
696
|
state: State,
|
|
597
697
|
source: Connection | "server" = "server"
|
|
598
698
|
) {
|
|
599
|
-
const previousState = this._state;
|
|
600
699
|
this._state = state;
|
|
601
700
|
this.sql`
|
|
602
701
|
INSERT OR REPLACE INTO cf_agents_state (id, state)
|
|
@@ -609,25 +708,22 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
609
708
|
this.broadcast(
|
|
610
709
|
JSON.stringify({
|
|
611
710
|
state: state,
|
|
612
|
-
type:
|
|
711
|
+
type: MessageType.CF_AGENT_STATE
|
|
613
712
|
}),
|
|
614
713
|
source !== "server" ? [source.id] : []
|
|
615
714
|
);
|
|
616
715
|
return this._tryCatch(() => {
|
|
617
|
-
const { connection, request } = agentContext.getStore() || {};
|
|
716
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
618
717
|
return agentContext.run(
|
|
619
|
-
{ agent: this, connection, request },
|
|
718
|
+
{ agent: this, connection, request, email },
|
|
620
719
|
async () => {
|
|
621
720
|
this.observability?.emit(
|
|
622
721
|
{
|
|
623
722
|
displayMessage: "State updated",
|
|
624
723
|
id: nanoid(),
|
|
625
|
-
payload: {
|
|
626
|
-
previousState,
|
|
627
|
-
state,
|
|
628
|
-
},
|
|
724
|
+
payload: {},
|
|
629
725
|
timestamp: Date.now(),
|
|
630
|
-
type: "state:update"
|
|
726
|
+
type: "state:update"
|
|
631
727
|
},
|
|
632
728
|
this.ctx
|
|
633
729
|
);
|
|
@@ -656,19 +752,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
656
752
|
}
|
|
657
753
|
|
|
658
754
|
/**
|
|
659
|
-
* Called when the Agent receives an email
|
|
755
|
+
* Called when the Agent receives an email via routeAgentEmail()
|
|
756
|
+
* Override this method to handle incoming emails
|
|
660
757
|
* @param email Email message to process
|
|
661
758
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
759
|
+
async _onEmail(email: AgentEmail) {
|
|
760
|
+
// nb: we use this roundabout way of getting to onEmail
|
|
761
|
+
// because of https://github.com/cloudflare/workerd/issues/4499
|
|
664
762
|
return agentContext.run(
|
|
665
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
763
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
666
764
|
async () => {
|
|
667
|
-
|
|
765
|
+
if ("onEmail" in this && typeof this.onEmail === "function") {
|
|
766
|
+
return this._tryCatch(() =>
|
|
767
|
+
(this.onEmail as (email: AgentEmail) => Promise<void>)(email)
|
|
768
|
+
);
|
|
769
|
+
} else {
|
|
770
|
+
console.log("Received email from:", email.from, "to:", email.to);
|
|
771
|
+
console.log("Subject:", email.headers.get("subject"));
|
|
772
|
+
console.log(
|
|
773
|
+
"Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
668
776
|
}
|
|
669
777
|
);
|
|
670
778
|
}
|
|
671
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Reply to an email
|
|
782
|
+
* @param email The email to reply to
|
|
783
|
+
* @param options Options for the reply
|
|
784
|
+
* @returns void
|
|
785
|
+
*/
|
|
786
|
+
async replyToEmail(
|
|
787
|
+
email: AgentEmail,
|
|
788
|
+
options: {
|
|
789
|
+
fromName: string;
|
|
790
|
+
subject?: string | undefined;
|
|
791
|
+
body: string;
|
|
792
|
+
contentType?: string;
|
|
793
|
+
headers?: Record<string, string>;
|
|
794
|
+
}
|
|
795
|
+
): Promise<void> {
|
|
796
|
+
return this._tryCatch(async () => {
|
|
797
|
+
const agentName = camelCaseToKebabCase(this._ParentClass.name);
|
|
798
|
+
const agentId = this.name;
|
|
799
|
+
|
|
800
|
+
const { createMimeMessage } = await import("mimetext");
|
|
801
|
+
const msg = createMimeMessage();
|
|
802
|
+
msg.setSender({ addr: email.to, name: options.fromName });
|
|
803
|
+
msg.setRecipient(email.from);
|
|
804
|
+
msg.setSubject(
|
|
805
|
+
options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
|
|
806
|
+
);
|
|
807
|
+
msg.addMessage({
|
|
808
|
+
contentType: options.contentType || "text/plain",
|
|
809
|
+
data: options.body
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const domain = email.from.split("@")[1];
|
|
813
|
+
const messageId = `<${agentId}@${domain}>`;
|
|
814
|
+
msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
|
|
815
|
+
msg.setHeader("Message-ID", messageId);
|
|
816
|
+
msg.setHeader("X-Agent-Name", agentName);
|
|
817
|
+
msg.setHeader("X-Agent-ID", agentId);
|
|
818
|
+
|
|
819
|
+
if (options.headers) {
|
|
820
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
821
|
+
msg.setHeader(key, value);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
await email.reply({
|
|
825
|
+
from: email.to,
|
|
826
|
+
raw: msg.asRaw(),
|
|
827
|
+
to: email.from
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
672
832
|
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
673
833
|
try {
|
|
674
834
|
return await fn();
|
|
@@ -677,6 +837,68 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
677
837
|
}
|
|
678
838
|
}
|
|
679
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Automatically wrap custom methods with agent context
|
|
842
|
+
* This ensures getCurrentAgent() works in all custom methods without decorators
|
|
843
|
+
*/
|
|
844
|
+
private _autoWrapCustomMethods() {
|
|
845
|
+
// Collect all methods from base prototypes (Agent and Server)
|
|
846
|
+
const basePrototypes = [Agent.prototype, Server.prototype];
|
|
847
|
+
const baseMethods = new Set<string>();
|
|
848
|
+
for (const baseProto of basePrototypes) {
|
|
849
|
+
let proto = baseProto;
|
|
850
|
+
while (proto && proto !== Object.prototype) {
|
|
851
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
852
|
+
for (const methodName of methodNames) {
|
|
853
|
+
baseMethods.add(methodName);
|
|
854
|
+
}
|
|
855
|
+
proto = Object.getPrototypeOf(proto);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// Get all methods from the current instance's prototype chain
|
|
859
|
+
let proto = Object.getPrototypeOf(this);
|
|
860
|
+
let depth = 0;
|
|
861
|
+
while (proto && proto !== Object.prototype && depth < 10) {
|
|
862
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
863
|
+
for (const methodName of methodNames) {
|
|
864
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
|
|
865
|
+
|
|
866
|
+
// Skip if it's a private method, a base method, a getter, or not a function,
|
|
867
|
+
if (
|
|
868
|
+
baseMethods.has(methodName) ||
|
|
869
|
+
methodName.startsWith("_") ||
|
|
870
|
+
!descriptor ||
|
|
871
|
+
!!descriptor.get ||
|
|
872
|
+
typeof descriptor.value !== "function"
|
|
873
|
+
) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Now, methodName is confirmed to be a custom method/function
|
|
878
|
+
// Wrap the custom method with context
|
|
879
|
+
const wrappedFunction = withAgentContext(
|
|
880
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
881
|
+
this[methodName as keyof this] as (...args: any[]) => any
|
|
882
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
883
|
+
) as any;
|
|
884
|
+
|
|
885
|
+
// if the method is callable, copy the metadata from the original method
|
|
886
|
+
if (this._isCallable(methodName)) {
|
|
887
|
+
callableMetadata.set(
|
|
888
|
+
wrappedFunction,
|
|
889
|
+
callableMetadata.get(this[methodName as keyof this] as Function)!
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// set the wrapped function on the prototype
|
|
894
|
+
this.constructor.prototype[methodName as keyof this] = wrappedFunction;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
proto = Object.getPrototypeOf(proto);
|
|
898
|
+
depth++;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
680
902
|
override onError(
|
|
681
903
|
connection: Connection,
|
|
682
904
|
error: unknown
|
|
@@ -711,6 +933,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
711
933
|
throw new Error("Not implemented");
|
|
712
934
|
}
|
|
713
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Queue a task to be executed in the future
|
|
938
|
+
* @param payload Payload to pass to the callback
|
|
939
|
+
* @param callback Name of the method to call
|
|
940
|
+
* @returns The ID of the queued task
|
|
941
|
+
*/
|
|
942
|
+
async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
|
|
943
|
+
const id = nanoid(9);
|
|
944
|
+
if (typeof callback !== "string") {
|
|
945
|
+
throw new Error("Callback must be a string");
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (typeof this[callback] !== "function") {
|
|
949
|
+
throw new Error(`this.${callback} is not a function`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
this.sql`
|
|
953
|
+
INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
|
|
954
|
+
VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
|
|
955
|
+
`;
|
|
956
|
+
|
|
957
|
+
void this._flushQueue().catch((e) => {
|
|
958
|
+
console.error("Error flushing queue:", e);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
return id;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
private _flushingQueue = false;
|
|
965
|
+
|
|
966
|
+
private async _flushQueue() {
|
|
967
|
+
if (this._flushingQueue) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
this._flushingQueue = true;
|
|
971
|
+
while (true) {
|
|
972
|
+
const result = this.sql<QueueItem<string>>`
|
|
973
|
+
SELECT * FROM cf_agents_queues
|
|
974
|
+
ORDER BY created_at ASC
|
|
975
|
+
`;
|
|
976
|
+
|
|
977
|
+
if (!result || result.length === 0) {
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
for (const row of result || []) {
|
|
982
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
983
|
+
if (!callback) {
|
|
984
|
+
console.error(`callback ${row.callback} not found`);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
988
|
+
await agentContext.run(
|
|
989
|
+
{
|
|
990
|
+
agent: this,
|
|
991
|
+
connection,
|
|
992
|
+
request,
|
|
993
|
+
email
|
|
994
|
+
},
|
|
995
|
+
async () => {
|
|
996
|
+
// TODO: add retries and backoff
|
|
997
|
+
await (
|
|
998
|
+
callback as (
|
|
999
|
+
payload: unknown,
|
|
1000
|
+
queueItem: QueueItem<string>
|
|
1001
|
+
) => Promise<void>
|
|
1002
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1003
|
+
await this.dequeue(row.id);
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
this._flushingQueue = false;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Dequeue a task by ID
|
|
1013
|
+
* @param id ID of the task to dequeue
|
|
1014
|
+
*/
|
|
1015
|
+
async dequeue(id: string) {
|
|
1016
|
+
this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Dequeue all tasks
|
|
1021
|
+
*/
|
|
1022
|
+
async dequeueAll() {
|
|
1023
|
+
this.sql`DELETE FROM cf_agents_queues`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Dequeue all tasks by callback
|
|
1028
|
+
* @param callback Name of the callback to dequeue
|
|
1029
|
+
*/
|
|
1030
|
+
async dequeueAllByCallback(callback: string) {
|
|
1031
|
+
this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Get a queued task by ID
|
|
1036
|
+
* @param id ID of the task to get
|
|
1037
|
+
* @returns The task or undefined if not found
|
|
1038
|
+
*/
|
|
1039
|
+
async getQueue(id: string): Promise<QueueItem<string> | undefined> {
|
|
1040
|
+
const result = this.sql<QueueItem<string>>`
|
|
1041
|
+
SELECT * FROM cf_agents_queues WHERE id = ${id}
|
|
1042
|
+
`;
|
|
1043
|
+
return result
|
|
1044
|
+
? { ...result[0], payload: JSON.parse(result[0].payload) }
|
|
1045
|
+
: undefined;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Get all queues by key and value
|
|
1050
|
+
* @param key Key to filter by
|
|
1051
|
+
* @param value Value to filter by
|
|
1052
|
+
* @returns Array of matching QueueItem objects
|
|
1053
|
+
*/
|
|
1054
|
+
async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
|
|
1055
|
+
const result = this.sql<QueueItem<string>>`
|
|
1056
|
+
SELECT * FROM cf_agents_queues
|
|
1057
|
+
`;
|
|
1058
|
+
return result.filter((row) => JSON.parse(row.payload)[key] === value);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
714
1061
|
/**
|
|
715
1062
|
* Schedule a task to be executed in the future
|
|
716
1063
|
* @template T Type of the payload data
|
|
@@ -731,9 +1078,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
731
1078
|
{
|
|
732
1079
|
displayMessage: `Schedule ${schedule.id} created`,
|
|
733
1080
|
id: nanoid(),
|
|
734
|
-
payload:
|
|
1081
|
+
payload: {
|
|
1082
|
+
callback: callback as string,
|
|
1083
|
+
id: id
|
|
1084
|
+
},
|
|
735
1085
|
timestamp: Date.now(),
|
|
736
|
-
type: "schedule:create"
|
|
1086
|
+
type: "schedule:create"
|
|
737
1087
|
},
|
|
738
1088
|
this.ctx
|
|
739
1089
|
);
|
|
@@ -762,7 +1112,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
762
1112
|
id,
|
|
763
1113
|
payload: payload as T,
|
|
764
1114
|
time: timestamp,
|
|
765
|
-
type: "scheduled"
|
|
1115
|
+
type: "scheduled"
|
|
766
1116
|
};
|
|
767
1117
|
|
|
768
1118
|
emitScheduleCreate(schedule);
|
|
@@ -788,7 +1138,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
788
1138
|
id,
|
|
789
1139
|
payload: payload as T,
|
|
790
1140
|
time: timestamp,
|
|
791
|
-
type: "delayed"
|
|
1141
|
+
type: "delayed"
|
|
792
1142
|
};
|
|
793
1143
|
|
|
794
1144
|
emitScheduleCreate(schedule);
|
|
@@ -814,7 +1164,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
814
1164
|
id,
|
|
815
1165
|
payload: payload as T,
|
|
816
1166
|
time: timestamp,
|
|
817
|
-
type: "cron"
|
|
1167
|
+
type: "cron"
|
|
818
1168
|
};
|
|
819
1169
|
|
|
820
1170
|
emitScheduleCreate(schedule);
|
|
@@ -883,7 +1233,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
883
1233
|
.toArray()
|
|
884
1234
|
.map((row) => ({
|
|
885
1235
|
...row,
|
|
886
|
-
payload: JSON.parse(row.payload as string) as T
|
|
1236
|
+
payload: JSON.parse(row.payload as string) as T
|
|
887
1237
|
})) as Schedule<T>[];
|
|
888
1238
|
|
|
889
1239
|
return result;
|
|
@@ -901,9 +1251,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
901
1251
|
{
|
|
902
1252
|
displayMessage: `Schedule ${id} cancelled`,
|
|
903
1253
|
id: nanoid(),
|
|
904
|
-
payload:
|
|
1254
|
+
payload: {
|
|
1255
|
+
callback: schedule.callback,
|
|
1256
|
+
id: schedule.id
|
|
1257
|
+
},
|
|
905
1258
|
timestamp: Date.now(),
|
|
906
|
-
type: "schedule:cancel"
|
|
1259
|
+
type: "schedule:cancel"
|
|
907
1260
|
},
|
|
908
1261
|
this.ctx
|
|
909
1262
|
);
|
|
@@ -917,9 +1270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
917
1270
|
private async _scheduleNextAlarm() {
|
|
918
1271
|
// Find the next schedule that needs to be executed
|
|
919
1272
|
const result = this.sql`
|
|
920
|
-
SELECT time FROM cf_agents_schedules
|
|
1273
|
+
SELECT time FROM cf_agents_schedules
|
|
921
1274
|
WHERE time > ${Math.floor(Date.now() / 1000)}
|
|
922
|
-
ORDER BY time ASC
|
|
1275
|
+
ORDER BY time ASC
|
|
923
1276
|
LIMIT 1
|
|
924
1277
|
`;
|
|
925
1278
|
if (!result) return;
|
|
@@ -946,51 +1299,61 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
946
1299
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
947
1300
|
`;
|
|
948
1301
|
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1302
|
+
if (result && Array.isArray(result)) {
|
|
1303
|
+
for (const row of result) {
|
|
1304
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
1305
|
+
if (!callback) {
|
|
1306
|
+
console.error(`callback ${row.callback} not found`);
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
await agentContext.run(
|
|
1310
|
+
{
|
|
1311
|
+
agent: this,
|
|
1312
|
+
connection: undefined,
|
|
1313
|
+
request: undefined,
|
|
1314
|
+
email: undefined
|
|
1315
|
+
},
|
|
1316
|
+
async () => {
|
|
1317
|
+
try {
|
|
1318
|
+
this.observability?.emit(
|
|
1319
|
+
{
|
|
1320
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
1321
|
+
id: nanoid(),
|
|
1322
|
+
payload: {
|
|
1323
|
+
callback: row.callback,
|
|
1324
|
+
id: row.id
|
|
1325
|
+
},
|
|
1326
|
+
timestamp: Date.now(),
|
|
1327
|
+
type: "schedule:execute"
|
|
1328
|
+
},
|
|
1329
|
+
this.ctx
|
|
1330
|
+
);
|
|
969
1331
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1332
|
+
await (
|
|
1333
|
+
callback as (
|
|
1334
|
+
payload: unknown,
|
|
1335
|
+
schedule: Schedule<unknown>
|
|
1336
|
+
) => Promise<void>
|
|
1337
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
console.error(`error executing callback "${row.callback}"`, e);
|
|
1340
|
+
}
|
|
978
1341
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
1342
|
+
);
|
|
1343
|
+
if (row.type === "cron") {
|
|
1344
|
+
// Update next execution time for cron schedules
|
|
1345
|
+
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1346
|
+
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
985
1347
|
|
|
986
|
-
|
|
1348
|
+
this.sql`
|
|
987
1349
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
988
1350
|
`;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1351
|
+
} else {
|
|
1352
|
+
// Delete one-time schedules after execution
|
|
1353
|
+
this.sql`
|
|
992
1354
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
993
1355
|
`;
|
|
1356
|
+
}
|
|
994
1357
|
}
|
|
995
1358
|
}
|
|
996
1359
|
|
|
@@ -1006,6 +1369,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1006
1369
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1007
1370
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1008
1371
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1372
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1009
1373
|
|
|
1010
1374
|
// delete all alarms
|
|
1011
1375
|
await this.ctx.storage.deleteAlarm();
|
|
@@ -1018,7 +1382,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1018
1382
|
id: nanoid(),
|
|
1019
1383
|
payload: {},
|
|
1020
1384
|
timestamp: Date.now(),
|
|
1021
|
-
type: "destroy"
|
|
1385
|
+
type: "destroy"
|
|
1022
1386
|
},
|
|
1023
1387
|
this.ctx
|
|
1024
1388
|
);
|
|
@@ -1078,7 +1442,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1078
1442
|
this.broadcast(
|
|
1079
1443
|
JSON.stringify({
|
|
1080
1444
|
mcp: this.getMcpServers(),
|
|
1081
|
-
type:
|
|
1445
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1082
1446
|
})
|
|
1083
1447
|
);
|
|
1084
1448
|
|
|
@@ -1134,12 +1498,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1134
1498
|
fetch: (url, init) =>
|
|
1135
1499
|
fetch(url, {
|
|
1136
1500
|
...init,
|
|
1137
|
-
headers: options?.transport?.headers
|
|
1138
|
-
})
|
|
1501
|
+
headers: options?.transport?.headers
|
|
1502
|
+
})
|
|
1139
1503
|
},
|
|
1140
1504
|
requestInit: {
|
|
1141
|
-
headers: options?.transport?.headers
|
|
1142
|
-
}
|
|
1505
|
+
headers: options?.transport?.headers
|
|
1506
|
+
}
|
|
1143
1507
|
};
|
|
1144
1508
|
}
|
|
1145
1509
|
|
|
@@ -1148,14 +1512,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1148
1512
|
reconnect,
|
|
1149
1513
|
transport: {
|
|
1150
1514
|
...headerTransportOpts,
|
|
1151
|
-
authProvider
|
|
1152
|
-
}
|
|
1515
|
+
authProvider
|
|
1516
|
+
}
|
|
1153
1517
|
});
|
|
1154
1518
|
|
|
1155
1519
|
return {
|
|
1156
1520
|
authUrl,
|
|
1157
1521
|
clientId,
|
|
1158
|
-
id
|
|
1522
|
+
id
|
|
1159
1523
|
};
|
|
1160
1524
|
}
|
|
1161
1525
|
|
|
@@ -1167,7 +1531,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1167
1531
|
this.broadcast(
|
|
1168
1532
|
JSON.stringify({
|
|
1169
1533
|
mcp: this.getMcpServers(),
|
|
1170
|
-
type:
|
|
1534
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1171
1535
|
})
|
|
1172
1536
|
);
|
|
1173
1537
|
}
|
|
@@ -1177,30 +1541,35 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1177
1541
|
prompts: this.mcp.listPrompts(),
|
|
1178
1542
|
resources: this.mcp.listResources(),
|
|
1179
1543
|
servers: {},
|
|
1180
|
-
tools: this.mcp.listTools()
|
|
1544
|
+
tools: this.mcp.listTools()
|
|
1181
1545
|
};
|
|
1182
1546
|
|
|
1183
1547
|
const servers = this.sql<MCPServerRow>`
|
|
1184
1548
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1185
1549
|
`;
|
|
1186
1550
|
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1551
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
1552
|
+
for (const server of servers) {
|
|
1553
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1554
|
+
mcpState.servers[server.id] = {
|
|
1555
|
+
auth_url: server.auth_url,
|
|
1556
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1557
|
+
instructions: serverConn?.instructions ?? null,
|
|
1558
|
+
name: server.name,
|
|
1559
|
+
server_url: server.server_url,
|
|
1560
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1561
|
+
state: serverConn?.connectionState ?? "authenticating"
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1198
1564
|
}
|
|
1199
1565
|
|
|
1200
1566
|
return mcpState;
|
|
1201
1567
|
}
|
|
1202
1568
|
}
|
|
1203
1569
|
|
|
1570
|
+
// A set of classes that have been wrapped with agent context
|
|
1571
|
+
const wrappedClasses = new Set<typeof Agent.prototype.constructor>();
|
|
1572
|
+
|
|
1204
1573
|
/**
|
|
1205
1574
|
* Namespace for creating Agent instances
|
|
1206
1575
|
* @template Agentic Type of the Agent class
|
|
@@ -1241,14 +1610,14 @@ export async function routeAgentRequest<Env>(
|
|
|
1241
1610
|
"Access-Control-Allow-Credentials": "true",
|
|
1242
1611
|
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1243
1612
|
"Access-Control-Allow-Origin": "*",
|
|
1244
|
-
"Access-Control-Max-Age": "86400"
|
|
1613
|
+
"Access-Control-Max-Age": "86400"
|
|
1245
1614
|
}
|
|
1246
1615
|
: options?.cors;
|
|
1247
1616
|
|
|
1248
1617
|
if (request.method === "OPTIONS") {
|
|
1249
1618
|
if (corsHeaders) {
|
|
1250
1619
|
return new Response(null, {
|
|
1251
|
-
headers: corsHeaders
|
|
1620
|
+
headers: corsHeaders
|
|
1252
1621
|
});
|
|
1253
1622
|
}
|
|
1254
1623
|
console.warn(
|
|
@@ -1261,7 +1630,7 @@ export async function routeAgentRequest<Env>(
|
|
|
1261
1630
|
env as Record<string, unknown>,
|
|
1262
1631
|
{
|
|
1263
1632
|
prefix: "agents",
|
|
1264
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1633
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1265
1634
|
}
|
|
1266
1635
|
);
|
|
1267
1636
|
|
|
@@ -1274,24 +1643,238 @@ export async function routeAgentRequest<Env>(
|
|
|
1274
1643
|
response = new Response(response.body, {
|
|
1275
1644
|
headers: {
|
|
1276
1645
|
...response.headers,
|
|
1277
|
-
...corsHeaders
|
|
1278
|
-
}
|
|
1646
|
+
...corsHeaders
|
|
1647
|
+
}
|
|
1279
1648
|
});
|
|
1280
1649
|
}
|
|
1281
1650
|
return response;
|
|
1282
1651
|
}
|
|
1283
1652
|
|
|
1653
|
+
export type EmailResolver<Env> = (
|
|
1654
|
+
email: ForwardableEmailMessage,
|
|
1655
|
+
env: Env
|
|
1656
|
+
) => Promise<{
|
|
1657
|
+
agentName: string;
|
|
1658
|
+
agentId: string;
|
|
1659
|
+
} | null>;
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Create a resolver that uses the message-id header to determine the agent to route the email to
|
|
1663
|
+
* @returns A function that resolves the agent to route the email to
|
|
1664
|
+
*/
|
|
1665
|
+
export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
|
|
1666
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1667
|
+
const messageId = email.headers.get("message-id");
|
|
1668
|
+
if (messageId) {
|
|
1669
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1670
|
+
if (messageIdMatch) {
|
|
1671
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1672
|
+
const agentName = domain.split(".")[0];
|
|
1673
|
+
return { agentName, agentId };
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const references = email.headers.get("references");
|
|
1678
|
+
if (references) {
|
|
1679
|
+
const referencesMatch = references.match(
|
|
1680
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1681
|
+
);
|
|
1682
|
+
if (referencesMatch) {
|
|
1683
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1684
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1685
|
+
const agentName = domain.split(".")[0];
|
|
1686
|
+
return { agentName, agentId };
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1691
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1692
|
+
if (agentName && agentId) {
|
|
1693
|
+
return { agentName, agentId };
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return null;
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
1702
|
+
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
1703
|
+
* @returns A function that resolves the agent to route the email to
|
|
1704
|
+
*/
|
|
1705
|
+
export function createAddressBasedEmailResolver<Env>(
|
|
1706
|
+
defaultAgentName: string
|
|
1707
|
+
): EmailResolver<Env> {
|
|
1708
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1709
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1710
|
+
if (!emailMatch) {
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1715
|
+
|
|
1716
|
+
if (subAddress) {
|
|
1717
|
+
return {
|
|
1718
|
+
agentName: localPart,
|
|
1719
|
+
agentId: subAddress
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1724
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1725
|
+
return {
|
|
1726
|
+
agentName: defaultAgentName,
|
|
1727
|
+
agentId: localPart
|
|
1728
|
+
};
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
1734
|
+
* @param agentName The name of the agent to route the email to
|
|
1735
|
+
* @param agentId The id of the agent to route the email to
|
|
1736
|
+
* @returns A function that resolves the agent to route the email to
|
|
1737
|
+
*/
|
|
1738
|
+
export function createCatchAllEmailResolver<Env>(
|
|
1739
|
+
agentName: string,
|
|
1740
|
+
agentId: string
|
|
1741
|
+
): EmailResolver<Env> {
|
|
1742
|
+
return async () => ({ agentName, agentId });
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1746
|
+
resolver: EmailResolver<Env>;
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
// Cache the agent namespace map for email routing
|
|
1750
|
+
// This maps both kebab-case and original names to namespaces
|
|
1751
|
+
const agentMapCache = new WeakMap<
|
|
1752
|
+
Record<string, unknown>,
|
|
1753
|
+
Record<string, unknown>
|
|
1754
|
+
>();
|
|
1755
|
+
|
|
1284
1756
|
/**
|
|
1285
1757
|
* Route an email to the appropriate Agent
|
|
1286
|
-
* @param email
|
|
1287
|
-
* @param env
|
|
1288
|
-
* @param options
|
|
1758
|
+
* @param email The email to route
|
|
1759
|
+
* @param env The environment containing the Agent bindings
|
|
1760
|
+
* @param options The options for routing the email
|
|
1761
|
+
* @returns A promise that resolves when the email has been routed
|
|
1289
1762
|
*/
|
|
1290
1763
|
export async function routeAgentEmail<Env>(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
): Promise<void> {
|
|
1764
|
+
email: ForwardableEmailMessage,
|
|
1765
|
+
env: Env,
|
|
1766
|
+
options: EmailRoutingOptions<Env>
|
|
1767
|
+
): Promise<void> {
|
|
1768
|
+
const routingInfo = await options.resolver(email, env);
|
|
1769
|
+
|
|
1770
|
+
if (!routingInfo) {
|
|
1771
|
+
console.warn("No routing information found for email, dropping message");
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// Build a map that includes both original names and kebab-case versions
|
|
1776
|
+
if (!agentMapCache.has(env as Record<string, unknown>)) {
|
|
1777
|
+
const map: Record<string, unknown> = {};
|
|
1778
|
+
for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
|
|
1779
|
+
if (
|
|
1780
|
+
value &&
|
|
1781
|
+
typeof value === "object" &&
|
|
1782
|
+
"idFromName" in value &&
|
|
1783
|
+
typeof value.idFromName === "function"
|
|
1784
|
+
) {
|
|
1785
|
+
// Add both the original name and kebab-case version
|
|
1786
|
+
map[key] = value;
|
|
1787
|
+
map[camelCaseToKebabCase(key)] = value;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
agentMapCache.set(env as Record<string, unknown>, map);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
|
|
1794
|
+
const namespace = agentMap[routingInfo.agentName];
|
|
1795
|
+
|
|
1796
|
+
if (!namespace) {
|
|
1797
|
+
// Provide helpful error message listing available agents
|
|
1798
|
+
const availableAgents = Object.keys(agentMap)
|
|
1799
|
+
.filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
|
|
1800
|
+
.join(", ");
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const agent = await getAgentByName(
|
|
1807
|
+
namespace as unknown as AgentNamespace<Agent<Env>>,
|
|
1808
|
+
routingInfo.agentId
|
|
1809
|
+
);
|
|
1810
|
+
|
|
1811
|
+
// let's make a serialisable version of the email
|
|
1812
|
+
const serialisableEmail: AgentEmail = {
|
|
1813
|
+
getRaw: async () => {
|
|
1814
|
+
const reader = email.raw.getReader();
|
|
1815
|
+
const chunks: Uint8Array[] = [];
|
|
1816
|
+
|
|
1817
|
+
let done = false;
|
|
1818
|
+
while (!done) {
|
|
1819
|
+
const { value, done: readerDone } = await reader.read();
|
|
1820
|
+
done = readerDone;
|
|
1821
|
+
if (value) {
|
|
1822
|
+
chunks.push(value);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1827
|
+
const combined = new Uint8Array(totalLength);
|
|
1828
|
+
let offset = 0;
|
|
1829
|
+
for (const chunk of chunks) {
|
|
1830
|
+
combined.set(chunk, offset);
|
|
1831
|
+
offset += chunk.length;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
return combined;
|
|
1835
|
+
},
|
|
1836
|
+
headers: email.headers,
|
|
1837
|
+
rawSize: email.rawSize,
|
|
1838
|
+
setReject: (reason: string) => {
|
|
1839
|
+
email.setReject(reason);
|
|
1840
|
+
},
|
|
1841
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1842
|
+
return email.forward(rcptTo, headers);
|
|
1843
|
+
},
|
|
1844
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1845
|
+
return email.reply(
|
|
1846
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1847
|
+
);
|
|
1848
|
+
},
|
|
1849
|
+
from: email.from,
|
|
1850
|
+
to: email.to
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
await agent._onEmail(serialisableEmail);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
export type AgentEmail = {
|
|
1857
|
+
from: string;
|
|
1858
|
+
to: string;
|
|
1859
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1860
|
+
headers: Headers;
|
|
1861
|
+
rawSize: number;
|
|
1862
|
+
setReject: (reason: string) => void;
|
|
1863
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1864
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
export type EmailSendOptions = {
|
|
1868
|
+
to: string;
|
|
1869
|
+
subject: string;
|
|
1870
|
+
body: string;
|
|
1871
|
+
contentType?: string;
|
|
1872
|
+
headers?: Record<string, string>;
|
|
1873
|
+
includeRoutingHeaders?: boolean;
|
|
1874
|
+
agentName?: string;
|
|
1875
|
+
agentId?: string;
|
|
1876
|
+
domain?: string;
|
|
1877
|
+
};
|
|
1295
1878
|
|
|
1296
1879
|
/**
|
|
1297
1880
|
* Get or create an Agent by name
|
|
@@ -1302,12 +1885,17 @@ export async function routeAgentEmail<Env>(
|
|
|
1302
1885
|
* @param options Options for Agent creation
|
|
1303
1886
|
* @returns Promise resolving to an Agent instance stub
|
|
1304
1887
|
*/
|
|
1305
|
-
export async function getAgentByName<
|
|
1888
|
+
export async function getAgentByName<
|
|
1889
|
+
Env,
|
|
1890
|
+
T extends Agent<Env>,
|
|
1891
|
+
Props extends Record<string, unknown> = Record<string, unknown>
|
|
1892
|
+
>(
|
|
1306
1893
|
namespace: AgentNamespace<T>,
|
|
1307
1894
|
name: string,
|
|
1308
1895
|
options?: {
|
|
1309
1896
|
jurisdiction?: DurableObjectJurisdiction;
|
|
1310
1897
|
locationHint?: DurableObjectLocationHint;
|
|
1898
|
+
props?: Props;
|
|
1311
1899
|
}
|
|
1312
1900
|
) {
|
|
1313
1901
|
return getServerByName<Env, T>(namespace, name, options);
|
|
@@ -1339,7 +1927,7 @@ export class StreamingResponse {
|
|
|
1339
1927
|
id: this._id,
|
|
1340
1928
|
result: chunk,
|
|
1341
1929
|
success: true,
|
|
1342
|
-
type:
|
|
1930
|
+
type: MessageType.RPC
|
|
1343
1931
|
};
|
|
1344
1932
|
this._connection.send(JSON.stringify(response));
|
|
1345
1933
|
}
|
|
@@ -1358,7 +1946,7 @@ export class StreamingResponse {
|
|
|
1358
1946
|
id: this._id,
|
|
1359
1947
|
result: finalChunk,
|
|
1360
1948
|
success: true,
|
|
1361
|
-
type:
|
|
1949
|
+
type: MessageType.RPC
|
|
1362
1950
|
};
|
|
1363
1951
|
this._connection.send(JSON.stringify(response));
|
|
1364
1952
|
}
|