agents 0.5.1 → 0.7.0
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/agent-DnmmRjyv.d.ts +231 -0
- package/dist/{client-connection-CGMuV62J.js → client-connection-D3Wcd6Q6.js} +154 -23
- package/dist/client-connection-D3Wcd6Q6.js.map +1 -0
- package/dist/{client-storage-D633wI1S.d.ts → client-storage-tusTuoSF.d.ts} +262 -13
- package/dist/experimental/forever.d.ts +7 -18
- package/dist/experimental/forever.js +4 -38
- package/dist/experimental/forever.js.map +1 -1
- package/dist/experimental/memory/session/index.d.ts +251 -0
- package/dist/experimental/memory/session/index.js +385 -0
- package/dist/experimental/memory/session/index.js.map +1 -0
- package/dist/index.d.ts +84 -22
- package/dist/index.js +324 -250
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +51 -7
- package/dist/mcp/client.js +157 -32
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/do-oauth-client-provider.js +1 -1
- package/dist/mcp/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/index.d.ts +27 -165
- package/dist/mcp/index.js +50 -10
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +72 -6
- package/dist/observability/index.js +61 -16
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/workflows.d.ts +15 -1
- package/dist/workflows.js +36 -3
- package/dist/workflows.js.map +1 -1
- package/package.json +16 -11
- package/dist/agent-B4_kEsdK.d.ts +0 -60
- package/dist/client-connection-CGMuV62J.js.map +0 -1
- package/dist/mcp-DA0kDE7K.d.ts +0 -61
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as MCPObservabilityEvent } from "./agent-DnmmRjyv.js";
|
|
2
2
|
import { AgentMcpOAuthProvider } from "./mcp/do-oauth-client-provider.js";
|
|
3
|
+
import { McpAgent } from "./mcp/index.js";
|
|
3
4
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
5
|
import {
|
|
5
6
|
SSEClientTransport,
|
|
@@ -12,12 +13,24 @@ import {
|
|
|
12
13
|
import {
|
|
13
14
|
ElicitRequest,
|
|
14
15
|
ElicitResult,
|
|
16
|
+
InitializeRequestParams,
|
|
17
|
+
JSONRPCMessage,
|
|
18
|
+
MessageExtraInfo,
|
|
15
19
|
Prompt,
|
|
20
|
+
RequestId,
|
|
16
21
|
Resource,
|
|
17
22
|
ResourceTemplate,
|
|
18
23
|
ServerCapabilities,
|
|
19
24
|
Tool
|
|
20
25
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
|
+
import {
|
|
28
|
+
Transport,
|
|
29
|
+
TransportSendOptions
|
|
30
|
+
} from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
31
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
32
|
+
import { Client as Client$1 } from "@modelcontextprotocol/sdk/client";
|
|
33
|
+
import { EventStore } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21
34
|
|
|
22
35
|
//#region src/core/events.d.ts
|
|
23
36
|
interface Disposable {
|
|
@@ -33,7 +46,8 @@ declare class Emitter<T> implements Disposable {
|
|
|
33
46
|
//#endregion
|
|
34
47
|
//#region src/mcp/types.d.ts
|
|
35
48
|
type MaybePromise<T> = T | Promise<T>;
|
|
36
|
-
type
|
|
49
|
+
type HttpTransportType = "sse" | "streamable-http";
|
|
50
|
+
type BaseTransportType = HttpTransportType | "rpc";
|
|
37
51
|
type TransportType = BaseTransportType | "auto";
|
|
38
52
|
interface CORSOptions {
|
|
39
53
|
origin?: string;
|
|
@@ -48,6 +62,221 @@ interface ServeOptions {
|
|
|
48
62
|
transport?: BaseTransportType;
|
|
49
63
|
jurisdiction?: DurableObjectJurisdiction;
|
|
50
64
|
}
|
|
65
|
+
type McpClientOptions = ConstructorParameters<typeof Client$1>[1];
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/mcp/client-transports.d.ts
|
|
68
|
+
/**
|
|
69
|
+
* @deprecated Use SSEClientTransport from @modelcontextprotocol/sdk/client/sse.js instead. This alias will be removed in the next major version.
|
|
70
|
+
*/
|
|
71
|
+
declare class SSEEdgeClientTransport extends SSEClientTransport {
|
|
72
|
+
constructor(url: URL, options: SSEClientTransportOptions);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated Use StreamableHTTPClientTransport from @modelcontextprotocol/sdk/client/streamableHttp.js instead. This alias will be removed in the next major version.
|
|
76
|
+
*/
|
|
77
|
+
declare class StreamableHTTPEdgeClientTransport extends StreamableHTTPClientTransport {
|
|
78
|
+
constructor(url: URL, options: StreamableHTTPClientTransportOptions);
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/mcp/worker-transport.d.ts
|
|
82
|
+
interface MCPStorageApi {
|
|
83
|
+
get(): Promise<TransportState | undefined> | TransportState | undefined;
|
|
84
|
+
set(state: TransportState): Promise<void> | void;
|
|
85
|
+
}
|
|
86
|
+
interface TransportState {
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
initialized: boolean;
|
|
89
|
+
initializeParams?: InitializeRequestParams;
|
|
90
|
+
}
|
|
91
|
+
interface WorkerTransportOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Function that generates a session ID for the transport.
|
|
94
|
+
* The session ID SHOULD be globally unique and cryptographically secure.
|
|
95
|
+
* Return undefined to disable session management (stateless mode).
|
|
96
|
+
*/
|
|
97
|
+
sessionIdGenerator?: () => string;
|
|
98
|
+
/**
|
|
99
|
+
* Enable traditional Request/Response mode, this will disable streaming.
|
|
100
|
+
*/
|
|
101
|
+
enableJsonResponse?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Callback fired when a new session is initialized.
|
|
104
|
+
*/
|
|
105
|
+
onsessioninitialized?: (sessionId: string) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Callback fired when a session is closed via DELETE request.
|
|
108
|
+
*/
|
|
109
|
+
onsessionclosed?: (sessionId: string) => void;
|
|
110
|
+
corsOptions?: CORSOptions;
|
|
111
|
+
/**
|
|
112
|
+
* Optional storage api for persisting transport state.
|
|
113
|
+
* Use this to store session state in Durable Object/Agent storage
|
|
114
|
+
* so it survives hibernation/restart.
|
|
115
|
+
*/
|
|
116
|
+
storage?: MCPStorageApi;
|
|
117
|
+
/**
|
|
118
|
+
* Event store for resumability support.
|
|
119
|
+
* If provided, enables clients to reconnect and resume messages using Last-Event-ID.
|
|
120
|
+
*/
|
|
121
|
+
eventStore?: EventStore;
|
|
122
|
+
/**
|
|
123
|
+
* Retry interval in milliseconds to suggest to clients in SSE retry field.
|
|
124
|
+
* Controls client reconnection timing for polling behavior.
|
|
125
|
+
*/
|
|
126
|
+
retryInterval?: number;
|
|
127
|
+
}
|
|
128
|
+
declare class WorkerTransport implements Transport {
|
|
129
|
+
started: boolean;
|
|
130
|
+
private initialized;
|
|
131
|
+
private sessionIdGenerator?;
|
|
132
|
+
private enableJsonResponse;
|
|
133
|
+
private onsessioninitialized?;
|
|
134
|
+
private onsessionclosed?;
|
|
135
|
+
private standaloneSseStreamId;
|
|
136
|
+
private streamMapping;
|
|
137
|
+
private requestToStreamMapping;
|
|
138
|
+
private requestResponseMap;
|
|
139
|
+
private corsOptions?;
|
|
140
|
+
private storage?;
|
|
141
|
+
private stateRestored;
|
|
142
|
+
private eventStore?;
|
|
143
|
+
private retryInterval?;
|
|
144
|
+
private initializeParams?;
|
|
145
|
+
sessionId?: string;
|
|
146
|
+
onclose?: () => void;
|
|
147
|
+
onerror?: (error: Error) => void;
|
|
148
|
+
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
|
|
149
|
+
constructor(options?: WorkerTransportOptions);
|
|
150
|
+
/**
|
|
151
|
+
* Restore transport state from persistent storage.
|
|
152
|
+
* This is automatically called on start.
|
|
153
|
+
*/
|
|
154
|
+
private restoreState;
|
|
155
|
+
/**
|
|
156
|
+
* Persist current transport state to storage.
|
|
157
|
+
*/
|
|
158
|
+
private saveState;
|
|
159
|
+
start(): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Validates the MCP-Protocol-Version header on incoming requests.
|
|
162
|
+
*
|
|
163
|
+
* This performs a simple check: if a version header is present, it must be
|
|
164
|
+
* in the SUPPORTED_PROTOCOL_VERSIONS list. We do not track the negotiated
|
|
165
|
+
* version or enforce version consistency across requests - the SDK handles
|
|
166
|
+
* version negotiation during initialization, and we simply reject any
|
|
167
|
+
* explicitly unsupported versions.
|
|
168
|
+
*
|
|
169
|
+
* - Header present and supported: Accept
|
|
170
|
+
* - Header present and unsupported: 400 Bad Request
|
|
171
|
+
* - Header missing: Accept (version validation is optional)
|
|
172
|
+
*/
|
|
173
|
+
private validateProtocolVersion;
|
|
174
|
+
private getHeaders;
|
|
175
|
+
handleRequest(request: Request, parsedBody?: unknown): Promise<Response>;
|
|
176
|
+
private handleGetRequest;
|
|
177
|
+
private handlePostRequest;
|
|
178
|
+
private handleDeleteRequest;
|
|
179
|
+
private handleOptionsRequest;
|
|
180
|
+
private handleUnsupportedRequest;
|
|
181
|
+
private validateSession;
|
|
182
|
+
close(): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Close an SSE stream for a specific request, triggering client reconnection.
|
|
185
|
+
* Use this to implement polling behavior during long-running operations -
|
|
186
|
+
* client will reconnect after the retry interval specified in the priming event.
|
|
187
|
+
*/
|
|
188
|
+
closeSSEStream(requestId: RequestId): void;
|
|
189
|
+
send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void>;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/mcp/auth-context.d.ts
|
|
193
|
+
interface McpAuthContext {
|
|
194
|
+
props: Record<string, unknown>;
|
|
195
|
+
}
|
|
196
|
+
declare function getMcpAuthContext(): McpAuthContext | undefined;
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/mcp/handler.d.ts
|
|
199
|
+
interface CreateMcpHandlerOptions extends WorkerTransportOptions {
|
|
200
|
+
/**
|
|
201
|
+
* The route path that this MCP handler should respond to.
|
|
202
|
+
* If specified, the handler will only process requests that match this route.
|
|
203
|
+
* @default "/mcp"
|
|
204
|
+
*/
|
|
205
|
+
route?: string;
|
|
206
|
+
/**
|
|
207
|
+
* An optional auth context to use for handling MCP requests.
|
|
208
|
+
* If not provided, the handler will look for props in the execution context.
|
|
209
|
+
*/
|
|
210
|
+
authContext?: McpAuthContext;
|
|
211
|
+
/**
|
|
212
|
+
* An optional transport to use for handling MCP requests.
|
|
213
|
+
* If not provided, a WorkerTransport will be created with the provided WorkerTransportOptions.
|
|
214
|
+
*/
|
|
215
|
+
transport?: WorkerTransport;
|
|
216
|
+
}
|
|
217
|
+
declare function createMcpHandler(
|
|
218
|
+
server: McpServer | Server,
|
|
219
|
+
options?: CreateMcpHandlerOptions
|
|
220
|
+
): (request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>;
|
|
221
|
+
/**
|
|
222
|
+
* @deprecated This has been renamed to createMcpHandler, and experimental_createMcpHandler will be removed in the next major version
|
|
223
|
+
*/
|
|
224
|
+
declare function experimental_createMcpHandler(
|
|
225
|
+
server: McpServer | Server,
|
|
226
|
+
options?: CreateMcpHandlerOptions
|
|
227
|
+
): (request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>;
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/mcp/rpc.d.ts
|
|
230
|
+
declare const RPC_DO_PREFIX = "rpc:";
|
|
231
|
+
interface RPCClientTransportOptions<T extends McpAgent = McpAgent> {
|
|
232
|
+
namespace: DurableObjectNamespace<T>;
|
|
233
|
+
name: string;
|
|
234
|
+
props?: Record<string, unknown>;
|
|
235
|
+
}
|
|
236
|
+
declare class RPCClientTransport implements Transport {
|
|
237
|
+
private _namespace;
|
|
238
|
+
private _name;
|
|
239
|
+
private _props?;
|
|
240
|
+
private _stub?;
|
|
241
|
+
private _started;
|
|
242
|
+
private _protocolVersion?;
|
|
243
|
+
sessionId?: string;
|
|
244
|
+
onclose?: () => void;
|
|
245
|
+
onerror?: (error: Error) => void;
|
|
246
|
+
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
|
|
247
|
+
constructor(options: RPCClientTransportOptions<McpAgent>);
|
|
248
|
+
setProtocolVersion(version: string): void;
|
|
249
|
+
getProtocolVersion(): string | undefined;
|
|
250
|
+
start(): Promise<void>;
|
|
251
|
+
close(): Promise<void>;
|
|
252
|
+
send(
|
|
253
|
+
message: JSONRPCMessage | JSONRPCMessage[],
|
|
254
|
+
options?: TransportSendOptions
|
|
255
|
+
): Promise<void>;
|
|
256
|
+
}
|
|
257
|
+
interface RPCServerTransportOptions {
|
|
258
|
+
timeout?: number;
|
|
259
|
+
}
|
|
260
|
+
declare class RPCServerTransport implements Transport {
|
|
261
|
+
private _started;
|
|
262
|
+
private _pendingResponse;
|
|
263
|
+
private _responseResolver;
|
|
264
|
+
private _protocolVersion?;
|
|
265
|
+
private _timeout;
|
|
266
|
+
sessionId?: string;
|
|
267
|
+
onclose?: () => void;
|
|
268
|
+
onerror?: (error: Error) => void;
|
|
269
|
+
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
|
|
270
|
+
constructor(options?: RPCServerTransportOptions);
|
|
271
|
+
setProtocolVersion(version: string): void;
|
|
272
|
+
getProtocolVersion(): string | undefined;
|
|
273
|
+
start(): Promise<void>;
|
|
274
|
+
close(): Promise<void>;
|
|
275
|
+
send(message: JSONRPCMessage, _options?: TransportSendOptions): Promise<void>;
|
|
276
|
+
handle(
|
|
277
|
+
message: JSONRPCMessage | JSONRPCMessage[]
|
|
278
|
+
): Promise<JSONRPCMessage | JSONRPCMessage[] | undefined>;
|
|
279
|
+
}
|
|
51
280
|
//#endregion
|
|
52
281
|
//#region src/mcp/client-connection.d.ts
|
|
53
282
|
/**
|
|
@@ -71,9 +300,14 @@ declare const MCPConnectionState: {
|
|
|
71
300
|
*/
|
|
72
301
|
type MCPConnectionState =
|
|
73
302
|
(typeof MCPConnectionState)[keyof typeof MCPConnectionState];
|
|
303
|
+
/**
|
|
304
|
+
* Transport options for MCP client connections.
|
|
305
|
+
* Combines transport-specific options with auth provider and type selection.
|
|
306
|
+
*/
|
|
74
307
|
type MCPTransportOptions = (
|
|
75
308
|
| SSEClientTransportOptions
|
|
76
309
|
| StreamableHTTPClientTransportOptions
|
|
310
|
+
| RPCClientTransportOptions
|
|
77
311
|
) & {
|
|
78
312
|
authProvider?: AgentMcpOAuthProvider;
|
|
79
313
|
type?: TransportType;
|
|
@@ -91,7 +325,7 @@ declare class MCPClientConnection {
|
|
|
91
325
|
url: URL;
|
|
92
326
|
options: {
|
|
93
327
|
transport: MCPTransportOptions;
|
|
94
|
-
client:
|
|
328
|
+
client: McpClientOptions;
|
|
95
329
|
};
|
|
96
330
|
client: Client;
|
|
97
331
|
connectionState: MCPConnectionState;
|
|
@@ -112,7 +346,7 @@ declare class MCPClientConnection {
|
|
|
112
346
|
info: ConstructorParameters<typeof Client>[0],
|
|
113
347
|
options?: {
|
|
114
348
|
transport: MCPTransportOptions;
|
|
115
|
-
client:
|
|
349
|
+
client: McpClientOptions;
|
|
116
350
|
}
|
|
117
351
|
);
|
|
118
352
|
/**
|
|
@@ -320,7 +554,7 @@ declare class MCPClientConnection {
|
|
|
320
554
|
*/
|
|
321
555
|
getTransport(
|
|
322
556
|
transportType: BaseTransportType
|
|
323
|
-
): StreamableHTTPClientTransport | SSEClientTransport;
|
|
557
|
+
): StreamableHTTPClientTransport | SSEClientTransport | RPCClientTransport;
|
|
324
558
|
private tryConnect;
|
|
325
559
|
private _capabilityErrorHandler;
|
|
326
560
|
}
|
|
@@ -340,16 +574,31 @@ type MCPServerRow = {
|
|
|
340
574
|
};
|
|
341
575
|
//#endregion
|
|
342
576
|
export {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
577
|
+
ServeOptions as C,
|
|
578
|
+
Event as E,
|
|
579
|
+
McpClientOptions as S,
|
|
580
|
+
Emitter as T,
|
|
581
|
+
WorkerTransportOptions as _,
|
|
582
|
+
RPCClientTransport as a,
|
|
583
|
+
BaseTransportType as b,
|
|
584
|
+
RPCServerTransportOptions as c,
|
|
585
|
+
createMcpHandler as d,
|
|
586
|
+
experimental_createMcpHandler as f,
|
|
587
|
+
WorkerTransport as g,
|
|
588
|
+
TransportState as h,
|
|
346
589
|
MCPTransportOptions as i,
|
|
347
|
-
|
|
590
|
+
RPC_DO_PREFIX as l,
|
|
591
|
+
getMcpAuthContext as m,
|
|
348
592
|
MCPClientConnection as n,
|
|
349
|
-
|
|
593
|
+
RPCClientTransportOptions as o,
|
|
594
|
+
McpAuthContext as p,
|
|
350
595
|
MCPConnectionState as r,
|
|
351
|
-
|
|
596
|
+
RPCServerTransport as s,
|
|
352
597
|
MCPServerRow as t,
|
|
353
|
-
|
|
598
|
+
CreateMcpHandlerOptions as u,
|
|
599
|
+
SSEEdgeClientTransport as v,
|
|
600
|
+
TransportType as w,
|
|
601
|
+
MaybePromise as x,
|
|
602
|
+
StreamableHTTPEdgeClientTransport as y
|
|
354
603
|
};
|
|
355
|
-
//# sourceMappingURL=client-storage-
|
|
604
|
+
//# sourceMappingURL=client-storage-tusTuoSF.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RetryOptions } from "../retries.js";
|
|
2
|
-
import "../client-storage-
|
|
2
|
+
import "../client-storage-tusTuoSF.js";
|
|
3
3
|
import { Agent, Schedule } from "../index.js";
|
|
4
4
|
|
|
5
5
|
//#region src/experimental/forever.d.ts
|
|
@@ -52,7 +52,7 @@ type RawFiberRow = {
|
|
|
52
52
|
created_at: number;
|
|
53
53
|
};
|
|
54
54
|
type Constructor<T = object> = new (...args: any[]) => T;
|
|
55
|
-
type AgentLike = Constructor<Pick<Agent<Cloudflare.Env>, "sql" | "scheduleEvery" | "cancelSchedule" | "alarm">>;
|
|
55
|
+
type AgentLike = Constructor<Pick<Agent<Cloudflare.Env>, "sql" | "scheduleEvery" | "cancelSchedule" | "alarm" | "keepAlive" | "keepAliveWhile">>;
|
|
56
56
|
declare function withFibers<TBase extends AgentLike>(Base: TBase, options?: {
|
|
57
57
|
debugFibers?: boolean;
|
|
58
58
|
}): {
|
|
@@ -61,8 +61,7 @@ declare function withFibers<TBase extends AgentLike>(Base: TBase, options?: {
|
|
|
61
61
|
_fiberRecoveryInProgress: boolean; /** @internal */
|
|
62
62
|
_fiberLastCleanupTime: number; /** @internal */
|
|
63
63
|
_fiberDebug(msg: string, ...args: unknown[]): void; /** @internal */
|
|
64
|
-
|
|
65
|
-
keepAlive(): Promise<() => void>;
|
|
64
|
+
_cf_keepAliveHeartbeat(): Promise<void>;
|
|
66
65
|
spawnFiber(methodName: keyof /*elided*/any, payload?: unknown, options?: {
|
|
67
66
|
maxRetries?: number;
|
|
68
67
|
}): string;
|
|
@@ -93,26 +92,16 @@ declare function withFibers<TBase extends AgentLike>(Base: TBase, options?: {
|
|
|
93
92
|
_checkInterruptedFibers(): Promise<void>; /** @internal */
|
|
94
93
|
_cleanupOrphanedHeartbeats(): void; /** @internal */
|
|
95
94
|
_maybeCleanupFibers(): void;
|
|
95
|
+
readonly alarm: () => Promise<void>;
|
|
96
96
|
sql: <T = Record<string, string | number | boolean | null>>(strings: TemplateStringsArray, ...values: (string | number | boolean | null)[]) => T[];
|
|
97
97
|
scheduleEvery: <T = string>(intervalSeconds: number, callback: keyof Agent<Cloudflare.Env, unknown, Record<string, unknown>>, payload?: T | undefined, options?: {
|
|
98
98
|
retry?: RetryOptions;
|
|
99
99
|
}) => Promise<Schedule<T>>;
|
|
100
100
|
cancelSchedule: (id: string) => Promise<boolean>;
|
|
101
|
-
|
|
101
|
+
keepAlive: () => Promise<() => void>;
|
|
102
|
+
keepAliveWhile: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
102
103
|
};
|
|
103
104
|
} & TBase;
|
|
104
|
-
/**
|
|
105
|
-
* Keep a Durable Object alive via a scheduled heartbeat.
|
|
106
|
-
* Returns a disposer function that cancels the heartbeat.
|
|
107
|
-
*
|
|
108
|
-
* Standalone version usable by any Agent subclass without requiring
|
|
109
|
-
* the full fiber mixin. The agent must have a no-op method with the
|
|
110
|
-
* given callbackName for the scheduler to invoke.
|
|
111
|
-
*
|
|
112
|
-
* @param agent - The agent instance (must have scheduleEvery and cancelSchedule)
|
|
113
|
-
* @param callbackName - Name of a no-op method on the agent class (must exist)
|
|
114
|
-
*/
|
|
115
|
-
declare function keepAlive(agent: Pick<Agent<Cloudflare.Env>, "scheduleEvery" | "cancelSchedule">, callbackName: string): Promise<() => void>;
|
|
116
105
|
//#endregion
|
|
117
|
-
export { FiberCompleteContext, FiberContext, FiberRecoveryContext, FiberState,
|
|
106
|
+
export { FiberCompleteContext, FiberContext, FiberRecoveryContext, FiberState, withFibers };
|
|
118
107
|
//# sourceMappingURL=forever.d.ts.map
|
|
@@ -33,8 +33,6 @@ import { nanoid } from "nanoid";
|
|
|
33
33
|
*
|
|
34
34
|
* @experimental This API is not yet stable and may change.
|
|
35
35
|
*/
|
|
36
|
-
console.warn("[agents/experimental/forever] WARNING: You are using an experimental API that WILL break between releases. Do not use in production.");
|
|
37
|
-
const KEEP_ALIVE_INTERVAL_MS = 1e4;
|
|
38
36
|
const FIBER_CLEANUP_INTERVAL_MS = 600 * 1e3;
|
|
39
37
|
const FIBER_CLEANUP_COMPLETED_MS = 1440 * 60 * 1e3;
|
|
40
38
|
const FIBER_CLEANUP_FAILED_MS = 10080 * 60 * 1e3;
|
|
@@ -47,6 +45,7 @@ function withFibers(Base, options) {
|
|
|
47
45
|
this._fiberActiveFibers = /* @__PURE__ */ new Set();
|
|
48
46
|
this._fiberRecoveryInProgress = false;
|
|
49
47
|
this._fiberLastCleanupTime = 0;
|
|
48
|
+
console.warn("[agents/experimental/forever] WARNING: You are using an experimental API that WILL break between releases. Do not use in production.");
|
|
50
49
|
this.sql`
|
|
51
50
|
CREATE TABLE IF NOT EXISTS cf_agents_fibers (
|
|
52
51
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -69,21 +68,9 @@ function withFibers(Base, options) {
|
|
|
69
68
|
/** @internal */ _fiberDebug(msg, ...args) {
|
|
70
69
|
if (debugEnabled) console.debug(`[fiber] ${msg}`, ...args);
|
|
71
70
|
}
|
|
72
|
-
/** @internal */ async
|
|
71
|
+
/** @internal */ async _cf_keepAliveHeartbeat() {
|
|
73
72
|
await this._checkInterruptedFibers();
|
|
74
73
|
}
|
|
75
|
-
async keepAlive() {
|
|
76
|
-
const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1e3);
|
|
77
|
-
const schedule = await this.scheduleEvery(heartbeatSeconds, "_cf_fiberHeartbeat");
|
|
78
|
-
this._fiberDebug("keepAlive started, schedule=%s", schedule.id);
|
|
79
|
-
let disposed = false;
|
|
80
|
-
return () => {
|
|
81
|
-
if (disposed) return;
|
|
82
|
-
disposed = true;
|
|
83
|
-
this._fiberDebug("keepAlive disposed, schedule=%s", schedule.id);
|
|
84
|
-
this.cancelSchedule(schedule.id);
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
74
|
spawnFiber(methodName, payload, options) {
|
|
88
75
|
this._maybeCleanupFibers();
|
|
89
76
|
const name = methodName;
|
|
@@ -333,7 +320,7 @@ function withFibers(Base, options) {
|
|
|
333
320
|
/** @internal */ _cleanupOrphanedHeartbeats() {
|
|
334
321
|
this.sql`
|
|
335
322
|
DELETE FROM cf_agents_schedules
|
|
336
|
-
WHERE callback = '
|
|
323
|
+
WHERE callback = '_cf_keepAliveHeartbeat'
|
|
337
324
|
`;
|
|
338
325
|
this._fiberDebug("cleaned up orphaned heartbeat schedules");
|
|
339
326
|
}
|
|
@@ -354,28 +341,7 @@ function withFibers(Base, options) {
|
|
|
354
341
|
}
|
|
355
342
|
return FiberAgent;
|
|
356
343
|
}
|
|
357
|
-
/**
|
|
358
|
-
* Keep a Durable Object alive via a scheduled heartbeat.
|
|
359
|
-
* Returns a disposer function that cancels the heartbeat.
|
|
360
|
-
*
|
|
361
|
-
* Standalone version usable by any Agent subclass without requiring
|
|
362
|
-
* the full fiber mixin. The agent must have a no-op method with the
|
|
363
|
-
* given callbackName for the scheduler to invoke.
|
|
364
|
-
*
|
|
365
|
-
* @param agent - The agent instance (must have scheduleEvery and cancelSchedule)
|
|
366
|
-
* @param callbackName - Name of a no-op method on the agent class (must exist)
|
|
367
|
-
*/
|
|
368
|
-
async function keepAlive(agent, callbackName) {
|
|
369
|
-
const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1e3);
|
|
370
|
-
const schedule = await agent.scheduleEvery(heartbeatSeconds, callbackName);
|
|
371
|
-
let disposed = false;
|
|
372
|
-
return () => {
|
|
373
|
-
if (disposed) return;
|
|
374
|
-
disposed = true;
|
|
375
|
-
agent.cancelSchedule(schedule.id);
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
344
|
|
|
379
345
|
//#endregion
|
|
380
|
-
export {
|
|
346
|
+
export { withFibers };
|
|
381
347
|
//# sourceMappingURL=forever.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forever.js","names":[],"sources":["../../src/experimental/forever.ts"],"sourcesContent":["/**\n * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n * !! WARNING: EXPERIMENTAL — DO NOT USE IN PRODUCTION !!\n * !! !!\n * !! This API is under active development and WILL break between !!\n * !! releases. Method names, types, behavior, and the mixin signature !!\n * !! are all subject to change without notice. !!\n * !! !!\n * !! If you use this, pin your agents version and expect to rewrite !!\n * !! your code when upgrading. !!\n * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n *\n * Experimental fiber mixin for durable long-running execution.\n *\n * Usage:\n * import { Agent } from \"agents\";\n * import { withFibers } from \"agents/experimental/forever\";\n *\n * class MyAgent extends withFibers(Agent)<Env, State> {\n * async doWork(payload, fiberCtx) { ... }\n * }\n *\n * This mixin adds:\n * - keepAlive() — keep the DO alive via scheduled heartbeats\n * - spawnFiber() — fire-and-forget durable execution\n * - stashFiber() — checkpoint progress that survives eviction\n * - cancelFiber() / getFiber() — manage running fibers\n * - onFiberComplete / onFiberRecovered / onFibersRecovered — lifecycle hooks\n *\n * @experimental This API is not yet stable and may change.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { nanoid } from \"nanoid\";\nimport type { Agent } from \"../index\";\n\nconsole.warn(\n \"[agents/experimental/forever] WARNING: You are using an experimental API that WILL break between releases. Do not use in production.\"\n);\n\n// ── Types ─────────────────────────────────────────────────────────────\n\nexport type FiberState = {\n id: string;\n callback: string;\n payload: unknown;\n snapshot: unknown | null;\n status: \"running\" | \"completed\" | \"failed\" | \"interrupted\" | \"cancelled\";\n retryCount: number;\n maxRetries: number;\n result: unknown | null;\n error: string | null;\n startedAt: number | null;\n updatedAt: number | null;\n completedAt: number | null;\n createdAt: number;\n};\n\nexport type FiberRecoveryContext = {\n id: string;\n methodName: string;\n payload: unknown;\n snapshot: unknown | null;\n retryCount: number;\n};\n\nexport type FiberContext = {\n id: string;\n snapshot: unknown | null;\n retryCount: number;\n};\n\nexport type FiberCompleteContext = {\n id: string;\n methodName: string;\n payload: unknown;\n result: unknown;\n};\n\n// ── Internal types ────────────────────────────────────────────────────\n\ntype RawFiberRow = {\n id: string;\n callback: string;\n payload: string | null;\n snapshot: string | null;\n status: string;\n retry_count: number;\n max_retries: number;\n result: string | null;\n error: string | null;\n started_at: number | null;\n updated_at: number | null;\n completed_at: number | null;\n created_at: number;\n};\n\n// ── Constants ─────────────────────────────────────────────────────────\n\nconst KEEP_ALIVE_INTERVAL_MS = 10_000;\nconst FIBER_CLEANUP_INTERVAL_MS = 10 * 60 * 1000;\nconst FIBER_CLEANUP_COMPLETED_MS = 24 * 60 * 60 * 1000;\nconst FIBER_CLEANUP_FAILED_MS = 7 * 24 * 60 * 60 * 1000;\n\nconst fiberContext = new AsyncLocalStorage<{ fiberId: string }>();\n\n// ── Mixin ─────────────────────────────────────────────────────────────\n\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor constraint\ntype Constructor<T = object> = new (...args: any[]) => T;\n\ntype AgentLike = Constructor<\n Pick<\n Agent<Cloudflare.Env>,\n \"sql\" | \"scheduleEvery\" | \"cancelSchedule\" | \"alarm\"\n >\n>;\n\nexport function withFibers<TBase extends AgentLike>(\n Base: TBase,\n options?: { debugFibers?: boolean }\n) {\n const debugEnabled = options?.debugFibers ?? false;\n\n class FiberAgent extends Base {\n // ── Fiber state ───────────────────────────────────────────────\n\n /** @internal */ _fiberActiveFibers = new Set<string>();\n /** @internal */ _fiberRecoveryInProgress = false;\n /** @internal */ _fiberLastCleanupTime = 0;\n\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor\n constructor(...args: any[]) {\n super(...args);\n\n // Create the fibers table\n (this as unknown as Agent<Cloudflare.Env>).sql`\n CREATE TABLE IF NOT EXISTS cf_agents_fibers (\n id TEXT PRIMARY KEY NOT NULL,\n callback TEXT NOT NULL,\n payload TEXT,\n snapshot TEXT,\n status TEXT NOT NULL DEFAULT 'running'\n CHECK(status IN ('running', 'completed', 'failed', 'interrupted', 'cancelled')),\n retry_count INTEGER NOT NULL DEFAULT 0,\n max_retries INTEGER NOT NULL DEFAULT 3,\n result TEXT,\n error TEXT,\n started_at INTEGER,\n updated_at INTEGER,\n completed_at INTEGER,\n created_at INTEGER NOT NULL\n )\n `;\n }\n\n // ── Debug logging ─────────────────────────────────────────────\n\n /** @internal */ _fiberDebug(msg: string, ...args: unknown[]) {\n if (debugEnabled) {\n console.debug(`[fiber] ${msg}`, ...args);\n }\n }\n\n // ── Heartbeat callback ────────────────────────────────────────\n\n // Note: TypeScript `private` is compile-time only. The scheduler\n // dispatches callbacks by string name (`this[row.callback]`),\n // which works at runtime. The name is stable (stored in SQLite).\n /** @internal */ async _cf_fiberHeartbeat() {\n await this._checkInterruptedFibers();\n }\n\n // ── Public API ────────────────────────────────────────────────\n\n async keepAlive(): Promise<() => void> {\n const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1000);\n const schedule = await (\n this as unknown as Agent<Cloudflare.Env>\n ).scheduleEvery(\n heartbeatSeconds,\n \"_cf_fiberHeartbeat\" as keyof Agent<Cloudflare.Env>\n );\n\n this._fiberDebug(\"keepAlive started, schedule=%s\", schedule.id);\n\n let disposed = false;\n return () => {\n if (disposed) return;\n disposed = true;\n this._fiberDebug(\"keepAlive disposed, schedule=%s\", schedule.id);\n void this.cancelSchedule(schedule.id);\n };\n }\n\n spawnFiber(\n methodName: keyof this,\n payload?: unknown,\n options?: { maxRetries?: number }\n ): string {\n this._maybeCleanupFibers();\n\n const name = methodName as string;\n if (typeof this[methodName] !== \"function\") {\n throw new Error(`this.${name} is not a function`);\n }\n\n const id = nanoid();\n const now = Date.now();\n const maxRetries = options?.maxRetries ?? 3;\n\n (this as unknown as Agent<Cloudflare.Env>).sql`\n INSERT INTO cf_agents_fibers (id, callback, payload, status, max_retries, retry_count, started_at, updated_at, created_at)\n VALUES (${id}, ${name}, ${JSON.stringify(payload ?? null)}, 'running', ${maxRetries}, 0, ${now}, ${now}, ${now})\n `;\n\n this._fiberActiveFibers.add(id);\n this._fiberDebug(\n \"spawned fiber=%s method=%s maxRetries=%d\",\n id,\n name,\n maxRetries\n );\n\n void this._startFiber(id, name, payload, maxRetries).catch((e) => {\n console.error(`Unhandled error in fiber ${id}:`, e);\n });\n\n return id;\n }\n\n stashFiber(data: unknown): void {\n const ctx = fiberContext.getStore();\n if (!ctx) {\n throw new Error(\n \"stashFiber() can only be called within a fiber execution context\"\n );\n }\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET snapshot = ${JSON.stringify(data)}, updated_at = ${now}\n WHERE id = ${ctx.fiberId}\n `;\n this._fiberDebug(\"stash fiber=%s\", ctx.fiberId);\n }\n\n /**\n * Note: cancellation is cooperative. The status is set to 'cancelled'\n * in SQLite, and the _runFiber retry loop checks for this status at\n * the top of each iteration.\n */\n cancelFiber(fiberId: string): boolean {\n const fiber = this._getRawFiber(fiberId);\n if (!fiber) return false;\n if (\n fiber.status === \"completed\" ||\n fiber.status === \"failed\" ||\n fiber.status === \"cancelled\"\n ) {\n return false;\n }\n\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'cancelled', updated_at = ${now}\n WHERE id = ${fiberId}\n `;\n this._fiberActiveFibers.delete(fiberId);\n this._fiberDebug(\"cancelled fiber=%s\", fiberId);\n return true;\n }\n\n getFiber(fiberId: string): FiberState | null {\n const raw = this._getRawFiber(fiberId);\n if (!raw) return null;\n return this._toFiberState(raw);\n }\n\n restartFiber(fiberId: string): void {\n const fiber = this._getRawFiber(fiberId);\n if (!fiber) {\n throw new Error(`Fiber ${fiberId} not found`);\n }\n\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'running', started_at = ${now}, updated_at = ${now}\n WHERE id = ${fiberId}\n `;\n\n this._fiberActiveFibers.add(fiberId);\n this._fiberDebug(\n \"restarting fiber=%s method=%s retryCount=%d\",\n fiberId,\n fiber.callback,\n fiber.retry_count\n );\n\n const parsedPayload = fiber.payload\n ? JSON.parse(fiber.payload)\n : undefined;\n\n void this._startFiber(\n fiberId,\n fiber.callback,\n parsedPayload,\n fiber.max_retries\n ).catch((e) => {\n console.error(`Error restarting fiber ${fiberId}:`, e);\n });\n }\n\n // ── Lifecycle hooks (override in subclass) ────────────────────\n\n /**\n * Manually trigger fiber recovery check.\n * In production, this runs automatically via the heartbeat schedule.\n * Useful for testing or when you need immediate recovery after\n * detecting an eviction.\n */\n async checkFibers(): Promise<void> {\n await this._checkInterruptedFibers();\n }\n\n // oxlint-disable-next-line @typescript-eslint/no-unused-vars -- overridable hook\n onFiberComplete(_ctx: FiberCompleteContext): void | Promise<void> {}\n\n onFiberRecovered(ctx: FiberRecoveryContext): void | Promise<void> {\n this.restartFiber(ctx.id);\n }\n\n async onFibersRecovered(fibers: FiberRecoveryContext[]): Promise<void> {\n for (const fiber of fibers) {\n await this.onFiberRecovered(fiber);\n }\n }\n\n // ── Private implementation ────────────────────────────────────\n\n /** @internal */ _getRawFiber(fiberId: string): RawFiberRow | null {\n const result = (this as unknown as Agent<Cloudflare.Env>)\n .sql<RawFiberRow>`\n SELECT * FROM cf_agents_fibers WHERE id = ${fiberId}\n `;\n return result && result.length > 0 ? result[0] : null;\n }\n\n /** @internal */ _safeJsonParse(value: string | null): unknown {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n }\n\n /** @internal */ _toFiberState(raw: RawFiberRow): FiberState {\n return {\n id: raw.id,\n callback: raw.callback,\n payload: this._safeJsonParse(raw.payload),\n snapshot: this._safeJsonParse(raw.snapshot),\n status: raw.status as FiberState[\"status\"],\n retryCount: raw.retry_count,\n maxRetries: raw.max_retries,\n result: this._safeJsonParse(raw.result),\n error: raw.error,\n startedAt: raw.started_at,\n updatedAt: raw.updated_at,\n completedAt: raw.completed_at,\n createdAt: raw.created_at\n };\n }\n\n /** @internal */ async _startFiber(\n id: string,\n methodName: string,\n payload: unknown,\n maxRetries: number\n ): Promise<void> {\n const disposeKeepAlive = await this.keepAlive();\n await this._runFiber(\n id,\n methodName,\n payload,\n maxRetries,\n disposeKeepAlive\n );\n }\n\n /** @internal */ async _runFiber(\n id: string,\n methodName: string,\n payload: unknown,\n maxRetries: number,\n disposeKeepAlive: () => void\n ): Promise<void> {\n try {\n while (true) {\n const fiber = this._getRawFiber(id);\n if (!fiber || fiber.status === \"cancelled\") {\n this._fiberDebug(\n \"fiber=%s exiting: %s\",\n id,\n !fiber ? \"not found\" : \"cancelled\"\n );\n return;\n }\n\n try {\n await fiberContext.run({ fiberId: id }, async () => {\n const snapshot = this._safeJsonParse(fiber.snapshot);\n const retryCount = fiber.retry_count;\n\n const callback = this[methodName as keyof this];\n if (typeof callback !== \"function\") {\n throw new Error(`Fiber method ${methodName} not found`);\n }\n\n const result = await (\n callback as (p: unknown, ctx: FiberContext) => Promise<unknown>\n ).call(this, payload, { id, snapshot, retryCount });\n\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'completed',\n result = ${JSON.stringify(result ?? null)},\n completed_at = ${now},\n updated_at = ${now}\n WHERE id = ${id}\n `;\n\n this._fiberDebug(\"fiber=%s completed method=%s\", id, methodName);\n\n try {\n await this.onFiberComplete({\n id,\n methodName,\n payload,\n result\n });\n } catch (e) {\n console.error(\"Error in onFiberComplete:\", e);\n }\n });\n\n return;\n } catch (e) {\n const now = Date.now();\n const currentFiber = this._getRawFiber(id);\n const newRetryCount = (currentFiber?.retry_count ?? 0) + 1;\n\n if (newRetryCount > maxRetries) {\n const errorMsg = e instanceof Error ? e.message : String(e);\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'failed',\n error = ${errorMsg},\n retry_count = ${newRetryCount},\n updated_at = ${now}\n WHERE id = ${id}\n `;\n this._fiberDebug(\n \"fiber=%s failed after %d retries: %s\",\n id,\n newRetryCount,\n errorMsg\n );\n return;\n }\n\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET retry_count = ${newRetryCount}, updated_at = ${now}\n WHERE id = ${id}\n `;\n this._fiberDebug(\n \"fiber=%s retrying (%d/%d)\",\n id,\n newRetryCount,\n maxRetries\n );\n continue;\n }\n }\n } finally {\n this._fiberActiveFibers.delete(id);\n disposeKeepAlive();\n }\n }\n\n /** @internal */ async _checkInterruptedFibers(): Promise<void> {\n if (this._fiberRecoveryInProgress) return;\n this._fiberRecoveryInProgress = true;\n\n try {\n const runningFibers = (this as unknown as Agent<Cloudflare.Env>)\n .sql<RawFiberRow>`\n SELECT * FROM cf_agents_fibers\n WHERE status = 'running'\n ORDER BY created_at ASC\n `;\n\n if (!runningFibers || runningFibers.length === 0) return;\n\n const interrupted: FiberRecoveryContext[] = [];\n\n for (const fiber of runningFibers) {\n if (this._fiberActiveFibers.has(fiber.id)) continue;\n\n const newRetryCount = fiber.retry_count + 1;\n const now = Date.now();\n\n if (newRetryCount > fiber.max_retries) {\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'failed',\n error = 'max retries exceeded (eviction recovery)',\n retry_count = ${newRetryCount},\n updated_at = ${now}\n WHERE id = ${fiber.id}\n `;\n this._fiberDebug(\n \"fiber=%s max retries exceeded on recovery\",\n fiber.id\n );\n } else {\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'interrupted',\n retry_count = ${newRetryCount},\n updated_at = ${now}\n WHERE id = ${fiber.id}\n `;\n\n interrupted.push({\n id: fiber.id,\n methodName: fiber.callback,\n payload: this._safeJsonParse(fiber.payload),\n snapshot: this._safeJsonParse(fiber.snapshot),\n retryCount: newRetryCount\n });\n }\n }\n\n if (interrupted.length > 0) {\n this._fiberDebug(\n \"recovering %d interrupted fibers\",\n interrupted.length\n );\n\n this._cleanupOrphanedHeartbeats();\n\n try {\n await this.onFibersRecovered(interrupted);\n } catch (e) {\n console.error(\"Error in onFibersRecovered:\", e);\n }\n }\n } finally {\n this._fiberRecoveryInProgress = false;\n }\n }\n\n /** @internal */ _cleanupOrphanedHeartbeats() {\n (this as unknown as Agent<Cloudflare.Env>).sql`\n DELETE FROM cf_agents_schedules\n WHERE callback = '_cf_fiberHeartbeat'\n `;\n this._fiberDebug(\"cleaned up orphaned heartbeat schedules\");\n }\n\n /** @internal */ _maybeCleanupFibers() {\n const now = Date.now();\n if (now - this._fiberLastCleanupTime < FIBER_CLEANUP_INTERVAL_MS) {\n return;\n }\n this._fiberLastCleanupTime = now;\n\n const completedCutoff = now - FIBER_CLEANUP_COMPLETED_MS;\n const failedCutoff = now - FIBER_CLEANUP_FAILED_MS;\n\n (this as unknown as Agent<Cloudflare.Env>).sql`\n DELETE FROM cf_agents_fibers\n WHERE (status = 'completed' AND completed_at < ${completedCutoff})\n OR (status = 'failed' AND updated_at < ${failedCutoff})\n OR (status = 'cancelled' AND updated_at < ${completedCutoff})\n `;\n\n this._fiberDebug(\n \"cleanup: checked for old completed/failed/cancelled fibers\"\n );\n }\n }\n\n return FiberAgent;\n}\n\n// ── Standalone keepAlive ──────────────────────────────────────────────\n\n/**\n * Keep a Durable Object alive via a scheduled heartbeat.\n * Returns a disposer function that cancels the heartbeat.\n *\n * Standalone version usable by any Agent subclass without requiring\n * the full fiber mixin. The agent must have a no-op method with the\n * given callbackName for the scheduler to invoke.\n *\n * @param agent - The agent instance (must have scheduleEvery and cancelSchedule)\n * @param callbackName - Name of a no-op method on the agent class (must exist)\n */\nexport async function keepAlive(\n agent: Pick<Agent<Cloudflare.Env>, \"scheduleEvery\" | \"cancelSchedule\">,\n callbackName: string\n): Promise<() => void> {\n const heartbeatSeconds = Math.ceil(KEEP_ALIVE_INTERVAL_MS / 1000);\n const schedule = await agent.scheduleEvery(\n heartbeatSeconds,\n callbackName as keyof Agent<Cloudflare.Env>\n );\n\n let disposed = false;\n return () => {\n if (disposed) return;\n disposed = true;\n void agent.cancelSchedule(schedule.id);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,QAAQ,KACN,uIACD;AA6DD,MAAM,yBAAyB;AAC/B,MAAM,4BAA4B,MAAU;AAC5C,MAAM,6BAA6B,OAAU,KAAK;AAClD,MAAM,0BAA0B,QAAc,KAAK;AAEnD,MAAM,eAAe,IAAI,mBAAwC;AAcjE,SAAgB,WACd,MACA,SACA;CACA,MAAM,eAAe,SAAS,eAAe;CAE7C,MAAM,mBAAmB,KAAK;EAQ5B,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;6CANsB,IAAI,KAAa;mCACX;gCACH;AAOvC,GAAC,KAA0C,GAAG;;;;;;;;;;;;;;;;;;;mBAsB/B,YAAY,KAAa,GAAG,MAAiB;AAC5D,OAAI,aACF,SAAQ,MAAM,WAAW,OAAO,GAAG,KAAK;;mBAS3B,MAAM,qBAAqB;AAC1C,SAAM,KAAK,yBAAyB;;EAKtC,MAAM,YAAiC;GACrC,MAAM,mBAAmB,KAAK,KAAK,yBAAyB,IAAK;GACjE,MAAM,WAAW,MACf,KACA,cACA,kBACA,qBACD;AAED,QAAK,YAAY,kCAAkC,SAAS,GAAG;GAE/D,IAAI,WAAW;AACf,gBAAa;AACX,QAAI,SAAU;AACd,eAAW;AACX,SAAK,YAAY,mCAAmC,SAAS,GAAG;AAChE,IAAK,KAAK,eAAe,SAAS,GAAG;;;EAIzC,WACE,YACA,SACA,SACQ;AACR,QAAK,qBAAqB;GAE1B,MAAM,OAAO;AACb,OAAI,OAAO,KAAK,gBAAgB,WAC9B,OAAM,IAAI,MAAM,QAAQ,KAAK,oBAAoB;GAGnD,MAAM,KAAK,QAAQ;GACnB,MAAM,MAAM,KAAK,KAAK;GACtB,MAAM,aAAa,SAAS,cAAc;AAE1C,GAAC,KAA0C,GAAG;;kBAElC,GAAG,IAAI,KAAK,IAAI,KAAK,UAAU,WAAW,KAAK,CAAC,eAAe,WAAW,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI;;AAGjH,QAAK,mBAAmB,IAAI,GAAG;AAC/B,QAAK,YACH,4CACA,IACA,MACA,WACD;AAED,GAAK,KAAK,YAAY,IAAI,MAAM,SAAS,WAAW,CAAC,OAAO,MAAM;AAChE,YAAQ,MAAM,4BAA4B,GAAG,IAAI,EAAE;KACnD;AAEF,UAAO;;EAGT,WAAW,MAAqB;GAC9B,MAAM,MAAM,aAAa,UAAU;AACnC,OAAI,CAAC,IACH,OAAM,IAAI,MACR,mEACD;GAEH,MAAM,MAAM,KAAK,KAAK;AACtB,GAAC,KAA0C,GAAG;;yBAE3B,KAAK,UAAU,KAAK,CAAC,iBAAiB,IAAI;qBAC9C,IAAI,QAAQ;;AAE3B,QAAK,YAAY,kBAAkB,IAAI,QAAQ;;;;;;;EAQjD,YAAY,SAA0B;GACpC,MAAM,QAAQ,KAAK,aAAa,QAAQ;AACxC,OAAI,CAAC,MAAO,QAAO;AACnB,OACE,MAAM,WAAW,eACjB,MAAM,WAAW,YACjB,MAAM,WAAW,YAEjB,QAAO;GAGT,MAAM,MAAM,KAAK,KAAK;AACtB,GAAC,KAA0C,GAAG;;iDAEH,IAAI;qBAChC,QAAQ;;AAEvB,QAAK,mBAAmB,OAAO,QAAQ;AACvC,QAAK,YAAY,sBAAsB,QAAQ;AAC/C,UAAO;;EAGT,SAAS,SAAoC;GAC3C,MAAM,MAAM,KAAK,aAAa,QAAQ;AACtC,OAAI,CAAC,IAAK,QAAO;AACjB,UAAO,KAAK,cAAc,IAAI;;EAGhC,aAAa,SAAuB;GAClC,MAAM,QAAQ,KAAK,aAAa,QAAQ;AACxC,OAAI,CAAC,MACH,OAAM,IAAI,MAAM,SAAS,QAAQ,YAAY;GAG/C,MAAM,MAAM,KAAK,KAAK;AACtB,GAAC,KAA0C,GAAG;;+CAEL,IAAI,iBAAiB,IAAI;qBACnD,QAAQ;;AAGvB,QAAK,mBAAmB,IAAI,QAAQ;AACpC,QAAK,YACH,+CACA,SACA,MAAM,UACN,MAAM,YACP;GAED,MAAM,gBAAgB,MAAM,UACxB,KAAK,MAAM,MAAM,QAAQ,GACzB;AAEJ,GAAK,KAAK,YACR,SACA,MAAM,UACN,eACA,MAAM,YACP,CAAC,OAAO,MAAM;AACb,YAAQ,MAAM,0BAA0B,QAAQ,IAAI,EAAE;KACtD;;;;;;;;EAWJ,MAAM,cAA6B;AACjC,SAAM,KAAK,yBAAyB;;EAItC,gBAAgB,MAAkD;EAElE,iBAAiB,KAAiD;AAChE,QAAK,aAAa,IAAI,GAAG;;EAG3B,MAAM,kBAAkB,QAA+C;AACrE,QAAK,MAAM,SAAS,OAClB,OAAM,KAAK,iBAAiB,MAAM;;mBAMrB,aAAa,SAAqC;GACjE,MAAM,SAAS,AAAC,KACb,GAAgB;oDAC2B,QAAQ;;AAEtD,UAAO,UAAU,OAAO,SAAS,IAAI,OAAO,KAAK;;mBAGlC,eAAe,OAA+B;AAC7D,OAAI,UAAU,KAAM,QAAO;AAC3B,OAAI;AACF,WAAO,KAAK,MAAM,MAAM;WAClB;AACN,WAAO;;;mBAIM,cAAc,KAA8B;AAC3D,UAAO;IACL,IAAI,IAAI;IACR,UAAU,IAAI;IACd,SAAS,KAAK,eAAe,IAAI,QAAQ;IACzC,UAAU,KAAK,eAAe,IAAI,SAAS;IAC3C,QAAQ,IAAI;IACZ,YAAY,IAAI;IAChB,YAAY,IAAI;IAChB,QAAQ,KAAK,eAAe,IAAI,OAAO;IACvC,OAAO,IAAI;IACX,WAAW,IAAI;IACf,WAAW,IAAI;IACf,aAAa,IAAI;IACjB,WAAW,IAAI;IAChB;;mBAGc,MAAM,YACrB,IACA,YACA,SACA,YACe;GACf,MAAM,mBAAmB,MAAM,KAAK,WAAW;AAC/C,SAAM,KAAK,UACT,IACA,YACA,SACA,YACA,iBACD;;mBAGc,MAAM,UACrB,IACA,YACA,SACA,YACA,kBACe;AACf,OAAI;AACF,WAAO,MAAM;KACX,MAAM,QAAQ,KAAK,aAAa,GAAG;AACnC,SAAI,CAAC,SAAS,MAAM,WAAW,aAAa;AAC1C,WAAK,YACH,wBACA,IACA,CAAC,QAAQ,cAAc,YACxB;AACD;;AAGF,SAAI;AACF,YAAM,aAAa,IAAI,EAAE,SAAS,IAAI,EAAE,YAAY;OAClD,MAAM,WAAW,KAAK,eAAe,MAAM,SAAS;OACpD,MAAM,aAAa,MAAM;OAEzB,MAAM,WAAW,KAAK;AACtB,WAAI,OAAO,aAAa,WACtB,OAAM,IAAI,MAAM,gBAAgB,WAAW,YAAY;OAGzD,MAAM,SAAS,MACb,SACA,KAAK,MAAM,SAAS;QAAE;QAAI;QAAU;QAAY,CAAC;OAEnD,MAAM,MAAM,KAAK,KAAK;AACtB,OAAC,KAA0C,GAAG;;;+BAG7B,KAAK,UAAU,UAAU,KAAK,CAAC;qCACzB,IAAI;mCACN,IAAI;6BACV,GAAG;;AAGlB,YAAK,YAAY,gCAAgC,IAAI,WAAW;AAEhE,WAAI;AACF,cAAM,KAAK,gBAAgB;SACzB;SACA;SACA;SACA;SACD,CAAC;gBACK,GAAG;AACV,gBAAQ,MAAM,6BAA6B,EAAE;;QAE/C;AAEF;cACO,GAAG;MACV,MAAM,MAAM,KAAK,KAAK;MAEtB,MAAM,iBADe,KAAK,aAAa,GAAG,EACL,eAAe,KAAK;AAEzD,UAAI,gBAAgB,YAAY;OAC9B,MAAM,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AAC3D,OAAC,KAA0C,GAAG;;;8BAG9B,SAAS;oCACH,cAAc;mCACf,IAAI;6BACV,GAAG;;AAElB,YAAK,YACH,wCACA,IACA,eACA,SACD;AACD;;AAGF,MAAC,KAA0C,GAAG;;kCAExB,cAAc,iBAAiB,IAAI;2BAC1C,GAAG;;AAElB,WAAK,YACH,6BACA,IACA,eACA,WACD;AACD;;;aAGI;AACR,SAAK,mBAAmB,OAAO,GAAG;AAClC,sBAAkB;;;mBAIL,MAAM,0BAAyC;AAC9D,OAAI,KAAK,yBAA0B;AACnC,QAAK,2BAA2B;AAEhC,OAAI;IACF,MAAM,gBAAgB,AAAC,KACpB,GAAgB;;;;;AAMnB,QAAI,CAAC,iBAAiB,cAAc,WAAW,EAAG;IAElD,MAAM,cAAsC,EAAE;AAE9C,SAAK,MAAM,SAAS,eAAe;AACjC,SAAI,KAAK,mBAAmB,IAAI,MAAM,GAAG,CAAE;KAE3C,MAAM,gBAAgB,MAAM,cAAc;KAC1C,MAAM,MAAM,KAAK,KAAK;AAEtB,SAAI,gBAAgB,MAAM,aAAa;AACrC,MAAC,KAA0C,GAAG;;;;kCAIxB,cAAc;iCACf,IAAI;2BACV,MAAM,GAAG;;AAExB,WAAK,YACH,6CACA,MAAM,GACP;YACI;AACL,MAAC,KAA0C,GAAG;;;kCAGxB,cAAc;iCACf,IAAI;2BACV,MAAM,GAAG;;AAGxB,kBAAY,KAAK;OACf,IAAI,MAAM;OACV,YAAY,MAAM;OAClB,SAAS,KAAK,eAAe,MAAM,QAAQ;OAC3C,UAAU,KAAK,eAAe,MAAM,SAAS;OAC7C,YAAY;OACb,CAAC;;;AAIN,QAAI,YAAY,SAAS,GAAG;AAC1B,UAAK,YACH,oCACA,YAAY,OACb;AAED,UAAK,4BAA4B;AAEjC,SAAI;AACF,YAAM,KAAK,kBAAkB,YAAY;cAClC,GAAG;AACV,cAAQ,MAAM,+BAA+B,EAAE;;;aAG3C;AACR,SAAK,2BAA2B;;;mBAInB,6BAA6B;AAC5C,GAAC,KAA0C,GAAG;;;;AAI9C,QAAK,YAAY,0CAA0C;;mBAG5C,sBAAsB;GACrC,MAAM,MAAM,KAAK,KAAK;AACtB,OAAI,MAAM,KAAK,wBAAwB,0BACrC;AAEF,QAAK,wBAAwB;GAE7B,MAAM,kBAAkB,MAAM;GAC9B,MAAM,eAAe,MAAM;AAE3B,GAAC,KAA0C,GAAG;;yDAEK,gBAAgB;oDACrB,aAAa;uDACV,gBAAgB;;AAGjE,QAAK,YACH,6DACD;;;AAIL,QAAO;;;;;;;;;;;;;AAgBT,eAAsB,UACpB,OACA,cACqB;CACrB,MAAM,mBAAmB,KAAK,KAAK,yBAAyB,IAAK;CACjE,MAAM,WAAW,MAAM,MAAM,cAC3B,kBACA,aACD;CAED,IAAI,WAAW;AACf,cAAa;AACX,MAAI,SAAU;AACd,aAAW;AACX,EAAK,MAAM,eAAe,SAAS,GAAG"}
|
|
1
|
+
{"version":3,"file":"forever.js","names":[],"sources":["../../src/experimental/forever.ts"],"sourcesContent":["/**\n * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n * !! WARNING: EXPERIMENTAL — DO NOT USE IN PRODUCTION !!\n * !! !!\n * !! This API is under active development and WILL break between !!\n * !! releases. Method names, types, behavior, and the mixin signature !!\n * !! are all subject to change without notice. !!\n * !! !!\n * !! If you use this, pin your agents version and expect to rewrite !!\n * !! your code when upgrading. !!\n * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n *\n * Experimental fiber mixin for durable long-running execution.\n *\n * Usage:\n * import { Agent } from \"agents\";\n * import { withFibers } from \"agents/experimental/forever\";\n *\n * class MyAgent extends withFibers(Agent)<Env, State> {\n * async doWork(payload, fiberCtx) { ... }\n * }\n *\n * This mixin adds:\n * - keepAlive() — keep the DO alive via scheduled heartbeats\n * - spawnFiber() — fire-and-forget durable execution\n * - stashFiber() — checkpoint progress that survives eviction\n * - cancelFiber() / getFiber() — manage running fibers\n * - onFiberComplete / onFiberRecovered / onFibersRecovered — lifecycle hooks\n *\n * @experimental This API is not yet stable and may change.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { nanoid } from \"nanoid\";\nimport type { Agent } from \"../index\";\n\n// ── Types ─────────────────────────────────────────────────────────────\n\nexport type FiberState = {\n id: string;\n callback: string;\n payload: unknown;\n snapshot: unknown | null;\n status: \"running\" | \"completed\" | \"failed\" | \"interrupted\" | \"cancelled\";\n retryCount: number;\n maxRetries: number;\n result: unknown | null;\n error: string | null;\n startedAt: number | null;\n updatedAt: number | null;\n completedAt: number | null;\n createdAt: number;\n};\n\nexport type FiberRecoveryContext = {\n id: string;\n methodName: string;\n payload: unknown;\n snapshot: unknown | null;\n retryCount: number;\n};\n\nexport type FiberContext = {\n id: string;\n snapshot: unknown | null;\n retryCount: number;\n};\n\nexport type FiberCompleteContext = {\n id: string;\n methodName: string;\n payload: unknown;\n result: unknown;\n};\n\n// ── Internal types ────────────────────────────────────────────────────\n\ntype RawFiberRow = {\n id: string;\n callback: string;\n payload: string | null;\n snapshot: string | null;\n status: string;\n retry_count: number;\n max_retries: number;\n result: string | null;\n error: string | null;\n started_at: number | null;\n updated_at: number | null;\n completed_at: number | null;\n created_at: number;\n};\n\n// ── Constants ─────────────────────────────────────────────────────────\n\nconst FIBER_CLEANUP_INTERVAL_MS = 10 * 60 * 1000;\nconst FIBER_CLEANUP_COMPLETED_MS = 24 * 60 * 60 * 1000;\nconst FIBER_CLEANUP_FAILED_MS = 7 * 24 * 60 * 60 * 1000;\n\nconst fiberContext = new AsyncLocalStorage<{ fiberId: string }>();\n\n// ── Mixin ─────────────────────────────────────────────────────────────\n\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor constraint\ntype Constructor<T = object> = new (...args: any[]) => T;\n\ntype AgentLike = Constructor<\n Pick<\n Agent<Cloudflare.Env>,\n | \"sql\"\n | \"scheduleEvery\"\n | \"cancelSchedule\"\n | \"alarm\"\n | \"keepAlive\"\n | \"keepAliveWhile\"\n >\n>;\n\nexport function withFibers<TBase extends AgentLike>(\n Base: TBase,\n options?: { debugFibers?: boolean }\n) {\n const debugEnabled = options?.debugFibers ?? false;\n\n class FiberAgent extends Base {\n // ── Fiber state ───────────────────────────────────────────────\n\n /** @internal */ _fiberActiveFibers = new Set<string>();\n /** @internal */ _fiberRecoveryInProgress = false;\n /** @internal */ _fiberLastCleanupTime = 0;\n\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- mixin constructor\n constructor(...args: any[]) {\n super(...args);\n\n console.warn(\n \"[agents/experimental/forever] WARNING: You are using an experimental API that WILL break between releases. Do not use in production.\"\n );\n\n // Create the fibers table\n (this as unknown as Agent<Cloudflare.Env>).sql`\n CREATE TABLE IF NOT EXISTS cf_agents_fibers (\n id TEXT PRIMARY KEY NOT NULL,\n callback TEXT NOT NULL,\n payload TEXT,\n snapshot TEXT,\n status TEXT NOT NULL DEFAULT 'running'\n CHECK(status IN ('running', 'completed', 'failed', 'interrupted', 'cancelled')),\n retry_count INTEGER NOT NULL DEFAULT 0,\n max_retries INTEGER NOT NULL DEFAULT 3,\n result TEXT,\n error TEXT,\n started_at INTEGER,\n updated_at INTEGER,\n completed_at INTEGER,\n created_at INTEGER NOT NULL\n )\n `;\n }\n\n // ── Debug logging ─────────────────────────────────────────────\n\n /** @internal */ _fiberDebug(msg: string, ...args: unknown[]) {\n if (debugEnabled) {\n console.debug(`[fiber] ${msg}`, ...args);\n }\n }\n\n // ── Heartbeat callback override ───────────────────────────────\n\n // Override the base Agent's no-op heartbeat to add fiber recovery.\n // The scheduler dispatches by string name, so this override runs\n // when the keepAlive schedule fires.\n /** @internal */ async _cf_keepAliveHeartbeat() {\n await this._checkInterruptedFibers();\n }\n\n // ── Public API ────────────────────────────────────────────────\n\n spawnFiber(\n methodName: keyof this,\n payload?: unknown,\n options?: { maxRetries?: number }\n ): string {\n this._maybeCleanupFibers();\n\n const name = methodName as string;\n if (typeof this[methodName] !== \"function\") {\n throw new Error(`this.${name} is not a function`);\n }\n\n const id = nanoid();\n const now = Date.now();\n const maxRetries = options?.maxRetries ?? 3;\n\n (this as unknown as Agent<Cloudflare.Env>).sql`\n INSERT INTO cf_agents_fibers (id, callback, payload, status, max_retries, retry_count, started_at, updated_at, created_at)\n VALUES (${id}, ${name}, ${JSON.stringify(payload ?? null)}, 'running', ${maxRetries}, 0, ${now}, ${now}, ${now})\n `;\n\n this._fiberActiveFibers.add(id);\n this._fiberDebug(\n \"spawned fiber=%s method=%s maxRetries=%d\",\n id,\n name,\n maxRetries\n );\n\n void this._startFiber(id, name, payload, maxRetries).catch((e) => {\n console.error(`Unhandled error in fiber ${id}:`, e);\n });\n\n return id;\n }\n\n stashFiber(data: unknown): void {\n const ctx = fiberContext.getStore();\n if (!ctx) {\n throw new Error(\n \"stashFiber() can only be called within a fiber execution context\"\n );\n }\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET snapshot = ${JSON.stringify(data)}, updated_at = ${now}\n WHERE id = ${ctx.fiberId}\n `;\n this._fiberDebug(\"stash fiber=%s\", ctx.fiberId);\n }\n\n /**\n * Note: cancellation is cooperative. The status is set to 'cancelled'\n * in SQLite, and the _runFiber retry loop checks for this status at\n * the top of each iteration.\n */\n cancelFiber(fiberId: string): boolean {\n const fiber = this._getRawFiber(fiberId);\n if (!fiber) return false;\n if (\n fiber.status === \"completed\" ||\n fiber.status === \"failed\" ||\n fiber.status === \"cancelled\"\n ) {\n return false;\n }\n\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'cancelled', updated_at = ${now}\n WHERE id = ${fiberId}\n `;\n this._fiberActiveFibers.delete(fiberId);\n this._fiberDebug(\"cancelled fiber=%s\", fiberId);\n return true;\n }\n\n getFiber(fiberId: string): FiberState | null {\n const raw = this._getRawFiber(fiberId);\n if (!raw) return null;\n return this._toFiberState(raw);\n }\n\n restartFiber(fiberId: string): void {\n const fiber = this._getRawFiber(fiberId);\n if (!fiber) {\n throw new Error(`Fiber ${fiberId} not found`);\n }\n\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'running', started_at = ${now}, updated_at = ${now}\n WHERE id = ${fiberId}\n `;\n\n this._fiberActiveFibers.add(fiberId);\n this._fiberDebug(\n \"restarting fiber=%s method=%s retryCount=%d\",\n fiberId,\n fiber.callback,\n fiber.retry_count\n );\n\n const parsedPayload = fiber.payload\n ? JSON.parse(fiber.payload)\n : undefined;\n\n void this._startFiber(\n fiberId,\n fiber.callback,\n parsedPayload,\n fiber.max_retries\n ).catch((e) => {\n console.error(`Error restarting fiber ${fiberId}:`, e);\n });\n }\n\n // ── Lifecycle hooks (override in subclass) ────────────────────\n\n /**\n * Manually trigger fiber recovery check.\n * In production, this runs automatically via the heartbeat schedule.\n * Useful for testing or when you need immediate recovery after\n * detecting an eviction.\n */\n async checkFibers(): Promise<void> {\n await this._checkInterruptedFibers();\n }\n\n // oxlint-disable-next-line @typescript-eslint/no-unused-vars -- overridable hook\n onFiberComplete(_ctx: FiberCompleteContext): void | Promise<void> {}\n\n onFiberRecovered(ctx: FiberRecoveryContext): void | Promise<void> {\n this.restartFiber(ctx.id);\n }\n\n async onFibersRecovered(fibers: FiberRecoveryContext[]): Promise<void> {\n for (const fiber of fibers) {\n await this.onFiberRecovered(fiber);\n }\n }\n\n // ── Private implementation ────────────────────────────────────\n\n /** @internal */ _getRawFiber(fiberId: string): RawFiberRow | null {\n const result = (this as unknown as Agent<Cloudflare.Env>)\n .sql<RawFiberRow>`\n SELECT * FROM cf_agents_fibers WHERE id = ${fiberId}\n `;\n return result && result.length > 0 ? result[0] : null;\n }\n\n /** @internal */ _safeJsonParse(value: string | null): unknown {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n }\n\n /** @internal */ _toFiberState(raw: RawFiberRow): FiberState {\n return {\n id: raw.id,\n callback: raw.callback,\n payload: this._safeJsonParse(raw.payload),\n snapshot: this._safeJsonParse(raw.snapshot),\n status: raw.status as FiberState[\"status\"],\n retryCount: raw.retry_count,\n maxRetries: raw.max_retries,\n result: this._safeJsonParse(raw.result),\n error: raw.error,\n startedAt: raw.started_at,\n updatedAt: raw.updated_at,\n completedAt: raw.completed_at,\n createdAt: raw.created_at\n };\n }\n\n /** @internal */ async _startFiber(\n id: string,\n methodName: string,\n payload: unknown,\n maxRetries: number\n ): Promise<void> {\n const disposeKeepAlive = await this.keepAlive();\n await this._runFiber(\n id,\n methodName,\n payload,\n maxRetries,\n disposeKeepAlive\n );\n }\n\n /** @internal */ async _runFiber(\n id: string,\n methodName: string,\n payload: unknown,\n maxRetries: number,\n disposeKeepAlive: () => void\n ): Promise<void> {\n try {\n while (true) {\n const fiber = this._getRawFiber(id);\n if (!fiber || fiber.status === \"cancelled\") {\n this._fiberDebug(\n \"fiber=%s exiting: %s\",\n id,\n !fiber ? \"not found\" : \"cancelled\"\n );\n return;\n }\n\n try {\n await fiberContext.run({ fiberId: id }, async () => {\n const snapshot = this._safeJsonParse(fiber.snapshot);\n const retryCount = fiber.retry_count;\n\n const callback = this[methodName as keyof this];\n if (typeof callback !== \"function\") {\n throw new Error(`Fiber method ${methodName} not found`);\n }\n\n const result = await (\n callback as (p: unknown, ctx: FiberContext) => Promise<unknown>\n ).call(this, payload, { id, snapshot, retryCount });\n\n const now = Date.now();\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'completed',\n result = ${JSON.stringify(result ?? null)},\n completed_at = ${now},\n updated_at = ${now}\n WHERE id = ${id}\n `;\n\n this._fiberDebug(\"fiber=%s completed method=%s\", id, methodName);\n\n try {\n await this.onFiberComplete({\n id,\n methodName,\n payload,\n result\n });\n } catch (e) {\n console.error(\"Error in onFiberComplete:\", e);\n }\n });\n\n return;\n } catch (e) {\n const now = Date.now();\n const currentFiber = this._getRawFiber(id);\n const newRetryCount = (currentFiber?.retry_count ?? 0) + 1;\n\n if (newRetryCount > maxRetries) {\n const errorMsg = e instanceof Error ? e.message : String(e);\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'failed',\n error = ${errorMsg},\n retry_count = ${newRetryCount},\n updated_at = ${now}\n WHERE id = ${id}\n `;\n this._fiberDebug(\n \"fiber=%s failed after %d retries: %s\",\n id,\n newRetryCount,\n errorMsg\n );\n return;\n }\n\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET retry_count = ${newRetryCount}, updated_at = ${now}\n WHERE id = ${id}\n `;\n this._fiberDebug(\n \"fiber=%s retrying (%d/%d)\",\n id,\n newRetryCount,\n maxRetries\n );\n continue;\n }\n }\n } finally {\n this._fiberActiveFibers.delete(id);\n disposeKeepAlive();\n }\n }\n\n /** @internal */ async _checkInterruptedFibers(): Promise<void> {\n if (this._fiberRecoveryInProgress) return;\n this._fiberRecoveryInProgress = true;\n\n try {\n const runningFibers = (this as unknown as Agent<Cloudflare.Env>)\n .sql<RawFiberRow>`\n SELECT * FROM cf_agents_fibers\n WHERE status = 'running'\n ORDER BY created_at ASC\n `;\n\n if (!runningFibers || runningFibers.length === 0) return;\n\n const interrupted: FiberRecoveryContext[] = [];\n\n for (const fiber of runningFibers) {\n if (this._fiberActiveFibers.has(fiber.id)) continue;\n\n const newRetryCount = fiber.retry_count + 1;\n const now = Date.now();\n\n if (newRetryCount > fiber.max_retries) {\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'failed',\n error = 'max retries exceeded (eviction recovery)',\n retry_count = ${newRetryCount},\n updated_at = ${now}\n WHERE id = ${fiber.id}\n `;\n this._fiberDebug(\n \"fiber=%s max retries exceeded on recovery\",\n fiber.id\n );\n } else {\n (this as unknown as Agent<Cloudflare.Env>).sql`\n UPDATE cf_agents_fibers\n SET status = 'interrupted',\n retry_count = ${newRetryCount},\n updated_at = ${now}\n WHERE id = ${fiber.id}\n `;\n\n interrupted.push({\n id: fiber.id,\n methodName: fiber.callback,\n payload: this._safeJsonParse(fiber.payload),\n snapshot: this._safeJsonParse(fiber.snapshot),\n retryCount: newRetryCount\n });\n }\n }\n\n if (interrupted.length > 0) {\n this._fiberDebug(\n \"recovering %d interrupted fibers\",\n interrupted.length\n );\n\n this._cleanupOrphanedHeartbeats();\n\n try {\n await this.onFibersRecovered(interrupted);\n } catch (e) {\n console.error(\"Error in onFibersRecovered:\", e);\n }\n }\n } finally {\n this._fiberRecoveryInProgress = false;\n }\n }\n\n /** @internal */ _cleanupOrphanedHeartbeats() {\n (this as unknown as Agent<Cloudflare.Env>).sql`\n DELETE FROM cf_agents_schedules\n WHERE callback = '_cf_keepAliveHeartbeat'\n `;\n this._fiberDebug(\"cleaned up orphaned heartbeat schedules\");\n }\n\n /** @internal */ _maybeCleanupFibers() {\n const now = Date.now();\n if (now - this._fiberLastCleanupTime < FIBER_CLEANUP_INTERVAL_MS) {\n return;\n }\n this._fiberLastCleanupTime = now;\n\n const completedCutoff = now - FIBER_CLEANUP_COMPLETED_MS;\n const failedCutoff = now - FIBER_CLEANUP_FAILED_MS;\n\n (this as unknown as Agent<Cloudflare.Env>).sql`\n DELETE FROM cf_agents_fibers\n WHERE (status = 'completed' AND completed_at < ${completedCutoff})\n OR (status = 'failed' AND updated_at < ${failedCutoff})\n OR (status = 'cancelled' AND updated_at < ${completedCutoff})\n `;\n\n this._fiberDebug(\n \"cleanup: checked for old completed/failed/cancelled fibers\"\n );\n }\n }\n\n return FiberAgent;\n}\n"],"mappings":"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"}
|