@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,13 @@
|
|
|
1
|
+
import { ToolResultPayload, ToolSchema } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate tool wrapper code that collects calls or returns cached results.
|
|
4
|
+
*
|
|
5
|
+
* Tool calls are identified by a sequential index (__toolCallIdx) rather than
|
|
6
|
+
* by hashing the input. This avoids mismatches when re-executing code whose
|
|
7
|
+
* inputs contain non-deterministic values (e.g. random UUIDs).
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateToolWrappers(tools: Array<ToolSchema>, toolResults?: Record<string, ToolResultPayload>): string;
|
|
10
|
+
/**
|
|
11
|
+
* Wrap user code in an async IIFE with tool wrappers
|
|
12
|
+
*/
|
|
13
|
+
export declare function wrapCode(code: string, tools: Array<ToolSchema>, toolResults?: Record<string, ToolResultPayload>): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function generateToolWrappers(tools, toolResults) {
|
|
2
|
+
const wrappers = [];
|
|
3
|
+
for (const tool of tools) {
|
|
4
|
+
if (toolResults) {
|
|
5
|
+
wrappers.push(`
|
|
6
|
+
async function ${tool.name}(input) {
|
|
7
|
+
const callId = 'tc_' + (__toolCallIdx++);
|
|
8
|
+
const result = __toolResults[callId];
|
|
9
|
+
if (!result) {
|
|
10
|
+
__pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });
|
|
11
|
+
throw new __ToolCallNeeded(callId);
|
|
12
|
+
}
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
throw new Error(result.error || 'Tool call failed');
|
|
15
|
+
}
|
|
16
|
+
return result.value;
|
|
17
|
+
}
|
|
18
|
+
`);
|
|
19
|
+
} else {
|
|
20
|
+
wrappers.push(`
|
|
21
|
+
async function ${tool.name}(input) {
|
|
22
|
+
const callId = 'tc_' + (__toolCallIdx++);
|
|
23
|
+
__pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });
|
|
24
|
+
throw new __ToolCallNeeded(callId);
|
|
25
|
+
}
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return wrappers.join("\n");
|
|
30
|
+
}
|
|
31
|
+
function wrapCode(code, tools, toolResults) {
|
|
32
|
+
const toolWrappers = generateToolWrappers(tools, toolResults);
|
|
33
|
+
const toolResultsJson = toolResults ? JSON.stringify(toolResults) : "{}";
|
|
34
|
+
return `
|
|
35
|
+
(async function() {
|
|
36
|
+
// Tool call tracking (sequential index for stable IDs across re-executions)
|
|
37
|
+
let __toolCallIdx = 0;
|
|
38
|
+
const __pendingToolCalls = [];
|
|
39
|
+
const __toolResults = ${toolResultsJson};
|
|
40
|
+
const __logs = [];
|
|
41
|
+
|
|
42
|
+
// Special error class for tool calls
|
|
43
|
+
class __ToolCallNeeded extends Error {
|
|
44
|
+
constructor(callId) {
|
|
45
|
+
super('Tool call needed: ' + callId);
|
|
46
|
+
this.callId = callId;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Console capture
|
|
51
|
+
const console = {
|
|
52
|
+
log: (...args) => __logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
|
|
53
|
+
error: (...args) => __logs.push('ERROR: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
|
|
54
|
+
warn: (...args) => __logs.push('WARN: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
|
|
55
|
+
info: (...args) => __logs.push('INFO: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Tool wrappers
|
|
59
|
+
${toolWrappers}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Execute user code
|
|
63
|
+
const __userResult = await (async function() {
|
|
64
|
+
${code}
|
|
65
|
+
})();
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
status: 'done',
|
|
69
|
+
success: true,
|
|
70
|
+
value: __userResult,
|
|
71
|
+
logs: __logs
|
|
72
|
+
};
|
|
73
|
+
} catch (__error) {
|
|
74
|
+
if (__error instanceof __ToolCallNeeded) {
|
|
75
|
+
// Tool calls needed - return pending calls
|
|
76
|
+
return {
|
|
77
|
+
status: 'need_tools',
|
|
78
|
+
toolCalls: __pendingToolCalls,
|
|
79
|
+
logs: __logs
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Regular error
|
|
84
|
+
return {
|
|
85
|
+
status: 'done',
|
|
86
|
+
success: false,
|
|
87
|
+
error: {
|
|
88
|
+
name: __error.name || 'Error',
|
|
89
|
+
message: __error.message || String(__error),
|
|
90
|
+
stack: __error.stack
|
|
91
|
+
},
|
|
92
|
+
logs: __logs
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
})()
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
generateToolWrappers,
|
|
100
|
+
wrapCode
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=wrap-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrap-code.js","sources":["../../../src/worker/wrap-code.ts"],"sourcesContent":["/**\n * Code wrapping utilities for the Cloudflare Worker.\n * Extracted for testability without UNSAFE_EVAL.\n */\n\nimport type { ToolResultPayload, ToolSchema } from '../types'\n\n/**\n * Generate tool wrapper code that collects calls or returns cached results.\n *\n * Tool calls are identified by a sequential index (__toolCallIdx) rather than\n * by hashing the input. This avoids mismatches when re-executing code whose\n * inputs contain non-deterministic values (e.g. random UUIDs).\n */\nexport function generateToolWrappers(\n tools: Array<ToolSchema>,\n toolResults?: Record<string, ToolResultPayload>,\n): string {\n const wrappers: Array<string> = []\n\n for (const tool of tools) {\n if (toolResults) {\n wrappers.push(`\n async function ${tool.name}(input) {\n const callId = 'tc_' + (__toolCallIdx++);\n const result = __toolResults[callId];\n if (!result) {\n __pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });\n throw new __ToolCallNeeded(callId);\n }\n if (!result.success) {\n throw new Error(result.error || 'Tool call failed');\n }\n return result.value;\n }\n `)\n } else {\n wrappers.push(`\n async function ${tool.name}(input) {\n const callId = 'tc_' + (__toolCallIdx++);\n __pendingToolCalls.push({ id: callId, name: '${tool.name}', args: input });\n throw new __ToolCallNeeded(callId);\n }\n `)\n }\n }\n\n return wrappers.join('\\n')\n}\n\n/**\n * Wrap user code in an async IIFE with tool wrappers\n */\nexport function wrapCode(\n code: string,\n tools: Array<ToolSchema>,\n toolResults?: Record<string, ToolResultPayload>,\n): string {\n const toolWrappers = generateToolWrappers(tools, toolResults)\n const toolResultsJson = toolResults ? JSON.stringify(toolResults) : '{}'\n\n return `\n (async function() {\n // Tool call tracking (sequential index for stable IDs across re-executions)\n let __toolCallIdx = 0;\n const __pendingToolCalls = [];\n const __toolResults = ${toolResultsJson};\n const __logs = [];\n\n // Special error class for tool calls\n class __ToolCallNeeded extends Error {\n constructor(callId) {\n super('Tool call needed: ' + callId);\n this.callId = callId;\n }\n }\n\n // Console capture\n const console = {\n log: (...args) => __logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),\n error: (...args) => __logs.push('ERROR: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),\n warn: (...args) => __logs.push('WARN: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),\n info: (...args) => __logs.push('INFO: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')),\n };\n\n // Tool wrappers\n ${toolWrappers}\n\n try {\n // Execute user code\n const __userResult = await (async function() {\n ${code}\n })();\n\n return {\n status: 'done',\n success: true,\n value: __userResult,\n logs: __logs\n };\n } catch (__error) {\n if (__error instanceof __ToolCallNeeded) {\n // Tool calls needed - return pending calls\n return {\n status: 'need_tools',\n toolCalls: __pendingToolCalls,\n logs: __logs\n };\n }\n\n // Regular error\n return {\n status: 'done',\n success: false,\n error: {\n name: __error.name || 'Error',\n message: __error.message || String(__error),\n stack: __error.stack\n },\n logs: __logs\n };\n }\n })()\n `\n}\n"],"names":[],"mappings":"AAcO,SAAS,qBACd,OACA,aACQ;AACR,QAAM,WAA0B,CAAA;AAEhC,aAAW,QAAQ,OAAO;AACxB,QAAI,aAAa;AACf,eAAS,KAAK;AAAA,yBACK,KAAK,IAAI;AAAA;AAAA;AAAA;AAAA,2DAIyB,KAAK,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAQ7D;AAAA,IACH,OAAO;AACL,eAAS,KAAK;AAAA,yBACK,KAAK,IAAI;AAAA;AAAA,yDAEuB,KAAK,IAAI;AAAA;AAAA;AAAA,OAG3D;AAAA,IACH;AAAA,EACF;AAEA,SAAO,SAAS,KAAK,IAAI;AAC3B;AAKO,SAAS,SACd,MACA,OACA,aACQ;AACR,QAAM,eAAe,qBAAqB,OAAO,WAAW;AAC5D,QAAM,kBAAkB,cAAc,KAAK,UAAU,WAAW,IAAI;AAEpE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,8BAKqB,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAoBrC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,YAKR,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiChB;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-isolate-cloudflare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cloudflare Workers driver for TanStack AI Code Mode - execute code on the edge",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
|
+
"directory": "packages/typescript/ai-isolate-cloudflare"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"module": "./dist/esm/index.js",
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/esm/index.d.ts",
|
|
21
|
+
"import": "./dist/esm/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./worker": {
|
|
24
|
+
"types": "./dist/esm/worker/index.d.ts",
|
|
25
|
+
"import": "./dist/esm/worker/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"worker",
|
|
36
|
+
"wrangler.toml"
|
|
37
|
+
],
|
|
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
|
+
"keywords": [
|
|
51
|
+
"ai",
|
|
52
|
+
"tanstack",
|
|
53
|
+
"code-mode",
|
|
54
|
+
"cloudflare",
|
|
55
|
+
"workers",
|
|
56
|
+
"edge",
|
|
57
|
+
"isolate"
|
|
58
|
+
],
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@tanstack/ai-code-mode": "workspace:*"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@cloudflare/workers-types": "^4.20241230.0",
|
|
64
|
+
"@vitest/coverage-v8": "4.0.14",
|
|
65
|
+
"esbuild": "^0.25.12",
|
|
66
|
+
"miniflare": "^4.20260305.0",
|
|
67
|
+
"wrangler": "^4.19.1"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
19
|
+
export {
|
|
20
|
+
createCloudflareIsolateDriver,
|
|
21
|
+
type CloudflareIsolateDriverConfig,
|
|
22
|
+
} from './isolate-driver'
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
ExecuteRequest,
|
|
26
|
+
ExecuteResponse,
|
|
27
|
+
ToolSchema,
|
|
28
|
+
ToolCallRequest,
|
|
29
|
+
ToolResultPayload,
|
|
30
|
+
} from './types'
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionResult,
|
|
3
|
+
IsolateConfig,
|
|
4
|
+
IsolateContext,
|
|
5
|
+
IsolateDriver,
|
|
6
|
+
ToolBinding,
|
|
7
|
+
} from '@tanstack/ai-code-mode'
|
|
8
|
+
import type {
|
|
9
|
+
ExecuteRequest,
|
|
10
|
+
ExecuteResponse,
|
|
11
|
+
ToolResultPayload,
|
|
12
|
+
ToolSchema,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for the Cloudflare Workers isolate driver
|
|
17
|
+
*/
|
|
18
|
+
export interface CloudflareIsolateDriverConfig {
|
|
19
|
+
/**
|
|
20
|
+
* URL of the deployed Cloudflare Worker
|
|
21
|
+
* For local development, use: http://localhost:8787
|
|
22
|
+
*/
|
|
23
|
+
workerUrl: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional authorization header value
|
|
27
|
+
* Useful for protecting your Worker endpoint
|
|
28
|
+
*/
|
|
29
|
+
authorization?: string
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default execution timeout in ms (default: 30000)
|
|
33
|
+
*/
|
|
34
|
+
timeout?: number
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of tool callback rounds (default: 10)
|
|
38
|
+
* Prevents infinite loops
|
|
39
|
+
*/
|
|
40
|
+
maxToolRounds?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert tool bindings to schemas for the Worker
|
|
45
|
+
*/
|
|
46
|
+
function bindingsToSchemas(
|
|
47
|
+
bindings: Record<string, ToolBinding>,
|
|
48
|
+
): Array<ToolSchema> {
|
|
49
|
+
return Object.entries(bindings).map(([name, binding]) => ({
|
|
50
|
+
name,
|
|
51
|
+
description: binding.description,
|
|
52
|
+
inputSchema: binding.inputSchema,
|
|
53
|
+
}))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize errors from various sources
|
|
58
|
+
*/
|
|
59
|
+
function normalizeError(error: unknown): { name: string; message: string } {
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
return { name: error.name, message: error.message }
|
|
62
|
+
}
|
|
63
|
+
if (typeof error === 'object' && error !== null) {
|
|
64
|
+
const e = error as Record<string, unknown>
|
|
65
|
+
return {
|
|
66
|
+
name: String(e.name || 'Error'),
|
|
67
|
+
message: String(e.message || JSON.stringify(error)),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { name: 'Error', message: String(error) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* IsolateContext implementation using Cloudflare Workers
|
|
75
|
+
*/
|
|
76
|
+
class CloudflareIsolateContext implements IsolateContext {
|
|
77
|
+
private workerUrl: string
|
|
78
|
+
private authorization?: string
|
|
79
|
+
private timeout: number
|
|
80
|
+
private maxToolRounds: number
|
|
81
|
+
private bindings: Record<string, ToolBinding>
|
|
82
|
+
private disposed = false
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
workerUrl: string,
|
|
86
|
+
bindings: Record<string, ToolBinding>,
|
|
87
|
+
timeout: number,
|
|
88
|
+
maxToolRounds: number,
|
|
89
|
+
authorization?: string,
|
|
90
|
+
) {
|
|
91
|
+
this.workerUrl = workerUrl
|
|
92
|
+
this.bindings = bindings
|
|
93
|
+
this.timeout = timeout
|
|
94
|
+
this.maxToolRounds = maxToolRounds
|
|
95
|
+
this.authorization = authorization
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async execute<T = unknown>(code: string): Promise<ExecutionResult<T>> {
|
|
99
|
+
if (this.disposed) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: {
|
|
103
|
+
name: 'DisposedError',
|
|
104
|
+
message: 'Context has been disposed',
|
|
105
|
+
},
|
|
106
|
+
logs: [],
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tools = bindingsToSchemas(this.bindings)
|
|
111
|
+
let toolResults: Record<string, ToolResultPayload> | undefined
|
|
112
|
+
let allLogs: Array<string> = []
|
|
113
|
+
let rounds = 0
|
|
114
|
+
|
|
115
|
+
// Request/response loop for tool callbacks
|
|
116
|
+
while (rounds < this.maxToolRounds) {
|
|
117
|
+
rounds++
|
|
118
|
+
|
|
119
|
+
const request: ExecuteRequest = {
|
|
120
|
+
code,
|
|
121
|
+
tools,
|
|
122
|
+
toolResults,
|
|
123
|
+
timeout: this.timeout,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const headers: Record<string, string> = {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.authorization) {
|
|
132
|
+
headers['Authorization'] = this.authorization
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = await fetch(this.workerUrl, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers,
|
|
138
|
+
body: JSON.stringify(request),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorText = await response.text()
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
error: {
|
|
146
|
+
name: 'WorkerError',
|
|
147
|
+
message: `Worker returned ${response.status}: ${errorText}`,
|
|
148
|
+
},
|
|
149
|
+
logs: allLogs,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result: ExecuteResponse = await response.json()
|
|
154
|
+
|
|
155
|
+
if (result.status === 'error') {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: result.error,
|
|
159
|
+
logs: allLogs,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (result.status === 'done') {
|
|
164
|
+
allLogs = [...allLogs, ...result.logs]
|
|
165
|
+
return {
|
|
166
|
+
success: result.success,
|
|
167
|
+
value: result.value as T,
|
|
168
|
+
error: result.error,
|
|
169
|
+
logs: allLogs,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// status === 'need_tools'
|
|
174
|
+
// Collect logs from this round
|
|
175
|
+
allLogs = [...allLogs, ...result.logs]
|
|
176
|
+
|
|
177
|
+
// Execute tool calls locally
|
|
178
|
+
toolResults = {}
|
|
179
|
+
|
|
180
|
+
for (const toolCall of result.toolCalls) {
|
|
181
|
+
const binding = this.bindings[toolCall.name] as
|
|
182
|
+
| ToolBinding
|
|
183
|
+
| undefined
|
|
184
|
+
|
|
185
|
+
if (!binding) {
|
|
186
|
+
toolResults[toolCall.id] = {
|
|
187
|
+
success: false,
|
|
188
|
+
error: `Unknown tool: ${toolCall.name}`,
|
|
189
|
+
}
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const toolResult = await binding.execute(toolCall.args)
|
|
195
|
+
toolResults[toolCall.id] = {
|
|
196
|
+
success: true,
|
|
197
|
+
value: toolResult,
|
|
198
|
+
}
|
|
199
|
+
} catch (toolError) {
|
|
200
|
+
const err = normalizeError(toolError)
|
|
201
|
+
toolResults[toolCall.id] = {
|
|
202
|
+
success: false,
|
|
203
|
+
error: err.message,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Continue loop to send results back to Worker
|
|
209
|
+
} catch (fetchError) {
|
|
210
|
+
const err = normalizeError(fetchError)
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: {
|
|
214
|
+
name: 'NetworkError',
|
|
215
|
+
message: `Failed to communicate with Worker: ${err.message}`,
|
|
216
|
+
},
|
|
217
|
+
logs: allLogs,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Max rounds exceeded
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
error: {
|
|
226
|
+
name: 'MaxRoundsExceeded',
|
|
227
|
+
message: `Exceeded maximum tool callback rounds (${this.maxToolRounds})`,
|
|
228
|
+
},
|
|
229
|
+
logs: allLogs,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dispose(): Promise<void> {
|
|
234
|
+
this.disposed = true
|
|
235
|
+
return Promise.resolve()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a Cloudflare Workers isolate driver
|
|
241
|
+
*
|
|
242
|
+
* This driver executes code on Cloudflare's global edge network,
|
|
243
|
+
* providing true distributed execution capabilities.
|
|
244
|
+
*
|
|
245
|
+
* Tool calls are handled via a request/response loop:
|
|
246
|
+
* 1. Code is sent to the Worker
|
|
247
|
+
* 2. Worker executes until it needs a tool
|
|
248
|
+
* 3. Tool call is returned to the driver
|
|
249
|
+
* 4. Driver executes the tool locally
|
|
250
|
+
* 5. Result is sent back to the Worker
|
|
251
|
+
* 6. Worker continues execution
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* import { createCloudflareIsolateDriver } from '@tanstack/ai-isolate-cloudflare'
|
|
256
|
+
*
|
|
257
|
+
* // For local development with wrangler
|
|
258
|
+
* const driver = createCloudflareIsolateDriver({
|
|
259
|
+
* workerUrl: 'http://localhost:8787',
|
|
260
|
+
* })
|
|
261
|
+
*
|
|
262
|
+
* // For production
|
|
263
|
+
* const driver = createCloudflareIsolateDriver({
|
|
264
|
+
* workerUrl: 'https://code-mode-worker.your-account.workers.dev',
|
|
265
|
+
* authorization: 'Bearer your-secret-token',
|
|
266
|
+
* })
|
|
267
|
+
*
|
|
268
|
+
* const context = await driver.createContext({
|
|
269
|
+
* bindings: {
|
|
270
|
+
* readFile: {
|
|
271
|
+
* name: 'readFile',
|
|
272
|
+
* description: 'Read a file',
|
|
273
|
+
* inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
274
|
+
* execute: async ({ path }) => fs.readFile(path, 'utf-8'),
|
|
275
|
+
* },
|
|
276
|
+
* },
|
|
277
|
+
* })
|
|
278
|
+
*
|
|
279
|
+
* const result = await context.execute(`
|
|
280
|
+
* const content = await readFile({ path: './data.json' })
|
|
281
|
+
* return JSON.parse(content)
|
|
282
|
+
* `)
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
export function createCloudflareIsolateDriver(
|
|
286
|
+
config: CloudflareIsolateDriverConfig,
|
|
287
|
+
): IsolateDriver {
|
|
288
|
+
const {
|
|
289
|
+
workerUrl,
|
|
290
|
+
authorization,
|
|
291
|
+
timeout: defaultTimeout = 30000,
|
|
292
|
+
maxToolRounds = 10,
|
|
293
|
+
} = config
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
createContext(isolateConfig: IsolateConfig): Promise<IsolateContext> {
|
|
297
|
+
const timeout = isolateConfig.timeout ?? defaultTimeout
|
|
298
|
+
|
|
299
|
+
return Promise.resolve(
|
|
300
|
+
new CloudflareIsolateContext(
|
|
301
|
+
workerUrl,
|
|
302
|
+
isolateConfig.bindings,
|
|
303
|
+
timeout,
|
|
304
|
+
maxToolRounds,
|
|
305
|
+
authorization,
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types between the Cloudflare Worker and the driver
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tool schema passed to the worker
|
|
7
|
+
*/
|
|
8
|
+
export interface ToolSchema {
|
|
9
|
+
name: string
|
|
10
|
+
description: string
|
|
11
|
+
inputSchema: Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request to execute code in the worker
|
|
16
|
+
*/
|
|
17
|
+
export interface ExecuteRequest {
|
|
18
|
+
/** The code to execute */
|
|
19
|
+
code: string
|
|
20
|
+
/** Tool schemas available for the code to call */
|
|
21
|
+
tools: Array<ToolSchema>
|
|
22
|
+
/** Results from previous tool calls (for continuation) */
|
|
23
|
+
toolResults?: Record<string, ToolResultPayload>
|
|
24
|
+
/** Execution timeout in ms */
|
|
25
|
+
timeout?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tool call requested by the worker
|
|
30
|
+
*/
|
|
31
|
+
export interface ToolCallRequest {
|
|
32
|
+
/** Unique ID for this tool call */
|
|
33
|
+
id: string
|
|
34
|
+
/** Name of the tool to call */
|
|
35
|
+
name: string
|
|
36
|
+
/** Arguments to pass to the tool */
|
|
37
|
+
args: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Result of a tool call
|
|
42
|
+
*/
|
|
43
|
+
export interface ToolResultPayload {
|
|
44
|
+
/** Whether the tool call succeeded */
|
|
45
|
+
success: boolean
|
|
46
|
+
/** The result value if successful */
|
|
47
|
+
value?: unknown
|
|
48
|
+
/** Error message if failed */
|
|
49
|
+
error?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Response from the worker - either done or needs tool calls
|
|
54
|
+
*/
|
|
55
|
+
export type ExecuteResponse =
|
|
56
|
+
| {
|
|
57
|
+
status: 'done'
|
|
58
|
+
success: boolean
|
|
59
|
+
value?: unknown
|
|
60
|
+
error?: {
|
|
61
|
+
name: string
|
|
62
|
+
message: string
|
|
63
|
+
stack?: string
|
|
64
|
+
}
|
|
65
|
+
logs: Array<string>
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
status: 'need_tools'
|
|
69
|
+
toolCalls: Array<ToolCallRequest>
|
|
70
|
+
logs: Array<string>
|
|
71
|
+
/** Continuation state to send back with tool results */
|
|
72
|
+
continuationId: string
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
status: 'error'
|
|
76
|
+
error: {
|
|
77
|
+
name: string
|
|
78
|
+
message: string
|
|
79
|
+
}
|
|
80
|
+
}
|