@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.
- package/README.md +118 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/isolate-driver.d.ts +72 -0
- package/dist/esm/isolate-driver.js +174 -0
- package/dist/esm/isolate-driver.js.map +1 -0
- package/dist/esm/types.d.ts +72 -0
- package/dist/esm/worker/index.d.ts +35 -0
- package/dist/esm/worker/index.js +132 -0
- package/dist/esm/worker/index.js.map +1 -0
- package/dist/esm/worker/wrap-code.d.ts +13 -0
- package/dist/esm/worker/wrap-code.js +102 -0
- package/dist/esm/worker/wrap-code.js.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +30 -0
- package/src/isolate-driver.ts +310 -0
- package/src/types.ts +80 -0
- package/src/worker/index.ts +207 -0
- package/src/worker/wrap-code.ts +125 -0
- package/wrangler.toml +31 -0
|
@@ -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"
|