@tanstack/ai-isolate-cloudflare 0.1.0 → 0.1.1
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/LICENSE +21 -0
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +18 -5
- package/dist/esm/isolate-driver.d.ts +2 -1
- package/dist/esm/isolate-driver.d.ts.map +1 -0
- package/dist/esm/isolate-driver.js +205 -159
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +4 -0
- package/dist/esm/worker/index.d.ts +1 -0
- package/dist/esm/worker/index.d.ts.map +1 -0
- package/dist/esm/worker/index.js +146 -120
- package/dist/esm/worker/wrap-code.d.ts +6 -1
- package/dist/esm/worker/wrap-code.d.ts.map +1 -0
- package/dist/esm/worker/wrap-code.js +28 -18
- package/package.json +14 -17
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tanner Linsley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -15,5 +15,6 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
|
-
export { createCloudflareIsolateDriver, type CloudflareIsolateDriverConfig, } from './isolate-driver
|
|
19
|
-
export type { ExecuteRequest, ExecuteResponse, ToolSchema, ToolCallRequest, ToolResultPayload, } from './types
|
|
18
|
+
export { createCloudflareIsolateDriver, type CloudflareIsolateDriverConfig, } from './isolate-driver';
|
|
19
|
+
export type { ExecuteRequest, ExecuteResponse, ToolSchema, ToolCallRequest, ToolResultPayload, } from './types';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,6BAA6B,EAC7B,KAAK,6BAA6B,GACnC,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EACV,cAAc,EACd,eAAe,EACf,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,SAAS,CAAA"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @tanstack/ai-isolate-cloudflare
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare Workers driver for TanStack AI Code Mode.
|
|
5
|
+
* Execute LLM-generated code on Cloudflare's global edge network.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createCloudflareIsolateDriver } from '@tanstack/ai-isolate-cloudflare'
|
|
10
|
+
*
|
|
11
|
+
* const driver = createCloudflareIsolateDriver({
|
|
12
|
+
* workerUrl: 'https://your-worker.workers.dev',
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
export { createCloudflareIsolateDriver, } from './isolate-driver';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IsolateDriver } from '@tanstack/ai-code-mode';
|
|
1
|
+
import type { IsolateDriver } from '@tanstack/ai-code-mode';
|
|
2
2
|
/**
|
|
3
3
|
* Configuration for the Cloudflare Workers isolate driver
|
|
4
4
|
*/
|
|
@@ -70,3 +70,4 @@ export interface CloudflareIsolateDriverConfig {
|
|
|
70
70
|
* ```
|
|
71
71
|
*/
|
|
72
72
|
export declare function createCloudflareIsolateDriver(config: CloudflareIsolateDriverConfig): IsolateDriver;
|
|
73
|
+
//# sourceMappingURL=isolate-driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isolate-driver.d.ts","sourceRoot":"","sources":["../../src/isolate-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EAEd,MAAM,wBAAwB,CAAA;AAQ/B;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAsMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,6BAA6B,GACpC,aAAa,CAuBf"}
|
|
@@ -1,174 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert tool bindings to schemas for the Worker
|
|
3
|
+
*/
|
|
1
4
|
function bindingsToSchemas(bindings) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
return Object.entries(bindings).map(([name, binding]) => ({
|
|
6
|
+
name,
|
|
7
|
+
description: binding.description,
|
|
8
|
+
inputSchema: binding.inputSchema,
|
|
9
|
+
}));
|
|
7
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Normalize errors from various sources
|
|
13
|
+
*/
|
|
8
14
|
function normalizeError(error) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
return { name: error.name, message: error.message };
|
|
17
|
+
}
|
|
18
|
+
if (typeof error === 'object' && error !== null) {
|
|
19
|
+
const e = error;
|
|
20
|
+
return {
|
|
21
|
+
name: String(e.name || 'Error'),
|
|
22
|
+
message: String(e.message || JSON.stringify(error)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { name: 'Error', message: String(error) };
|
|
20
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* IsolateContext implementation using Cloudflare Workers
|
|
29
|
+
*/
|
|
21
30
|
class CloudflareIsolateContext {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
async execute(code) {
|
|
36
|
-
if (this.disposed) {
|
|
37
|
-
return {
|
|
38
|
-
success: false,
|
|
39
|
-
error: {
|
|
40
|
-
name: "DisposedError",
|
|
41
|
-
message: "Context has been disposed"
|
|
42
|
-
},
|
|
43
|
-
logs: []
|
|
44
|
-
};
|
|
31
|
+
workerUrl;
|
|
32
|
+
authorization;
|
|
33
|
+
timeout;
|
|
34
|
+
maxToolRounds;
|
|
35
|
+
bindings;
|
|
36
|
+
disposed = false;
|
|
37
|
+
constructor(workerUrl, bindings, timeout, maxToolRounds, authorization) {
|
|
38
|
+
this.workerUrl = workerUrl;
|
|
39
|
+
this.bindings = bindings;
|
|
40
|
+
this.timeout = timeout;
|
|
41
|
+
this.maxToolRounds = maxToolRounds;
|
|
42
|
+
this.authorization = authorization;
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
toolResults,
|
|
56
|
-
timeout: this.timeout
|
|
57
|
-
};
|
|
58
|
-
try {
|
|
59
|
-
const headers = {
|
|
60
|
-
"Content-Type": "application/json"
|
|
61
|
-
};
|
|
62
|
-
if (this.authorization) {
|
|
63
|
-
headers["Authorization"] = this.authorization;
|
|
64
|
-
}
|
|
65
|
-
const response = await fetch(this.workerUrl, {
|
|
66
|
-
method: "POST",
|
|
67
|
-
headers,
|
|
68
|
-
body: JSON.stringify(request)
|
|
69
|
-
});
|
|
70
|
-
if (!response.ok) {
|
|
71
|
-
const errorText = await response.text();
|
|
72
|
-
return {
|
|
73
|
-
success: false,
|
|
74
|
-
error: {
|
|
75
|
-
name: "WorkerError",
|
|
76
|
-
message: `Worker returned ${response.status}: ${errorText}`
|
|
77
|
-
},
|
|
78
|
-
logs: allLogs
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
const result = await response.json();
|
|
82
|
-
if (result.status === "error") {
|
|
83
|
-
return {
|
|
84
|
-
success: false,
|
|
85
|
-
error: result.error,
|
|
86
|
-
logs: allLogs
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
if (result.status === "done") {
|
|
90
|
-
allLogs = [...allLogs, ...result.logs];
|
|
91
|
-
return {
|
|
92
|
-
success: result.success,
|
|
93
|
-
value: result.value,
|
|
94
|
-
error: result.error,
|
|
95
|
-
logs: allLogs
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
allLogs = [...allLogs, ...result.logs];
|
|
99
|
-
toolResults = {};
|
|
100
|
-
for (const toolCall of result.toolCalls) {
|
|
101
|
-
const binding = this.bindings[toolCall.name];
|
|
102
|
-
if (!binding) {
|
|
103
|
-
toolResults[toolCall.id] = {
|
|
104
|
-
success: false,
|
|
105
|
-
error: `Unknown tool: ${toolCall.name}`
|
|
106
|
-
};
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
const toolResult = await binding.execute(toolCall.args);
|
|
111
|
-
toolResults[toolCall.id] = {
|
|
112
|
-
success: true,
|
|
113
|
-
value: toolResult
|
|
44
|
+
async execute(code) {
|
|
45
|
+
if (this.disposed) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: {
|
|
49
|
+
name: 'DisposedError',
|
|
50
|
+
message: 'Context has been disposed',
|
|
51
|
+
},
|
|
52
|
+
logs: [],
|
|
114
53
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
54
|
+
}
|
|
55
|
+
const tools = bindingsToSchemas(this.bindings);
|
|
56
|
+
let toolResults;
|
|
57
|
+
let allLogs = [];
|
|
58
|
+
let rounds = 0;
|
|
59
|
+
// Request/response loop for tool callbacks
|
|
60
|
+
while (rounds < this.maxToolRounds) {
|
|
61
|
+
rounds++;
|
|
62
|
+
const request = {
|
|
63
|
+
code,
|
|
64
|
+
tools,
|
|
65
|
+
toolResults,
|
|
66
|
+
timeout: this.timeout,
|
|
120
67
|
};
|
|
121
|
-
|
|
68
|
+
try {
|
|
69
|
+
const headers = {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
};
|
|
72
|
+
if (this.authorization) {
|
|
73
|
+
headers['Authorization'] = this.authorization;
|
|
74
|
+
}
|
|
75
|
+
const response = await fetch(this.workerUrl, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers,
|
|
78
|
+
body: JSON.stringify(request),
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errorText = await response.text();
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: {
|
|
85
|
+
name: 'WorkerError',
|
|
86
|
+
message: `Worker returned ${response.status}: ${errorText}`,
|
|
87
|
+
},
|
|
88
|
+
logs: allLogs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const result = await response.json();
|
|
92
|
+
if (result.status === 'error') {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: result.error,
|
|
96
|
+
logs: allLogs,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (result.status === 'done') {
|
|
100
|
+
allLogs = [...allLogs, ...result.logs];
|
|
101
|
+
return {
|
|
102
|
+
success: result.success,
|
|
103
|
+
value: result.value,
|
|
104
|
+
error: result.error,
|
|
105
|
+
logs: allLogs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// status === 'need_tools'
|
|
109
|
+
// Collect logs from this round
|
|
110
|
+
allLogs = [...allLogs, ...result.logs];
|
|
111
|
+
// Execute tool calls locally
|
|
112
|
+
toolResults = {};
|
|
113
|
+
for (const toolCall of result.toolCalls) {
|
|
114
|
+
const binding = this.bindings[toolCall.name];
|
|
115
|
+
if (!binding) {
|
|
116
|
+
toolResults[toolCall.id] = {
|
|
117
|
+
success: false,
|
|
118
|
+
error: `Unknown tool: ${toolCall.name}`,
|
|
119
|
+
};
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const toolResult = await binding.execute(toolCall.args);
|
|
124
|
+
toolResults[toolCall.id] = {
|
|
125
|
+
success: true,
|
|
126
|
+
value: toolResult,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (toolError) {
|
|
130
|
+
const err = normalizeError(toolError);
|
|
131
|
+
toolResults[toolCall.id] = {
|
|
132
|
+
success: false,
|
|
133
|
+
error: err.message,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Continue loop to send results back to Worker
|
|
138
|
+
}
|
|
139
|
+
catch (fetchError) {
|
|
140
|
+
const err = normalizeError(fetchError);
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: {
|
|
144
|
+
name: 'NetworkError',
|
|
145
|
+
message: `Failed to communicate with Worker: ${err.message}`,
|
|
146
|
+
},
|
|
147
|
+
logs: allLogs,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
122
150
|
}
|
|
123
|
-
|
|
124
|
-
const err = normalizeError(fetchError);
|
|
151
|
+
// Max rounds exceeded
|
|
125
152
|
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
success: false,
|
|
154
|
+
error: {
|
|
155
|
+
name: 'MaxRoundsExceeded',
|
|
156
|
+
message: `Exceeded maximum tool callback rounds (${this.maxToolRounds})`,
|
|
157
|
+
},
|
|
158
|
+
logs: allLogs,
|
|
132
159
|
};
|
|
133
|
-
}
|
|
134
160
|
}
|
|
161
|
+
dispose() {
|
|
162
|
+
this.disposed = true;
|
|
163
|
+
return Promise.resolve();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create a Cloudflare Workers isolate driver
|
|
168
|
+
*
|
|
169
|
+
* This driver executes code on Cloudflare's global edge network,
|
|
170
|
+
* providing true distributed execution capabilities.
|
|
171
|
+
*
|
|
172
|
+
* Tool calls are handled via a request/response loop:
|
|
173
|
+
* 1. Code is sent to the Worker
|
|
174
|
+
* 2. Worker executes until it needs a tool
|
|
175
|
+
* 3. Tool call is returned to the driver
|
|
176
|
+
* 4. Driver executes the tool locally
|
|
177
|
+
* 5. Result is sent back to the Worker
|
|
178
|
+
* 6. Worker continues execution
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* import { createCloudflareIsolateDriver } from '@tanstack/ai-isolate-cloudflare'
|
|
183
|
+
*
|
|
184
|
+
* // For local development with wrangler
|
|
185
|
+
* const driver = createCloudflareIsolateDriver({
|
|
186
|
+
* workerUrl: 'http://localhost:8787',
|
|
187
|
+
* })
|
|
188
|
+
*
|
|
189
|
+
* // For production
|
|
190
|
+
* const driver = createCloudflareIsolateDriver({
|
|
191
|
+
* workerUrl: 'https://code-mode-worker.your-account.workers.dev',
|
|
192
|
+
* authorization: 'Bearer your-secret-token',
|
|
193
|
+
* })
|
|
194
|
+
*
|
|
195
|
+
* const context = await driver.createContext({
|
|
196
|
+
* bindings: {
|
|
197
|
+
* readFile: {
|
|
198
|
+
* name: 'readFile',
|
|
199
|
+
* description: 'Read a file',
|
|
200
|
+
* inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
201
|
+
* execute: async ({ path }) => fs.readFile(path, 'utf-8'),
|
|
202
|
+
* },
|
|
203
|
+
* },
|
|
204
|
+
* })
|
|
205
|
+
*
|
|
206
|
+
* const result = await context.execute(`
|
|
207
|
+
* const content = await readFile({ path: './data.json' })
|
|
208
|
+
* return JSON.parse(content)
|
|
209
|
+
* `)
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function createCloudflareIsolateDriver(config) {
|
|
213
|
+
const { workerUrl, authorization, timeout: defaultTimeout = 30000, maxToolRounds = 10, } = config;
|
|
135
214
|
return {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
},
|
|
141
|
-
logs: allLogs
|
|
215
|
+
createContext(isolateConfig) {
|
|
216
|
+
const timeout = isolateConfig.timeout ?? defaultTimeout;
|
|
217
|
+
return Promise.resolve(new CloudflareIsolateContext(workerUrl, isolateConfig.bindings, timeout, maxToolRounds, authorization));
|
|
218
|
+
},
|
|
142
219
|
};
|
|
143
|
-
}
|
|
144
|
-
dispose() {
|
|
145
|
-
this.disposed = true;
|
|
146
|
-
return Promise.resolve();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function createCloudflareIsolateDriver(config) {
|
|
150
|
-
const {
|
|
151
|
-
workerUrl,
|
|
152
|
-
authorization,
|
|
153
|
-
timeout: defaultTimeout = 3e4,
|
|
154
|
-
maxToolRounds = 10
|
|
155
|
-
} = config;
|
|
156
|
-
return {
|
|
157
|
-
createContext(isolateConfig) {
|
|
158
|
-
const timeout = isolateConfig.timeout ?? defaultTimeout;
|
|
159
|
-
return Promise.resolve(
|
|
160
|
-
new CloudflareIsolateContext(
|
|
161
|
-
workerUrl,
|
|
162
|
-
isolateConfig.bindings,
|
|
163
|
-
timeout,
|
|
164
|
-
maxToolRounds,
|
|
165
|
-
authorization
|
|
166
|
-
)
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
220
|
}
|
|
171
|
-
export {
|
|
172
|
-
createCloudflareIsolateDriver
|
|
173
|
-
};
|
|
174
|
-
//# sourceMappingURL=isolate-driver.js.map
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,kDAAkD;IAClD,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACxB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAC/C,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB;IACE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CACpB,GACD;IACE,MAAM,EAAE,YAAY,CAAA;IACpB,SAAS,EAAE,KAAK,CAAC,eAAe,CAAC,CAAA;IACjC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACnB,wDAAwD;IACxD,cAAc,EAAE,MAAM,CAAA;CACvB,GACD;IACE,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/worker/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH;;;GAGG;AACH,UAAU,UAAU;IAClB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAChC;AAED,UAAU,GAAG;IACX;;;OAGG;IACH,WAAW,CAAC,EAAE,UAAU,CAAA;CACzB;AAmGD;;GAEG;;mBAGU,OAAO,OACX,GAAG,QACF,gBAAgB,GACrB,OAAO,CAAC,QAAQ,CAAC;;AALtB,wBAwEC"}
|
package/dist/esm/worker/index.js
CHANGED
|
@@ -1,132 +1,158 @@
|
|
|
1
|
-
|
|
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
|
+
import { wrapCode } from './wrap-code';
|
|
16
|
+
/**
|
|
17
|
+
* Execute code in the Worker's V8 isolate
|
|
18
|
+
*/
|
|
2
19
|
async function executeCode(request, env) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
status: "error",
|
|
7
|
-
error: {
|
|
8
|
-
name: "UnsafeEvalNotAvailable",
|
|
9
|
-
message: "UNSAFE_EVAL binding is not available. This Worker requires the unsafe_eval binding for local development. For production, consider using Workers for Platforms."
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
try {
|
|
14
|
-
const wrappedCode = wrapCode(code, tools, toolResults);
|
|
15
|
-
const controller = new AbortController();
|
|
16
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
17
|
-
try {
|
|
18
|
-
const result = await env.UNSAFE_EVAL.eval(wrappedCode);
|
|
19
|
-
clearTimeout(timeoutId);
|
|
20
|
-
if (result.status === "need_tools") {
|
|
20
|
+
const { code, tools, toolResults, timeout = 30000 } = request;
|
|
21
|
+
// Check if UNSAFE_EVAL binding is available
|
|
22
|
+
if (!env.UNSAFE_EVAL) {
|
|
21
23
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
status: 'error',
|
|
25
|
+
error: {
|
|
26
|
+
name: 'UnsafeEvalNotAvailable',
|
|
27
|
+
message: 'UNSAFE_EVAL binding is not available. ' +
|
|
28
|
+
'This Worker requires the unsafe_eval binding for local development. ' +
|
|
29
|
+
'For production, consider using Workers for Platforms.',
|
|
30
|
+
},
|
|
26
31
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const wrappedCode = wrapCode(code, tools, toolResults);
|
|
35
|
+
// Execute with timeout
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
38
|
+
try {
|
|
39
|
+
// Use UNSAFE_EVAL binding to execute the code
|
|
40
|
+
// This is only available in local development with wrangler dev
|
|
41
|
+
const result = (await env.UNSAFE_EVAL.eval(wrappedCode));
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
if (result.status === 'need_tools') {
|
|
44
|
+
return {
|
|
45
|
+
status: 'need_tools',
|
|
46
|
+
toolCalls: result.toolCalls || [],
|
|
47
|
+
logs: result.logs,
|
|
48
|
+
continuationId: crypto.randomUUID(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
status: 'done',
|
|
53
|
+
success: result.success ?? false,
|
|
54
|
+
value: result.value,
|
|
55
|
+
error: result.error,
|
|
56
|
+
logs: result.logs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (evalError) {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
if (controller.signal.aborted) {
|
|
62
|
+
return {
|
|
63
|
+
status: 'error',
|
|
64
|
+
error: {
|
|
65
|
+
name: 'TimeoutError',
|
|
66
|
+
message: `Execution timed out after ${timeout}ms`,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const error = evalError;
|
|
71
|
+
return {
|
|
72
|
+
status: 'done',
|
|
73
|
+
success: false,
|
|
74
|
+
error: {
|
|
75
|
+
name: error.name || 'EvalError',
|
|
76
|
+
message: error.message || String(error),
|
|
77
|
+
stack: error.stack,
|
|
78
|
+
},
|
|
79
|
+
logs: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const err = error;
|
|
38
85
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
86
|
+
status: 'error',
|
|
87
|
+
error: {
|
|
88
|
+
name: err.name || 'Error',
|
|
89
|
+
message: err.message || String(err),
|
|
90
|
+
},
|
|
44
91
|
};
|
|
45
|
-
}
|
|
46
|
-
const error = evalError;
|
|
47
|
-
return {
|
|
48
|
-
status: "done",
|
|
49
|
-
success: false,
|
|
50
|
-
error: {
|
|
51
|
-
name: error.name || "EvalError",
|
|
52
|
-
message: error.message || String(error),
|
|
53
|
-
stack: error.stack
|
|
54
|
-
},
|
|
55
|
-
logs: []
|
|
56
|
-
};
|
|
57
92
|
}
|
|
58
|
-
} catch (error) {
|
|
59
|
-
const err = error;
|
|
60
|
-
return {
|
|
61
|
-
status: "error",
|
|
62
|
-
error: {
|
|
63
|
-
name: err.name || "Error",
|
|
64
|
-
message: err.message || String(err)
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
93
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Main Worker fetch handler
|
|
96
|
+
*/
|
|
97
|
+
export default {
|
|
98
|
+
async fetch(request, env, _ctx) {
|
|
99
|
+
// Handle CORS preflight
|
|
100
|
+
if (request.method === 'OPTIONS') {
|
|
101
|
+
return new Response(null, {
|
|
102
|
+
headers: {
|
|
103
|
+
'Access-Control-Allow-Origin': '*',
|
|
104
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
105
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
77
108
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
// Only accept POST requests
|
|
110
|
+
if (request.method !== 'POST') {
|
|
111
|
+
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
|
112
|
+
status: 405,
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'Access-Control-Allow-Origin': '*',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
86
118
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
try {
|
|
120
|
+
const body = await request.json();
|
|
121
|
+
// Validate request
|
|
122
|
+
if (!body.code || typeof body.code !== 'string') {
|
|
123
|
+
return new Response(JSON.stringify({ error: 'Code is required' }), {
|
|
124
|
+
status: 400,
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
'Access-Control-Allow-Origin': '*',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Execute the code
|
|
132
|
+
const result = await executeCode(body, env);
|
|
133
|
+
return new Response(JSON.stringify(result), {
|
|
134
|
+
status: 200,
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'Access-Control-Allow-Origin': '*',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
106
140
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"Access-Control-Allow-Origin": "*"
|
|
123
|
-
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const err = error;
|
|
143
|
+
return new Response(JSON.stringify({
|
|
144
|
+
status: 'error',
|
|
145
|
+
error: {
|
|
146
|
+
name: 'RequestError',
|
|
147
|
+
message: err.message || 'Failed to process request',
|
|
148
|
+
},
|
|
149
|
+
}), {
|
|
150
|
+
status: 500,
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'Access-Control-Allow-Origin': '*',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
124
156
|
}
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
export {
|
|
130
|
-
index as default
|
|
157
|
+
},
|
|
131
158
|
};
|
|
132
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Code wrapping utilities for the Cloudflare Worker.
|
|
3
|
+
* Extracted for testability without UNSAFE_EVAL.
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolResultPayload, ToolSchema } from '../types';
|
|
2
6
|
/**
|
|
3
7
|
* Generate tool wrapper code that collects calls or returns cached results.
|
|
4
8
|
*
|
|
@@ -11,3 +15,4 @@ export declare function generateToolWrappers(tools: Array<ToolSchema>, toolResul
|
|
|
11
15
|
* Wrap user code in an async IIFE with tool wrappers
|
|
12
16
|
*/
|
|
13
17
|
export declare function wrapCode(code: string, tools: Array<ToolSchema>, toolResults?: Record<string, ToolResultPayload>): string;
|
|
18
|
+
//# sourceMappingURL=wrap-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrap-code.d.ts","sourceRoot":"","sources":["../../../src/worker/wrap-code.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE7D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,EACxB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC9C,MAAM,CA+BR;AAED;;GAEG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,EACxB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC9C,MAAM,CAmER"}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Code wrapping utilities for the Cloudflare Worker.
|
|
3
|
+
* Extracted for testability without UNSAFE_EVAL.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate tool wrapper code that collects calls or returns cached results.
|
|
7
|
+
*
|
|
8
|
+
* Tool calls are identified by a sequential index (__toolCallIdx) rather than
|
|
9
|
+
* by hashing the input. This avoids mismatches when re-executing code whose
|
|
10
|
+
* inputs contain non-deterministic values (e.g. random UUIDs).
|
|
11
|
+
*/
|
|
12
|
+
export function generateToolWrappers(tools, toolResults) {
|
|
13
|
+
const wrappers = [];
|
|
14
|
+
for (const tool of tools) {
|
|
15
|
+
if (toolResults) {
|
|
16
|
+
wrappers.push(`
|
|
6
17
|
async function ${tool.name}(input) {
|
|
7
18
|
const callId = 'tc_' + (__toolCallIdx++);
|
|
8
19
|
const result = __toolResults[callId];
|
|
@@ -16,22 +27,26 @@ function generateToolWrappers(tools, toolResults) {
|
|
|
16
27
|
return result.value;
|
|
17
28
|
}
|
|
18
29
|
`);
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
wrappers.push(`
|
|
21
33
|
async function ${tool.name}(input) {
|
|
22
34
|
const callId = 'tc_' + (__toolCallIdx++);
|
|
23
35
|
__pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });
|
|
24
36
|
throw new __ToolCallNeeded(callId);
|
|
25
37
|
}
|
|
26
38
|
`);
|
|
39
|
+
}
|
|
27
40
|
}
|
|
28
|
-
|
|
29
|
-
return wrappers.join("\n");
|
|
41
|
+
return wrappers.join('\n');
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Wrap user code in an async IIFE with tool wrappers
|
|
45
|
+
*/
|
|
46
|
+
export function wrapCode(code, tools, toolResults) {
|
|
47
|
+
const toolWrappers = generateToolWrappers(tools, toolResults);
|
|
48
|
+
const toolResultsJson = toolResults ? JSON.stringify(toolResults) : '{}';
|
|
49
|
+
return `
|
|
35
50
|
(async function() {
|
|
36
51
|
// Tool call tracking (sequential index for stable IDs across re-executions)
|
|
37
52
|
let __toolCallIdx = 0;
|
|
@@ -95,8 +110,3 @@ function wrapCode(code, tools, toolResults) {
|
|
|
95
110
|
})()
|
|
96
111
|
`;
|
|
97
112
|
}
|
|
98
|
-
export {
|
|
99
|
-
generateToolWrappers,
|
|
100
|
-
wrapCode
|
|
101
|
-
};
|
|
102
|
-
//# sourceMappingURL=wrap-code.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-isolate-cloudflare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Cloudflare Workers driver for TanStack AI Code Mode - execute code on the edge",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
10
|
"directory": "packages/typescript/ai-isolate-cloudflare"
|
|
11
11
|
},
|
|
12
|
-
"publishConfig": {
|
|
13
|
-
"access": "public"
|
|
14
|
-
},
|
|
15
12
|
"type": "module",
|
|
16
13
|
"module": "./dist/esm/index.js",
|
|
17
14
|
"types": "./dist/esm/index.d.ts",
|
|
@@ -35,18 +32,6 @@
|
|
|
35
32
|
"worker",
|
|
36
33
|
"wrangler.toml"
|
|
37
34
|
],
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "vite build",
|
|
40
|
-
"clean": "premove ./build ./dist",
|
|
41
|
-
"dev:worker": "node dev-server.mjs",
|
|
42
|
-
"deploy:worker": "wrangler deploy",
|
|
43
|
-
"lint:fix": "eslint ./src --fix",
|
|
44
|
-
"test:build": "publint --strict",
|
|
45
|
-
"test:eslint": "eslint ./src",
|
|
46
|
-
"test:lib": "vitest --passWithNoTests",
|
|
47
|
-
"test:lib:dev": "pnpm test:lib --watch",
|
|
48
|
-
"test:types": "tsc"
|
|
49
|
-
},
|
|
50
35
|
"keywords": [
|
|
51
36
|
"ai",
|
|
52
37
|
"tanstack",
|
|
@@ -57,7 +42,7 @@
|
|
|
57
42
|
"isolate"
|
|
58
43
|
],
|
|
59
44
|
"dependencies": {
|
|
60
|
-
"@tanstack/ai-code-mode": "
|
|
45
|
+
"@tanstack/ai-code-mode": "0.1.1"
|
|
61
46
|
},
|
|
62
47
|
"devDependencies": {
|
|
63
48
|
"@cloudflare/workers-types": "^4.20241230.0",
|
|
@@ -65,5 +50,17 @@
|
|
|
65
50
|
"esbuild": "^0.25.12",
|
|
66
51
|
"miniflare": "^4.20260305.0",
|
|
67
52
|
"wrangler": "^4.19.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "vite build",
|
|
56
|
+
"clean": "premove ./build ./dist",
|
|
57
|
+
"dev:worker": "node dev-server.mjs",
|
|
58
|
+
"deploy:worker": "wrangler deploy",
|
|
59
|
+
"lint:fix": "eslint ./src --fix",
|
|
60
|
+
"test:build": "publint --strict",
|
|
61
|
+
"test:eslint": "eslint ./src",
|
|
62
|
+
"test:lib": "vitest --passWithNoTests",
|
|
63
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
64
|
+
"test:types": "tsc"
|
|
68
65
|
}
|
|
69
66
|
}
|