agents 0.0.0-7f84d28 → 0.0.0-804e02e
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/_esm-LV5FJ3HK.js +3922 -0
- package/dist/_esm-LV5FJ3HK.js.map +1 -0
- package/dist/ai-chat-agent.d.ts +13 -9
- package/dist/ai-chat-agent.js +189 -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 +20 -0
- package/dist/ai-chat-v5-migration.js.map +1 -0
- package/dist/ai-react.d.ts +70 -72
- package/dist/ai-react.js +260 -107
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +37 -19
- package/dist/ai-types.js +7 -0
- package/dist/ccip-CMBYN64O.js +15 -0
- package/dist/ccip-CMBYN64O.js.map +1 -0
- package/dist/chunk-5Y6BEZDY.js +276 -0
- package/dist/chunk-5Y6BEZDY.js.map +1 -0
- package/dist/chunk-BER7KXUJ.js +18 -0
- package/dist/chunk-BER7KXUJ.js.map +1 -0
- package/dist/chunk-JJBFIGUC.js +5202 -0
- package/dist/chunk-JJBFIGUC.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +43 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/{chunk-Y67CHZBI.js → chunk-QEPGNUG6.js} +231 -45
- package/dist/chunk-QEPGNUG6.js.map +1 -0
- package/dist/{chunk-QSGN3REV.js → chunk-QEVM4BVL.js} +10 -17
- package/dist/chunk-QEVM4BVL.js.map +1 -0
- package/dist/chunk-RS5OCNEQ.js +1323 -0
- package/dist/chunk-RS5OCNEQ.js.map +1 -0
- package/dist/chunk-TYAY6AU6.js +159 -0
- package/dist/chunk-TYAY6AU6.js.map +1 -0
- package/dist/chunk-UJVEAURM.js +150 -0
- package/dist/chunk-UJVEAURM.js.map +1 -0
- package/dist/{chunk-BZXOAZUX.js → chunk-XFS5ERG3.js} +27 -6
- package/dist/chunk-XFS5ERG3.js.map +1 -0
- package/dist/client-BohGLma8.d.ts +5041 -0
- package/dist/client.d.ts +8 -2
- package/dist/client.js +3 -1
- package/dist/index.d.ts +196 -25
- package/dist/index.js +14 -4
- package/dist/mcp/client.d.ts +9 -781
- package/dist/mcp/client.js +2 -1
- package/dist/mcp/do-oauth-client-provider.d.ts +9 -0
- package/dist/mcp/do-oauth-client-provider.js +2 -1
- package/dist/mcp/index.d.ts +58 -64
- package/dist/mcp/index.js +957 -637
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/x402.d.ts +31 -0
- package/dist/mcp/x402.js +3195 -0
- package/dist/mcp/x402.js.map +1 -0
- package/dist/observability/index.d.ts +46 -0
- package/dist/observability/index.js +12 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/react.d.ts +81 -11
- package/dist/react.js +22 -9
- package/dist/react.js.map +1 -1
- package/dist/schedule.d.ts +81 -7
- package/dist/schedule.js +21 -6
- package/dist/schedule.js.map +1 -1
- package/dist/secp256k1-M22GZP2U.js +2193 -0
- package/dist/secp256k1-M22GZP2U.js.map +1 -0
- package/dist/serializable.d.ts +32 -0
- package/dist/serializable.js +1 -0
- package/dist/serializable.js.map +1 -0
- package/package.json +91 -70
- package/src/index.ts +947 -203
- package/dist/chunk-BZXOAZUX.js.map +0 -1
- package/dist/chunk-QSGN3REV.js.map +0 -1
- package/dist/chunk-RIYR6FR6.js +0 -777
- package/dist/chunk-RIYR6FR6.js.map +0 -1
- package/dist/chunk-Y67CHZBI.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
+
import type { env } from "cloudflare:workers";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Prompt,
|
|
8
|
+
Resource,
|
|
9
|
+
ServerCapabilities,
|
|
10
|
+
Tool
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { parseCronExpression } from "cron-schedule";
|
|
13
|
+
import { nanoid } from "nanoid";
|
|
14
|
+
import { EmailMessage } from "cloudflare:email";
|
|
1
15
|
import {
|
|
2
|
-
Server,
|
|
3
|
-
routePartykitRequest,
|
|
4
|
-
type PartyServerOptions,
|
|
5
|
-
getServerByName,
|
|
6
16
|
type Connection,
|
|
7
17
|
type ConnectionContext,
|
|
18
|
+
type PartyServerOptions,
|
|
19
|
+
Server,
|
|
8
20
|
type WSMessage,
|
|
21
|
+
getServerByName,
|
|
22
|
+
routePartykitRequest
|
|
9
23
|
} from "partyserver";
|
|
10
|
-
|
|
11
|
-
import { parseCronExpression } from "cron-schedule";
|
|
12
|
-
import { nanoid } from "nanoid";
|
|
13
|
-
|
|
14
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
-
import { MCPClientManager } from "./mcp/client";
|
|
16
|
-
import {
|
|
17
|
-
DurableObjectOAuthClientProvider,
|
|
18
|
-
type AgentsOAuthProvider,
|
|
19
|
-
} from "./mcp/do-oauth-client-provider";
|
|
20
|
-
import type {
|
|
21
|
-
Tool,
|
|
22
|
-
Resource,
|
|
23
|
-
Prompt,
|
|
24
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
-
|
|
26
|
-
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
27
|
-
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
28
|
-
|
|
29
24
|
import { camelCaseToKebabCase } from "./client";
|
|
25
|
+
import { MCPClientManager } from "./mcp/client";
|
|
26
|
+
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
|
|
27
|
+
import type { TransportType } from "./mcp/types";
|
|
28
|
+
import { genericObservability, type Observability } from "./observability";
|
|
29
|
+
import { MessageType } from "./ai-types";
|
|
30
30
|
|
|
31
|
-
export type { Connection,
|
|
31
|
+
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* RPC request message from client
|
|
@@ -44,7 +44,7 @@ export type RPCRequest = {
|
|
|
44
44
|
* State update message from client
|
|
45
45
|
*/
|
|
46
46
|
export type StateUpdateMessage = {
|
|
47
|
-
type:
|
|
47
|
+
type: MessageType.CF_AGENT_STATE;
|
|
48
48
|
state: unknown;
|
|
49
49
|
};
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@ export type StateUpdateMessage = {
|
|
|
52
52
|
* RPC response message to client
|
|
53
53
|
*/
|
|
54
54
|
export type RPCResponse = {
|
|
55
|
-
type:
|
|
55
|
+
type: MessageType.RPC;
|
|
56
56
|
id: string;
|
|
57
57
|
} & (
|
|
58
58
|
| {
|
|
@@ -79,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
|
|
|
79
79
|
typeof msg === "object" &&
|
|
80
80
|
msg !== null &&
|
|
81
81
|
"type" in msg &&
|
|
82
|
-
msg.type ===
|
|
82
|
+
msg.type === MessageType.RPC &&
|
|
83
83
|
"id" in msg &&
|
|
84
84
|
typeof msg.id === "string" &&
|
|
85
85
|
"method" in msg &&
|
|
@@ -97,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
|
|
|
97
97
|
typeof msg === "object" &&
|
|
98
98
|
msg !== null &&
|
|
99
99
|
"type" in msg &&
|
|
100
|
-
msg.type ===
|
|
100
|
+
msg.type === MessageType.CF_AGENT_STATE &&
|
|
101
101
|
"state" in msg
|
|
102
102
|
);
|
|
103
103
|
}
|
|
@@ -112,16 +112,16 @@ export type CallableMetadata = {
|
|
|
112
112
|
streaming?: boolean;
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
116
115
|
const callableMetadata = new Map<Function, CallableMetadata>();
|
|
117
116
|
|
|
118
117
|
/**
|
|
119
118
|
* Decorator that marks a method as callable by clients
|
|
120
119
|
* @param metadata Optional metadata about the callable method
|
|
121
120
|
*/
|
|
122
|
-
export function
|
|
121
|
+
export function callable(metadata: CallableMetadata = {}) {
|
|
123
122
|
return function callableDecorator<This, Args extends unknown[], Return>(
|
|
124
123
|
target: (this: This, ...args: Args) => Return,
|
|
124
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: later
|
|
125
125
|
context: ClassMethodDecoratorContext
|
|
126
126
|
) {
|
|
127
127
|
if (!callableMetadata.has(target)) {
|
|
@@ -132,6 +132,30 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
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
|
+
|
|
135
159
|
/**
|
|
136
160
|
* Represents a scheduled task within an Agent
|
|
137
161
|
* @template T Type of the payload data
|
|
@@ -173,11 +197,13 @@ function getNextCronTime(cron: string) {
|
|
|
173
197
|
return interval.getNextDate();
|
|
174
198
|
}
|
|
175
199
|
|
|
200
|
+
export type { TransportType } from "./mcp/types";
|
|
201
|
+
|
|
176
202
|
/**
|
|
177
203
|
* MCP Server state update message from server -> Client
|
|
178
204
|
*/
|
|
179
205
|
export type MCPServerMessage = {
|
|
180
|
-
type:
|
|
206
|
+
type: MessageType.CF_AGENT_MCP_SERVERS;
|
|
181
207
|
mcp: MCPServersState;
|
|
182
208
|
};
|
|
183
209
|
|
|
@@ -194,7 +220,12 @@ export type MCPServer = {
|
|
|
194
220
|
name: string;
|
|
195
221
|
server_url: string;
|
|
196
222
|
auth_url: string | null;
|
|
223
|
+
// This state is specifically about the temporary process of getting a token (if needed).
|
|
224
|
+
// Scope outside of that can't be relied upon because when the DO sleeps, there's no way
|
|
225
|
+
// to communicate a change to a non-ready state.
|
|
197
226
|
state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
|
|
227
|
+
instructions: string | null;
|
|
228
|
+
capabilities: ServerCapabilities | null;
|
|
198
229
|
};
|
|
199
230
|
|
|
200
231
|
/**
|
|
@@ -216,23 +247,26 @@ const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
|
216
247
|
const DEFAULT_STATE = {} as unknown;
|
|
217
248
|
|
|
218
249
|
const agentContext = new AsyncLocalStorage<{
|
|
219
|
-
agent: Agent<unknown>;
|
|
250
|
+
agent: Agent<unknown, unknown>;
|
|
220
251
|
connection: Connection | undefined;
|
|
221
252
|
request: Request | undefined;
|
|
253
|
+
email: AgentEmail | undefined;
|
|
222
254
|
}>();
|
|
223
255
|
|
|
224
256
|
export function getCurrentAgent<
|
|
225
|
-
T extends Agent<unknown, unknown> = Agent<unknown, unknown
|
|
257
|
+
T extends Agent<unknown, unknown> = Agent<unknown, unknown>
|
|
226
258
|
>(): {
|
|
227
259
|
agent: T | undefined;
|
|
228
260
|
connection: Connection | undefined;
|
|
229
|
-
request: Request
|
|
261
|
+
request: Request | undefined;
|
|
262
|
+
email: AgentEmail | undefined;
|
|
230
263
|
} {
|
|
231
264
|
const store = agentContext.getStore() as
|
|
232
265
|
| {
|
|
233
266
|
agent: T;
|
|
234
267
|
connection: Connection | undefined;
|
|
235
|
-
request: Request
|
|
268
|
+
request: Request | undefined;
|
|
269
|
+
email: AgentEmail | undefined;
|
|
236
270
|
}
|
|
237
271
|
| undefined;
|
|
238
272
|
if (!store) {
|
|
@@ -240,23 +274,56 @@ export function getCurrentAgent<
|
|
|
240
274
|
agent: undefined,
|
|
241
275
|
connection: undefined,
|
|
242
276
|
request: undefined,
|
|
277
|
+
email: undefined
|
|
243
278
|
};
|
|
244
279
|
}
|
|
245
280
|
return store;
|
|
246
281
|
}
|
|
247
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
|
|
285
|
+
* @param agent The agent instance
|
|
286
|
+
* @param method The method to wrap
|
|
287
|
+
* @returns A wrapped method that runs within the agent context
|
|
288
|
+
*/
|
|
289
|
+
|
|
290
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
291
|
+
function withAgentContext<T extends (...args: any[]) => any>(
|
|
292
|
+
method: T
|
|
293
|
+
): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
|
|
294
|
+
return function (...args: Parameters<T>): ReturnType<T> {
|
|
295
|
+
const { connection, request, email, agent } = getCurrentAgent();
|
|
296
|
+
|
|
297
|
+
if (agent === this) {
|
|
298
|
+
// already wrapped, so we can just call the method
|
|
299
|
+
return method.apply(this, args);
|
|
300
|
+
}
|
|
301
|
+
// not wrapped, so we need to wrap it
|
|
302
|
+
return agentContext.run({ agent: this, connection, request, email }, () => {
|
|
303
|
+
return method.apply(this, args);
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
248
308
|
/**
|
|
249
309
|
* Base class for creating Agent implementations
|
|
250
310
|
* @template Env Environment type containing bindings
|
|
251
311
|
* @template State State type to store within the Agent
|
|
252
312
|
*/
|
|
253
|
-
export class Agent<
|
|
313
|
+
export class Agent<
|
|
314
|
+
Env = typeof env,
|
|
315
|
+
State = unknown,
|
|
316
|
+
Props extends Record<string, unknown> = Record<string, unknown>
|
|
317
|
+
> extends Server<Env, Props> {
|
|
254
318
|
private _state = DEFAULT_STATE as State;
|
|
255
319
|
|
|
256
320
|
private _ParentClass: typeof Agent<Env, State> =
|
|
257
321
|
Object.getPrototypeOf(this).constructor;
|
|
258
322
|
|
|
259
|
-
mcp: MCPClientManager = new MCPClientManager(
|
|
323
|
+
readonly mcp: MCPClientManager = new MCPClientManager(
|
|
324
|
+
this._ParentClass.name,
|
|
325
|
+
"0.0.1"
|
|
326
|
+
);
|
|
260
327
|
|
|
261
328
|
/**
|
|
262
329
|
* Initial state for the Agent
|
|
@@ -312,9 +379,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
312
379
|
*/
|
|
313
380
|
static options = {
|
|
314
381
|
/** Whether the Agent should hibernate when inactive */
|
|
315
|
-
hibernate: true
|
|
382
|
+
hibernate: true // default to hibernate
|
|
316
383
|
};
|
|
317
384
|
|
|
385
|
+
/**
|
|
386
|
+
* The observability implementation to use for the Agent
|
|
387
|
+
*/
|
|
388
|
+
observability?: Observability = genericObservability;
|
|
389
|
+
|
|
318
390
|
/**
|
|
319
391
|
* Execute SQL queries against the Agent's database
|
|
320
392
|
* @template T Type of the returned rows
|
|
@@ -344,6 +416,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
344
416
|
constructor(ctx: AgentContext, env: Env) {
|
|
345
417
|
super(ctx, env);
|
|
346
418
|
|
|
419
|
+
if (!wrappedClasses.has(this.constructor)) {
|
|
420
|
+
// Auto-wrap custom methods with agent context
|
|
421
|
+
this._autoWrapCustomMethods();
|
|
422
|
+
wrappedClasses.add(this.constructor);
|
|
423
|
+
}
|
|
424
|
+
|
|
347
425
|
this.sql`
|
|
348
426
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
349
427
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -351,6 +429,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
351
429
|
)
|
|
352
430
|
`;
|
|
353
431
|
|
|
432
|
+
this.sql`
|
|
433
|
+
CREATE TABLE IF NOT EXISTS cf_agents_queues (
|
|
434
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
435
|
+
payload TEXT,
|
|
436
|
+
callback TEXT,
|
|
437
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
438
|
+
)
|
|
439
|
+
`;
|
|
440
|
+
|
|
354
441
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
355
442
|
return this._tryCatch(async () => {
|
|
356
443
|
// Create alarms table if it doesn't exist
|
|
@@ -387,7 +474,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
387
474
|
const _onRequest = this.onRequest.bind(this);
|
|
388
475
|
this.onRequest = (request: Request) => {
|
|
389
476
|
return agentContext.run(
|
|
390
|
-
{ agent: this, connection: undefined, request },
|
|
477
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
391
478
|
async () => {
|
|
392
479
|
if (this.mcp.isCallbackRequest(request)) {
|
|
393
480
|
await this.mcp.handleCallbackRequest(request);
|
|
@@ -395,15 +482,15 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
395
482
|
// after the MCP connection handshake, we can send updated mcp state
|
|
396
483
|
this.broadcast(
|
|
397
484
|
JSON.stringify({
|
|
398
|
-
|
|
399
|
-
|
|
485
|
+
mcp: this.getMcpServers(),
|
|
486
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
400
487
|
})
|
|
401
488
|
);
|
|
402
489
|
|
|
403
490
|
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
404
491
|
return new Response("<script>window.close();</script>", {
|
|
405
|
-
status: 200,
|
|
406
492
|
headers: { "content-type": "text/html" },
|
|
493
|
+
status: 200
|
|
407
494
|
});
|
|
408
495
|
}
|
|
409
496
|
|
|
@@ -415,7 +502,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
415
502
|
const _onMessage = this.onMessage.bind(this);
|
|
416
503
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
417
504
|
return agentContext.run(
|
|
418
|
-
{ agent: this, connection, request: undefined },
|
|
505
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
419
506
|
async () => {
|
|
420
507
|
if (typeof message !== "string") {
|
|
421
508
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
@@ -424,7 +511,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
424
511
|
let parsed: unknown;
|
|
425
512
|
try {
|
|
426
513
|
parsed = JSON.parse(message);
|
|
427
|
-
} catch (
|
|
514
|
+
} catch (_e) {
|
|
428
515
|
// silently fail and let the onMessage handler handle it
|
|
429
516
|
return this._tryCatch(() => _onMessage(connection, message));
|
|
430
517
|
}
|
|
@@ -448,7 +535,6 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
448
535
|
throw new Error(`Method ${method} is not callable`);
|
|
449
536
|
}
|
|
450
537
|
|
|
451
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
452
538
|
const metadata = callableMetadata.get(methodFn as Function);
|
|
453
539
|
|
|
454
540
|
// For streaming methods, pass a StreamingResponse object
|
|
@@ -460,22 +546,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
460
546
|
|
|
461
547
|
// For regular methods, execute and send response
|
|
462
548
|
const result = await methodFn.apply(this, args);
|
|
549
|
+
|
|
550
|
+
this.observability?.emit(
|
|
551
|
+
{
|
|
552
|
+
displayMessage: `RPC call to ${method}`,
|
|
553
|
+
id: nanoid(),
|
|
554
|
+
payload: {
|
|
555
|
+
method,
|
|
556
|
+
streaming: metadata?.streaming
|
|
557
|
+
},
|
|
558
|
+
timestamp: Date.now(),
|
|
559
|
+
type: "rpc"
|
|
560
|
+
},
|
|
561
|
+
this.ctx
|
|
562
|
+
);
|
|
563
|
+
|
|
463
564
|
const response: RPCResponse = {
|
|
464
|
-
|
|
565
|
+
done: true,
|
|
465
566
|
id,
|
|
466
|
-
success: true,
|
|
467
567
|
result,
|
|
468
|
-
|
|
568
|
+
success: true,
|
|
569
|
+
type: MessageType.RPC
|
|
469
570
|
};
|
|
470
571
|
connection.send(JSON.stringify(response));
|
|
471
572
|
} catch (e) {
|
|
472
573
|
// Send error response
|
|
473
574
|
const response: RPCResponse = {
|
|
474
|
-
type: "rpc",
|
|
475
|
-
id: parsed.id,
|
|
476
|
-
success: false,
|
|
477
575
|
error:
|
|
478
576
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
577
|
+
id: parsed.id,
|
|
578
|
+
success: false,
|
|
579
|
+
type: MessageType.RPC
|
|
479
580
|
};
|
|
480
581
|
connection.send(JSON.stringify(response));
|
|
481
582
|
console.error("RPC error:", e);
|
|
@@ -493,66 +594,111 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
493
594
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
494
595
|
// must fix this
|
|
495
596
|
return agentContext.run(
|
|
496
|
-
{ agent: this, connection, request: ctx.request },
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (this.state) {
|
|
500
|
-
connection.send(
|
|
501
|
-
JSON.stringify({
|
|
502
|
-
type: "cf_agent_state",
|
|
503
|
-
state: this.state,
|
|
504
|
-
})
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
|
|
597
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
598
|
+
() => {
|
|
599
|
+
if (this.state) {
|
|
508
600
|
connection.send(
|
|
509
601
|
JSON.stringify({
|
|
510
|
-
|
|
511
|
-
|
|
602
|
+
state: this.state,
|
|
603
|
+
type: MessageType.CF_AGENT_STATE
|
|
512
604
|
})
|
|
513
605
|
);
|
|
606
|
+
}
|
|
514
607
|
|
|
515
|
-
|
|
516
|
-
|
|
608
|
+
connection.send(
|
|
609
|
+
JSON.stringify({
|
|
610
|
+
mcp: this.getMcpServers(),
|
|
611
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
this.observability?.emit(
|
|
616
|
+
{
|
|
617
|
+
displayMessage: "Connection established",
|
|
618
|
+
id: nanoid(),
|
|
619
|
+
payload: {
|
|
620
|
+
connectionId: connection.id
|
|
621
|
+
},
|
|
622
|
+
timestamp: Date.now(),
|
|
623
|
+
type: "connect"
|
|
624
|
+
},
|
|
625
|
+
this.ctx
|
|
626
|
+
);
|
|
627
|
+
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
517
628
|
}
|
|
518
629
|
);
|
|
519
630
|
};
|
|
520
631
|
|
|
521
632
|
const _onStart = this.onStart.bind(this);
|
|
522
|
-
this.onStart = async () => {
|
|
633
|
+
this.onStart = async (props?: Props) => {
|
|
523
634
|
return agentContext.run(
|
|
524
|
-
{
|
|
635
|
+
{
|
|
636
|
+
agent: this,
|
|
637
|
+
connection: undefined,
|
|
638
|
+
request: undefined,
|
|
639
|
+
email: undefined
|
|
640
|
+
},
|
|
525
641
|
async () => {
|
|
526
|
-
|
|
642
|
+
await this._tryCatch(() => {
|
|
643
|
+
const servers = this.sql<MCPServerRow>`
|
|
527
644
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
528
645
|
`;
|
|
529
646
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
server.callback_url,
|
|
537
|
-
server.server_options
|
|
538
|
-
? JSON.parse(server.server_options)
|
|
539
|
-
: undefined,
|
|
540
|
-
{
|
|
541
|
-
id: server.id,
|
|
542
|
-
oauthClientId: server.client_id ?? undefined,
|
|
543
|
-
}
|
|
544
|
-
);
|
|
545
|
-
})
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
this.broadcast(
|
|
549
|
-
JSON.stringify({
|
|
550
|
-
type: "cf_agent_mcp_servers",
|
|
551
|
-
mcp: this._getMcpServerStateInternal(),
|
|
552
|
-
})
|
|
553
|
-
);
|
|
647
|
+
this.broadcast(
|
|
648
|
+
JSON.stringify({
|
|
649
|
+
mcp: this.getMcpServers(),
|
|
650
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
651
|
+
})
|
|
652
|
+
);
|
|
554
653
|
|
|
555
|
-
|
|
654
|
+
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
655
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
656
|
+
// Restore callback URLs for OAuth-enabled servers
|
|
657
|
+
servers.forEach((server) => {
|
|
658
|
+
if (server.callback_url) {
|
|
659
|
+
this.mcp.registerCallbackUrl(server.callback_url);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
servers.forEach((server) => {
|
|
664
|
+
this._connectToMcpServerInternal(
|
|
665
|
+
server.name,
|
|
666
|
+
server.server_url,
|
|
667
|
+
server.callback_url,
|
|
668
|
+
server.server_options
|
|
669
|
+
? JSON.parse(server.server_options)
|
|
670
|
+
: undefined,
|
|
671
|
+
{
|
|
672
|
+
id: server.id,
|
|
673
|
+
oauthClientId: server.client_id ?? undefined
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
.then(() => {
|
|
677
|
+
// Broadcast updated MCP servers state after each server connects
|
|
678
|
+
this.broadcast(
|
|
679
|
+
JSON.stringify({
|
|
680
|
+
mcp: this.getMcpServers(),
|
|
681
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
682
|
+
})
|
|
683
|
+
);
|
|
684
|
+
})
|
|
685
|
+
.catch((error) => {
|
|
686
|
+
console.error(
|
|
687
|
+
`Error connecting to MCP server: ${server.name} (${server.server_url})`,
|
|
688
|
+
error
|
|
689
|
+
);
|
|
690
|
+
// Still broadcast even if connection fails, so clients know about the failure
|
|
691
|
+
this.broadcast(
|
|
692
|
+
JSON.stringify({
|
|
693
|
+
mcp: this.getMcpServers(),
|
|
694
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return _onStart(props);
|
|
701
|
+
});
|
|
556
702
|
}
|
|
557
703
|
);
|
|
558
704
|
};
|
|
@@ -573,16 +719,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
573
719
|
`;
|
|
574
720
|
this.broadcast(
|
|
575
721
|
JSON.stringify({
|
|
576
|
-
type: "cf_agent_state",
|
|
577
722
|
state: state,
|
|
723
|
+
type: MessageType.CF_AGENT_STATE
|
|
578
724
|
}),
|
|
579
725
|
source !== "server" ? [source.id] : []
|
|
580
726
|
);
|
|
581
727
|
return this._tryCatch(() => {
|
|
582
|
-
const { connection, request } = agentContext.getStore() || {};
|
|
728
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
583
729
|
return agentContext.run(
|
|
584
|
-
{ agent: this, connection, request },
|
|
730
|
+
{ agent: this, connection, request, email },
|
|
585
731
|
async () => {
|
|
732
|
+
this.observability?.emit(
|
|
733
|
+
{
|
|
734
|
+
displayMessage: "State updated",
|
|
735
|
+
id: nanoid(),
|
|
736
|
+
payload: {},
|
|
737
|
+
timestamp: Date.now(),
|
|
738
|
+
type: "state:update"
|
|
739
|
+
},
|
|
740
|
+
this.ctx
|
|
741
|
+
);
|
|
586
742
|
return this.onStateUpdate(state, source);
|
|
587
743
|
}
|
|
588
744
|
);
|
|
@@ -602,23 +758,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
602
758
|
* @param state Updated state
|
|
603
759
|
* @param source Source of the state update ("server" or a client connection)
|
|
604
760
|
*/
|
|
761
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
|
|
605
762
|
onStateUpdate(state: State | undefined, source: Connection | "server") {
|
|
606
763
|
// override this to handle state updates
|
|
607
764
|
}
|
|
608
765
|
|
|
609
766
|
/**
|
|
610
|
-
* Called when the Agent receives an email
|
|
767
|
+
* Called when the Agent receives an email via routeAgentEmail()
|
|
768
|
+
* Override this method to handle incoming emails
|
|
611
769
|
* @param email Email message to process
|
|
612
770
|
*/
|
|
613
|
-
|
|
771
|
+
async _onEmail(email: AgentEmail) {
|
|
772
|
+
// nb: we use this roundabout way of getting to onEmail
|
|
773
|
+
// because of https://github.com/cloudflare/workerd/issues/4499
|
|
614
774
|
return agentContext.run(
|
|
615
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
775
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
616
776
|
async () => {
|
|
617
|
-
|
|
777
|
+
if ("onEmail" in this && typeof this.onEmail === "function") {
|
|
778
|
+
return this._tryCatch(() =>
|
|
779
|
+
(this.onEmail as (email: AgentEmail) => Promise<void>)(email)
|
|
780
|
+
);
|
|
781
|
+
} else {
|
|
782
|
+
console.log("Received email from:", email.from, "to:", email.to);
|
|
783
|
+
console.log("Subject:", email.headers.get("subject"));
|
|
784
|
+
console.log(
|
|
785
|
+
"Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
|
|
786
|
+
);
|
|
787
|
+
}
|
|
618
788
|
}
|
|
619
789
|
);
|
|
620
790
|
}
|
|
621
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Reply to an email
|
|
794
|
+
* @param email The email to reply to
|
|
795
|
+
* @param options Options for the reply
|
|
796
|
+
* @returns void
|
|
797
|
+
*/
|
|
798
|
+
async replyToEmail(
|
|
799
|
+
email: AgentEmail,
|
|
800
|
+
options: {
|
|
801
|
+
fromName: string;
|
|
802
|
+
subject?: string | undefined;
|
|
803
|
+
body: string;
|
|
804
|
+
contentType?: string;
|
|
805
|
+
headers?: Record<string, string>;
|
|
806
|
+
}
|
|
807
|
+
): Promise<void> {
|
|
808
|
+
return this._tryCatch(async () => {
|
|
809
|
+
const agentName = camelCaseToKebabCase(this._ParentClass.name);
|
|
810
|
+
const agentId = this.name;
|
|
811
|
+
|
|
812
|
+
const { createMimeMessage } = await import("mimetext");
|
|
813
|
+
const msg = createMimeMessage();
|
|
814
|
+
msg.setSender({ addr: email.to, name: options.fromName });
|
|
815
|
+
msg.setRecipient(email.from);
|
|
816
|
+
msg.setSubject(
|
|
817
|
+
options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
|
|
818
|
+
);
|
|
819
|
+
msg.addMessage({
|
|
820
|
+
contentType: options.contentType || "text/plain",
|
|
821
|
+
data: options.body
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const domain = email.from.split("@")[1];
|
|
825
|
+
const messageId = `<${agentId}@${domain}>`;
|
|
826
|
+
msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
|
|
827
|
+
msg.setHeader("Message-ID", messageId);
|
|
828
|
+
msg.setHeader("X-Agent-Name", agentName);
|
|
829
|
+
msg.setHeader("X-Agent-ID", agentId);
|
|
830
|
+
|
|
831
|
+
if (options.headers) {
|
|
832
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
833
|
+
msg.setHeader(key, value);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
await email.reply({
|
|
837
|
+
from: email.to,
|
|
838
|
+
raw: msg.asRaw(),
|
|
839
|
+
to: email.from
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
622
844
|
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
623
845
|
try {
|
|
624
846
|
return await fn();
|
|
@@ -627,6 +849,68 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
627
849
|
}
|
|
628
850
|
}
|
|
629
851
|
|
|
852
|
+
/**
|
|
853
|
+
* Automatically wrap custom methods with agent context
|
|
854
|
+
* This ensures getCurrentAgent() works in all custom methods without decorators
|
|
855
|
+
*/
|
|
856
|
+
private _autoWrapCustomMethods() {
|
|
857
|
+
// Collect all methods from base prototypes (Agent and Server)
|
|
858
|
+
const basePrototypes = [Agent.prototype, Server.prototype];
|
|
859
|
+
const baseMethods = new Set<string>();
|
|
860
|
+
for (const baseProto of basePrototypes) {
|
|
861
|
+
let proto = baseProto;
|
|
862
|
+
while (proto && proto !== Object.prototype) {
|
|
863
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
864
|
+
for (const methodName of methodNames) {
|
|
865
|
+
baseMethods.add(methodName);
|
|
866
|
+
}
|
|
867
|
+
proto = Object.getPrototypeOf(proto);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// Get all methods from the current instance's prototype chain
|
|
871
|
+
let proto = Object.getPrototypeOf(this);
|
|
872
|
+
let depth = 0;
|
|
873
|
+
while (proto && proto !== Object.prototype && depth < 10) {
|
|
874
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
875
|
+
for (const methodName of methodNames) {
|
|
876
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
|
|
877
|
+
|
|
878
|
+
// Skip if it's a private method, a base method, a getter, or not a function,
|
|
879
|
+
if (
|
|
880
|
+
baseMethods.has(methodName) ||
|
|
881
|
+
methodName.startsWith("_") ||
|
|
882
|
+
!descriptor ||
|
|
883
|
+
!!descriptor.get ||
|
|
884
|
+
typeof descriptor.value !== "function"
|
|
885
|
+
) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Now, methodName is confirmed to be a custom method/function
|
|
890
|
+
// Wrap the custom method with context
|
|
891
|
+
const wrappedFunction = withAgentContext(
|
|
892
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
893
|
+
this[methodName as keyof this] as (...args: any[]) => any
|
|
894
|
+
// biome-ignore lint/suspicious/noExplicitAny: I can't typescript
|
|
895
|
+
) as any;
|
|
896
|
+
|
|
897
|
+
// if the method is callable, copy the metadata from the original method
|
|
898
|
+
if (this._isCallable(methodName)) {
|
|
899
|
+
callableMetadata.set(
|
|
900
|
+
wrappedFunction,
|
|
901
|
+
callableMetadata.get(this[methodName as keyof this] as Function)!
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// set the wrapped function on the prototype
|
|
906
|
+
this.constructor.prototype[methodName as keyof this] = wrappedFunction;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
proto = Object.getPrototypeOf(proto);
|
|
910
|
+
depth++;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
630
914
|
override onError(
|
|
631
915
|
connection: Connection,
|
|
632
916
|
error: unknown
|
|
@@ -661,6 +945,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
661
945
|
throw new Error("Not implemented");
|
|
662
946
|
}
|
|
663
947
|
|
|
948
|
+
/**
|
|
949
|
+
* Queue a task to be executed in the future
|
|
950
|
+
* @param payload Payload to pass to the callback
|
|
951
|
+
* @param callback Name of the method to call
|
|
952
|
+
* @returns The ID of the queued task
|
|
953
|
+
*/
|
|
954
|
+
async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
|
|
955
|
+
const id = nanoid(9);
|
|
956
|
+
if (typeof callback !== "string") {
|
|
957
|
+
throw new Error("Callback must be a string");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (typeof this[callback] !== "function") {
|
|
961
|
+
throw new Error(`this.${callback} is not a function`);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
this.sql`
|
|
965
|
+
INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
|
|
966
|
+
VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
|
|
967
|
+
`;
|
|
968
|
+
|
|
969
|
+
void this._flushQueue().catch((e) => {
|
|
970
|
+
console.error("Error flushing queue:", e);
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
return id;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private _flushingQueue = false;
|
|
977
|
+
|
|
978
|
+
private async _flushQueue() {
|
|
979
|
+
if (this._flushingQueue) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
this._flushingQueue = true;
|
|
983
|
+
while (true) {
|
|
984
|
+
const result = this.sql<QueueItem<string>>`
|
|
985
|
+
SELECT * FROM cf_agents_queues
|
|
986
|
+
ORDER BY created_at ASC
|
|
987
|
+
`;
|
|
988
|
+
|
|
989
|
+
if (!result || result.length === 0) {
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for (const row of result || []) {
|
|
994
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
995
|
+
if (!callback) {
|
|
996
|
+
console.error(`callback ${row.callback} not found`);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
1000
|
+
await agentContext.run(
|
|
1001
|
+
{
|
|
1002
|
+
agent: this,
|
|
1003
|
+
connection,
|
|
1004
|
+
request,
|
|
1005
|
+
email
|
|
1006
|
+
},
|
|
1007
|
+
async () => {
|
|
1008
|
+
// TODO: add retries and backoff
|
|
1009
|
+
await (
|
|
1010
|
+
callback as (
|
|
1011
|
+
payload: unknown,
|
|
1012
|
+
queueItem: QueueItem<string>
|
|
1013
|
+
) => Promise<void>
|
|
1014
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1015
|
+
await this.dequeue(row.id);
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
this._flushingQueue = false;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Dequeue a task by ID
|
|
1025
|
+
* @param id ID of the task to dequeue
|
|
1026
|
+
*/
|
|
1027
|
+
async dequeue(id: string) {
|
|
1028
|
+
this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Dequeue all tasks
|
|
1033
|
+
*/
|
|
1034
|
+
async dequeueAll() {
|
|
1035
|
+
this.sql`DELETE FROM cf_agents_queues`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Dequeue all tasks by callback
|
|
1040
|
+
* @param callback Name of the callback to dequeue
|
|
1041
|
+
*/
|
|
1042
|
+
async dequeueAllByCallback(callback: string) {
|
|
1043
|
+
this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Get a queued task by ID
|
|
1048
|
+
* @param id ID of the task to get
|
|
1049
|
+
* @returns The task or undefined if not found
|
|
1050
|
+
*/
|
|
1051
|
+
async getQueue(id: string): Promise<QueueItem<string> | undefined> {
|
|
1052
|
+
const result = this.sql<QueueItem<string>>`
|
|
1053
|
+
SELECT * FROM cf_agents_queues WHERE id = ${id}
|
|
1054
|
+
`;
|
|
1055
|
+
return result
|
|
1056
|
+
? { ...result[0], payload: JSON.parse(result[0].payload) }
|
|
1057
|
+
: undefined;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Get all queues by key and value
|
|
1062
|
+
* @param key Key to filter by
|
|
1063
|
+
* @param value Value to filter by
|
|
1064
|
+
* @returns Array of matching QueueItem objects
|
|
1065
|
+
*/
|
|
1066
|
+
async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
|
|
1067
|
+
const result = this.sql<QueueItem<string>>`
|
|
1068
|
+
SELECT * FROM cf_agents_queues
|
|
1069
|
+
`;
|
|
1070
|
+
return result.filter((row) => JSON.parse(row.payload)[key] === value);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
664
1073
|
/**
|
|
665
1074
|
* Schedule a task to be executed in the future
|
|
666
1075
|
* @template T Type of the payload data
|
|
@@ -676,6 +1085,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
676
1085
|
): Promise<Schedule<T>> {
|
|
677
1086
|
const id = nanoid(9);
|
|
678
1087
|
|
|
1088
|
+
const emitScheduleCreate = (schedule: Schedule<T>) =>
|
|
1089
|
+
this.observability?.emit(
|
|
1090
|
+
{
|
|
1091
|
+
displayMessage: `Schedule ${schedule.id} created`,
|
|
1092
|
+
id: nanoid(),
|
|
1093
|
+
payload: {
|
|
1094
|
+
callback: callback as string,
|
|
1095
|
+
id: id
|
|
1096
|
+
},
|
|
1097
|
+
timestamp: Date.now(),
|
|
1098
|
+
type: "schedule:create"
|
|
1099
|
+
},
|
|
1100
|
+
this.ctx
|
|
1101
|
+
);
|
|
1102
|
+
|
|
679
1103
|
if (typeof callback !== "string") {
|
|
680
1104
|
throw new Error("Callback must be a string");
|
|
681
1105
|
}
|
|
@@ -695,13 +1119,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
695
1119
|
|
|
696
1120
|
await this._scheduleNextAlarm();
|
|
697
1121
|
|
|
698
|
-
|
|
699
|
-
id,
|
|
1122
|
+
const schedule: Schedule<T> = {
|
|
700
1123
|
callback: callback,
|
|
1124
|
+
id,
|
|
701
1125
|
payload: payload as T,
|
|
702
1126
|
time: timestamp,
|
|
703
|
-
type: "scheduled"
|
|
1127
|
+
type: "scheduled"
|
|
704
1128
|
};
|
|
1129
|
+
|
|
1130
|
+
emitScheduleCreate(schedule);
|
|
1131
|
+
|
|
1132
|
+
return schedule;
|
|
705
1133
|
}
|
|
706
1134
|
if (typeof when === "number") {
|
|
707
1135
|
const time = new Date(Date.now() + when * 1000);
|
|
@@ -716,14 +1144,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
716
1144
|
|
|
717
1145
|
await this._scheduleNextAlarm();
|
|
718
1146
|
|
|
719
|
-
|
|
720
|
-
id,
|
|
1147
|
+
const schedule: Schedule<T> = {
|
|
721
1148
|
callback: callback,
|
|
722
|
-
payload: payload as T,
|
|
723
1149
|
delayInSeconds: when,
|
|
1150
|
+
id,
|
|
1151
|
+
payload: payload as T,
|
|
724
1152
|
time: timestamp,
|
|
725
|
-
type: "delayed"
|
|
1153
|
+
type: "delayed"
|
|
726
1154
|
};
|
|
1155
|
+
|
|
1156
|
+
emitScheduleCreate(schedule);
|
|
1157
|
+
|
|
1158
|
+
return schedule;
|
|
727
1159
|
}
|
|
728
1160
|
if (typeof when === "string") {
|
|
729
1161
|
const nextExecutionTime = getNextCronTime(when);
|
|
@@ -738,14 +1170,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
738
1170
|
|
|
739
1171
|
await this._scheduleNextAlarm();
|
|
740
1172
|
|
|
741
|
-
|
|
742
|
-
id,
|
|
1173
|
+
const schedule: Schedule<T> = {
|
|
743
1174
|
callback: callback,
|
|
744
|
-
payload: payload as T,
|
|
745
1175
|
cron: when,
|
|
1176
|
+
id,
|
|
1177
|
+
payload: payload as T,
|
|
746
1178
|
time: timestamp,
|
|
747
|
-
type: "cron"
|
|
1179
|
+
type: "cron"
|
|
748
1180
|
};
|
|
1181
|
+
|
|
1182
|
+
emitScheduleCreate(schedule);
|
|
1183
|
+
|
|
1184
|
+
return schedule;
|
|
749
1185
|
}
|
|
750
1186
|
throw new Error("Invalid schedule type");
|
|
751
1187
|
}
|
|
@@ -809,7 +1245,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
809
1245
|
.toArray()
|
|
810
1246
|
.map((row) => ({
|
|
811
1247
|
...row,
|
|
812
|
-
payload: JSON.parse(row.payload as string) as T
|
|
1248
|
+
payload: JSON.parse(row.payload as string) as T
|
|
813
1249
|
})) as Schedule<T>[];
|
|
814
1250
|
|
|
815
1251
|
return result;
|
|
@@ -821,6 +1257,22 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
821
1257
|
* @returns true if the task was cancelled, false otherwise
|
|
822
1258
|
*/
|
|
823
1259
|
async cancelSchedule(id: string): Promise<boolean> {
|
|
1260
|
+
const schedule = await this.getSchedule(id);
|
|
1261
|
+
if (schedule) {
|
|
1262
|
+
this.observability?.emit(
|
|
1263
|
+
{
|
|
1264
|
+
displayMessage: `Schedule ${id} cancelled`,
|
|
1265
|
+
id: nanoid(),
|
|
1266
|
+
payload: {
|
|
1267
|
+
callback: schedule.callback,
|
|
1268
|
+
id: schedule.id
|
|
1269
|
+
},
|
|
1270
|
+
timestamp: Date.now(),
|
|
1271
|
+
type: "schedule:cancel"
|
|
1272
|
+
},
|
|
1273
|
+
this.ctx
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
824
1276
|
this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
|
|
825
1277
|
|
|
826
1278
|
await this._scheduleNextAlarm();
|
|
@@ -830,9 +1282,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
830
1282
|
private async _scheduleNextAlarm() {
|
|
831
1283
|
// Find the next schedule that needs to be executed
|
|
832
1284
|
const result = this.sql`
|
|
833
|
-
SELECT time FROM cf_agents_schedules
|
|
1285
|
+
SELECT time FROM cf_agents_schedules
|
|
834
1286
|
WHERE time > ${Math.floor(Date.now() / 1000)}
|
|
835
|
-
ORDER BY time ASC
|
|
1287
|
+
ORDER BY time ASC
|
|
836
1288
|
LIMIT 1
|
|
837
1289
|
`;
|
|
838
1290
|
if (!result) return;
|
|
@@ -859,40 +1311,61 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
859
1311
|
SELECT * FROM cf_agents_schedules WHERE time <= ${now}
|
|
860
1312
|
`;
|
|
861
1313
|
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
await agentContext.run(
|
|
869
|
-
{ agent: this, connection: undefined, request: undefined },
|
|
870
|
-
async () => {
|
|
871
|
-
try {
|
|
872
|
-
await (
|
|
873
|
-
callback as (
|
|
874
|
-
payload: unknown,
|
|
875
|
-
schedule: Schedule<unknown>
|
|
876
|
-
) => Promise<void>
|
|
877
|
-
).bind(this)(JSON.parse(row.payload as string), row);
|
|
878
|
-
} catch (e) {
|
|
879
|
-
console.error(`error executing callback "${row.callback}"`, e);
|
|
880
|
-
}
|
|
1314
|
+
if (result && Array.isArray(result)) {
|
|
1315
|
+
for (const row of result) {
|
|
1316
|
+
const callback = this[row.callback as keyof Agent<Env>];
|
|
1317
|
+
if (!callback) {
|
|
1318
|
+
console.error(`callback ${row.callback} not found`);
|
|
1319
|
+
continue;
|
|
881
1320
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1321
|
+
await agentContext.run(
|
|
1322
|
+
{
|
|
1323
|
+
agent: this,
|
|
1324
|
+
connection: undefined,
|
|
1325
|
+
request: undefined,
|
|
1326
|
+
email: undefined
|
|
1327
|
+
},
|
|
1328
|
+
async () => {
|
|
1329
|
+
try {
|
|
1330
|
+
this.observability?.emit(
|
|
1331
|
+
{
|
|
1332
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
1333
|
+
id: nanoid(),
|
|
1334
|
+
payload: {
|
|
1335
|
+
callback: row.callback,
|
|
1336
|
+
id: row.id
|
|
1337
|
+
},
|
|
1338
|
+
timestamp: Date.now(),
|
|
1339
|
+
type: "schedule:execute"
|
|
1340
|
+
},
|
|
1341
|
+
this.ctx
|
|
1342
|
+
);
|
|
887
1343
|
|
|
888
|
-
|
|
1344
|
+
await (
|
|
1345
|
+
callback as (
|
|
1346
|
+
payload: unknown,
|
|
1347
|
+
schedule: Schedule<unknown>
|
|
1348
|
+
) => Promise<void>
|
|
1349
|
+
).bind(this)(JSON.parse(row.payload as string), row);
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
console.error(`error executing callback "${row.callback}"`, e);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
);
|
|
1355
|
+
if (row.type === "cron") {
|
|
1356
|
+
// Update next execution time for cron schedules
|
|
1357
|
+
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1358
|
+
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
|
|
1359
|
+
|
|
1360
|
+
this.sql`
|
|
889
1361
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
890
1362
|
`;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1363
|
+
} else {
|
|
1364
|
+
// Delete one-time schedules after execution
|
|
1365
|
+
this.sql`
|
|
894
1366
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
895
1367
|
`;
|
|
1368
|
+
}
|
|
896
1369
|
}
|
|
897
1370
|
}
|
|
898
1371
|
|
|
@@ -908,22 +1381,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
908
1381
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
909
1382
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
910
1383
|
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1384
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
911
1385
|
|
|
912
1386
|
// delete all alarms
|
|
913
1387
|
await this.ctx.storage.deleteAlarm();
|
|
914
1388
|
await this.ctx.storage.deleteAll();
|
|
1389
|
+
this.ctx.abort("destroyed"); // enforce that the agent is evicted
|
|
1390
|
+
|
|
1391
|
+
this.observability?.emit(
|
|
1392
|
+
{
|
|
1393
|
+
displayMessage: "Agent destroyed",
|
|
1394
|
+
id: nanoid(),
|
|
1395
|
+
payload: {},
|
|
1396
|
+
timestamp: Date.now(),
|
|
1397
|
+
type: "destroy"
|
|
1398
|
+
},
|
|
1399
|
+
this.ctx
|
|
1400
|
+
);
|
|
915
1401
|
}
|
|
916
1402
|
|
|
1403
|
+
/**
|
|
1404
|
+
* Get all methods marked as callable on this Agent
|
|
1405
|
+
* @returns A map of method names to their metadata
|
|
1406
|
+
*/
|
|
917
1407
|
private _isCallable(method: string): boolean {
|
|
918
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
919
1408
|
return callableMetadata.has(this[method as keyof this] as Function);
|
|
920
1409
|
}
|
|
921
1410
|
|
|
922
1411
|
/**
|
|
923
1412
|
* Connect to a new MCP Server
|
|
924
1413
|
*
|
|
1414
|
+
* @param serverName Name of the MCP server
|
|
925
1415
|
* @param url MCP Server SSE URL
|
|
926
|
-
* @param callbackHost Base host for the agent, used for the redirect URI.
|
|
1416
|
+
* @param callbackHost Base host for the agent, used for the redirect URI. If not provided, will be derived from the current request.
|
|
927
1417
|
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
928
1418
|
* @param options MCP client and transport (header) options
|
|
929
1419
|
* @returns authUrl
|
|
@@ -931,7 +1421,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
931
1421
|
async addMcpServer(
|
|
932
1422
|
serverName: string,
|
|
933
1423
|
url: string,
|
|
934
|
-
callbackHost
|
|
1424
|
+
callbackHost?: string,
|
|
935
1425
|
agentsPrefix = "agents",
|
|
936
1426
|
options?: {
|
|
937
1427
|
client?: ConstructorParameters<typeof Client>[1];
|
|
@@ -940,7 +1430,22 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
940
1430
|
};
|
|
941
1431
|
}
|
|
942
1432
|
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
943
|
-
|
|
1433
|
+
// If callbackHost is not provided, derive it from the current request
|
|
1434
|
+
let resolvedCallbackHost = callbackHost;
|
|
1435
|
+
if (!resolvedCallbackHost) {
|
|
1436
|
+
const { request } = getCurrentAgent();
|
|
1437
|
+
if (!request) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
"callbackHost is required when not called within a request context"
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Extract the origin from the request
|
|
1444
|
+
const requestUrl = new URL(request.url);
|
|
1445
|
+
resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const callbackUrl = `${resolvedCallbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
944
1449
|
|
|
945
1450
|
const result = await this._connectToMcpServerInternal(
|
|
946
1451
|
serverName,
|
|
@@ -948,11 +1453,24 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
948
1453
|
callbackUrl,
|
|
949
1454
|
options
|
|
950
1455
|
);
|
|
1456
|
+
this.sql`
|
|
1457
|
+
INSERT
|
|
1458
|
+
OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1459
|
+
VALUES (
|
|
1460
|
+
${result.id},
|
|
1461
|
+
${serverName},
|
|
1462
|
+
${url},
|
|
1463
|
+
${result.clientId ?? null},
|
|
1464
|
+
${result.authUrl ?? null},
|
|
1465
|
+
${callbackUrl},
|
|
1466
|
+
${options ? JSON.stringify(options) : null}
|
|
1467
|
+
);
|
|
1468
|
+
`;
|
|
951
1469
|
|
|
952
1470
|
this.broadcast(
|
|
953
1471
|
JSON.stringify({
|
|
954
|
-
|
|
955
|
-
|
|
1472
|
+
mcp: this.getMcpServers(),
|
|
1473
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
956
1474
|
})
|
|
957
1475
|
);
|
|
958
1476
|
|
|
@@ -960,7 +1478,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
960
1478
|
}
|
|
961
1479
|
|
|
962
1480
|
async _connectToMcpServerInternal(
|
|
963
|
-
|
|
1481
|
+
_serverName: string,
|
|
964
1482
|
url: string,
|
|
965
1483
|
callbackUrl: string,
|
|
966
1484
|
// it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
|
|
@@ -975,13 +1493,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
975
1493
|
*/
|
|
976
1494
|
transport?: {
|
|
977
1495
|
headers?: HeadersInit;
|
|
1496
|
+
type?: TransportType;
|
|
978
1497
|
};
|
|
979
1498
|
},
|
|
980
1499
|
reconnect?: {
|
|
981
1500
|
id: string;
|
|
982
1501
|
oauthClientId?: string;
|
|
983
1502
|
}
|
|
984
|
-
): Promise<{
|
|
1503
|
+
): Promise<{
|
|
1504
|
+
id: string;
|
|
1505
|
+
authUrl: string | undefined;
|
|
1506
|
+
clientId: string | undefined;
|
|
1507
|
+
}> {
|
|
985
1508
|
const authProvider = new DurableObjectOAuthClientProvider(
|
|
986
1509
|
this.ctx.storage,
|
|
987
1510
|
this.name,
|
|
@@ -1004,81 +1527,83 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
1004
1527
|
fetch: (url, init) =>
|
|
1005
1528
|
fetch(url, {
|
|
1006
1529
|
...init,
|
|
1007
|
-
headers: options?.transport?.headers
|
|
1008
|
-
})
|
|
1530
|
+
headers: options?.transport?.headers
|
|
1531
|
+
})
|
|
1009
1532
|
},
|
|
1010
1533
|
requestInit: {
|
|
1011
|
-
headers: options?.transport?.headers
|
|
1012
|
-
}
|
|
1534
|
+
headers: options?.transport?.headers
|
|
1535
|
+
}
|
|
1013
1536
|
};
|
|
1014
1537
|
}
|
|
1015
1538
|
|
|
1539
|
+
// Use the transport type specified in options, or default to "auto"
|
|
1540
|
+
const transportType = options?.transport?.type || "auto";
|
|
1541
|
+
|
|
1016
1542
|
const { id, authUrl, clientId } = await this.mcp.connect(url, {
|
|
1543
|
+
client: options?.client,
|
|
1017
1544
|
reconnect,
|
|
1018
1545
|
transport: {
|
|
1019
1546
|
...headerTransportOpts,
|
|
1020
1547
|
authProvider,
|
|
1021
|
-
|
|
1022
|
-
|
|
1548
|
+
type: transportType
|
|
1549
|
+
}
|
|
1023
1550
|
});
|
|
1024
1551
|
|
|
1025
|
-
this.sql`
|
|
1026
|
-
INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1027
|
-
VALUES (
|
|
1028
|
-
${id},
|
|
1029
|
-
${serverName},
|
|
1030
|
-
${url},
|
|
1031
|
-
${clientId ?? null},
|
|
1032
|
-
${authUrl ?? null},
|
|
1033
|
-
${callbackUrl},
|
|
1034
|
-
${options ? JSON.stringify(options) : null}
|
|
1035
|
-
);
|
|
1036
|
-
`;
|
|
1037
|
-
|
|
1038
1552
|
return {
|
|
1039
|
-
id,
|
|
1040
1553
|
authUrl,
|
|
1554
|
+
clientId,
|
|
1555
|
+
id
|
|
1041
1556
|
};
|
|
1042
1557
|
}
|
|
1043
1558
|
|
|
1044
1559
|
async removeMcpServer(id: string) {
|
|
1045
1560
|
this.mcp.closeConnection(id);
|
|
1561
|
+
this.mcp.unregisterCallbackUrl(id);
|
|
1046
1562
|
this.sql`
|
|
1047
1563
|
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1048
1564
|
`;
|
|
1049
1565
|
this.broadcast(
|
|
1050
1566
|
JSON.stringify({
|
|
1051
|
-
|
|
1052
|
-
|
|
1567
|
+
mcp: this.getMcpServers(),
|
|
1568
|
+
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1053
1569
|
})
|
|
1054
1570
|
);
|
|
1055
1571
|
}
|
|
1056
1572
|
|
|
1057
|
-
|
|
1573
|
+
getMcpServers(): MCPServersState {
|
|
1058
1574
|
const mcpState: MCPServersState = {
|
|
1059
|
-
servers: {},
|
|
1060
|
-
tools: this.mcp.listTools(),
|
|
1061
1575
|
prompts: this.mcp.listPrompts(),
|
|
1062
1576
|
resources: this.mcp.listResources(),
|
|
1577
|
+
servers: {},
|
|
1578
|
+
tools: this.mcp.listTools()
|
|
1063
1579
|
};
|
|
1064
1580
|
|
|
1065
1581
|
const servers = this.sql<MCPServerRow>`
|
|
1066
1582
|
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1067
1583
|
`;
|
|
1068
1584
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1585
|
+
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
1586
|
+
for (const server of servers) {
|
|
1587
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1588
|
+
mcpState.servers[server.id] = {
|
|
1589
|
+
auth_url: server.auth_url,
|
|
1590
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1591
|
+
instructions: serverConn?.instructions ?? null,
|
|
1592
|
+
name: server.name,
|
|
1593
|
+
server_url: server.server_url,
|
|
1594
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1595
|
+
state: serverConn?.connectionState ?? "authenticating"
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1076
1598
|
}
|
|
1077
1599
|
|
|
1078
1600
|
return mcpState;
|
|
1079
1601
|
}
|
|
1080
1602
|
}
|
|
1081
1603
|
|
|
1604
|
+
// A set of classes that have been wrapped with agent context
|
|
1605
|
+
const wrappedClasses = new Set<typeof Agent.prototype.constructor>();
|
|
1606
|
+
|
|
1082
1607
|
/**
|
|
1083
1608
|
* Namespace for creating Agent instances
|
|
1084
1609
|
* @template Agentic Type of the Agent class
|
|
@@ -1116,17 +1641,17 @@ export async function routeAgentRequest<Env>(
|
|
|
1116
1641
|
const corsHeaders =
|
|
1117
1642
|
options?.cors === true
|
|
1118
1643
|
? {
|
|
1119
|
-
"Access-Control-Allow-Origin": "*",
|
|
1120
|
-
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1121
1644
|
"Access-Control-Allow-Credentials": "true",
|
|
1122
|
-
"Access-Control-
|
|
1645
|
+
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1646
|
+
"Access-Control-Allow-Origin": "*",
|
|
1647
|
+
"Access-Control-Max-Age": "86400"
|
|
1123
1648
|
}
|
|
1124
1649
|
: options?.cors;
|
|
1125
1650
|
|
|
1126
1651
|
if (request.method === "OPTIONS") {
|
|
1127
1652
|
if (corsHeaders) {
|
|
1128
1653
|
return new Response(null, {
|
|
1129
|
-
headers: corsHeaders
|
|
1654
|
+
headers: corsHeaders
|
|
1130
1655
|
});
|
|
1131
1656
|
}
|
|
1132
1657
|
console.warn(
|
|
@@ -1139,7 +1664,7 @@ export async function routeAgentRequest<Env>(
|
|
|
1139
1664
|
env as Record<string, unknown>,
|
|
1140
1665
|
{
|
|
1141
1666
|
prefix: "agents",
|
|
1142
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1667
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1143
1668
|
}
|
|
1144
1669
|
);
|
|
1145
1670
|
|
|
@@ -1152,24 +1677,238 @@ export async function routeAgentRequest<Env>(
|
|
|
1152
1677
|
response = new Response(response.body, {
|
|
1153
1678
|
headers: {
|
|
1154
1679
|
...response.headers,
|
|
1155
|
-
...corsHeaders
|
|
1156
|
-
}
|
|
1680
|
+
...corsHeaders
|
|
1681
|
+
}
|
|
1157
1682
|
});
|
|
1158
1683
|
}
|
|
1159
1684
|
return response;
|
|
1160
1685
|
}
|
|
1161
1686
|
|
|
1687
|
+
export type EmailResolver<Env> = (
|
|
1688
|
+
email: ForwardableEmailMessage,
|
|
1689
|
+
env: Env
|
|
1690
|
+
) => Promise<{
|
|
1691
|
+
agentName: string;
|
|
1692
|
+
agentId: string;
|
|
1693
|
+
} | null>;
|
|
1694
|
+
|
|
1695
|
+
/**
|
|
1696
|
+
* Create a resolver that uses the message-id header to determine the agent to route the email to
|
|
1697
|
+
* @returns A function that resolves the agent to route the email to
|
|
1698
|
+
*/
|
|
1699
|
+
export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
|
|
1700
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1701
|
+
const messageId = email.headers.get("message-id");
|
|
1702
|
+
if (messageId) {
|
|
1703
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1704
|
+
if (messageIdMatch) {
|
|
1705
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1706
|
+
const agentName = domain.split(".")[0];
|
|
1707
|
+
return { agentName, agentId };
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const references = email.headers.get("references");
|
|
1712
|
+
if (references) {
|
|
1713
|
+
const referencesMatch = references.match(
|
|
1714
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1715
|
+
);
|
|
1716
|
+
if (referencesMatch) {
|
|
1717
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1718
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1719
|
+
const agentName = domain.split(".")[0];
|
|
1720
|
+
return { agentName, agentId };
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1725
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1726
|
+
if (agentName && agentId) {
|
|
1727
|
+
return { agentName, agentId };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return null;
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
1736
|
+
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
1737
|
+
* @returns A function that resolves the agent to route the email to
|
|
1738
|
+
*/
|
|
1739
|
+
export function createAddressBasedEmailResolver<Env>(
|
|
1740
|
+
defaultAgentName: string
|
|
1741
|
+
): EmailResolver<Env> {
|
|
1742
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1743
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1744
|
+
if (!emailMatch) {
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1749
|
+
|
|
1750
|
+
if (subAddress) {
|
|
1751
|
+
return {
|
|
1752
|
+
agentName: localPart,
|
|
1753
|
+
agentId: subAddress
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1758
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1759
|
+
return {
|
|
1760
|
+
agentName: defaultAgentName,
|
|
1761
|
+
agentId: localPart
|
|
1762
|
+
};
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
1768
|
+
* @param agentName The name of the agent to route the email to
|
|
1769
|
+
* @param agentId The id of the agent to route the email to
|
|
1770
|
+
* @returns A function that resolves the agent to route the email to
|
|
1771
|
+
*/
|
|
1772
|
+
export function createCatchAllEmailResolver<Env>(
|
|
1773
|
+
agentName: string,
|
|
1774
|
+
agentId: string
|
|
1775
|
+
): EmailResolver<Env> {
|
|
1776
|
+
return async () => ({ agentName, agentId });
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1780
|
+
resolver: EmailResolver<Env>;
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
// Cache the agent namespace map for email routing
|
|
1784
|
+
// This maps both kebab-case and original names to namespaces
|
|
1785
|
+
const agentMapCache = new WeakMap<
|
|
1786
|
+
Record<string, unknown>,
|
|
1787
|
+
Record<string, unknown>
|
|
1788
|
+
>();
|
|
1789
|
+
|
|
1162
1790
|
/**
|
|
1163
1791
|
* Route an email to the appropriate Agent
|
|
1164
|
-
* @param email
|
|
1165
|
-
* @param env
|
|
1166
|
-
* @param options
|
|
1792
|
+
* @param email The email to route
|
|
1793
|
+
* @param env The environment containing the Agent bindings
|
|
1794
|
+
* @param options The options for routing the email
|
|
1795
|
+
* @returns A promise that resolves when the email has been routed
|
|
1167
1796
|
*/
|
|
1168
1797
|
export async function routeAgentEmail<Env>(
|
|
1169
1798
|
email: ForwardableEmailMessage,
|
|
1170
1799
|
env: Env,
|
|
1171
|
-
options
|
|
1172
|
-
): Promise<void> {
|
|
1800
|
+
options: EmailRoutingOptions<Env>
|
|
1801
|
+
): Promise<void> {
|
|
1802
|
+
const routingInfo = await options.resolver(email, env);
|
|
1803
|
+
|
|
1804
|
+
if (!routingInfo) {
|
|
1805
|
+
console.warn("No routing information found for email, dropping message");
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Build a map that includes both original names and kebab-case versions
|
|
1810
|
+
if (!agentMapCache.has(env as Record<string, unknown>)) {
|
|
1811
|
+
const map: Record<string, unknown> = {};
|
|
1812
|
+
for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
|
|
1813
|
+
if (
|
|
1814
|
+
value &&
|
|
1815
|
+
typeof value === "object" &&
|
|
1816
|
+
"idFromName" in value &&
|
|
1817
|
+
typeof value.idFromName === "function"
|
|
1818
|
+
) {
|
|
1819
|
+
// Add both the original name and kebab-case version
|
|
1820
|
+
map[key] = value;
|
|
1821
|
+
map[camelCaseToKebabCase(key)] = value;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
agentMapCache.set(env as Record<string, unknown>, map);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
|
|
1828
|
+
const namespace = agentMap[routingInfo.agentName];
|
|
1829
|
+
|
|
1830
|
+
if (!namespace) {
|
|
1831
|
+
// Provide helpful error message listing available agents
|
|
1832
|
+
const availableAgents = Object.keys(agentMap)
|
|
1833
|
+
.filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
|
|
1834
|
+
.join(", ");
|
|
1835
|
+
throw new Error(
|
|
1836
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const agent = await getAgentByName(
|
|
1841
|
+
namespace as unknown as AgentNamespace<Agent<Env>>,
|
|
1842
|
+
routingInfo.agentId
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
// let's make a serialisable version of the email
|
|
1846
|
+
const serialisableEmail: AgentEmail = {
|
|
1847
|
+
getRaw: async () => {
|
|
1848
|
+
const reader = email.raw.getReader();
|
|
1849
|
+
const chunks: Uint8Array[] = [];
|
|
1850
|
+
|
|
1851
|
+
let done = false;
|
|
1852
|
+
while (!done) {
|
|
1853
|
+
const { value, done: readerDone } = await reader.read();
|
|
1854
|
+
done = readerDone;
|
|
1855
|
+
if (value) {
|
|
1856
|
+
chunks.push(value);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1861
|
+
const combined = new Uint8Array(totalLength);
|
|
1862
|
+
let offset = 0;
|
|
1863
|
+
for (const chunk of chunks) {
|
|
1864
|
+
combined.set(chunk, offset);
|
|
1865
|
+
offset += chunk.length;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
return combined;
|
|
1869
|
+
},
|
|
1870
|
+
headers: email.headers,
|
|
1871
|
+
rawSize: email.rawSize,
|
|
1872
|
+
setReject: (reason: string) => {
|
|
1873
|
+
email.setReject(reason);
|
|
1874
|
+
},
|
|
1875
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1876
|
+
return email.forward(rcptTo, headers);
|
|
1877
|
+
},
|
|
1878
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1879
|
+
return email.reply(
|
|
1880
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1881
|
+
);
|
|
1882
|
+
},
|
|
1883
|
+
from: email.from,
|
|
1884
|
+
to: email.to
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
await agent._onEmail(serialisableEmail);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
export type AgentEmail = {
|
|
1891
|
+
from: string;
|
|
1892
|
+
to: string;
|
|
1893
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1894
|
+
headers: Headers;
|
|
1895
|
+
rawSize: number;
|
|
1896
|
+
setReject: (reason: string) => void;
|
|
1897
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1898
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
export type EmailSendOptions = {
|
|
1902
|
+
to: string;
|
|
1903
|
+
subject: string;
|
|
1904
|
+
body: string;
|
|
1905
|
+
contentType?: string;
|
|
1906
|
+
headers?: Record<string, string>;
|
|
1907
|
+
includeRoutingHeaders?: boolean;
|
|
1908
|
+
agentName?: string;
|
|
1909
|
+
agentId?: string;
|
|
1910
|
+
domain?: string;
|
|
1911
|
+
};
|
|
1173
1912
|
|
|
1174
1913
|
/**
|
|
1175
1914
|
* Get or create an Agent by name
|
|
@@ -1180,12 +1919,17 @@ export async function routeAgentEmail<Env>(
|
|
|
1180
1919
|
* @param options Options for Agent creation
|
|
1181
1920
|
* @returns Promise resolving to an Agent instance stub
|
|
1182
1921
|
*/
|
|
1183
|
-
export async function getAgentByName<
|
|
1922
|
+
export async function getAgentByName<
|
|
1923
|
+
Env,
|
|
1924
|
+
T extends Agent<Env>,
|
|
1925
|
+
Props extends Record<string, unknown> = Record<string, unknown>
|
|
1926
|
+
>(
|
|
1184
1927
|
namespace: AgentNamespace<T>,
|
|
1185
1928
|
name: string,
|
|
1186
1929
|
options?: {
|
|
1187
1930
|
jurisdiction?: DurableObjectJurisdiction;
|
|
1188
1931
|
locationHint?: DurableObjectLocationHint;
|
|
1932
|
+
props?: Props;
|
|
1189
1933
|
}
|
|
1190
1934
|
) {
|
|
1191
1935
|
return getServerByName<Env, T>(namespace, name, options);
|
|
@@ -1213,11 +1957,11 @@ export class StreamingResponse {
|
|
|
1213
1957
|
throw new Error("StreamingResponse is already closed");
|
|
1214
1958
|
}
|
|
1215
1959
|
const response: RPCResponse = {
|
|
1216
|
-
|
|
1960
|
+
done: false,
|
|
1217
1961
|
id: this._id,
|
|
1218
|
-
success: true,
|
|
1219
1962
|
result: chunk,
|
|
1220
|
-
|
|
1963
|
+
success: true,
|
|
1964
|
+
type: MessageType.RPC
|
|
1221
1965
|
};
|
|
1222
1966
|
this._connection.send(JSON.stringify(response));
|
|
1223
1967
|
}
|
|
@@ -1232,11 +1976,11 @@ export class StreamingResponse {
|
|
|
1232
1976
|
}
|
|
1233
1977
|
this._closed = true;
|
|
1234
1978
|
const response: RPCResponse = {
|
|
1235
|
-
|
|
1979
|
+
done: true,
|
|
1236
1980
|
id: this._id,
|
|
1237
|
-
success: true,
|
|
1238
1981
|
result: finalChunk,
|
|
1239
|
-
|
|
1982
|
+
success: true,
|
|
1983
|
+
type: MessageType.RPC
|
|
1240
1984
|
};
|
|
1241
1985
|
this._connection.send(JSON.stringify(response));
|
|
1242
1986
|
}
|