code-mode-core 2.0.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/dist/bridges.d.ts +28 -0
- package/dist/bridges.d.ts.map +1 -0
- package/dist/bridges.js +130 -0
- package/dist/bridges.js.map +1 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +43 -0
- package/dist/cache.js.map +1 -0
- package/dist/engine.d.ts +32 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +163 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox.d.ts +36 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +101 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schema-to-ts.d.ts +32 -0
- package/dist/schema-to-ts.d.ts.map +1 -0
- package/dist/schema-to-ts.js +223 -0
- package/dist/schema-to-ts.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/bridges.ts +160 -0
- package/src/cache.ts +48 -0
- package/src/engine.ts +211 -0
- package/src/index.ts +20 -0
- package/src/sandbox.ts +85 -0
- package/src/schema-to-ts.ts +262 -0
- package/src/types.ts +77 -0
package/src/engine.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @code-mode/core — CodeModeEngine
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic engine for executing TypeScript code with tool access
|
|
5
|
+
* in a secure isolated-vm sandbox.
|
|
6
|
+
*
|
|
7
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
8
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
9
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { UtcpClient } from '@utcp/sdk';
|
|
13
|
+
import type {
|
|
14
|
+
ExecutionOptions,
|
|
15
|
+
ExecutionResult,
|
|
16
|
+
ToolSourceConfig,
|
|
17
|
+
RegisterResult,
|
|
18
|
+
ToolCallRecord,
|
|
19
|
+
ExecutionStats,
|
|
20
|
+
} from './types';
|
|
21
|
+
import {
|
|
22
|
+
toolToTypeScriptInterface,
|
|
23
|
+
clearInterfaceCache,
|
|
24
|
+
} from './schema-to-ts';
|
|
25
|
+
import type { ToolLike } from './schema-to-ts';
|
|
26
|
+
import { executeInSandbox } from './sandbox';
|
|
27
|
+
import {
|
|
28
|
+
computeToolsHash,
|
|
29
|
+
getCachedSetup,
|
|
30
|
+
setCachedSetup,
|
|
31
|
+
clearSetupCache,
|
|
32
|
+
} from './cache';
|
|
33
|
+
import type { CachedSetup } from './cache';
|
|
34
|
+
|
|
35
|
+
const DEFAULT_OPTIONS: ExecutionOptions = {
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
memoryLimit: 128,
|
|
38
|
+
enableTrace: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class CodeModeEngine {
|
|
42
|
+
private client: UtcpClient;
|
|
43
|
+
private toolsHash: string = '';
|
|
44
|
+
|
|
45
|
+
private constructor(client: UtcpClient) {
|
|
46
|
+
this.client = client;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Create a new CodeModeEngine instance. */
|
|
50
|
+
static async create(): Promise<CodeModeEngine> {
|
|
51
|
+
const client = await UtcpClient.create();
|
|
52
|
+
return new CodeModeEngine(client);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Register a tool source (MCP server, HTTP API, etc.). */
|
|
56
|
+
async registerToolSource(config: ToolSourceConfig): Promise<RegisterResult> {
|
|
57
|
+
// Always invalidate cache — tool state may have changed regardless of outcome
|
|
58
|
+
this.toolsHash = '';
|
|
59
|
+
try {
|
|
60
|
+
const result = await this.client.registerManual(config as any);
|
|
61
|
+
if (result.success) {
|
|
62
|
+
const toolNames = (result.manual?.tools ?? []).map((t: any) => t.name ?? 'unknown');
|
|
63
|
+
return { success: true, toolNames };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
toolNames: [],
|
|
68
|
+
errors: result.errors ?? ['Registration failed'],
|
|
69
|
+
};
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
toolNames: [],
|
|
74
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Execute code in the sandbox with registered tools. */
|
|
80
|
+
async execute(
|
|
81
|
+
code: string,
|
|
82
|
+
options?: Partial<ExecutionOptions>,
|
|
83
|
+
): Promise<ExecutionResult> {
|
|
84
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
85
|
+
const tools = await this.getToolsAsToolLike();
|
|
86
|
+
|
|
87
|
+
// Get or compute cached setup
|
|
88
|
+
const setup = await this.getOrComputeSetup(tools);
|
|
89
|
+
|
|
90
|
+
// Build callToolFn — optionally with tracing
|
|
91
|
+
const trace: ToolCallRecord[] = [];
|
|
92
|
+
const callToolFn = opts.enableTrace
|
|
93
|
+
? this.createTracedCallTool(trace)
|
|
94
|
+
: this.createCallTool();
|
|
95
|
+
|
|
96
|
+
const sandboxResult = await executeInSandbox(
|
|
97
|
+
code,
|
|
98
|
+
tools,
|
|
99
|
+
callToolFn,
|
|
100
|
+
setup.interfacesString,
|
|
101
|
+
setup.interfaceMap,
|
|
102
|
+
{ timeout: opts.timeout, memoryLimit: opts.memoryLimit },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const executionResult: ExecutionResult = {
|
|
106
|
+
result: sandboxResult.result,
|
|
107
|
+
logs: sandboxResult.logs,
|
|
108
|
+
...(sandboxResult.error ? { error: sandboxResult.error } : {}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (opts.enableTrace) {
|
|
112
|
+
executionResult.trace = trace;
|
|
113
|
+
executionResult.stats = this.computeStats(trace);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return executionResult;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Get tool descriptions for LLM prompt generation. */
|
|
120
|
+
async getToolDescription(): Promise<string> {
|
|
121
|
+
const tools = await this.getToolsAsToolLike();
|
|
122
|
+
const setup = await this.getOrComputeSetup(tools);
|
|
123
|
+
return setup.toolDescription;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Clean up resources including the underlying UTCP client and MCP transports. */
|
|
127
|
+
async close(): Promise<void> {
|
|
128
|
+
this.toolsHash = '';
|
|
129
|
+
clearInterfaceCache();
|
|
130
|
+
clearSetupCache();
|
|
131
|
+
await this.client.close();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async getToolsAsToolLike(): Promise<ToolLike[]> {
|
|
135
|
+
const tools = await this.client.getTools();
|
|
136
|
+
return tools.map((t: any) => ({
|
|
137
|
+
name: t.name,
|
|
138
|
+
description: t.description,
|
|
139
|
+
inputs: t.inputs,
|
|
140
|
+
outputs: t.outputs,
|
|
141
|
+
tags: t.tags ?? [],
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async getOrComputeSetup(tools: ToolLike[]): Promise<CachedSetup> {
|
|
146
|
+
const hash = computeToolsHash(tools);
|
|
147
|
+
|
|
148
|
+
if (hash === this.toolsHash) {
|
|
149
|
+
const cached = getCachedSetup(hash);
|
|
150
|
+
if (cached) return cached;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Compute fresh — build interfaceMap in one pass, then join for interfacesString
|
|
154
|
+
clearInterfaceCache();
|
|
155
|
+
const interfaceMap: Record<string, string> = {};
|
|
156
|
+
for (const tool of tools) {
|
|
157
|
+
interfaceMap[tool.name] = toolToTypeScriptInterface(tool);
|
|
158
|
+
}
|
|
159
|
+
const interfacesString = `// Auto-generated TypeScript interfaces for UTCP tools\n${Object.values(interfaceMap).join('\n\n')}`;
|
|
160
|
+
|
|
161
|
+
const toolDescription = [
|
|
162
|
+
'Execute a TypeScript code block with access to registered UTCP tools.',
|
|
163
|
+
'The code runs in an isolated sandbox. Tools are called as synchronous functions: manual.tool({ param: value }).',
|
|
164
|
+
'Use `return` to return the final result.',
|
|
165
|
+
'',
|
|
166
|
+
'Available tool interfaces:',
|
|
167
|
+
interfacesString || '(no tools registered — configure tool sources on the node)',
|
|
168
|
+
].join('\n');
|
|
169
|
+
|
|
170
|
+
const setup: CachedSetup = { interfacesString, interfaceMap, toolDescription };
|
|
171
|
+
setCachedSetup(hash, setup);
|
|
172
|
+
this.toolsHash = hash;
|
|
173
|
+
|
|
174
|
+
return setup;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private createCallTool(): (toolName: string, args: Record<string, unknown>) => Promise<unknown> {
|
|
178
|
+
return async (toolName: string, args: Record<string, unknown>) => {
|
|
179
|
+
return this.client.callTool(toolName, args);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private createTracedCallTool(
|
|
184
|
+
trace: ToolCallRecord[],
|
|
185
|
+
): (toolName: string, args: Record<string, unknown>) => Promise<unknown> {
|
|
186
|
+
return async (toolName: string, args: Record<string, unknown>) => {
|
|
187
|
+
const start = performance.now();
|
|
188
|
+
try {
|
|
189
|
+
const result = await this.client.callTool(toolName, args);
|
|
190
|
+
trace.push({ toolName, args, result, durationMs: performance.now() - start });
|
|
191
|
+
return result;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
trace.push({
|
|
194
|
+
toolName,
|
|
195
|
+
args,
|
|
196
|
+
error: err instanceof Error ? err.message : String(err),
|
|
197
|
+
durationMs: performance.now() - start,
|
|
198
|
+
});
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private computeStats(trace: ToolCallRecord[]): ExecutionStats {
|
|
205
|
+
return {
|
|
206
|
+
totalCalls: trace.length,
|
|
207
|
+
totalDurationMs: trace.reduce((sum, r) => sum + r.durationMs, 0),
|
|
208
|
+
failures: trace.filter(r => r.error !== undefined).length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @code-mode/core — Public API
|
|
3
|
+
*
|
|
4
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { CodeModeEngine } from './engine';
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
ExecutionOptions,
|
|
13
|
+
ExecutionResult,
|
|
14
|
+
ToolDefinition,
|
|
15
|
+
ToolCallRecord,
|
|
16
|
+
ExecutionStats,
|
|
17
|
+
ToolSourceConfig,
|
|
18
|
+
RegisterResult,
|
|
19
|
+
JsonSchemaLike,
|
|
20
|
+
} from './types';
|
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @code-mode/core — SandboxManager
|
|
3
|
+
*
|
|
4
|
+
* Only file that imports isolated-vm. Contains isolate creation and
|
|
5
|
+
* code execution within the sandbox.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from @utcp/code-mode (CodeModeUtcpClient) by UTCP Contributors.
|
|
8
|
+
* Original: https://github.com/universal-tool-calling-protocol/code-mode
|
|
9
|
+
*
|
|
10
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
11
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
12
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ToolLike } from './schema-to-ts';
|
|
16
|
+
import { setupConsoleBridge, setupToolBridges, setupUtilities } from './bridges';
|
|
17
|
+
import type { CallToolFn } from './bridges';
|
|
18
|
+
|
|
19
|
+
/** Create an isolated-vm isolate with the given memory limit. */
|
|
20
|
+
export async function createIsolate(memoryLimit: number) {
|
|
21
|
+
const ivm = await import('isolated-vm');
|
|
22
|
+
const isolate = new ivm.default.Isolate({ memoryLimit });
|
|
23
|
+
return { isolate, ivm: ivm.default };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SandboxExecuteOptions {
|
|
27
|
+
timeout: number;
|
|
28
|
+
memoryLimit: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute code in an isolated sandbox with tool access.
|
|
33
|
+
*
|
|
34
|
+
* Sets up console bridges, tool bridges, and utilities, then runs the
|
|
35
|
+
* user code. Returns the result and captured logs.
|
|
36
|
+
*/
|
|
37
|
+
export async function executeInSandbox(
|
|
38
|
+
code: string,
|
|
39
|
+
tools: ToolLike[],
|
|
40
|
+
callToolFn: CallToolFn,
|
|
41
|
+
interfacesString: string,
|
|
42
|
+
interfaceMap: Record<string, string>,
|
|
43
|
+
options: SandboxExecuteOptions,
|
|
44
|
+
): Promise<{ result: unknown; logs: string[]; error?: string }> {
|
|
45
|
+
const { isolate, ivm: ivm_module } = await createIsolate(options.memoryLimit);
|
|
46
|
+
const logs: string[] = [];
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const context = await isolate.createContext();
|
|
50
|
+
const jail = context.global;
|
|
51
|
+
|
|
52
|
+
await jail.set('global', jail.derefInto());
|
|
53
|
+
|
|
54
|
+
await setupConsoleBridge(isolate, context, jail, logs, ivm_module);
|
|
55
|
+
await setupToolBridges(isolate, context, jail, tools, callToolFn, ivm_module);
|
|
56
|
+
await setupUtilities(isolate, context, jail, tools, interfacesString, interfaceMap);
|
|
57
|
+
|
|
58
|
+
const wrappedCode = `
|
|
59
|
+
(function() {
|
|
60
|
+
var __result = (function() {
|
|
61
|
+
${code}
|
|
62
|
+
})();
|
|
63
|
+
return JSON.stringify({ __result: __result });
|
|
64
|
+
})()
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const script = await isolate.compileScript(wrappedCode);
|
|
68
|
+
const resultJson = await script.run(context, { timeout: options.timeout });
|
|
69
|
+
|
|
70
|
+
const result = typeof resultJson === 'string'
|
|
71
|
+
? JSON.parse(resultJson).__result
|
|
72
|
+
: resultJson;
|
|
73
|
+
|
|
74
|
+
return { result, logs };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
77
|
+
return {
|
|
78
|
+
result: null,
|
|
79
|
+
logs: [...logs, `[ERROR] Code execution failed: ${errorMessage}`],
|
|
80
|
+
error: errorMessage,
|
|
81
|
+
};
|
|
82
|
+
} finally {
|
|
83
|
+
isolate.dispose();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @code-mode/core — JSON Schema to TypeScript converter
|
|
3
|
+
*
|
|
4
|
+
* Adapted from @utcp/code-mode (CodeModeUtcpClient) by UTCP Contributors.
|
|
5
|
+
* Original: https://github.com/universal-tool-calling-protocol/code-mode
|
|
6
|
+
*
|
|
7
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
8
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
9
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { JsonSchemaLike } from './types';
|
|
13
|
+
|
|
14
|
+
/** Tool-like shape expected by the converter. */
|
|
15
|
+
export interface ToolLike {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputs: JsonSchemaLike;
|
|
19
|
+
outputs: JsonSchemaLike;
|
|
20
|
+
tags: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MAX_CACHE_SIZE = 16;
|
|
24
|
+
const interfaceCache = new Map<string, string>();
|
|
25
|
+
|
|
26
|
+
// Names that would pollute the global/prototype chain if set on `global`
|
|
27
|
+
const RESERVED_NAMES = new Set([
|
|
28
|
+
'__proto__', 'constructor', 'prototype', 'toString', 'valueOf',
|
|
29
|
+
'hasOwnProperty', '__defineGetter__', '__defineSetter__',
|
|
30
|
+
'__lookupGetter__', '__lookupSetter__', 'global',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** Sanitize a name to be a valid, safe TypeScript identifier. */
|
|
34
|
+
export function sanitizeIdentifier(name: string): string {
|
|
35
|
+
const id = name
|
|
36
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
37
|
+
.replace(/^[0-9]/, '_$&');
|
|
38
|
+
return RESERVED_NAMES.has(id) ? `_${id}` : id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Escape text for safe use in JSDoc comments. */
|
|
42
|
+
export function escapeComment(text: string): string {
|
|
43
|
+
return text.replace(/\*\//g, '*\\/').replace(/\n/g, ' ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Map a JSON Schema type string to a TypeScript type. */
|
|
47
|
+
function mapJsonTypeToTS(type: string): string {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'string': return 'string';
|
|
50
|
+
case 'number':
|
|
51
|
+
case 'integer': return 'number';
|
|
52
|
+
case 'boolean': return 'boolean';
|
|
53
|
+
case 'null': return 'null';
|
|
54
|
+
case 'object': return 'object';
|
|
55
|
+
case 'array': return 'any[]';
|
|
56
|
+
default: return 'any';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Convert a JSON Schema to an inline TypeScript type string. */
|
|
61
|
+
export function jsonSchemaToTypeScriptType(schema: JsonSchemaLike): string {
|
|
62
|
+
if (!schema || typeof schema !== 'object') {
|
|
63
|
+
return 'any';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (schema.enum) {
|
|
67
|
+
return schema.enum.map(val =>
|
|
68
|
+
typeof val === 'string' ? JSON.stringify(val) : String(val)
|
|
69
|
+
).join(' | ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (schema.type) {
|
|
73
|
+
case 'object':
|
|
74
|
+
if (!schema.properties) return '{ [key: string]: any }';
|
|
75
|
+
const props = Object.entries(schema.properties).map(([key, propSchema]) => {
|
|
76
|
+
const isRequired = schema.required?.includes(key) ?? false;
|
|
77
|
+
const optional = isRequired ? '' : '?';
|
|
78
|
+
const propType = jsonSchemaToTypeScriptType(propSchema);
|
|
79
|
+
return `${key}${optional}: ${propType}`;
|
|
80
|
+
}).join('; ');
|
|
81
|
+
return `{ ${props} }`;
|
|
82
|
+
|
|
83
|
+
case 'array':
|
|
84
|
+
if (!schema.items) return 'any[]';
|
|
85
|
+
const itemType = Array.isArray(schema.items)
|
|
86
|
+
? schema.items.map(item => jsonSchemaToTypeScriptType(item)).join(' | ')
|
|
87
|
+
: jsonSchemaToTypeScriptType(schema.items);
|
|
88
|
+
return `(${itemType})[]`;
|
|
89
|
+
|
|
90
|
+
case 'string': return 'string';
|
|
91
|
+
case 'number':
|
|
92
|
+
case 'integer': return 'number';
|
|
93
|
+
case 'boolean': return 'boolean';
|
|
94
|
+
case 'null': return 'null';
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
if (Array.isArray(schema.type)) {
|
|
98
|
+
return schema.type.map(t => mapJsonTypeToTS(t)).join(' | ');
|
|
99
|
+
}
|
|
100
|
+
return 'any';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Convert a JSON Schema to TypeScript object content (properties only). */
|
|
105
|
+
function jsonSchemaToObjectContent(schema: JsonSchemaLike): string {
|
|
106
|
+
if (!schema || typeof schema !== 'object' || schema.type !== 'object') {
|
|
107
|
+
return ' [key: string]: any;';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const properties = schema.properties || {};
|
|
111
|
+
const required = schema.required || [];
|
|
112
|
+
const lines: string[] = [];
|
|
113
|
+
|
|
114
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
115
|
+
const isRequired = required.includes(propName);
|
|
116
|
+
const optionalMarker = isRequired ? '' : '?';
|
|
117
|
+
const description = propSchema.description || '';
|
|
118
|
+
const tsType = jsonSchemaToTypeScriptType(propSchema);
|
|
119
|
+
|
|
120
|
+
if (description) {
|
|
121
|
+
lines.push(` /** ${escapeComment(description)} */`);
|
|
122
|
+
}
|
|
123
|
+
lines.push(` ${propName}${optionalMarker}: ${tsType};`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines.length > 0 ? lines.join('\n') : ' [key: string]: any;';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Convert a JSON Schema to a full TypeScript type definition. */
|
|
130
|
+
function jsonSchemaToTypeScript(schema: JsonSchemaLike, typeName: string): string {
|
|
131
|
+
if (!schema || typeof schema !== 'object') {
|
|
132
|
+
return `type ${typeName} = any;`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
switch (schema.type) {
|
|
136
|
+
case 'object':
|
|
137
|
+
return objectSchemaToTypeScript(schema, typeName);
|
|
138
|
+
case 'array':
|
|
139
|
+
return arraySchemaToTypeScript(schema, typeName);
|
|
140
|
+
case 'string':
|
|
141
|
+
return primitiveSchemaToTypeScript(schema, typeName, 'string');
|
|
142
|
+
case 'number':
|
|
143
|
+
case 'integer':
|
|
144
|
+
return primitiveSchemaToTypeScript(schema, typeName, 'number');
|
|
145
|
+
case 'boolean':
|
|
146
|
+
return primitiveSchemaToTypeScript(schema, typeName, 'boolean');
|
|
147
|
+
case 'null':
|
|
148
|
+
return `type ${typeName} = null;`;
|
|
149
|
+
default:
|
|
150
|
+
if (Array.isArray(schema.type)) {
|
|
151
|
+
const types = schema.type.map(t => mapJsonTypeToTS(t)).join(' | ');
|
|
152
|
+
return `type ${typeName} = ${types};`;
|
|
153
|
+
}
|
|
154
|
+
return `type ${typeName} = any;`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function objectSchemaToTypeScript(schema: JsonSchemaLike, typeName: string): string {
|
|
159
|
+
if (!schema.properties) {
|
|
160
|
+
return `interface ${typeName} {\n [key: string]: any;\n}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const properties = Object.entries(schema.properties).map(([key, propSchema]) => {
|
|
164
|
+
const isRequired = schema.required?.includes(key) ?? false;
|
|
165
|
+
const optional = isRequired ? '' : '?';
|
|
166
|
+
const propType = jsonSchemaToTypeScriptType(propSchema);
|
|
167
|
+
const description = propSchema.description ? ` /** ${escapeComment(propSchema.description)} */\n` : '';
|
|
168
|
+
|
|
169
|
+
return `${description} ${key}${optional}: ${propType};`;
|
|
170
|
+
}).join('\n');
|
|
171
|
+
|
|
172
|
+
return `interface ${typeName} {\n${properties}\n}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function arraySchemaToTypeScript(schema: JsonSchemaLike, typeName: string): string {
|
|
176
|
+
if (!schema.items) {
|
|
177
|
+
return `type ${typeName} = any[];`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const itemType = Array.isArray(schema.items)
|
|
181
|
+
? schema.items.map(item => jsonSchemaToTypeScriptType(item)).join(' | ')
|
|
182
|
+
: jsonSchemaToTypeScriptType(schema.items);
|
|
183
|
+
|
|
184
|
+
return `type ${typeName} = (${itemType})[];`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function primitiveSchemaToTypeScript(schema: JsonSchemaLike, typeName: string, baseType: string): string {
|
|
188
|
+
if (schema.enum) {
|
|
189
|
+
const enumValues = schema.enum.map(val =>
|
|
190
|
+
typeof val === 'string' ? JSON.stringify(val) : String(val)
|
|
191
|
+
).join(' | ');
|
|
192
|
+
return `type ${typeName} = ${enumValues};`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return `type ${typeName} = ${baseType};`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Convert a Tool to its TypeScript interface string (cached). */
|
|
199
|
+
export function toolToTypeScriptInterface(tool: ToolLike): string {
|
|
200
|
+
if (interfaceCache.has(tool.name)) {
|
|
201
|
+
return interfaceCache.get(tool.name)!;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let interfaceContent: string;
|
|
205
|
+
let accessPattern: string;
|
|
206
|
+
|
|
207
|
+
if (tool.name.includes('.')) {
|
|
208
|
+
const [manualName, ...toolParts] = tool.name.split('.');
|
|
209
|
+
const sanitizedManualName = sanitizeIdentifier(manualName);
|
|
210
|
+
const toolName = toolParts.map(part => sanitizeIdentifier(part)).join('_');
|
|
211
|
+
accessPattern = `${sanitizedManualName}.${toolName}`;
|
|
212
|
+
|
|
213
|
+
const inputInterfaceContent = jsonSchemaToObjectContent(tool.inputs);
|
|
214
|
+
const outputInterfaceContent = jsonSchemaToObjectContent(tool.outputs);
|
|
215
|
+
|
|
216
|
+
interfaceContent = `
|
|
217
|
+
namespace ${sanitizedManualName} {
|
|
218
|
+
interface ${toolName}Input {
|
|
219
|
+
${inputInterfaceContent}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface ${toolName}Output {
|
|
223
|
+
${outputInterfaceContent}
|
|
224
|
+
}
|
|
225
|
+
}`;
|
|
226
|
+
} else {
|
|
227
|
+
const sanitizedToolName = sanitizeIdentifier(tool.name);
|
|
228
|
+
accessPattern = sanitizedToolName;
|
|
229
|
+
const inputType = jsonSchemaToTypeScript(tool.inputs, `${sanitizedToolName}Input`);
|
|
230
|
+
const outputType = jsonSchemaToTypeScript(tool.outputs, `${sanitizedToolName}Output`);
|
|
231
|
+
interfaceContent = `${inputType}\n\n${outputType}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const interfaceString = `
|
|
235
|
+
${interfaceContent}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* ${escapeComment(tool.description)}
|
|
239
|
+
* Tags: ${escapeComment(tool.tags.join(', '))}
|
|
240
|
+
* Access as: ${accessPattern}(args)
|
|
241
|
+
*/`;
|
|
242
|
+
|
|
243
|
+
if (!interfaceCache.has(tool.name) && interfaceCache.size >= MAX_CACHE_SIZE) {
|
|
244
|
+
const oldestKey = interfaceCache.keys().next().value;
|
|
245
|
+
if (oldestKey !== undefined) interfaceCache.delete(oldestKey);
|
|
246
|
+
}
|
|
247
|
+
interfaceCache.set(tool.name, interfaceString);
|
|
248
|
+
return interfaceString;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Generate TypeScript interfaces for all tools. */
|
|
252
|
+
export function getAllToolsTypeScriptInterfaces(tools: ToolLike[]): string {
|
|
253
|
+
const interfaces = tools.map(tool => toolToTypeScriptInterface(tool));
|
|
254
|
+
|
|
255
|
+
return `// Auto-generated TypeScript interfaces for UTCP tools
|
|
256
|
+
${interfaces.join('\n\n')}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Clear the interface cache (e.g., when tools change). */
|
|
260
|
+
export function clearInterfaceCache(): void {
|
|
261
|
+
interfaceCache.clear();
|
|
262
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @code-mode/core — Type definitions
|
|
3
|
+
*
|
|
4
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Minimal JSON Schema subset used for tool input/output definitions. */
|
|
10
|
+
export interface JsonSchemaLike {
|
|
11
|
+
type?: string | string[];
|
|
12
|
+
properties?: Record<string, JsonSchemaLike>;
|
|
13
|
+
required?: string[];
|
|
14
|
+
items?: JsonSchemaLike | JsonSchemaLike[];
|
|
15
|
+
enum?: unknown[];
|
|
16
|
+
description?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A tool registered with the engine. */
|
|
20
|
+
export interface ToolDefinition {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
inputSchema: JsonSchemaLike;
|
|
24
|
+
outputSchema: JsonSchemaLike;
|
|
25
|
+
tags: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Options for code execution. */
|
|
29
|
+
export interface ExecutionOptions {
|
|
30
|
+
/** Timeout in milliseconds. Default: 30000 */
|
|
31
|
+
timeout: number;
|
|
32
|
+
/** Memory limit in MB for the isolate. Default: 128 */
|
|
33
|
+
memoryLimit: number;
|
|
34
|
+
/** Enable execution tracing (tool call timing). Default: false */
|
|
35
|
+
enableTrace: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A single tool call recorded during traced execution. */
|
|
39
|
+
export interface ToolCallRecord {
|
|
40
|
+
toolName: string;
|
|
41
|
+
args: Record<string, unknown>;
|
|
42
|
+
result?: unknown;
|
|
43
|
+
error?: string;
|
|
44
|
+
durationMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Aggregate execution statistics. */
|
|
48
|
+
export interface ExecutionStats {
|
|
49
|
+
totalCalls: number;
|
|
50
|
+
totalDurationMs: number;
|
|
51
|
+
failures: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Result of executing code in the sandbox. */
|
|
55
|
+
export interface ExecutionResult {
|
|
56
|
+
result: unknown;
|
|
57
|
+
logs: string[];
|
|
58
|
+
/** Set when sandbox execution fails (timeout, syntax error, runtime error). */
|
|
59
|
+
error?: string;
|
|
60
|
+
trace?: ToolCallRecord[];
|
|
61
|
+
stats?: ExecutionStats;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Configuration for a tool source to register with the engine. */
|
|
65
|
+
export interface ToolSourceConfig {
|
|
66
|
+
name: string;
|
|
67
|
+
call_template_type: string;
|
|
68
|
+
callable_name?: string;
|
|
69
|
+
config?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Result of registering a tool source. */
|
|
73
|
+
export interface RegisterResult {
|
|
74
|
+
success: boolean;
|
|
75
|
+
toolNames: string[];
|
|
76
|
+
errors?: string[];
|
|
77
|
+
}
|