@tanstack/ai-isolate-cloudflare 0.1.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.
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Cloudflare Worker for Code Mode execution
3
+ *
4
+ * This Worker executes JavaScript code in a V8 isolate on Cloudflare's edge network.
5
+ * Tool calls are handled via a request/response loop with the driver.
6
+ *
7
+ * Flow:
8
+ * 1. Receive code + tool schemas
9
+ * 2. Execute code, collecting any tool calls
10
+ * 3. If tool calls are needed, return them to the driver
11
+ * 4. Driver executes tools locally, sends results back
12
+ * 5. Re-execute with tool results injected
13
+ * 6. Return final result
14
+ */
15
+
16
+ import { wrapCode } from './wrap-code'
17
+ import type { ExecuteRequest, ExecuteResponse, ToolCallRequest } from '../types'
18
+
19
+ /**
20
+ * UnsafeEval binding type
21
+ * This is only available in local development with wrangler dev
22
+ */
23
+ interface UnsafeEval {
24
+ eval: (code: string) => unknown
25
+ }
26
+
27
+ interface Env {
28
+ /**
29
+ * UnsafeEval binding - provides eval() for local development
30
+ * Configured in wrangler.toml as an unsafe binding
31
+ */
32
+ UNSAFE_EVAL?: UnsafeEval
33
+ }
34
+
35
+ /**
36
+ * Execute code in the Worker's V8 isolate
37
+ */
38
+ async function executeCode(
39
+ request: ExecuteRequest,
40
+ env: Env,
41
+ ): Promise<ExecuteResponse> {
42
+ const { code, tools, toolResults, timeout = 30000 } = request
43
+
44
+ // Check if UNSAFE_EVAL binding is available
45
+ if (!env.UNSAFE_EVAL) {
46
+ return {
47
+ status: 'error',
48
+ error: {
49
+ name: 'UnsafeEvalNotAvailable',
50
+ message:
51
+ 'UNSAFE_EVAL binding is not available. ' +
52
+ 'This Worker requires the unsafe_eval binding for local development. ' +
53
+ 'For production, consider using Workers for Platforms.',
54
+ },
55
+ }
56
+ }
57
+
58
+ try {
59
+ const wrappedCode = wrapCode(code, tools, toolResults)
60
+
61
+ // Execute with timeout
62
+ const controller = new AbortController()
63
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
64
+
65
+ try {
66
+ // Use UNSAFE_EVAL binding to execute the code
67
+ // This is only available in local development with wrangler dev
68
+ const result = (await env.UNSAFE_EVAL.eval(wrappedCode)) as {
69
+ status: string
70
+ success?: boolean
71
+ value?: unknown
72
+ error?: { name: string; message: string; stack?: string }
73
+ logs: Array<string>
74
+ toolCalls?: Array<ToolCallRequest>
75
+ }
76
+
77
+ clearTimeout(timeoutId)
78
+
79
+ if (result.status === 'need_tools') {
80
+ return {
81
+ status: 'need_tools',
82
+ toolCalls: result.toolCalls || [],
83
+ logs: result.logs,
84
+ continuationId: crypto.randomUUID(),
85
+ }
86
+ }
87
+
88
+ return {
89
+ status: 'done',
90
+ success: result.success ?? false,
91
+ value: result.value,
92
+ error: result.error,
93
+ logs: result.logs,
94
+ }
95
+ } catch (evalError: unknown) {
96
+ clearTimeout(timeoutId)
97
+
98
+ if (controller.signal.aborted) {
99
+ return {
100
+ status: 'error',
101
+ error: {
102
+ name: 'TimeoutError',
103
+ message: `Execution timed out after ${timeout}ms`,
104
+ },
105
+ }
106
+ }
107
+
108
+ const error = evalError as Error
109
+ return {
110
+ status: 'done',
111
+ success: false,
112
+ error: {
113
+ name: error.name || 'EvalError',
114
+ message: error.message || String(error),
115
+ stack: error.stack,
116
+ },
117
+ logs: [],
118
+ }
119
+ }
120
+ } catch (error: unknown) {
121
+ const err = error as Error
122
+ return {
123
+ status: 'error',
124
+ error: {
125
+ name: err.name || 'Error',
126
+ message: err.message || String(err),
127
+ },
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Main Worker fetch handler
134
+ */
135
+ export default {
136
+ async fetch(
137
+ request: Request,
138
+ env: Env,
139
+ _ctx: ExecutionContext,
140
+ ): Promise<Response> {
141
+ // Handle CORS preflight
142
+ if (request.method === 'OPTIONS') {
143
+ return new Response(null, {
144
+ headers: {
145
+ 'Access-Control-Allow-Origin': '*',
146
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
147
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
148
+ },
149
+ })
150
+ }
151
+
152
+ // Only accept POST requests
153
+ if (request.method !== 'POST') {
154
+ return new Response(JSON.stringify({ error: 'Method not allowed' }), {
155
+ status: 405,
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ 'Access-Control-Allow-Origin': '*',
159
+ },
160
+ })
161
+ }
162
+
163
+ try {
164
+ const body: ExecuteRequest = await request.json()
165
+
166
+ // Validate request
167
+ if (!body.code || typeof body.code !== 'string') {
168
+ return new Response(JSON.stringify({ error: 'Code is required' }), {
169
+ status: 400,
170
+ headers: {
171
+ 'Content-Type': 'application/json',
172
+ 'Access-Control-Allow-Origin': '*',
173
+ },
174
+ })
175
+ }
176
+
177
+ // Execute the code
178
+ const result = await executeCode(body, env)
179
+
180
+ return new Response(JSON.stringify(result), {
181
+ status: 200,
182
+ headers: {
183
+ 'Content-Type': 'application/json',
184
+ 'Access-Control-Allow-Origin': '*',
185
+ },
186
+ })
187
+ } catch (error: unknown) {
188
+ const err = error as Error
189
+ return new Response(
190
+ JSON.stringify({
191
+ status: 'error',
192
+ error: {
193
+ name: 'RequestError',
194
+ message: err.message || 'Failed to process request',
195
+ },
196
+ }),
197
+ {
198
+ status: 500,
199
+ headers: {
200
+ 'Content-Type': 'application/json',
201
+ 'Access-Control-Allow-Origin': '*',
202
+ },
203
+ },
204
+ )
205
+ }
206
+ },
207
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Code wrapping utilities for the Cloudflare Worker.
3
+ * Extracted for testability without UNSAFE_EVAL.
4
+ */
5
+
6
+ import type { ToolResultPayload, ToolSchema } from '../types'
7
+
8
+ /**
9
+ * Generate tool wrapper code that collects calls or returns cached results.
10
+ *
11
+ * Tool calls are identified by a sequential index (__toolCallIdx) rather than
12
+ * by hashing the input. This avoids mismatches when re-executing code whose
13
+ * inputs contain non-deterministic values (e.g. random UUIDs).
14
+ */
15
+ export function generateToolWrappers(
16
+ tools: Array<ToolSchema>,
17
+ toolResults?: Record<string, ToolResultPayload>,
18
+ ): string {
19
+ const wrappers: Array<string> = []
20
+
21
+ for (const tool of tools) {
22
+ if (toolResults) {
23
+ wrappers.push(`
24
+ async function ${tool.name}(input) {
25
+ const callId = 'tc_' + (__toolCallIdx++);
26
+ const result = __toolResults[callId];
27
+ if (!result) {
28
+ __pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });
29
+ throw new __ToolCallNeeded(callId);
30
+ }
31
+ if (!result.success) {
32
+ throw new Error(result.error || 'Tool call failed');
33
+ }
34
+ return result.value;
35
+ }
36
+ `)
37
+ } else {
38
+ wrappers.push(`
39
+ async function ${tool.name}(input) {
40
+ const callId = 'tc_' + (__toolCallIdx++);
41
+ __pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });
42
+ throw new __ToolCallNeeded(callId);
43
+ }
44
+ `)
45
+ }
46
+ }
47
+
48
+ return wrappers.join('\n')
49
+ }
50
+
51
+ /**
52
+ * Wrap user code in an async IIFE with tool wrappers
53
+ */
54
+ export function wrapCode(
55
+ code: string,
56
+ tools: Array<ToolSchema>,
57
+ toolResults?: Record<string, ToolResultPayload>,
58
+ ): string {
59
+ const toolWrappers = generateToolWrappers(tools, toolResults)
60
+ const toolResultsJson = toolResults ? JSON.stringify(toolResults) : '{}'
61
+
62
+ return `
63
+ (async function() {
64
+ // Tool call tracking (sequential index for stable IDs across re-executions)
65
+ let __toolCallIdx = 0;
66
+ const __pendingToolCalls = [];
67
+ const __toolResults = ${toolResultsJson};
68
+ const __logs = [];
69
+
70
+ // Special error class for tool calls
71
+ class __ToolCallNeeded extends Error {
72
+ constructor(callId) {
73
+ super('Tool call needed: ' + callId);
74
+ this.callId = callId;
75
+ }
76
+ }
77
+
78
+ // Console capture
79
+ const console = {
80
+ log: (...args) => __logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
81
+ error: (...args) => __logs.push('ERROR: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
82
+ warn: (...args) => __logs.push('WARN: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
83
+ info: (...args) => __logs.push('INFO: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
84
+ };
85
+
86
+ // Tool wrappers
87
+ ${toolWrappers}
88
+
89
+ try {
90
+ // Execute user code
91
+ const __userResult = await (async function() {
92
+ ${code}
93
+ })();
94
+
95
+ return {
96
+ status: 'done',
97
+ success: true,
98
+ value: __userResult,
99
+ logs: __logs
100
+ };
101
+ } catch (__error) {
102
+ if (__error instanceof __ToolCallNeeded) {
103
+ // Tool calls needed - return pending calls
104
+ return {
105
+ status: 'need_tools',
106
+ toolCalls: __pendingToolCalls,
107
+ logs: __logs
108
+ };
109
+ }
110
+
111
+ // Regular error
112
+ return {
113
+ status: 'done',
114
+ success: false,
115
+ error: {
116
+ name: __error.name || 'Error',
117
+ message: __error.message || String(__error),
118
+ stack: __error.stack
119
+ },
120
+ logs: __logs
121
+ };
122
+ }
123
+ })()
124
+ `
125
+ }
package/wrangler.toml ADDED
@@ -0,0 +1,31 @@
1
+ #:schema node_modules/wrangler/config-schema.json
2
+
3
+ # Cloudflare Worker configuration for Code Mode execution
4
+ # Run locally: pnpm dev:worker (or wrangler dev)
5
+ # Deploy: pnpm deploy:worker (or wrangler deploy)
6
+
7
+ name = "tanstack-ai-code-mode"
8
+ main = "src/worker/index.ts"
9
+ compatibility_date = "2024-12-01"
10
+ compatibility_flags = ["nodejs_compat"]
11
+
12
+ # UnsafeEval binding - provides eval() for local development
13
+ # NOTE: This only works locally with wrangler dev.
14
+ # For production deployment, you need Workers for Platforms (enterprise)
15
+ # or a different execution strategy.
16
+ [[unsafe.bindings]]
17
+ name = "UNSAFE_EVAL"
18
+ type = "unsafe_eval"
19
+
20
+ # Local development settings
21
+ [dev]
22
+ port = 8787
23
+ local_protocol = "http"
24
+
25
+ # Production settings (uncomment and configure for deployment)
26
+ # [vars]
27
+ # ALLOWED_ORIGINS = "https://your-app.com"
28
+
29
+ # Optional: Add authentication via Cloudflare Access
30
+ # [env.production]
31
+ # name = "tanstack-ai-code-mode-prod"