agents 0.0.0-dc0e8de → 0.0.0-dc7a99c
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/dist/ai-chat-agent.d.ts +32 -5
- package/dist/ai-chat-agent.js +149 -115
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-react.d.ts +17 -4
- package/dist/ai-react.js +28 -29
- package/dist/ai-react.js.map +1 -1
- package/dist/chunk-767EASBA.js +106 -0
- package/dist/chunk-767EASBA.js.map +1 -0
- package/dist/{chunk-Q5ZBHY4Z.js → chunk-E3LCYPCB.js} +49 -36
- package/dist/chunk-E3LCYPCB.js.map +1 -0
- package/dist/{chunk-HD4VEHBA.js → chunk-JFRK72K3.js} +463 -161
- package/dist/chunk-JFRK72K3.js.map +1 -0
- package/dist/chunk-NKZZ66QY.js +116 -0
- package/dist/chunk-NKZZ66QY.js.map +1 -0
- package/dist/client.d.ts +15 -1
- package/dist/client.js +6 -126
- package/dist/client.js.map +1 -1
- package/dist/index-CITGJflw.d.ts +486 -0
- package/dist/index.d.ts +25 -308
- package/dist/index.js +4 -3
- package/dist/mcp/client.d.ts +301 -23
- package/dist/mcp/client.js +1 -2
- package/dist/mcp/do-oauth-client-provider.d.ts +3 -3
- package/dist/mcp/do-oauth-client-provider.js +3 -103
- package/dist/mcp/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/index.d.ts +17 -7
- package/dist/mcp/index.js +147 -173
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +12 -0
- package/dist/observability/index.js +10 -0
- package/dist/react.d.ts +85 -5
- package/dist/react.js +20 -8
- package/dist/react.js.map +1 -1
- package/dist/schedule.d.ts +6 -6
- package/dist/schedule.js +4 -6
- package/dist/schedule.js.map +1 -1
- package/dist/serializable.d.ts +32 -0
- package/dist/serializable.js +1 -0
- package/dist/serializable.js.map +1 -0
- package/package.json +75 -68
- package/src/index.ts +506 -83
- package/dist/chunk-HD4VEHBA.js.map +0 -1
- package/dist/chunk-HMLY7DHA.js +0 -16
- package/dist/chunk-Q5ZBHY4Z.js.map +0 -1
- /package/dist/{chunk-HMLY7DHA.js.map → observability/index.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Prompt,
|
|
7
|
+
Resource,
|
|
8
|
+
ServerCapabilities,
|
|
9
|
+
Tool,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { parseCronExpression } from "cron-schedule";
|
|
12
|
+
import { nanoid } from "nanoid";
|
|
1
13
|
import {
|
|
2
|
-
Server,
|
|
3
|
-
routePartykitRequest,
|
|
4
|
-
type PartyServerOptions,
|
|
5
|
-
getServerByName,
|
|
6
14
|
type Connection,
|
|
7
15
|
type ConnectionContext,
|
|
16
|
+
getServerByName,
|
|
17
|
+
type PartyServerOptions,
|
|
18
|
+
routePartykitRequest,
|
|
19
|
+
Server,
|
|
8
20
|
type WSMessage,
|
|
9
21
|
} from "partyserver";
|
|
10
|
-
|
|
11
|
-
import { parseCronExpression } from "cron-schedule";
|
|
12
|
-
import { nanoid } from "nanoid";
|
|
13
|
-
|
|
14
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
22
|
+
import { camelCaseToKebabCase } from "./client";
|
|
15
23
|
import { MCPClientManager } from "./mcp/client";
|
|
24
|
+
// import type { MCPClientConnection } from "./mcp/client-connection";
|
|
25
|
+
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
|
|
26
|
+
import { genericObservability, type Observability } from "./observability";
|
|
16
27
|
|
|
17
|
-
export type { Connection,
|
|
28
|
+
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
31
|
* RPC request message from client
|
|
@@ -98,7 +109,6 @@ export type CallableMetadata = {
|
|
|
98
109
|
streaming?: boolean;
|
|
99
110
|
};
|
|
100
111
|
|
|
101
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
102
112
|
const callableMetadata = new Map<Function, CallableMetadata>();
|
|
103
113
|
|
|
104
114
|
/**
|
|
@@ -108,6 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
|
|
|
108
118
|
export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
109
119
|
return function callableDecorator<This, Args extends unknown[], Return>(
|
|
110
120
|
target: (this: This, ...args: Args) => Return,
|
|
121
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: later
|
|
111
122
|
context: ClassMethodDecoratorContext
|
|
112
123
|
) {
|
|
113
124
|
if (!callableMetadata.has(target)) {
|
|
@@ -159,6 +170,48 @@ function getNextCronTime(cron: string) {
|
|
|
159
170
|
return interval.getNextDate();
|
|
160
171
|
}
|
|
161
172
|
|
|
173
|
+
/**
|
|
174
|
+
* MCP Server state update message from server -> Client
|
|
175
|
+
*/
|
|
176
|
+
export type MCPServerMessage = {
|
|
177
|
+
type: "cf_agent_mcp_servers";
|
|
178
|
+
mcp: MCPServersState;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type MCPServersState = {
|
|
182
|
+
servers: {
|
|
183
|
+
[id: string]: MCPServer;
|
|
184
|
+
};
|
|
185
|
+
tools: Tool[];
|
|
186
|
+
prompts: Prompt[];
|
|
187
|
+
resources: Resource[];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type MCPServer = {
|
|
191
|
+
name: string;
|
|
192
|
+
server_url: string;
|
|
193
|
+
auth_url: string | null;
|
|
194
|
+
// This state is specifically about the temporary process of getting a token (if needed).
|
|
195
|
+
// Scope outside of that can't be relied upon because when the DO sleeps, there's no way
|
|
196
|
+
// to communicate a change to a non-ready state.
|
|
197
|
+
state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
|
|
198
|
+
instructions: string | null;
|
|
199
|
+
capabilities: ServerCapabilities | null;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* MCP Server data stored in DO SQL for resuming MCP Server connections
|
|
204
|
+
*/
|
|
205
|
+
type MCPServerRow = {
|
|
206
|
+
id: string;
|
|
207
|
+
name: string;
|
|
208
|
+
server_url: string;
|
|
209
|
+
client_id: string | null;
|
|
210
|
+
auth_url: string | null;
|
|
211
|
+
callback_url: string;
|
|
212
|
+
server_options: string;
|
|
213
|
+
};
|
|
214
|
+
|
|
162
215
|
const STATE_ROW_ID = "cf_state_row_id";
|
|
163
216
|
const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
164
217
|
|
|
@@ -200,12 +253,12 @@ export function getCurrentAgent<
|
|
|
200
253
|
* @template State State type to store within the Agent
|
|
201
254
|
*/
|
|
202
255
|
export class Agent<Env, State = unknown> extends Server<Env> {
|
|
203
|
-
|
|
256
|
+
private _state = DEFAULT_STATE as State;
|
|
204
257
|
|
|
205
|
-
|
|
258
|
+
private _ParentClass: typeof Agent<Env, State> =
|
|
206
259
|
Object.getPrototypeOf(this).constructor;
|
|
207
260
|
|
|
208
|
-
mcp: MCPClientManager = new MCPClientManager(this
|
|
261
|
+
mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
|
|
209
262
|
|
|
210
263
|
/**
|
|
211
264
|
* Initial state for the Agent
|
|
@@ -217,9 +270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
217
270
|
* Current state of the Agent
|
|
218
271
|
*/
|
|
219
272
|
get state(): State {
|
|
220
|
-
if (this
|
|
273
|
+
if (this._state !== DEFAULT_STATE) {
|
|
221
274
|
// state was previously set, and populated internal state
|
|
222
|
-
return this
|
|
275
|
+
return this._state;
|
|
223
276
|
}
|
|
224
277
|
// looks like this is the first time the state is being accessed
|
|
225
278
|
// check if the state was set in a previous life
|
|
@@ -239,8 +292,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
239
292
|
) {
|
|
240
293
|
const state = result[0]?.state as string; // could be null?
|
|
241
294
|
|
|
242
|
-
this
|
|
243
|
-
return this
|
|
295
|
+
this._state = JSON.parse(state);
|
|
296
|
+
return this._state;
|
|
244
297
|
}
|
|
245
298
|
|
|
246
299
|
// ok, this is the first time the state is being accessed
|
|
@@ -264,6 +317,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
264
317
|
hibernate: true, // default to hibernate
|
|
265
318
|
};
|
|
266
319
|
|
|
320
|
+
/**
|
|
321
|
+
* The observability implementation to use for the Agent
|
|
322
|
+
*/
|
|
323
|
+
observability?: Observability = genericObservability;
|
|
324
|
+
|
|
267
325
|
/**
|
|
268
326
|
* Execute SQL queries against the Agent's database
|
|
269
327
|
* @template T Type of the returned rows
|
|
@@ -301,7 +359,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
301
359
|
`;
|
|
302
360
|
|
|
303
361
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
304
|
-
return this
|
|
362
|
+
return this._tryCatch(async () => {
|
|
305
363
|
// Create alarms table if it doesn't exist
|
|
306
364
|
this.sql`
|
|
307
365
|
CREATE TABLE IF NOT EXISTS cf_agents_schedules (
|
|
@@ -321,25 +379,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
321
379
|
});
|
|
322
380
|
});
|
|
323
381
|
|
|
382
|
+
this.sql`
|
|
383
|
+
CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
|
|
384
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
385
|
+
name TEXT NOT NULL,
|
|
386
|
+
server_url TEXT NOT NULL,
|
|
387
|
+
callback_url TEXT NOT NULL,
|
|
388
|
+
client_id TEXT,
|
|
389
|
+
auth_url TEXT,
|
|
390
|
+
server_options TEXT
|
|
391
|
+
)
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
const _onRequest = this.onRequest.bind(this);
|
|
395
|
+
this.onRequest = (request: Request) => {
|
|
396
|
+
return agentContext.run(
|
|
397
|
+
{ agent: this, connection: undefined, request },
|
|
398
|
+
async () => {
|
|
399
|
+
if (this.mcp.isCallbackRequest(request)) {
|
|
400
|
+
await this.mcp.handleCallbackRequest(request);
|
|
401
|
+
|
|
402
|
+
// after the MCP connection handshake, we can send updated mcp state
|
|
403
|
+
this.broadcast(
|
|
404
|
+
JSON.stringify({
|
|
405
|
+
mcp: this.getMcpServers(),
|
|
406
|
+
type: "cf_agent_mcp_servers",
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
411
|
+
return new Response("<script>window.close();</script>", {
|
|
412
|
+
headers: { "content-type": "text/html" },
|
|
413
|
+
status: 200,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return this._tryCatch(() => _onRequest(request));
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
};
|
|
421
|
+
|
|
324
422
|
const _onMessage = this.onMessage.bind(this);
|
|
325
423
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
326
424
|
return agentContext.run(
|
|
327
425
|
{ agent: this, connection, request: undefined },
|
|
328
426
|
async () => {
|
|
329
427
|
if (typeof message !== "string") {
|
|
330
|
-
return this
|
|
428
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
331
429
|
}
|
|
332
430
|
|
|
333
431
|
let parsed: unknown;
|
|
334
432
|
try {
|
|
335
433
|
parsed = JSON.parse(message);
|
|
336
|
-
} catch (
|
|
434
|
+
} catch (_e) {
|
|
337
435
|
// silently fail and let the onMessage handler handle it
|
|
338
|
-
return this
|
|
436
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
339
437
|
}
|
|
340
438
|
|
|
341
439
|
if (isStateUpdateMessage(parsed)) {
|
|
342
|
-
this
|
|
440
|
+
this._setStateInternal(parsed.state as State, connection);
|
|
343
441
|
return;
|
|
344
442
|
}
|
|
345
443
|
|
|
@@ -353,11 +451,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
353
451
|
throw new Error(`Method ${method} does not exist`);
|
|
354
452
|
}
|
|
355
453
|
|
|
356
|
-
if (!this
|
|
454
|
+
if (!this._isCallable(method)) {
|
|
357
455
|
throw new Error(`Method ${method} is not callable`);
|
|
358
456
|
}
|
|
359
457
|
|
|
360
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
361
458
|
const metadata = callableMetadata.get(methodFn as Function);
|
|
362
459
|
|
|
363
460
|
// For streaming methods, pass a StreamingResponse object
|
|
@@ -369,22 +466,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
369
466
|
|
|
370
467
|
// For regular methods, execute and send response
|
|
371
468
|
const result = await methodFn.apply(this, args);
|
|
469
|
+
|
|
470
|
+
this.observability?.emit(
|
|
471
|
+
{
|
|
472
|
+
displayMessage: `RPC call to ${method}`,
|
|
473
|
+
id: nanoid(),
|
|
474
|
+
payload: {
|
|
475
|
+
args,
|
|
476
|
+
method,
|
|
477
|
+
streaming: metadata?.streaming,
|
|
478
|
+
success: true,
|
|
479
|
+
},
|
|
480
|
+
timestamp: Date.now(),
|
|
481
|
+
type: "rpc",
|
|
482
|
+
},
|
|
483
|
+
this.ctx
|
|
484
|
+
);
|
|
485
|
+
|
|
372
486
|
const response: RPCResponse = {
|
|
373
|
-
|
|
487
|
+
done: true,
|
|
374
488
|
id,
|
|
375
|
-
success: true,
|
|
376
489
|
result,
|
|
377
|
-
|
|
490
|
+
success: true,
|
|
491
|
+
type: "rpc",
|
|
378
492
|
};
|
|
379
493
|
connection.send(JSON.stringify(response));
|
|
380
494
|
} catch (e) {
|
|
381
495
|
// Send error response
|
|
382
496
|
const response: RPCResponse = {
|
|
383
|
-
type: "rpc",
|
|
384
|
-
id: parsed.id,
|
|
385
|
-
success: false,
|
|
386
497
|
error:
|
|
387
498
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
499
|
+
id: parsed.id,
|
|
500
|
+
success: false,
|
|
501
|
+
type: "rpc",
|
|
388
502
|
};
|
|
389
503
|
connection.send(JSON.stringify(response));
|
|
390
504
|
console.error("RPC error:", e);
|
|
@@ -392,7 +506,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
392
506
|
return;
|
|
393
507
|
}
|
|
394
508
|
|
|
395
|
-
return this
|
|
509
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
396
510
|
}
|
|
397
511
|
);
|
|
398
512
|
};
|
|
@@ -408,20 +522,82 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
408
522
|
if (this.state) {
|
|
409
523
|
connection.send(
|
|
410
524
|
JSON.stringify({
|
|
411
|
-
type: "cf_agent_state",
|
|
412
525
|
state: this.state,
|
|
526
|
+
type: "cf_agent_state",
|
|
413
527
|
})
|
|
414
528
|
);
|
|
415
529
|
}
|
|
416
|
-
|
|
530
|
+
|
|
531
|
+
connection.send(
|
|
532
|
+
JSON.stringify({
|
|
533
|
+
mcp: this.getMcpServers(),
|
|
534
|
+
type: "cf_agent_mcp_servers",
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
this.observability?.emit(
|
|
539
|
+
{
|
|
540
|
+
displayMessage: "Connection established",
|
|
541
|
+
id: nanoid(),
|
|
542
|
+
payload: {
|
|
543
|
+
connectionId: connection.id,
|
|
544
|
+
},
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
type: "connect",
|
|
547
|
+
},
|
|
548
|
+
this.ctx
|
|
549
|
+
);
|
|
550
|
+
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
417
551
|
}, 20);
|
|
418
552
|
}
|
|
419
553
|
);
|
|
420
554
|
};
|
|
555
|
+
|
|
556
|
+
const _onStart = this.onStart.bind(this);
|
|
557
|
+
this.onStart = async () => {
|
|
558
|
+
return agentContext.run(
|
|
559
|
+
{ agent: this, connection: undefined, request: undefined },
|
|
560
|
+
async () => {
|
|
561
|
+
const servers = this.sql<MCPServerRow>`
|
|
562
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
563
|
+
`;
|
|
564
|
+
|
|
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
|
+
this.broadcast(
|
|
583
|
+
JSON.stringify({
|
|
584
|
+
mcp: this.getMcpServers(),
|
|
585
|
+
type: "cf_agent_mcp_servers",
|
|
586
|
+
})
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
await this._tryCatch(() => _onStart());
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
};
|
|
421
593
|
}
|
|
422
594
|
|
|
423
|
-
|
|
424
|
-
|
|
595
|
+
private _setStateInternal(
|
|
596
|
+
state: State,
|
|
597
|
+
source: Connection | "server" = "server"
|
|
598
|
+
) {
|
|
599
|
+
const previousState = this._state;
|
|
600
|
+
this._state = state;
|
|
425
601
|
this.sql`
|
|
426
602
|
INSERT OR REPLACE INTO cf_agents_state (id, state)
|
|
427
603
|
VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
|
|
@@ -432,16 +608,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
432
608
|
`;
|
|
433
609
|
this.broadcast(
|
|
434
610
|
JSON.stringify({
|
|
435
|
-
type: "cf_agent_state",
|
|
436
611
|
state: state,
|
|
612
|
+
type: "cf_agent_state",
|
|
437
613
|
}),
|
|
438
614
|
source !== "server" ? [source.id] : []
|
|
439
615
|
);
|
|
440
|
-
return this
|
|
616
|
+
return this._tryCatch(() => {
|
|
441
617
|
const { connection, request } = agentContext.getStore() || {};
|
|
442
618
|
return agentContext.run(
|
|
443
619
|
{ agent: this, connection, request },
|
|
444
620
|
async () => {
|
|
621
|
+
this.observability?.emit(
|
|
622
|
+
{
|
|
623
|
+
displayMessage: "State updated",
|
|
624
|
+
id: nanoid(),
|
|
625
|
+
payload: {
|
|
626
|
+
previousState,
|
|
627
|
+
state,
|
|
628
|
+
},
|
|
629
|
+
timestamp: Date.now(),
|
|
630
|
+
type: "state:update",
|
|
631
|
+
},
|
|
632
|
+
this.ctx
|
|
633
|
+
);
|
|
445
634
|
return this.onStateUpdate(state, source);
|
|
446
635
|
}
|
|
447
636
|
);
|
|
@@ -453,7 +642,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
453
642
|
* @param state New state to set
|
|
454
643
|
*/
|
|
455
644
|
setState(state: State) {
|
|
456
|
-
this
|
|
645
|
+
this._setStateInternal(state, "server");
|
|
457
646
|
}
|
|
458
647
|
|
|
459
648
|
/**
|
|
@@ -461,6 +650,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
461
650
|
* @param state Updated state
|
|
462
651
|
* @param source Source of the state update ("server" or a client connection)
|
|
463
652
|
*/
|
|
653
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
|
|
464
654
|
onStateUpdate(state: State | undefined, source: Connection | "server") {
|
|
465
655
|
// override this to handle state updates
|
|
466
656
|
}
|
|
@@ -469,6 +659,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
469
659
|
* Called when the Agent receives an email
|
|
470
660
|
* @param email Email message to process
|
|
471
661
|
*/
|
|
662
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
|
|
472
663
|
onEmail(email: ForwardableEmailMessage) {
|
|
473
664
|
return agentContext.run(
|
|
474
665
|
{ agent: this, connection: undefined, request: undefined },
|
|
@@ -478,7 +669,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
478
669
|
);
|
|
479
670
|
}
|
|
480
671
|
|
|
481
|
-
async
|
|
672
|
+
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
482
673
|
try {
|
|
483
674
|
return await fn();
|
|
484
675
|
} catch (e) {
|
|
@@ -535,6 +726,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
535
726
|
): Promise<Schedule<T>> {
|
|
536
727
|
const id = nanoid(9);
|
|
537
728
|
|
|
729
|
+
const emitScheduleCreate = (schedule: Schedule<T>) =>
|
|
730
|
+
this.observability?.emit(
|
|
731
|
+
{
|
|
732
|
+
displayMessage: `Schedule ${schedule.id} created`,
|
|
733
|
+
id: nanoid(),
|
|
734
|
+
payload: schedule,
|
|
735
|
+
timestamp: Date.now(),
|
|
736
|
+
type: "schedule:create",
|
|
737
|
+
},
|
|
738
|
+
this.ctx
|
|
739
|
+
);
|
|
740
|
+
|
|
538
741
|
if (typeof callback !== "string") {
|
|
539
742
|
throw new Error("Callback must be a string");
|
|
540
743
|
}
|
|
@@ -552,15 +755,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
552
755
|
)}, 'scheduled', ${timestamp})
|
|
553
756
|
`;
|
|
554
757
|
|
|
555
|
-
await this
|
|
758
|
+
await this._scheduleNextAlarm();
|
|
556
759
|
|
|
557
|
-
|
|
558
|
-
id,
|
|
760
|
+
const schedule: Schedule<T> = {
|
|
559
761
|
callback: callback,
|
|
762
|
+
id,
|
|
560
763
|
payload: payload as T,
|
|
561
764
|
time: timestamp,
|
|
562
765
|
type: "scheduled",
|
|
563
766
|
};
|
|
767
|
+
|
|
768
|
+
emitScheduleCreate(schedule);
|
|
769
|
+
|
|
770
|
+
return schedule;
|
|
564
771
|
}
|
|
565
772
|
if (typeof when === "number") {
|
|
566
773
|
const time = new Date(Date.now() + when * 1000);
|
|
@@ -573,16 +780,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
573
780
|
)}, 'delayed', ${when}, ${timestamp})
|
|
574
781
|
`;
|
|
575
782
|
|
|
576
|
-
await this
|
|
783
|
+
await this._scheduleNextAlarm();
|
|
577
784
|
|
|
578
|
-
|
|
579
|
-
id,
|
|
785
|
+
const schedule: Schedule<T> = {
|
|
580
786
|
callback: callback,
|
|
581
|
-
payload: payload as T,
|
|
582
787
|
delayInSeconds: when,
|
|
788
|
+
id,
|
|
789
|
+
payload: payload as T,
|
|
583
790
|
time: timestamp,
|
|
584
791
|
type: "delayed",
|
|
585
792
|
};
|
|
793
|
+
|
|
794
|
+
emitScheduleCreate(schedule);
|
|
795
|
+
|
|
796
|
+
return schedule;
|
|
586
797
|
}
|
|
587
798
|
if (typeof when === "string") {
|
|
588
799
|
const nextExecutionTime = getNextCronTime(when);
|
|
@@ -595,16 +806,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
595
806
|
)}, 'cron', ${when}, ${timestamp})
|
|
596
807
|
`;
|
|
597
808
|
|
|
598
|
-
await this
|
|
809
|
+
await this._scheduleNextAlarm();
|
|
599
810
|
|
|
600
|
-
|
|
601
|
-
id,
|
|
811
|
+
const schedule: Schedule<T> = {
|
|
602
812
|
callback: callback,
|
|
603
|
-
payload: payload as T,
|
|
604
813
|
cron: when,
|
|
814
|
+
id,
|
|
815
|
+
payload: payload as T,
|
|
605
816
|
time: timestamp,
|
|
606
817
|
type: "cron",
|
|
607
818
|
};
|
|
819
|
+
|
|
820
|
+
emitScheduleCreate(schedule);
|
|
821
|
+
|
|
822
|
+
return schedule;
|
|
608
823
|
}
|
|
609
824
|
throw new Error("Invalid schedule type");
|
|
610
825
|
}
|
|
@@ -680,13 +895,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
680
895
|
* @returns true if the task was cancelled, false otherwise
|
|
681
896
|
*/
|
|
682
897
|
async cancelSchedule(id: string): Promise<boolean> {
|
|
898
|
+
const schedule = await this.getSchedule(id);
|
|
899
|
+
if (schedule) {
|
|
900
|
+
this.observability?.emit(
|
|
901
|
+
{
|
|
902
|
+
displayMessage: `Schedule ${id} cancelled`,
|
|
903
|
+
id: nanoid(),
|
|
904
|
+
payload: schedule,
|
|
905
|
+
timestamp: Date.now(),
|
|
906
|
+
type: "schedule:cancel",
|
|
907
|
+
},
|
|
908
|
+
this.ctx
|
|
909
|
+
);
|
|
910
|
+
}
|
|
683
911
|
this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
|
|
684
912
|
|
|
685
|
-
await this
|
|
913
|
+
await this._scheduleNextAlarm();
|
|
686
914
|
return true;
|
|
687
915
|
}
|
|
688
916
|
|
|
689
|
-
async
|
|
917
|
+
private async _scheduleNextAlarm() {
|
|
690
918
|
// Find the next schedule that needs to be executed
|
|
691
919
|
const result = this.sql`
|
|
692
920
|
SELECT time FROM cf_agents_schedules
|
|
@@ -703,10 +931,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
703
931
|
}
|
|
704
932
|
|
|
705
933
|
/**
|
|
706
|
-
* Method called when an alarm fires
|
|
707
|
-
* Executes any scheduled tasks that are due
|
|
934
|
+
* Method called when an alarm fires.
|
|
935
|
+
* Executes any scheduled tasks that are due.
|
|
936
|
+
*
|
|
937
|
+
* @remarks
|
|
938
|
+
* To schedule a task, please use the `this.schedule` method instead.
|
|
939
|
+
* See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
|
|
708
940
|
*/
|
|
709
|
-
async
|
|
941
|
+
public readonly alarm = async () => {
|
|
710
942
|
const now = Math.floor(Date.now() / 1000);
|
|
711
943
|
|
|
712
944
|
// Get all schedules that should be executed now
|
|
@@ -724,6 +956,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
724
956
|
{ agent: this, connection: undefined, request: undefined },
|
|
725
957
|
async () => {
|
|
726
958
|
try {
|
|
959
|
+
this.observability?.emit(
|
|
960
|
+
{
|
|
961
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
962
|
+
id: nanoid(),
|
|
963
|
+
payload: row,
|
|
964
|
+
timestamp: Date.now(),
|
|
965
|
+
type: "schedule:execute",
|
|
966
|
+
},
|
|
967
|
+
this.ctx
|
|
968
|
+
);
|
|
969
|
+
|
|
727
970
|
await (
|
|
728
971
|
callback as (
|
|
729
972
|
payload: unknown,
|
|
@@ -752,8 +995,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
752
995
|
}
|
|
753
996
|
|
|
754
997
|
// Schedule the next alarm
|
|
755
|
-
await this
|
|
756
|
-
}
|
|
998
|
+
await this._scheduleNextAlarm();
|
|
999
|
+
};
|
|
757
1000
|
|
|
758
1001
|
/**
|
|
759
1002
|
* Destroy the Agent, removing all state and scheduled tasks
|
|
@@ -762,20 +1005,200 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
762
1005
|
// drop all tables
|
|
763
1006
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
764
1007
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1008
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
765
1009
|
|
|
766
1010
|
// delete all alarms
|
|
767
1011
|
await this.ctx.storage.deleteAlarm();
|
|
768
1012
|
await this.ctx.storage.deleteAll();
|
|
1013
|
+
this.ctx.abort("destroyed"); // enforce that the agent is evicted
|
|
1014
|
+
|
|
1015
|
+
this.observability?.emit(
|
|
1016
|
+
{
|
|
1017
|
+
displayMessage: "Agent destroyed",
|
|
1018
|
+
id: nanoid(),
|
|
1019
|
+
payload: {},
|
|
1020
|
+
timestamp: Date.now(),
|
|
1021
|
+
type: "destroy",
|
|
1022
|
+
},
|
|
1023
|
+
this.ctx
|
|
1024
|
+
);
|
|
769
1025
|
}
|
|
770
1026
|
|
|
771
1027
|
/**
|
|
772
1028
|
* Get all methods marked as callable on this Agent
|
|
773
1029
|
* @returns A map of method names to their metadata
|
|
774
1030
|
*/
|
|
775
|
-
|
|
776
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
1031
|
+
private _isCallable(method: string): boolean {
|
|
777
1032
|
return callableMetadata.has(this[method as keyof this] as Function);
|
|
778
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Connect to a new MCP Server
|
|
1037
|
+
*
|
|
1038
|
+
* @param url MCP Server SSE URL
|
|
1039
|
+
* @param callbackHost Base host for the agent, used for the redirect URI.
|
|
1040
|
+
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
1041
|
+
* @param options MCP client and transport (header) options
|
|
1042
|
+
* @returns authUrl
|
|
1043
|
+
*/
|
|
1044
|
+
async addMcpServer(
|
|
1045
|
+
serverName: string,
|
|
1046
|
+
url: string,
|
|
1047
|
+
callbackHost: string,
|
|
1048
|
+
agentsPrefix = "agents",
|
|
1049
|
+
options?: {
|
|
1050
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
1051
|
+
transport?: {
|
|
1052
|
+
headers: HeadersInit;
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
1056
|
+
const callbackUrl = `${callbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
1057
|
+
|
|
1058
|
+
const result = await this._connectToMcpServerInternal(
|
|
1059
|
+
serverName,
|
|
1060
|
+
url,
|
|
1061
|
+
callbackUrl,
|
|
1062
|
+
options
|
|
1063
|
+
);
|
|
1064
|
+
this.sql`
|
|
1065
|
+
INSERT
|
|
1066
|
+
OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1067
|
+
VALUES (
|
|
1068
|
+
${result.id},
|
|
1069
|
+
${serverName},
|
|
1070
|
+
${url},
|
|
1071
|
+
${result.clientId ?? null},
|
|
1072
|
+
${result.authUrl ?? null},
|
|
1073
|
+
${callbackUrl},
|
|
1074
|
+
${options ? JSON.stringify(options) : null}
|
|
1075
|
+
);
|
|
1076
|
+
`;
|
|
1077
|
+
|
|
1078
|
+
this.broadcast(
|
|
1079
|
+
JSON.stringify({
|
|
1080
|
+
mcp: this.getMcpServers(),
|
|
1081
|
+
type: "cf_agent_mcp_servers",
|
|
1082
|
+
})
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async _connectToMcpServerInternal(
|
|
1089
|
+
_serverName: string,
|
|
1090
|
+
url: string,
|
|
1091
|
+
callbackUrl: string,
|
|
1092
|
+
// it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
|
|
1093
|
+
options?: {
|
|
1094
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
1095
|
+
/**
|
|
1096
|
+
* We don't expose the normal set of transport options because:
|
|
1097
|
+
* 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
|
|
1098
|
+
* 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
|
|
1099
|
+
*
|
|
1100
|
+
* This has the limitation that you can't override fetch, but I think headers should handle nearly all cases needed (i.e. non-standard bearer auth).
|
|
1101
|
+
*/
|
|
1102
|
+
transport?: {
|
|
1103
|
+
headers?: HeadersInit;
|
|
1104
|
+
};
|
|
1105
|
+
},
|
|
1106
|
+
reconnect?: {
|
|
1107
|
+
id: string;
|
|
1108
|
+
oauthClientId?: string;
|
|
1109
|
+
}
|
|
1110
|
+
): Promise<{
|
|
1111
|
+
id: string;
|
|
1112
|
+
authUrl: string | undefined;
|
|
1113
|
+
clientId: string | undefined;
|
|
1114
|
+
}> {
|
|
1115
|
+
const authProvider = new DurableObjectOAuthClientProvider(
|
|
1116
|
+
this.ctx.storage,
|
|
1117
|
+
this.name,
|
|
1118
|
+
callbackUrl
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
if (reconnect) {
|
|
1122
|
+
authProvider.serverId = reconnect.id;
|
|
1123
|
+
if (reconnect.oauthClientId) {
|
|
1124
|
+
authProvider.clientId = reconnect.oauthClientId;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// allows passing through transport headers if necessary
|
|
1129
|
+
// this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
|
|
1130
|
+
let headerTransportOpts: SSEClientTransportOptions = {};
|
|
1131
|
+
if (options?.transport?.headers) {
|
|
1132
|
+
headerTransportOpts = {
|
|
1133
|
+
eventSourceInit: {
|
|
1134
|
+
fetch: (url, init) =>
|
|
1135
|
+
fetch(url, {
|
|
1136
|
+
...init,
|
|
1137
|
+
headers: options?.transport?.headers,
|
|
1138
|
+
}),
|
|
1139
|
+
},
|
|
1140
|
+
requestInit: {
|
|
1141
|
+
headers: options?.transport?.headers,
|
|
1142
|
+
},
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const { id, authUrl, clientId } = await this.mcp.connect(url, {
|
|
1147
|
+
client: options?.client,
|
|
1148
|
+
reconnect,
|
|
1149
|
+
transport: {
|
|
1150
|
+
...headerTransportOpts,
|
|
1151
|
+
authProvider,
|
|
1152
|
+
},
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
return {
|
|
1156
|
+
authUrl,
|
|
1157
|
+
clientId,
|
|
1158
|
+
id,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async removeMcpServer(id: string) {
|
|
1163
|
+
this.mcp.closeConnection(id);
|
|
1164
|
+
this.sql`
|
|
1165
|
+
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1166
|
+
`;
|
|
1167
|
+
this.broadcast(
|
|
1168
|
+
JSON.stringify({
|
|
1169
|
+
mcp: this.getMcpServers(),
|
|
1170
|
+
type: "cf_agent_mcp_servers",
|
|
1171
|
+
})
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
getMcpServers(): MCPServersState {
|
|
1176
|
+
const mcpState: MCPServersState = {
|
|
1177
|
+
prompts: this.mcp.listPrompts(),
|
|
1178
|
+
resources: this.mcp.listResources(),
|
|
1179
|
+
servers: {},
|
|
1180
|
+
tools: this.mcp.listTools(),
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const servers = this.sql<MCPServerRow>`
|
|
1184
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1185
|
+
`;
|
|
1186
|
+
|
|
1187
|
+
for (const server of servers) {
|
|
1188
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1189
|
+
mcpState.servers[server.id] = {
|
|
1190
|
+
auth_url: server.auth_url,
|
|
1191
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1192
|
+
instructions: serverConn?.instructions ?? null,
|
|
1193
|
+
name: server.name,
|
|
1194
|
+
server_url: server.server_url,
|
|
1195
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1196
|
+
state: serverConn?.connectionState ?? "authenticating",
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return mcpState;
|
|
1201
|
+
}
|
|
779
1202
|
}
|
|
780
1203
|
|
|
781
1204
|
/**
|
|
@@ -815,9 +1238,9 @@ export async function routeAgentRequest<Env>(
|
|
|
815
1238
|
const corsHeaders =
|
|
816
1239
|
options?.cors === true
|
|
817
1240
|
? {
|
|
818
|
-
"Access-Control-Allow-Origin": "*",
|
|
819
|
-
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
820
1241
|
"Access-Control-Allow-Credentials": "true",
|
|
1242
|
+
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1243
|
+
"Access-Control-Allow-Origin": "*",
|
|
821
1244
|
"Access-Control-Max-Age": "86400",
|
|
822
1245
|
}
|
|
823
1246
|
: options?.cors;
|
|
@@ -865,9 +1288,9 @@ export async function routeAgentRequest<Env>(
|
|
|
865
1288
|
* @param options Routing options
|
|
866
1289
|
*/
|
|
867
1290
|
export async function routeAgentEmail<Env>(
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1291
|
+
_email: ForwardableEmailMessage,
|
|
1292
|
+
_env: Env,
|
|
1293
|
+
_options?: AgentOptions<Env>
|
|
871
1294
|
): Promise<void> {}
|
|
872
1295
|
|
|
873
1296
|
/**
|
|
@@ -894,13 +1317,13 @@ export async function getAgentByName<Env, T extends Agent<Env>>(
|
|
|
894
1317
|
* A wrapper for streaming responses in callable methods
|
|
895
1318
|
*/
|
|
896
1319
|
export class StreamingResponse {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1320
|
+
private _connection: Connection;
|
|
1321
|
+
private _id: string;
|
|
1322
|
+
private _closed = false;
|
|
900
1323
|
|
|
901
1324
|
constructor(connection: Connection, id: string) {
|
|
902
|
-
this
|
|
903
|
-
this
|
|
1325
|
+
this._connection = connection;
|
|
1326
|
+
this._id = id;
|
|
904
1327
|
}
|
|
905
1328
|
|
|
906
1329
|
/**
|
|
@@ -908,17 +1331,17 @@ export class StreamingResponse {
|
|
|
908
1331
|
* @param chunk The data to send
|
|
909
1332
|
*/
|
|
910
1333
|
send(chunk: unknown) {
|
|
911
|
-
if (this
|
|
1334
|
+
if (this._closed) {
|
|
912
1335
|
throw new Error("StreamingResponse is already closed");
|
|
913
1336
|
}
|
|
914
1337
|
const response: RPCResponse = {
|
|
915
|
-
type: "rpc",
|
|
916
|
-
id: this.#id,
|
|
917
|
-
success: true,
|
|
918
|
-
result: chunk,
|
|
919
1338
|
done: false,
|
|
1339
|
+
id: this._id,
|
|
1340
|
+
result: chunk,
|
|
1341
|
+
success: true,
|
|
1342
|
+
type: "rpc",
|
|
920
1343
|
};
|
|
921
|
-
this
|
|
1344
|
+
this._connection.send(JSON.stringify(response));
|
|
922
1345
|
}
|
|
923
1346
|
|
|
924
1347
|
/**
|
|
@@ -926,17 +1349,17 @@ export class StreamingResponse {
|
|
|
926
1349
|
* @param finalChunk Optional final chunk of data to send
|
|
927
1350
|
*/
|
|
928
1351
|
end(finalChunk?: unknown) {
|
|
929
|
-
if (this
|
|
1352
|
+
if (this._closed) {
|
|
930
1353
|
throw new Error("StreamingResponse is already closed");
|
|
931
1354
|
}
|
|
932
|
-
this
|
|
1355
|
+
this._closed = true;
|
|
933
1356
|
const response: RPCResponse = {
|
|
934
|
-
type: "rpc",
|
|
935
|
-
id: this.#id,
|
|
936
|
-
success: true,
|
|
937
|
-
result: finalChunk,
|
|
938
1357
|
done: true,
|
|
1358
|
+
id: this._id,
|
|
1359
|
+
result: finalChunk,
|
|
1360
|
+
success: true,
|
|
1361
|
+
type: "rpc",
|
|
939
1362
|
};
|
|
940
|
-
this
|
|
1363
|
+
this._connection.send(JSON.stringify(response));
|
|
941
1364
|
}
|
|
942
1365
|
}
|