agents 0.7.4 → 0.7.6
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.js +3 -4
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-chat-v5-migration.js +3 -4
- package/dist/ai-chat-v5-migration.js.map +1 -1
- package/dist/ai-react.js +3 -4
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.js +1 -2
- package/dist/ai-types.js.map +1 -1
- package/dist/cli/index.js +2 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/client-K8Z-u76l.js +1468 -0
- package/dist/client-K8Z-u76l.js.map +1 -0
- package/dist/client.js +1 -2
- package/dist/client.js.map +1 -1
- package/dist/codemode/ai.js +2 -2
- package/dist/do-oauth-client-provider-C2jurFjW.d.ts +78 -0
- package/dist/email-U_MG7UET.d.ts +157 -0
- package/dist/email.d.ts +16 -146
- package/dist/email.js +2 -2
- package/dist/email.js.map +1 -1
- package/dist/experimental/forever.d.ts +26 -71
- package/dist/experimental/forever.js +2 -3
- package/dist/experimental/forever.js.map +1 -1
- package/dist/experimental/memory/session/index.js +3 -12
- package/dist/experimental/memory/session/index.js.map +1 -1
- package/dist/experimental/workspace.d.ts +273 -0
- package/dist/experimental/workspace.js +1265 -0
- package/dist/experimental/workspace.js.map +1 -0
- package/dist/index-BS_jL8MI.d.ts +492 -0
- package/dist/index-WBy5hmm3.d.ts +2840 -0
- package/dist/index.d.ts +49 -1320
- package/dist/index.js +281 -138
- package/dist/index.js.map +1 -1
- package/dist/internal_context-DgcmHqS1.d.ts +37 -0
- package/dist/internal_context.d.ts +5 -32
- package/dist/internal_context.js +1 -2
- package/dist/internal_context.js.map +1 -1
- package/dist/mcp/client.d.ts +2 -575
- package/dist/mcp/client.js +1 -847
- package/dist/mcp/do-oauth-client-provider.d.ts +2 -61
- package/dist/mcp/do-oauth-client-provider.js +1 -2
- package/dist/mcp/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -95
- package/dist/mcp/index.js +60 -57
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/x402.js +1 -2
- package/dist/mcp/x402.js.map +1 -1
- package/dist/observability/index.d.ts +2 -93
- package/dist/observability/index.js +4 -3
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +1 -2
- package/dist/react.js +1 -2
- package/dist/react.js.map +1 -1
- package/dist/retries-DXMQGhG3.d.ts +79 -0
- package/dist/retries.d.ts +7 -72
- package/dist/retries.js +1 -1
- package/dist/retries.js.map +1 -1
- package/dist/schedule.js +1 -2
- package/dist/schedule.js.map +1 -1
- package/dist/serializable.js +1 -1
- package/dist/types-BB1plA51.d.ts +15 -0
- package/dist/types.d.ts +1 -14
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/workflow-types-CZNXKj_D.d.ts +260 -0
- package/dist/workflow-types.d.ts +23 -235
- package/dist/workflow-types.js +1 -1
- package/dist/workflow-types.js.map +1 -1
- package/dist/workflows.d.ts +22 -23
- package/dist/workflows.js +5 -6
- package/dist/workflows.js.map +1 -1
- package/package.json +25 -13
- package/dist/agent-eZnMHidZ.d.ts +0 -273
- package/dist/client-connection-D3Wcd6Q6.js +0 -603
- package/dist/client-connection-D3Wcd6Q6.js.map +0 -1
- package/dist/client-storage-BPjfP_is.d.ts +0 -604
- package/dist/experimental/sub-agent.d.ts +0 -205
- package/dist/experimental/sub-agent.js +0 -191
- package/dist/experimental/sub-agent.js.map +0 -1
- package/dist/mcp/client.js.map +0 -1
package/dist/email.d.ts
CHANGED
|
@@ -1,149 +1,20 @@
|
|
|
1
|
-
import { AgentEmail } from "./internal_context.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @param headers - Headers array from postal-mime Email.headers or similar format
|
|
17
|
-
* @returns true if email appears to be an auto-reply
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```typescript
|
|
21
|
-
* if (isAutoReplyEmail(parsed.headers)) {
|
|
22
|
-
* // Skip processing auto-replies
|
|
23
|
-
* return;
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
declare function isAutoReplyEmail(headers: EmailHeader[]): boolean;
|
|
28
|
-
/** Default signature expiration: 30 days in seconds */
|
|
29
|
-
declare const DEFAULT_MAX_AGE_SECONDS: number;
|
|
30
|
-
/**
|
|
31
|
-
* Sign agent routing headers for secure reply flows.
|
|
32
|
-
* Use this when sending outbound emails to ensure replies can be securely routed back.
|
|
33
|
-
*
|
|
34
|
-
* @param secret - Secret key for HMAC signing (store in environment variables)
|
|
35
|
-
* @param agentName - Name of the agent
|
|
36
|
-
* @param agentId - ID of the agent instance
|
|
37
|
-
* @returns Headers object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```typescript
|
|
41
|
-
* const headers = await signAgentHeaders(env.EMAIL_SECRET, "MyAgent", this.name);
|
|
42
|
-
* // Use these headers when sending outbound emails
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
declare function signAgentHeaders(
|
|
46
|
-
secret: string,
|
|
47
|
-
agentName: string,
|
|
48
|
-
agentId: string
|
|
49
|
-
): Promise<Record<string, string>>;
|
|
50
|
-
type EmailResolverResult = {
|
|
51
|
-
agentName: string;
|
|
52
|
-
agentId: string /** @internal Indicates this resolver requires secure reply signing */;
|
|
53
|
-
_secureRouted?: boolean;
|
|
54
|
-
} | null;
|
|
55
|
-
type EmailResolver<Env> = (
|
|
56
|
-
email: ForwardableEmailMessage,
|
|
57
|
-
env: Env
|
|
58
|
-
) => Promise<EmailResolverResult>;
|
|
59
|
-
/**
|
|
60
|
-
* Reason for signature verification failure
|
|
61
|
-
*/
|
|
62
|
-
type SignatureFailureReason =
|
|
63
|
-
| "missing_headers"
|
|
64
|
-
| "expired"
|
|
65
|
-
| "invalid"
|
|
66
|
-
| "malformed_timestamp";
|
|
67
|
-
/**
|
|
68
|
-
* Options for createSecureReplyEmailResolver
|
|
69
|
-
*/
|
|
70
|
-
type SecureReplyResolverOptions = {
|
|
71
|
-
/**
|
|
72
|
-
* Maximum age of signature in seconds.
|
|
73
|
-
* Signatures older than this will be rejected.
|
|
74
|
-
* Default: 30 days (2592000 seconds)
|
|
75
|
-
*/
|
|
76
|
-
maxAge?: number;
|
|
77
|
-
/**
|
|
78
|
-
* Callback invoked when signature verification fails.
|
|
79
|
-
* Useful for logging and debugging.
|
|
80
|
-
*/
|
|
81
|
-
onInvalidSignature?: (
|
|
82
|
-
email: ForwardableEmailMessage,
|
|
83
|
-
reason: SignatureFailureReason
|
|
84
|
-
) => void;
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* @deprecated REMOVED due to security vulnerability (IDOR via spoofed headers).
|
|
88
|
-
* @throws Always throws an error with migration guidance.
|
|
89
|
-
*/
|
|
90
|
-
declare function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env>;
|
|
91
|
-
/**
|
|
92
|
-
* Create a resolver for routing email replies with signature verification.
|
|
93
|
-
* This resolver verifies that replies contain a valid HMAC signature, preventing
|
|
94
|
-
* attackers from routing emails to arbitrary agent instances.
|
|
95
|
-
*
|
|
96
|
-
* @param secret - Secret key for HMAC verification (must match the key used with signAgentHeaders)
|
|
97
|
-
* @param options - Optional configuration for signature verification
|
|
98
|
-
* @returns A function that resolves the agent to route the email to, or null if signature is invalid
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* ```typescript
|
|
102
|
-
* // In your email handler
|
|
103
|
-
* const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
|
|
104
|
-
* maxAge: 7 * 24 * 60 * 60, // 7 days
|
|
105
|
-
* onInvalidSignature: (email, reason) => {
|
|
106
|
-
* console.warn(`Invalid signature from ${email.from}: ${reason}`);
|
|
107
|
-
* }
|
|
108
|
-
* });
|
|
109
|
-
* const addressResolver = createAddressBasedEmailResolver("MyAgent");
|
|
110
|
-
*
|
|
111
|
-
* await routeAgentEmail(email, env, {
|
|
112
|
-
* resolver: async (email, env) => {
|
|
113
|
-
* // Try secure reply routing first
|
|
114
|
-
* const replyRouting = await secureResolver(email, env);
|
|
115
|
-
* if (replyRouting) return replyRouting;
|
|
116
|
-
* // Fall back to address-based routing
|
|
117
|
-
* return addressResolver(email, env);
|
|
118
|
-
* }
|
|
119
|
-
* });
|
|
120
|
-
* ```
|
|
121
|
-
*/
|
|
122
|
-
declare function createSecureReplyEmailResolver<Env>(
|
|
123
|
-
secret: string,
|
|
124
|
-
options?: SecureReplyResolverOptions
|
|
125
|
-
): EmailResolver<Env>;
|
|
126
|
-
/**
|
|
127
|
-
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
128
|
-
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
129
|
-
* @returns A function that resolves the agent to route the email to
|
|
130
|
-
*/
|
|
131
|
-
declare function createAddressBasedEmailResolver<Env>(
|
|
132
|
-
defaultAgentName: string
|
|
133
|
-
): EmailResolver<Env>;
|
|
134
|
-
/**
|
|
135
|
-
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
136
|
-
* @param agentName The name of the agent to route the email to
|
|
137
|
-
* @param agentId The id of the agent to route the email to
|
|
138
|
-
* @returns A function that resolves the agent to route the email to
|
|
139
|
-
*/
|
|
140
|
-
declare function createCatchAllEmailResolver<Env>(
|
|
141
|
-
agentName: string,
|
|
142
|
-
agentId: string
|
|
143
|
-
): EmailResolver<Env>;
|
|
144
|
-
//#endregion
|
|
1
|
+
import { n as AgentEmail } from "./internal_context-DgcmHqS1.js";
|
|
2
|
+
import {
|
|
3
|
+
a as SecureReplyResolverOptions,
|
|
4
|
+
c as createCatchAllEmailResolver,
|
|
5
|
+
d as isAutoReplyEmail,
|
|
6
|
+
f as signAgentHeaders,
|
|
7
|
+
i as EmailResolverResult,
|
|
8
|
+
l as createHeaderBasedEmailResolver,
|
|
9
|
+
n as EmailHeader,
|
|
10
|
+
o as SignatureFailureReason,
|
|
11
|
+
r as EmailResolver,
|
|
12
|
+
s as createAddressBasedEmailResolver,
|
|
13
|
+
t as DEFAULT_MAX_AGE_SECONDS,
|
|
14
|
+
u as createSecureReplyEmailResolver
|
|
15
|
+
} from "./email-U_MG7UET.js";
|
|
145
16
|
export {
|
|
146
|
-
|
|
17
|
+
AgentEmail,
|
|
147
18
|
DEFAULT_MAX_AGE_SECONDS,
|
|
148
19
|
EmailHeader,
|
|
149
20
|
EmailResolver,
|
|
@@ -157,4 +28,3 @@ export {
|
|
|
157
28
|
isAutoReplyEmail,
|
|
158
29
|
signAgentHeaders
|
|
159
30
|
};
|
|
160
|
-
//# sourceMappingURL=email.d.ts.map
|
package/dist/email.js
CHANGED
|
@@ -162,7 +162,7 @@ function createHeaderBasedEmailResolver() {
|
|
|
162
162
|
*/
|
|
163
163
|
function createSecureReplyEmailResolver(secret, options) {
|
|
164
164
|
if (!secret) throw new Error("secret is required for createSecureReplyEmailResolver");
|
|
165
|
-
const maxAge = options?.maxAge ??
|
|
165
|
+
const maxAge = options?.maxAge ?? 2592e3;
|
|
166
166
|
const onInvalidSignature = options?.onInvalidSignature;
|
|
167
167
|
return async (email, _env) => {
|
|
168
168
|
const agentName = email.headers.get("x-agent-name");
|
|
@@ -217,7 +217,7 @@ function createCatchAllEmailResolver(agentName, agentId) {
|
|
|
217
217
|
agentId
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
|
-
|
|
221
220
|
//#endregion
|
|
222
221
|
export { DEFAULT_MAX_AGE_SECONDS, createAddressBasedEmailResolver, createCatchAllEmailResolver, createHeaderBasedEmailResolver, createSecureReplyEmailResolver, isAutoReplyEmail, signAgentHeaders };
|
|
222
|
+
|
|
223
223
|
//# sourceMappingURL=email.js.map
|
package/dist/email.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.js","names":[],"sources":["../src/email.ts"],"sourcesContent":["/**\n * Email routing types, resolvers, and signing utilities for Agents\n */\n\n// Re-export AgentEmail type\nexport type { AgentEmail } from \"./internal_context\";\n\n// ============================================================================\n// Email header utilities\n// ============================================================================\n\n/**\n * Header object as returned by postal-mime and similar email parsing libraries.\n * Each header has a lowercase key and a string value.\n */\nexport type EmailHeader = {\n /** Lowercase header name (e.g., \"content-type\", \"x-custom-header\") */\n key: string;\n /** Header value */\n value: string;\n};\n\n/**\n * Check if an email appears to be an auto-reply based on standard headers.\n * Checks for Auto-Submitted (RFC 3834), X-Auto-Response-Suppress, and Precedence headers.\n *\n * @param headers - Headers array from postal-mime Email.headers or similar format\n * @returns true if email appears to be an auto-reply\n *\n * @example\n * ```typescript\n * if (isAutoReplyEmail(parsed.headers)) {\n * // Skip processing auto-replies\n * return;\n * }\n * ```\n */\nexport function isAutoReplyEmail(headers: EmailHeader[]): boolean {\n return headers.some((h) => {\n const key = h.key.toLowerCase();\n const value = h.value.toLowerCase();\n\n // RFC 3834: Auto-Submitted header\n // \"no\" means normal (human-sent) email, anything else indicates auto-reply\n if (key === \"auto-submitted\") {\n return value !== \"no\";\n }\n\n // X-Auto-Response-Suppress: any value indicates sender doesn't want auto-replies\n if (key === \"x-auto-response-suppress\") {\n return true;\n }\n\n // Precedence: only bulk/junk/list indicate automated/mass mail\n if (key === \"precedence\") {\n return value === \"bulk\" || value === \"junk\" || value === \"list\";\n }\n\n return false;\n });\n}\n\n// ============================================================================\n// Signing utilities\n// ============================================================================\n\n/** Default signature expiration: 30 days in seconds */\nexport const DEFAULT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\n\n/** Maximum allowed clock skew for future timestamps: 5 minutes */\nconst MAX_CLOCK_SKEW_SECONDS = 5 * 60;\n\n/**\n * Compute HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param timestamp - Unix timestamp in seconds\n * @returns Base64-encoded HMAC signature\n */\nasync function computeAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n timestamp: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n const data = encoder.encode(`${agentName}:${agentId}:${timestamp}`);\n const signature = await crypto.subtle.sign(\"HMAC\", key, data);\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n/**\n * Result of signature verification\n */\ntype SignatureVerificationResult =\n | { valid: true }\n | { valid: false; reason: \"expired\" | \"invalid\" | \"malformed_timestamp\" };\n\n/**\n * Verify HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param signature - Base64-encoded signature to verify\n * @param timestamp - Unix timestamp in seconds when signature was created\n * @param maxAgeSeconds - Maximum age of signature in seconds (default: 30 days)\n * @returns Verification result with reason if invalid\n */\nasync function verifyAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n signature: string,\n timestamp: string,\n maxAgeSeconds: number = DEFAULT_MAX_AGE_SECONDS\n): Promise<SignatureVerificationResult> {\n try {\n // Validate timestamp format\n const timestampNum = Number.parseInt(timestamp, 10);\n if (Number.isNaN(timestampNum)) {\n return { valid: false, reason: \"malformed_timestamp\" };\n }\n\n // Check timestamp validity\n const now = Math.floor(Date.now() / 1000);\n\n // Reject timestamps too far in the future (prevents extending signature validity)\n if (timestampNum > now + MAX_CLOCK_SKEW_SECONDS) {\n return { valid: false, reason: \"invalid\" };\n }\n\n // Check if signature has expired\n if (now - timestampNum > maxAgeSeconds) {\n return { valid: false, reason: \"expired\" };\n }\n\n const expected = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) {\n return { valid: false, reason: \"invalid\" };\n }\n let result = 0;\n for (let i = 0; i < expected.length; i++) {\n result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n if (result !== 0) {\n return { valid: false, reason: \"invalid\" };\n }\n return { valid: true };\n } catch (error) {\n console.warn(\"[agents] Signature verification error:\", error);\n return { valid: false, reason: \"invalid\" };\n }\n}\n\n/**\n * Sign agent routing headers for secure reply flows.\n * Use this when sending outbound emails to ensure replies can be securely routed back.\n *\n * @param secret - Secret key for HMAC signing (store in environment variables)\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @returns Headers object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts\n *\n * @example\n * ```typescript\n * const headers = await signAgentHeaders(env.EMAIL_SECRET, \"MyAgent\", this.name);\n * // Use these headers when sending outbound emails\n * ```\n */\nexport async function signAgentHeaders(\n secret: string,\n agentName: string,\n agentId: string\n): Promise<Record<string, string>> {\n if (!secret) {\n throw new Error(\"secret is required for signing agent headers\");\n }\n if (!agentName) {\n throw new Error(\"agentName is required for signing agent headers\");\n }\n if (!agentId) {\n throw new Error(\"agentId is required for signing agent headers\");\n }\n // Reject colons to prevent signature confusion attacks\n // (signature payload uses colon as delimiter: \"agentName:agentId:timestamp\")\n if (agentName.includes(\":\")) {\n throw new Error(\"agentName cannot contain colons\");\n }\n if (agentId.includes(\":\")) {\n throw new Error(\"agentId cannot contain colons\");\n }\n\n const timestamp = Math.floor(Date.now() / 1000).toString();\n const signature = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n return {\n \"X-Agent-Name\": agentName,\n \"X-Agent-ID\": agentId,\n \"X-Agent-Sig\": signature,\n \"X-Agent-Sig-Ts\": timestamp\n };\n}\n\n// ============================================================================\n// Email routing types and resolvers\n// ============================================================================\n\nexport type EmailResolverResult = {\n agentName: string;\n agentId: string;\n /** @internal Indicates this resolver requires secure reply signing */\n _secureRouted?: boolean;\n} | null;\n\nexport type EmailResolver<Env> = (\n email: ForwardableEmailMessage,\n env: Env\n) => Promise<EmailResolverResult>;\n\n/**\n * Reason for signature verification failure\n */\nexport type SignatureFailureReason =\n | \"missing_headers\"\n | \"expired\"\n | \"invalid\"\n | \"malformed_timestamp\";\n\n/**\n * Options for createSecureReplyEmailResolver\n */\nexport type SecureReplyResolverOptions = {\n /**\n * Maximum age of signature in seconds.\n * Signatures older than this will be rejected.\n * Default: 30 days (2592000 seconds)\n */\n maxAge?: number;\n /**\n * Callback invoked when signature verification fails.\n * Useful for logging and debugging.\n */\n onInvalidSignature?: (\n email: ForwardableEmailMessage,\n reason: SignatureFailureReason\n ) => void;\n};\n\n/**\n * @deprecated REMOVED due to security vulnerability (IDOR via spoofed headers).\n * @throws Always throws an error with migration guidance.\n */\nexport function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {\n throw new Error(\n \"createHeaderBasedEmailResolver has been removed due to a security vulnerability. \" +\n \"It trusted attacker-controlled email headers for routing, enabling IDOR attacks.\\n\\n\" +\n \"Migration options:\\n\" +\n \" - For inbound mail: use createAddressBasedEmailResolver(agentName)\\n\" +\n \" - For reply flows: use createSecureReplyEmailResolver(secret) with signed headers\\n\\n\" +\n \"See https://github.com/cloudflare/agents/blob/main/docs/email.md for details.\"\n );\n}\n\n/**\n * Create a resolver for routing email replies with signature verification.\n * This resolver verifies that replies contain a valid HMAC signature, preventing\n * attackers from routing emails to arbitrary agent instances.\n *\n * @param secret - Secret key for HMAC verification (must match the key used with signAgentHeaders)\n * @param options - Optional configuration for signature verification\n * @returns A function that resolves the agent to route the email to, or null if signature is invalid\n *\n * @example\n * ```typescript\n * // In your email handler\n * const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {\n * maxAge: 7 * 24 * 60 * 60, // 7 days\n * onInvalidSignature: (email, reason) => {\n * console.warn(`Invalid signature from ${email.from}: ${reason}`);\n * }\n * });\n * const addressResolver = createAddressBasedEmailResolver(\"MyAgent\");\n *\n * await routeAgentEmail(email, env, {\n * resolver: async (email, env) => {\n * // Try secure reply routing first\n * const replyRouting = await secureResolver(email, env);\n * if (replyRouting) return replyRouting;\n * // Fall back to address-based routing\n * return addressResolver(email, env);\n * }\n * });\n * ```\n */\nexport function createSecureReplyEmailResolver<Env>(\n secret: string,\n options?: SecureReplyResolverOptions\n): EmailResolver<Env> {\n if (!secret) {\n throw new Error(\"secret is required for createSecureReplyEmailResolver\");\n }\n\n const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE_SECONDS;\n const onInvalidSignature = options?.onInvalidSignature;\n\n return async (email: ForwardableEmailMessage, _env: Env) => {\n const agentName = email.headers.get(\"x-agent-name\");\n const agentId = email.headers.get(\"x-agent-id\");\n const signature = email.headers.get(\"x-agent-sig\");\n const timestamp = email.headers.get(\"x-agent-sig-ts\");\n\n if (!agentName || !agentId || !signature || !timestamp) {\n onInvalidSignature?.(email, \"missing_headers\");\n return null;\n }\n\n const result = await verifyAgentSignature(\n secret,\n agentName,\n agentId,\n signature,\n timestamp,\n maxAge\n );\n\n if (!result.valid) {\n onInvalidSignature?.(email, result.reason);\n return null;\n }\n\n return { agentName, agentId, _secureRouted: true };\n };\n}\n\n/**\n * Create a resolver that uses the email address to determine the agent to route the email to\n * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address\n * @returns A function that resolves the agent to route the email to\n */\nexport function createAddressBasedEmailResolver<Env>(\n defaultAgentName: string\n): EmailResolver<Env> {\n return async (email: ForwardableEmailMessage, _env: Env) => {\n // Length limits per RFC 5321: local part max 64 chars, domain max 253 chars\n const emailMatch = email.to.match(\n /^([^+@]{1,64})(?:\\+([^@]{1,64}))?@(.{1,253})$/\n );\n if (!emailMatch) {\n return null;\n }\n\n const [, localPart, subAddress] = emailMatch;\n\n if (subAddress) {\n return {\n agentName: localPart,\n agentId: subAddress\n };\n }\n\n // Option 2: Use defaultAgentName namespace, localPart as agentId\n // Common for catch-all email routing to a single EmailAgent namespace\n return {\n agentName: defaultAgentName,\n agentId: localPart\n };\n };\n}\n\n/**\n * Create a resolver that uses the agentName and agentId to determine the agent to route the email to\n * @param agentName The name of the agent to route the email to\n * @param agentId The id of the agent to route the email to\n * @returns A function that resolves the agent to route the email to\n */\nexport function createCatchAllEmailResolver<Env>(\n agentName: string,\n agentId: string\n): EmailResolver<Env> {\n return async () => ({ agentName, agentId });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqCA,SAAgB,iBAAiB,SAAiC;AAChE,QAAO,QAAQ,MAAM,MAAM;EACzB,MAAM,MAAM,EAAE,IAAI,aAAa;EAC/B,MAAM,QAAQ,EAAE,MAAM,aAAa;AAInC,MAAI,QAAQ,iBACV,QAAO,UAAU;AAInB,MAAI,QAAQ,2BACV,QAAO;AAIT,MAAI,QAAQ,aACV,QAAO,UAAU,UAAU,UAAU,UAAU,UAAU;AAG3D,SAAO;GACP;;;AAQJ,MAAa,0BAA0B,MAAU,KAAK;;AAGtD,MAAM,yBAAyB;;;;;;;;;AAU/B,eAAe,sBACb,QACA,WACA,SACA,WACiB;CACjB,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,MAAM,MAAM,OAAO,OAAO,UAC9B,OACA,QAAQ,OAAO,OAAO,EACtB;EAAE,MAAM;EAAQ,MAAM;EAAW,EACjC,OACA,CAAC,OAAO,CACT;CACD,MAAM,OAAO,QAAQ,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY;CACnE,MAAM,YAAY,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,KAAK;AAC7D,QAAO,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,UAAU,CAAC,CAAC;;;;;;;;;;;;AAoBhE,eAAe,qBACb,QACA,WACA,SACA,WACA,WACA,gBAAwB,yBACc;AACtC,KAAI;EAEF,MAAM,eAAe,OAAO,SAAS,WAAW,GAAG;AACnD,MAAI,OAAO,MAAM,aAAa,CAC5B,QAAO;GAAE,OAAO;GAAO,QAAQ;GAAuB;EAIxD,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;AAGzC,MAAI,eAAe,MAAM,uBACvB,QAAO;GAAE,OAAO;GAAO,QAAQ;GAAW;AAI5C,MAAI,MAAM,eAAe,cACvB,QAAO;GAAE,OAAO;GAAO,QAAQ;GAAW;EAG5C,MAAM,WAAW,MAAM,sBACrB,QACA,WACA,SACA,UACD;AAED,MAAI,SAAS,WAAW,UAAU,OAChC,QAAO;GAAE,OAAO;GAAO,QAAQ;GAAW;EAE5C,IAAI,SAAS;AACb,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,WAAU,SAAS,WAAW,EAAE,GAAG,UAAU,WAAW,EAAE;AAE5D,MAAI,WAAW,EACb,QAAO;GAAE,OAAO;GAAO,QAAQ;GAAW;AAE5C,SAAO,EAAE,OAAO,MAAM;UACf,OAAO;AACd,UAAQ,KAAK,0CAA0C,MAAM;AAC7D,SAAO;GAAE,OAAO;GAAO,QAAQ;GAAW;;;;;;;;;;;;;;;;;;AAmB9C,eAAsB,iBACpB,QACA,WACA,SACiC;AACjC,KAAI,CAAC,OACH,OAAM,IAAI,MAAM,+CAA+C;AAEjE,KAAI,CAAC,UACH,OAAM,IAAI,MAAM,kDAAkD;AAEpE,KAAI,CAAC,QACH,OAAM,IAAI,MAAM,gDAAgD;AAIlE,KAAI,UAAU,SAAS,IAAI,CACzB,OAAM,IAAI,MAAM,kCAAkC;AAEpD,KAAI,QAAQ,SAAS,IAAI,CACvB,OAAM,IAAI,MAAM,gCAAgC;CAGlD,MAAM,YAAY,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,CAAC,UAAU;CAC1D,MAAM,YAAY,MAAM,sBACtB,QACA,WACA,SACA,UACD;AACD,QAAO;EACL,gBAAgB;EAChB,cAAc;EACd,eAAe;EACf,kBAAkB;EACnB;;;;;;AAoDH,SAAgB,iCAA0D;AACxE,OAAM,IAAI,MACR,saAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCH,SAAgB,+BACd,QACA,SACoB;AACpB,KAAI,CAAC,OACH,OAAM,IAAI,MAAM,wDAAwD;CAG1E,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,qBAAqB,SAAS;AAEpC,QAAO,OAAO,OAAgC,SAAc;EAC1D,MAAM,YAAY,MAAM,QAAQ,IAAI,eAAe;EACnD,MAAM,UAAU,MAAM,QAAQ,IAAI,aAAa;EAC/C,MAAM,YAAY,MAAM,QAAQ,IAAI,cAAc;EAClD,MAAM,YAAY,MAAM,QAAQ,IAAI,iBAAiB;AAErD,MAAI,CAAC,aAAa,CAAC,WAAW,CAAC,aAAa,CAAC,WAAW;AACtD,wBAAqB,OAAO,kBAAkB;AAC9C,UAAO;;EAGT,MAAM,SAAS,MAAM,qBACnB,QACA,WACA,SACA,WACA,WACA,OACD;AAED,MAAI,CAAC,OAAO,OAAO;AACjB,wBAAqB,OAAO,OAAO,OAAO;AAC1C,UAAO;;AAGT,SAAO;GAAE;GAAW;GAAS,eAAe;GAAM;;;;;;;;AAStD,SAAgB,gCACd,kBACoB;AACpB,QAAO,OAAO,OAAgC,SAAc;EAE1D,MAAM,aAAa,MAAM,GAAG,MAC1B,gDACD;AACD,MAAI,CAAC,WACH,QAAO;EAGT,MAAM,GAAG,WAAW,cAAc;AAElC,MAAI,WACF,QAAO;GACL,WAAW;GACX,SAAS;GACV;AAKH,SAAO;GACL,WAAW;GACX,SAAS;GACV;;;;;;;;;AAUL,SAAgB,4BACd,WACA,SACoB;AACpB,QAAO,aAAa;EAAE;EAAW;EAAS"}
|
|
1
|
+
{"version":3,"file":"email.js","names":[],"sources":["../src/email.ts"],"sourcesContent":["/**\n * Email routing types, resolvers, and signing utilities for Agents\n */\n\n// Re-export AgentEmail type\nexport type { AgentEmail } from \"./internal_context\";\n\n// ============================================================================\n// Email header utilities\n// ============================================================================\n\n/**\n * Header object as returned by postal-mime and similar email parsing libraries.\n * Each header has a lowercase key and a string value.\n */\nexport type EmailHeader = {\n /** Lowercase header name (e.g., \"content-type\", \"x-custom-header\") */\n key: string;\n /** Header value */\n value: string;\n};\n\n/**\n * Check if an email appears to be an auto-reply based on standard headers.\n * Checks for Auto-Submitted (RFC 3834), X-Auto-Response-Suppress, and Precedence headers.\n *\n * @param headers - Headers array from postal-mime Email.headers or similar format\n * @returns true if email appears to be an auto-reply\n *\n * @example\n * ```typescript\n * if (isAutoReplyEmail(parsed.headers)) {\n * // Skip processing auto-replies\n * return;\n * }\n * ```\n */\nexport function isAutoReplyEmail(headers: EmailHeader[]): boolean {\n return headers.some((h) => {\n const key = h.key.toLowerCase();\n const value = h.value.toLowerCase();\n\n // RFC 3834: Auto-Submitted header\n // \"no\" means normal (human-sent) email, anything else indicates auto-reply\n if (key === \"auto-submitted\") {\n return value !== \"no\";\n }\n\n // X-Auto-Response-Suppress: any value indicates sender doesn't want auto-replies\n if (key === \"x-auto-response-suppress\") {\n return true;\n }\n\n // Precedence: only bulk/junk/list indicate automated/mass mail\n if (key === \"precedence\") {\n return value === \"bulk\" || value === \"junk\" || value === \"list\";\n }\n\n return false;\n });\n}\n\n// ============================================================================\n// Signing utilities\n// ============================================================================\n\n/** Default signature expiration: 30 days in seconds */\nexport const DEFAULT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;\n\n/** Maximum allowed clock skew for future timestamps: 5 minutes */\nconst MAX_CLOCK_SKEW_SECONDS = 5 * 60;\n\n/**\n * Compute HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param timestamp - Unix timestamp in seconds\n * @returns Base64-encoded HMAC signature\n */\nasync function computeAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n timestamp: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n const data = encoder.encode(`${agentName}:${agentId}:${timestamp}`);\n const signature = await crypto.subtle.sign(\"HMAC\", key, data);\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n/**\n * Result of signature verification\n */\ntype SignatureVerificationResult =\n | { valid: true }\n | { valid: false; reason: \"expired\" | \"invalid\" | \"malformed_timestamp\" };\n\n/**\n * Verify HMAC-SHA256 signature for agent routing headers\n * @param secret - Secret key for HMAC\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @param signature - Base64-encoded signature to verify\n * @param timestamp - Unix timestamp in seconds when signature was created\n * @param maxAgeSeconds - Maximum age of signature in seconds (default: 30 days)\n * @returns Verification result with reason if invalid\n */\nasync function verifyAgentSignature(\n secret: string,\n agentName: string,\n agentId: string,\n signature: string,\n timestamp: string,\n maxAgeSeconds: number = DEFAULT_MAX_AGE_SECONDS\n): Promise<SignatureVerificationResult> {\n try {\n // Validate timestamp format\n const timestampNum = Number.parseInt(timestamp, 10);\n if (Number.isNaN(timestampNum)) {\n return { valid: false, reason: \"malformed_timestamp\" };\n }\n\n // Check timestamp validity\n const now = Math.floor(Date.now() / 1000);\n\n // Reject timestamps too far in the future (prevents extending signature validity)\n if (timestampNum > now + MAX_CLOCK_SKEW_SECONDS) {\n return { valid: false, reason: \"invalid\" };\n }\n\n // Check if signature has expired\n if (now - timestampNum > maxAgeSeconds) {\n return { valid: false, reason: \"expired\" };\n }\n\n const expected = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n // Constant-time comparison to prevent timing attacks\n if (expected.length !== signature.length) {\n return { valid: false, reason: \"invalid\" };\n }\n let result = 0;\n for (let i = 0; i < expected.length; i++) {\n result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);\n }\n if (result !== 0) {\n return { valid: false, reason: \"invalid\" };\n }\n return { valid: true };\n } catch (error) {\n console.warn(\"[agents] Signature verification error:\", error);\n return { valid: false, reason: \"invalid\" };\n }\n}\n\n/**\n * Sign agent routing headers for secure reply flows.\n * Use this when sending outbound emails to ensure replies can be securely routed back.\n *\n * @param secret - Secret key for HMAC signing (store in environment variables)\n * @param agentName - Name of the agent\n * @param agentId - ID of the agent instance\n * @returns Headers object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts\n *\n * @example\n * ```typescript\n * const headers = await signAgentHeaders(env.EMAIL_SECRET, \"MyAgent\", this.name);\n * // Use these headers when sending outbound emails\n * ```\n */\nexport async function signAgentHeaders(\n secret: string,\n agentName: string,\n agentId: string\n): Promise<Record<string, string>> {\n if (!secret) {\n throw new Error(\"secret is required for signing agent headers\");\n }\n if (!agentName) {\n throw new Error(\"agentName is required for signing agent headers\");\n }\n if (!agentId) {\n throw new Error(\"agentId is required for signing agent headers\");\n }\n // Reject colons to prevent signature confusion attacks\n // (signature payload uses colon as delimiter: \"agentName:agentId:timestamp\")\n if (agentName.includes(\":\")) {\n throw new Error(\"agentName cannot contain colons\");\n }\n if (agentId.includes(\":\")) {\n throw new Error(\"agentId cannot contain colons\");\n }\n\n const timestamp = Math.floor(Date.now() / 1000).toString();\n const signature = await computeAgentSignature(\n secret,\n agentName,\n agentId,\n timestamp\n );\n return {\n \"X-Agent-Name\": agentName,\n \"X-Agent-ID\": agentId,\n \"X-Agent-Sig\": signature,\n \"X-Agent-Sig-Ts\": timestamp\n };\n}\n\n// ============================================================================\n// Email routing types and resolvers\n// ============================================================================\n\nexport type EmailResolverResult = {\n agentName: string;\n agentId: string;\n /** @internal Indicates this resolver requires secure reply signing */\n _secureRouted?: boolean;\n} | null;\n\nexport type EmailResolver<Env> = (\n email: ForwardableEmailMessage,\n env: Env\n) => Promise<EmailResolverResult>;\n\n/**\n * Reason for signature verification failure\n */\nexport type SignatureFailureReason =\n | \"missing_headers\"\n | \"expired\"\n | \"invalid\"\n | \"malformed_timestamp\";\n\n/**\n * Options for createSecureReplyEmailResolver\n */\nexport type SecureReplyResolverOptions = {\n /**\n * Maximum age of signature in seconds.\n * Signatures older than this will be rejected.\n * Default: 30 days (2592000 seconds)\n */\n maxAge?: number;\n /**\n * Callback invoked when signature verification fails.\n * Useful for logging and debugging.\n */\n onInvalidSignature?: (\n email: ForwardableEmailMessage,\n reason: SignatureFailureReason\n ) => void;\n};\n\n/**\n * @deprecated REMOVED due to security vulnerability (IDOR via spoofed headers).\n * @throws Always throws an error with migration guidance.\n */\nexport function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {\n throw new Error(\n \"createHeaderBasedEmailResolver has been removed due to a security vulnerability. \" +\n \"It trusted attacker-controlled email headers for routing, enabling IDOR attacks.\\n\\n\" +\n \"Migration options:\\n\" +\n \" - For inbound mail: use createAddressBasedEmailResolver(agentName)\\n\" +\n \" - For reply flows: use createSecureReplyEmailResolver(secret) with signed headers\\n\\n\" +\n \"See https://github.com/cloudflare/agents/blob/main/docs/email.md for details.\"\n );\n}\n\n/**\n * Create a resolver for routing email replies with signature verification.\n * This resolver verifies that replies contain a valid HMAC signature, preventing\n * attackers from routing emails to arbitrary agent instances.\n *\n * @param secret - Secret key for HMAC verification (must match the key used with signAgentHeaders)\n * @param options - Optional configuration for signature verification\n * @returns A function that resolves the agent to route the email to, or null if signature is invalid\n *\n * @example\n * ```typescript\n * // In your email handler\n * const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {\n * maxAge: 7 * 24 * 60 * 60, // 7 days\n * onInvalidSignature: (email, reason) => {\n * console.warn(`Invalid signature from ${email.from}: ${reason}`);\n * }\n * });\n * const addressResolver = createAddressBasedEmailResolver(\"MyAgent\");\n *\n * await routeAgentEmail(email, env, {\n * resolver: async (email, env) => {\n * // Try secure reply routing first\n * const replyRouting = await secureResolver(email, env);\n * if (replyRouting) return replyRouting;\n * // Fall back to address-based routing\n * return addressResolver(email, env);\n * }\n * });\n * ```\n */\nexport function createSecureReplyEmailResolver<Env>(\n secret: string,\n options?: SecureReplyResolverOptions\n): EmailResolver<Env> {\n if (!secret) {\n throw new Error(\"secret is required for createSecureReplyEmailResolver\");\n }\n\n const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE_SECONDS;\n const onInvalidSignature = options?.onInvalidSignature;\n\n return async (email: ForwardableEmailMessage, _env: Env) => {\n const agentName = email.headers.get(\"x-agent-name\");\n const agentId = email.headers.get(\"x-agent-id\");\n const signature = email.headers.get(\"x-agent-sig\");\n const timestamp = email.headers.get(\"x-agent-sig-ts\");\n\n if (!agentName || !agentId || !signature || !timestamp) {\n onInvalidSignature?.(email, \"missing_headers\");\n return null;\n }\n\n const result = await verifyAgentSignature(\n secret,\n agentName,\n agentId,\n signature,\n timestamp,\n maxAge\n );\n\n if (!result.valid) {\n onInvalidSignature?.(email, result.reason);\n return null;\n }\n\n return { agentName, agentId, _secureRouted: true };\n };\n}\n\n/**\n * Create a resolver that uses the email address to determine the agent to route the email to\n * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address\n * @returns A function that resolves the agent to route the email to\n */\nexport function createAddressBasedEmailResolver<Env>(\n defaultAgentName: string\n): EmailResolver<Env> {\n return async (email: ForwardableEmailMessage, _env: Env) => {\n // Length limits per RFC 5321: local part max 64 chars, domain max 253 chars\n const emailMatch = email.to.match(\n /^([^+@]{1,64})(?:\\+([^@]{1,64}))?@(.{1,253})$/\n );\n if (!emailMatch) {\n return null;\n }\n\n const [, localPart, subAddress] = emailMatch;\n\n if (subAddress) {\n return {\n agentName: localPart,\n agentId: subAddress\n };\n }\n\n // Option 2: Use defaultAgentName namespace, localPart as agentId\n // Common for catch-all email routing to a single EmailAgent namespace\n return {\n agentName: defaultAgentName,\n agentId: localPart\n };\n };\n}\n\n/**\n * Create a resolver that uses the agentName and agentId to determine the agent to route the email to\n * @param agentName The name of the agent to route the email to\n * @param agentId The id of the agent to route the email to\n * @returns A function that resolves the agent to route the email to\n */\nexport function createCatchAllEmailResolver<Env>(\n agentName: string,\n agentId: string\n): EmailResolver<Env> {\n return async () => ({ agentName, agentId });\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "../client-storage-BPjfP_is.js";
|
|
3
|
-
import { Agent, Schedule } from "../index.js";
|
|
4
|
-
|
|
1
|
+
import { r as Agent } from "../index-WBy5hmm3.js";
|
|
5
2
|
//#region src/experimental/forever.d.ts
|
|
6
3
|
type FiberState = {
|
|
7
4
|
id: string;
|
|
@@ -36,73 +33,31 @@ type FiberCompleteContext = {
|
|
|
36
33
|
payload: unknown;
|
|
37
34
|
result: unknown;
|
|
38
35
|
};
|
|
39
|
-
|
|
40
|
-
id: string;
|
|
41
|
-
callback: string;
|
|
42
|
-
payload: string | null;
|
|
43
|
-
snapshot: string | null;
|
|
44
|
-
status: string;
|
|
45
|
-
retry_count: number;
|
|
46
|
-
max_retries: number;
|
|
47
|
-
result: string | null;
|
|
48
|
-
error: string | null;
|
|
49
|
-
started_at: number | null;
|
|
50
|
-
updated_at: number | null;
|
|
51
|
-
completed_at: number | null;
|
|
52
|
-
created_at: number;
|
|
53
|
-
};
|
|
54
|
-
type Constructor<T = object> = new (...args: any[]) => T;
|
|
55
|
-
type AgentLike = Constructor<Pick<Agent<Cloudflare.Env>, "sql" | "scheduleEvery" | "cancelSchedule" | "alarm" | "keepAlive" | "keepAliveWhile">>;
|
|
56
|
-
declare function withFibers<TBase extends AgentLike>(Base: TBase, options?: {
|
|
36
|
+
declare function withFibers<TBase extends typeof Agent>(Base: TBase, options?: {
|
|
57
37
|
debugFibers?: boolean;
|
|
58
|
-
}):
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* detecting an eviction.
|
|
82
|
-
*/
|
|
83
|
-
checkFibers(): Promise<void>;
|
|
84
|
-
onFiberComplete(_ctx: FiberCompleteContext): void | Promise<void>;
|
|
85
|
-
onFiberRecovered(ctx: FiberRecoveryContext): void | Promise<void>;
|
|
86
|
-
onFibersRecovered(fibers: FiberRecoveryContext[]): Promise<void>; /** @internal */
|
|
87
|
-
_getRawFiber(fiberId: string): RawFiberRow | null; /** @internal */
|
|
88
|
-
_safeJsonParse(value: string | null): unknown; /** @internal */
|
|
89
|
-
_toFiberState(raw: RawFiberRow): FiberState; /** @internal */
|
|
90
|
-
_startFiber(id: string, methodName: string, payload: unknown, maxRetries: number): Promise<void>; /** @internal */
|
|
91
|
-
_runFiber(id: string, methodName: string, payload: unknown, maxRetries: number, disposeKeepAlive: () => void): Promise<void>; /** @internal */
|
|
92
|
-
_checkInterruptedFibers(): Promise<void>; /** @internal */
|
|
93
|
-
_cleanupOrphanedHeartbeats(): void; /** @internal */
|
|
94
|
-
_maybeCleanupFibers(): void;
|
|
95
|
-
sql: <T = Record<string, string | number | boolean | null>>(strings: TemplateStringsArray, ...values: (string | number | boolean | null)[]) => T[];
|
|
96
|
-
scheduleEvery: <T = string>(intervalSeconds: number, callback: keyof Agent<Cloudflare.Env, unknown, Record<string, unknown>>, payload?: T | undefined, options?: {
|
|
97
|
-
retry?: RetryOptions;
|
|
98
|
-
_idempotent?: boolean;
|
|
99
|
-
}) => Promise<Schedule<T>>;
|
|
100
|
-
cancelSchedule: (id: string) => Promise<boolean>;
|
|
101
|
-
alarm: () => Promise<void>;
|
|
102
|
-
keepAlive: () => Promise<() => void>;
|
|
103
|
-
keepAliveWhile: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
104
|
-
};
|
|
105
|
-
} & TBase;
|
|
38
|
+
}): FiberAgentClass;
|
|
39
|
+
type FiberAgentClass = {
|
|
40
|
+
new <Env extends Cloudflare.Env = Cloudflare.Env, State = unknown, Props extends Record<string, unknown> = Record<string, unknown>>(ctx: DurableObjectState, env: Env): Agent<Env, State, Props> & FiberMethods;
|
|
41
|
+
};
|
|
42
|
+
interface FiberMethods {
|
|
43
|
+
spawnFiber(methodName: string, payload?: unknown, options?: {
|
|
44
|
+
maxRetries?: number;
|
|
45
|
+
}): string;
|
|
46
|
+
stashFiber(data: unknown): void;
|
|
47
|
+
cancelFiber(fiberId: string): boolean;
|
|
48
|
+
getFiber(fiberId: string): FiberState | null;
|
|
49
|
+
restartFiber(fiberId: string): void;
|
|
50
|
+
checkFibers(): Promise<void>;
|
|
51
|
+
onFiberComplete(ctx: FiberCompleteContext): void | Promise<void>;
|
|
52
|
+
onFiberRecovered(ctx: FiberRecoveryContext): void | Promise<void>;
|
|
53
|
+
onFibersRecovered(fibers: FiberRecoveryContext[]): Promise<void>;
|
|
54
|
+
/** @internal */
|
|
55
|
+
_fiberActiveFibers: Set<string>;
|
|
56
|
+
/** @internal */
|
|
57
|
+
_fiberRecoveryInProgress: boolean;
|
|
58
|
+
/** @internal */
|
|
59
|
+
_fiberLastCleanupTime: number;
|
|
60
|
+
}
|
|
106
61
|
//#endregion
|
|
107
|
-
export { FiberCompleteContext, FiberContext, FiberRecoveryContext, FiberState, withFibers };
|
|
62
|
+
export { FiberCompleteContext, FiberContext, FiberMethods, FiberRecoveryContext, FiberState, withFibers };
|
|
108
63
|
//# sourceMappingURL=forever.d.ts.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
|
-
|
|
4
3
|
//#region src/experimental/forever.ts
|
|
5
4
|
/**
|
|
6
5
|
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
@@ -45,7 +44,7 @@ function withFibers(Base, options) {
|
|
|
45
44
|
this._fiberActiveFibers = /* @__PURE__ */ new Set();
|
|
46
45
|
this._fiberRecoveryInProgress = false;
|
|
47
46
|
this._fiberLastCleanupTime = 0;
|
|
48
|
-
console.warn("[agents/experimental/forever] WARNING: You are using an experimental API that WILL break between releases.
|
|
47
|
+
console.warn("[agents/experimental/forever] WARNING: You are using an experimental API that WILL break between releases. Use with caution.");
|
|
49
48
|
this.sql`
|
|
50
49
|
CREATE TABLE IF NOT EXISTS cf_agents_fibers (
|
|
51
50
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -341,7 +340,7 @@ function withFibers(Base, options) {
|
|
|
341
340
|
}
|
|
342
341
|
return FiberAgent;
|
|
343
342
|
}
|
|
344
|
-
|
|
345
343
|
//#endregion
|
|
346
344
|
export { withFibers };
|
|
345
|
+
|
|
347
346
|
//# 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\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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"}
|
|
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 pattern\ntype AgentConstructor = new (...args: any[]) => Agent;\n\nexport function withFibers<TBase extends typeof Agent>(\n Base: TBase,\n options?: { debugFibers?: boolean }\n) {\n const debugEnabled = options?.debugFibers ?? false;\n\n class FiberAgent extends (Base as AgentConstructor) {\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. Use with caution.\"\n );\n\n // Create the fibers table\n this.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.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.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.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.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.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.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.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.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.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.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.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.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.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 as unknown as FiberAgentClass;\n}\n\n// ── Return type ──────────────────────────────────────────────────────\n//\n// Explicit generic constructor so consumers can write:\n// const FA = withFibers(Agent);\n// class MyAgent extends FA<Env> { ... }\n// and still see all FiberMethods on `this`.\n//\n// We define this as a standalone constructor (not intersected with\n// typeof Agent) to avoid overload-resolution issues where TypeScript\n// would pick Agent's constructor and lose FiberMethods.\n\ntype FiberAgentClass = {\n new <\n Env extends Cloudflare.Env = Cloudflare.Env,\n State = unknown,\n Props extends Record<string, unknown> = Record<string, unknown>\n >(\n ctx: DurableObjectState,\n env: Env\n ): Agent<Env, State, Props> & FiberMethods;\n};\n\n// ── FiberMethods interface ───────────────────────────────────────────\n//\n// Describes the methods added by the mixin. Exported so users can\n// reference the shape in their own type annotations if needed.\n\nexport interface FiberMethods {\n spawnFiber(\n methodName: string,\n payload?: unknown,\n options?: { maxRetries?: number }\n ): string;\n stashFiber(data: unknown): void;\n cancelFiber(fiberId: string): boolean;\n getFiber(fiberId: string): FiberState | null;\n restartFiber(fiberId: string): void;\n checkFibers(): Promise<void>;\n onFiberComplete(ctx: FiberCompleteContext): void | Promise<void>;\n onFiberRecovered(ctx: FiberRecoveryContext): void | Promise<void>;\n onFibersRecovered(fibers: FiberRecoveryContext[]): Promise<void>;\n\n /** @internal */ _fiberActiveFibers: Set<string>;\n /** @internal */ _fiberRecoveryInProgress: boolean;\n /** @internal */ _fiberLastCleanupTime: number;\n}\n"],"mappings":"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"}
|
|
@@ -74,15 +74,8 @@ function truncateMessageParts(msg, rules) {
|
|
|
74
74
|
function microCompact(messages, rules) {
|
|
75
75
|
return messages.map((msg) => truncateMessageParts(msg, rules));
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
//#endregion
|
|
79
|
-
//#region src/experimental/memory/utils/tokens.ts
|
|
80
|
-
/** Approximate characters per token for English text */
|
|
81
|
-
const CHARS_PER_TOKEN = 4;
|
|
82
77
|
/** Approximate token multiplier per whitespace-separated word */
|
|
83
78
|
const WORDS_TOKEN_MULTIPLIER = 1.3;
|
|
84
|
-
/** Approximate overhead tokens per message (role, framing) */
|
|
85
|
-
const TOKENS_PER_MESSAGE = 4;
|
|
86
79
|
/**
|
|
87
80
|
* Estimate token count for a string using a hybrid heuristic.
|
|
88
81
|
*
|
|
@@ -94,7 +87,7 @@ const TOKENS_PER_MESSAGE = 4;
|
|
|
94
87
|
*/
|
|
95
88
|
function estimateStringTokens(text) {
|
|
96
89
|
if (!text) return 0;
|
|
97
|
-
const charEstimate = text.length /
|
|
90
|
+
const charEstimate = text.length / 4;
|
|
98
91
|
const wordEstimate = text.split(/\s+/).filter(Boolean).length * WORDS_TOKEN_MULTIPLIER;
|
|
99
92
|
return Math.ceil(Math.max(charEstimate, wordEstimate));
|
|
100
93
|
}
|
|
@@ -109,7 +102,7 @@ function estimateStringTokens(text) {
|
|
|
109
102
|
function estimateMessageTokens(messages) {
|
|
110
103
|
let tokens = 0;
|
|
111
104
|
for (const msg of messages) {
|
|
112
|
-
tokens +=
|
|
105
|
+
tokens += 4;
|
|
113
106
|
for (const part of msg.parts) if (part.type === "text") tokens += estimateStringTokens(part.text);
|
|
114
107
|
else if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
|
|
115
108
|
const toolPart = part;
|
|
@@ -119,7 +112,6 @@ function estimateMessageTokens(messages) {
|
|
|
119
112
|
}
|
|
120
113
|
return tokens;
|
|
121
114
|
}
|
|
122
|
-
|
|
123
115
|
//#endregion
|
|
124
116
|
//#region src/experimental/memory/session/session.ts
|
|
125
117
|
var Session = class {
|
|
@@ -183,7 +175,6 @@ var Session = class {
|
|
|
183
175
|
return estimateMessageTokens(this.storage.getMessages()) > this.compactionConfig.tokenThreshold;
|
|
184
176
|
}
|
|
185
177
|
};
|
|
186
|
-
|
|
187
178
|
//#endregion
|
|
188
179
|
//#region src/experimental/memory/session/providers/agent.ts
|
|
189
180
|
/**
|
|
@@ -379,7 +370,7 @@ var AgentSessionProvider = class {
|
|
|
379
370
|
return messages;
|
|
380
371
|
}
|
|
381
372
|
};
|
|
382
|
-
|
|
383
373
|
//#endregion
|
|
384
374
|
export { AgentSessionProvider, Session };
|
|
375
|
+
|
|
385
376
|
//# sourceMappingURL=index.js.map
|