@liquidmetal-ai/precip 1.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/.prettierrc +9 -0
- package/CHANGELOG.md +8 -0
- package/eslint.config.mjs +28 -0
- package/package.json +53 -0
- package/src/engine/agent.ts +478 -0
- package/src/engine/llm-provider.test.ts +275 -0
- package/src/engine/llm-provider.ts +330 -0
- package/src/engine/stream-parser.ts +170 -0
- package/src/index.ts +142 -0
- package/src/mounts/mount-manager.test.ts +516 -0
- package/src/mounts/mount-manager.ts +327 -0
- package/src/mounts/mount-registry.ts +196 -0
- package/src/mounts/zod-to-string.test.ts +154 -0
- package/src/mounts/zod-to-string.ts +213 -0
- package/src/presets/agent-tools.ts +57 -0
- package/src/presets/index.ts +5 -0
- package/src/sandbox/README.md +1321 -0
- package/src/sandbox/bridges/README.md +571 -0
- package/src/sandbox/bridges/actor.test.ts +229 -0
- package/src/sandbox/bridges/actor.ts +195 -0
- package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
- package/src/sandbox/bridges/bucket.test.ts +300 -0
- package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
- package/src/sandbox/bridges/console-multiple.test.ts +187 -0
- package/src/sandbox/bridges/console.test.ts +157 -0
- package/src/sandbox/bridges/console.ts +122 -0
- package/src/sandbox/bridges/fetch.ts +93 -0
- package/src/sandbox/bridges/index.ts +78 -0
- package/src/sandbox/bridges/readable-stream.ts +323 -0
- package/src/sandbox/bridges/response.test.ts +154 -0
- package/src/sandbox/bridges/response.ts +123 -0
- package/src/sandbox/bridges/review-fixes.test.ts +331 -0
- package/src/sandbox/bridges/search.test.ts +475 -0
- package/src/sandbox/bridges/search.ts +264 -0
- package/src/sandbox/bridges/shared/body-methods.ts +93 -0
- package/src/sandbox/bridges/shared/cleanup.ts +112 -0
- package/src/sandbox/bridges/shared/convert.ts +76 -0
- package/src/sandbox/bridges/shared/headers.ts +181 -0
- package/src/sandbox/bridges/shared/index.ts +36 -0
- package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
- package/src/sandbox/bridges/shared/path-parser.ts +109 -0
- package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
- package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
- package/src/sandbox/bridges/shared/response-object.ts +280 -0
- package/src/sandbox/bridges/shared/result-builder.ts +130 -0
- package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
- package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
- package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
- package/src/sandbox/bridges/storage.ts +421 -0
- package/src/sandbox/bridges/text-decoder.ts +190 -0
- package/src/sandbox/bridges/text-encoder.ts +102 -0
- package/src/sandbox/bridges/types.ts +39 -0
- package/src/sandbox/bridges/utils.ts +123 -0
- package/src/sandbox/index.ts +6 -0
- package/src/sandbox/quickjs-wasm.d.ts +9 -0
- package/src/sandbox/sandbox.test.ts +191 -0
- package/src/sandbox/sandbox.ts +831 -0
- package/src/sandbox/test-helper.ts +43 -0
- package/src/sandbox/test-mocks.ts +154 -0
- package/src/sandbox/user-stream.test.ts +77 -0
- package/src/skills/frontmatter.test.ts +305 -0
- package/src/skills/frontmatter.ts +200 -0
- package/src/skills/index.ts +9 -0
- package/src/skills/skills-loader.test.ts +237 -0
- package/src/skills/skills-loader.ts +200 -0
- package/src/tools/actor-storage-tools.ts +250 -0
- package/src/tools/code-tools.test.ts +199 -0
- package/src/tools/code-tools.ts +444 -0
- package/src/tools/file-tools.ts +206 -0
- package/src/tools/registry.ts +125 -0
- package/src/tools/script-tools.ts +145 -0
- package/src/tools/smartbucket-tools.ts +203 -0
- package/src/tools/sql-tools.ts +213 -0
- package/src/tools/tool-factory.ts +119 -0
- package/src/types.ts +512 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +33 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Execution Tool - Run JavaScript in a sandboxed environment
|
|
3
|
+
* All mounts are automatically available via path-based API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool, ToolContext, MountInfo, MountProvider, SandboxGlobals } from '../types.js';
|
|
7
|
+
import { sandboxAsync } from '../types.js';
|
|
8
|
+
import type {
|
|
9
|
+
Bucket,
|
|
10
|
+
SmartBucket,
|
|
11
|
+
KvCache,
|
|
12
|
+
SqlDatabase,
|
|
13
|
+
ActorStorage
|
|
14
|
+
} from '@liquidmetal-ai/raindrop-framework';
|
|
15
|
+
import { Sandbox } from '../sandbox/index.js';
|
|
16
|
+
import { installStorage, type StorageMountInfo } from '../sandbox/bridges/storage.js';
|
|
17
|
+
import { installSearch, type SearchMountInfo } from '../sandbox/bridges/search.js';
|
|
18
|
+
import { installActor } from '../sandbox/bridges/actor.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create bridge installers for all mounted resources.
|
|
22
|
+
* Shared by code-tools and script-tools to avoid duplication.
|
|
23
|
+
*/
|
|
24
|
+
export function createBridgeInstallers(
|
|
25
|
+
mountManager: MountProvider
|
|
26
|
+
): Array<(ctx: any) => void> {
|
|
27
|
+
const bridgeInstallers: Array<(ctx: any) => void> = [];
|
|
28
|
+
|
|
29
|
+
const bucketMounts = mountManager.getMountsByType('bucket');
|
|
30
|
+
const smartbucketMounts = mountManager.getMountsByType('smartbucket');
|
|
31
|
+
const kvMounts = mountManager.getMountsByType('kv');
|
|
32
|
+
const actorStorageMounts = mountManager.getMountsByType('actor-storage');
|
|
33
|
+
const allStorageMounts = [
|
|
34
|
+
...bucketMounts,
|
|
35
|
+
...smartbucketMounts,
|
|
36
|
+
...kvMounts,
|
|
37
|
+
...actorStorageMounts
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (allStorageMounts.length > 0) {
|
|
41
|
+
const storageMountMap = new Map<string, StorageMountInfo>(
|
|
42
|
+
allStorageMounts.map(m => [
|
|
43
|
+
m.name,
|
|
44
|
+
{
|
|
45
|
+
name: m.name,
|
|
46
|
+
type: m.type as 'bucket' | 'smartbucket' | 'kv' | 'actor-storage',
|
|
47
|
+
resource: m.resource as Bucket | KvCache | ActorStorage,
|
|
48
|
+
mode: m.mode
|
|
49
|
+
}
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
bridgeInstallers.push(ctx => installStorage(ctx, storageMountMap));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (smartbucketMounts.length > 0) {
|
|
56
|
+
const searchMountMap = new Map<string, SearchMountInfo>(
|
|
57
|
+
smartbucketMounts.map(m => [
|
|
58
|
+
m.name,
|
|
59
|
+
{
|
|
60
|
+
name: m.name,
|
|
61
|
+
smartbucket: m.resource as SmartBucket
|
|
62
|
+
}
|
|
63
|
+
])
|
|
64
|
+
);
|
|
65
|
+
bridgeInstallers.push(ctx => installSearch(ctx, searchMountMap));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const actorMounts = mountManager.getActorMounts();
|
|
69
|
+
if (actorMounts.size > 0) {
|
|
70
|
+
bridgeInstallers.push(ctx => installActor(ctx, actorMounts));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return bridgeInstallers;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Options for creating a customized code execution tool
|
|
77
|
+
*/
|
|
78
|
+
export interface CodeToolOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Custom functions/objects to expose in the sandbox.
|
|
81
|
+
* Can be a static object or a factory function that receives ToolContext.
|
|
82
|
+
* Values must be typed sandbox globals (use sandboxAsync, sandboxSync, sandboxObject helpers).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Static bridge
|
|
86
|
+
* bridge: {
|
|
87
|
+
* myApi: sandboxObject({
|
|
88
|
+
* getData: sandboxAsync(async (key) => storage.get(key)),
|
|
89
|
+
* setData: sandboxAsync(async (key, val) => storage.set(key, val))
|
|
90
|
+
* })
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Dynamic bridge with access to context
|
|
95
|
+
* bridge: (ctx) => ({
|
|
96
|
+
* rpc: sandboxObject({
|
|
97
|
+
* call: sandboxAsync(async (method, params) => client.call(method, params))
|
|
98
|
+
* })
|
|
99
|
+
* })
|
|
100
|
+
*/
|
|
101
|
+
bridge?: SandboxGlobals | ((ctx: ToolContext) => SandboxGlobals);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Custom tool name (default: 'run_code')
|
|
105
|
+
*/
|
|
106
|
+
name?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Additional description to append to the base description.
|
|
110
|
+
* Use this to document your custom bridge functions.
|
|
111
|
+
*/
|
|
112
|
+
descriptionSuffix?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Base description for the code execution tool
|
|
117
|
+
*/
|
|
118
|
+
const BASE_DESCRIPTION = `Execute JavaScript code in a secure sandbox with async/await support.
|
|
119
|
+
|
|
120
|
+
IMPORTANT RULES:
|
|
121
|
+
- Write plain top-level code. Do NOT wrap in an IIFE or async function wrapper.
|
|
122
|
+
- Do NOT use require() or import statements. All APIs below are pre-installed globals.
|
|
123
|
+
- fetch, console, TextEncoder, TextDecoder, and Response are all available as globals.
|
|
124
|
+
- Use \`return\` to produce a result value. Use console.log() for intermediate output.
|
|
125
|
+
- await is supported at the top level — just use it directly.
|
|
126
|
+
|
|
127
|
+
All storage is accessed via paths: /<mount-name>/path/to/resource
|
|
128
|
+
|
|
129
|
+
STORAGE API:
|
|
130
|
+
• read(path) - Read content from any mount
|
|
131
|
+
Returns: Response-like object with .text(), .json(), .arrayBuffer() methods
|
|
132
|
+
For files: also has .size, .uploaded, .body (ReadableStream)
|
|
133
|
+
For KV: also has .metadata (if set)
|
|
134
|
+
Example: const data = await (await read("/uploads/config.json")).json();
|
|
135
|
+
const text = await (await read("/cache/user:123")).text();
|
|
136
|
+
const state = await (await read("/state/counter")).json();
|
|
137
|
+
|
|
138
|
+
• write(path, content, options?) - Write content to any mount
|
|
139
|
+
Returns: { success: boolean }
|
|
140
|
+
Options vary by mount type:
|
|
141
|
+
KV cache: { ttl: 300 } or { expiration: 1720000000 } or { metadata: {...} }
|
|
142
|
+
File storage: { contentType: "application/json" } or { customMetadata: { author: "alice" } }
|
|
143
|
+
Actor state: (no options)
|
|
144
|
+
Example: await write("/uploads/output.txt", "Hello world");
|
|
145
|
+
await write("/cache/session:abc", JSON.stringify(data), { ttl: 300 });
|
|
146
|
+
await write("/state/prefs", JSON.stringify({ theme: "dark", lang: "en" }));
|
|
147
|
+
|
|
148
|
+
• list(path, options?) - List files or keys
|
|
149
|
+
Returns: { files: Array<{key, size?, uploaded?, expiration?}>, count: number }
|
|
150
|
+
Options vary by mount type:
|
|
151
|
+
Common: { limit: 100 }
|
|
152
|
+
File storage: { delimiter: "/", startAfter: "key" }
|
|
153
|
+
Actor state: { start: "a", end: "z", reverse: true, limit: 50 }
|
|
154
|
+
Example: const result = await list("/uploads/images/");
|
|
155
|
+
const entries = await list("/state/", { prefix: "user:", reverse: true });
|
|
156
|
+
|
|
157
|
+
• remove(path) - Delete a file or key
|
|
158
|
+
Returns: { success: boolean }
|
|
159
|
+
Example: await remove("/uploads/old-file.txt");
|
|
160
|
+
await remove("/cache/expired-key");
|
|
161
|
+
|
|
162
|
+
DATABASE API:
|
|
163
|
+
• query(path, sql, params?) - Execute SELECT query
|
|
164
|
+
Returns: { results: Array<object>, rowCount: number }
|
|
165
|
+
Example: const data = await query("/analytics", "SELECT * FROM users LIMIT 10");
|
|
166
|
+
|
|
167
|
+
• execute(path, sql, params?) - Execute INSERT/UPDATE/DELETE
|
|
168
|
+
Returns: { success: boolean, changes: number }
|
|
169
|
+
Example: await execute("/analytics", "INSERT INTO logs (msg) VALUES (?)", ["hello"]);
|
|
170
|
+
|
|
171
|
+
SEARCH API (for searchable storage):
|
|
172
|
+
• search(path, query) - Semantic search with pagination
|
|
173
|
+
Returns: { results, hasMore, nextPage(), total, page, pageSize }
|
|
174
|
+
|
|
175
|
+
Simple usage:
|
|
176
|
+
const results = await search("/knowledge", "machine learning");
|
|
177
|
+
for (const r of results.results) {
|
|
178
|
+
console.log(r.text, r.score);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
With pagination:
|
|
182
|
+
let results = await search("/knowledge", "query");
|
|
183
|
+
while (results.hasMore) {
|
|
184
|
+
results = await results.nextPage();
|
|
185
|
+
// process results.results
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Auto-pagination with for-await:
|
|
189
|
+
const results = await search("/knowledge", "query");
|
|
190
|
+
for await (const r of results) {
|
|
191
|
+
console.log(r.text); // automatically fetches next pages
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
• chunkSearch(path, query) - RAG chunk search for text passages
|
|
195
|
+
Returns: { results: Array<{text, source, score, chunkSignature}> }
|
|
196
|
+
Example: const results = await chunkSearch("/knowledge", "how to configure");
|
|
197
|
+
|
|
198
|
+
ACTOR API:
|
|
199
|
+
• actor(path, instanceId) - Get an actor stub for RPC calls
|
|
200
|
+
Returns: Actor stub with callable methods
|
|
201
|
+
Example: const session = await actor("/sessions", "user-123");
|
|
202
|
+
const state = await session.getState();
|
|
203
|
+
await session.updatePrefs({ theme: "dark" });
|
|
204
|
+
|
|
205
|
+
FETCH API:
|
|
206
|
+
• fetch(url, options?) - Make HTTP requests
|
|
207
|
+
Returns: Response object with json(), text(), arrayBuffer() methods
|
|
208
|
+
Example: const res = await fetch("https://api.example.com/data");
|
|
209
|
+
const data = await res.json();
|
|
210
|
+
|
|
211
|
+
CONSOLE:
|
|
212
|
+
• console.log(...) - Log output (shown in [stdout] section)
|
|
213
|
+
• console.warn(...), console.error(...) - Log with level prefix
|
|
214
|
+
|
|
215
|
+
UTILITIES:
|
|
216
|
+
• new TextEncoder() / new TextDecoder() - Encode/decode text
|
|
217
|
+
• new Response(body, init?) - Create Response objects
|
|
218
|
+
|
|
219
|
+
TIPS:
|
|
220
|
+
- All paths start with / followed by the mount name
|
|
221
|
+
- Always use await with async operations
|
|
222
|
+
- Always return a value so results are visible
|
|
223
|
+
- Use console.log() for debugging
|
|
224
|
+
- Do NOT wrap code in (async () => { })() — await works at the top level
|
|
225
|
+
- Do NOT use require() or import — all APIs are globals`;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Create a code execution tool with optional custom bridges
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* // Basic usage - same as CodeExecutionTool
|
|
232
|
+
* const tool = createCodeExecutionTool();
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* // With static bridge
|
|
236
|
+
* const tool = createCodeExecutionTool({
|
|
237
|
+
* bridge: {
|
|
238
|
+
* myApi: {
|
|
239
|
+
* getData: async (key) => storage.get(key)
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* });
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* // With dynamic bridge (has access to ToolContext)
|
|
246
|
+
* const tool = createCodeExecutionTool({
|
|
247
|
+
* bridge: (ctx) => ({
|
|
248
|
+
* rpc: createConnectClient(ctx.env.RPC_BINDING)
|
|
249
|
+
* }),
|
|
250
|
+
* descriptionSuffix: `
|
|
251
|
+
*
|
|
252
|
+
* RPC API:
|
|
253
|
+
* • rpc.users.getUser({ id }) - Get user by ID
|
|
254
|
+
* • rpc.users.createUser({ name, email }) - Create new user`
|
|
255
|
+
* });
|
|
256
|
+
*/
|
|
257
|
+
export function createCodeExecutionTool(options: CodeToolOptions = {}): Tool {
|
|
258
|
+
const description = options.descriptionSuffix
|
|
259
|
+
? BASE_DESCRIPTION + options.descriptionSuffix
|
|
260
|
+
: BASE_DESCRIPTION;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
definition: {
|
|
264
|
+
name: options.name || 'run_code',
|
|
265
|
+
description,
|
|
266
|
+
parameters: {
|
|
267
|
+
code: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'JavaScript code to execute. Must use async/await for I/O operations.',
|
|
270
|
+
required: true
|
|
271
|
+
},
|
|
272
|
+
timeout: {
|
|
273
|
+
type: 'number',
|
|
274
|
+
description: 'Execution timeout in milliseconds (default: 30000)',
|
|
275
|
+
required: false
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
async execute(params: { code: string; timeout?: number }, context: ToolContext) {
|
|
281
|
+
const { mounts, logger } = context;
|
|
282
|
+
const mountManager = mounts;
|
|
283
|
+
|
|
284
|
+
// Resolve custom bridge - static object or dynamic factory
|
|
285
|
+
const customBridge =
|
|
286
|
+
typeof options.bridge === 'function' ? options.bridge(context) : options.bridge || {};
|
|
287
|
+
|
|
288
|
+
// Create mount globals (database functions only - storage and search handled by bridges)
|
|
289
|
+
const mountGlobals = createMountGlobals(mountManager);
|
|
290
|
+
|
|
291
|
+
// Merge mount globals with custom bridge (custom bridge takes precedence)
|
|
292
|
+
const asyncGlobals = { ...mountGlobals, ...customBridge };
|
|
293
|
+
|
|
294
|
+
// Create bridge installers for all mounted resources
|
|
295
|
+
const bridgeInstallers = createBridgeInstallers(mountManager);
|
|
296
|
+
|
|
297
|
+
// Execute in sandbox
|
|
298
|
+
const sandbox = await Sandbox.getInstance();
|
|
299
|
+
const result = await sandbox.execute(params.code, asyncGlobals, {
|
|
300
|
+
timeoutMs: params.timeout || 30000,
|
|
301
|
+
memoryLimitBytes: 64 * 1024 * 1024,
|
|
302
|
+
logger,
|
|
303
|
+
bridgeInstallers
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Format output to include console.log and result
|
|
307
|
+
return formatToolOutput(result);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Default code execution tool (backward compatible)
|
|
314
|
+
*
|
|
315
|
+
* Execute JavaScript code with access to all mounted resources
|
|
316
|
+
*
|
|
317
|
+
* Example: run_code(`
|
|
318
|
+
* const files = await bucket.list("@user-data/");
|
|
319
|
+
* return files.length;
|
|
320
|
+
* `)
|
|
321
|
+
*/
|
|
322
|
+
export const CodeExecutionTool: Tool = createCodeExecutionTool();
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Format tool output with console.log and result sections
|
|
326
|
+
* Returns a consistent string format for OpenAI chat completions
|
|
327
|
+
*/
|
|
328
|
+
export function formatToolOutput(result: any): any {
|
|
329
|
+
const hasConsoleOutput = result.consoleOutput && result.consoleOutput.length > 0;
|
|
330
|
+
const hasError = !result.success && result.error;
|
|
331
|
+
// Only show result section if there's a meaningful result (not undefined)
|
|
332
|
+
const hasResult = result.success && 'result' in result && result.result !== undefined;
|
|
333
|
+
|
|
334
|
+
if (!hasConsoleOutput && !hasResult && !hasError) {
|
|
335
|
+
// No output at all - return empty
|
|
336
|
+
return { output: '(no output)' };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const lines: string[] = [];
|
|
340
|
+
|
|
341
|
+
if (hasError) {
|
|
342
|
+
lines.push('[error]');
|
|
343
|
+
lines.push(result.error);
|
|
344
|
+
lines.push('');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (hasConsoleOutput) {
|
|
348
|
+
lines.push('[stdout]');
|
|
349
|
+
lines.push(...result.consoleOutput);
|
|
350
|
+
lines.push('');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (hasResult) {
|
|
354
|
+
lines.push('[result]');
|
|
355
|
+
const MAX_OUTPUT_LEN = 50000;
|
|
356
|
+
const resultStr =
|
|
357
|
+
typeof result.result === 'object'
|
|
358
|
+
? (() => {
|
|
359
|
+
try {
|
|
360
|
+
const json = JSON.stringify(result.result, null, 2);
|
|
361
|
+
return json.length > MAX_OUTPUT_LEN
|
|
362
|
+
? json.substring(0, MAX_OUTPUT_LEN) + '\n...(truncated)'
|
|
363
|
+
: json;
|
|
364
|
+
} catch {
|
|
365
|
+
return String(result.result);
|
|
366
|
+
}
|
|
367
|
+
})()
|
|
368
|
+
: String(result.result).substring(0, MAX_OUTPUT_LEN);
|
|
369
|
+
lines.push(resultStr);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
output: lines.join('\n')
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Create sandbox globals from mount manager
|
|
379
|
+
* Returns database and search functions (storage is handled by the storage bridge)
|
|
380
|
+
*/
|
|
381
|
+
export function createMountGlobals(mountManager: MountProvider): SandboxGlobals {
|
|
382
|
+
const globals: SandboxGlobals = {};
|
|
383
|
+
|
|
384
|
+
// Helper to parse path and get mount
|
|
385
|
+
const resolvePath = (path: string): { mount: MountInfo; resourcePath: string } => {
|
|
386
|
+
const parsed = mountManager.parsePath(path);
|
|
387
|
+
const mount = mountManager.getMount(parsed.mountName);
|
|
388
|
+
if (!mount) {
|
|
389
|
+
const available = Array.from(mountManager.getAllMounts().keys()).join(', ');
|
|
390
|
+
throw new Error(`Mount not found: ${parsed.mountName}. Available: ${available}`);
|
|
391
|
+
}
|
|
392
|
+
return { mount, resourcePath: parsed.path };
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// query(path, sql, params?) - Execute SELECT query
|
|
396
|
+
globals.query = sandboxAsync(async (path: string, sql: string, params?: any[]) => {
|
|
397
|
+
const { mount } = resolvePath(path);
|
|
398
|
+
|
|
399
|
+
if (mount.type !== 'database') {
|
|
400
|
+
throw new Error(`Not a database mount: ${path}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const db = mount.resource as SqlDatabase;
|
|
404
|
+
const stmt = params ? db.prepare(sql).bind(...params) : db.prepare(sql);
|
|
405
|
+
const result = await stmt.all();
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
results: result.results,
|
|
409
|
+
rowCount: result.results?.length || 0
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// execute(path, sql, params?) - Execute INSERT/UPDATE/DELETE
|
|
414
|
+
globals.execute = sandboxAsync(async (path: string, sql: string, params?: any[]) => {
|
|
415
|
+
const { mount } = resolvePath(path);
|
|
416
|
+
|
|
417
|
+
if (mount.type !== 'database') {
|
|
418
|
+
throw new Error(`Not a database mount: ${path}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Check if mount allows writes (default is read-only)
|
|
422
|
+
if (mount.mode !== 'rw') {
|
|
423
|
+
throw new Error(`Cannot execute write operations on read-only mount: ${path}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const db = mount.resource as SqlDatabase;
|
|
427
|
+
const stmt = params ? db.prepare(sql).bind(...params) : db.prepare(sql);
|
|
428
|
+
const result = await stmt.run();
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
success: result.success,
|
|
432
|
+
changes: result.meta?.changes || 0
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Note: search and chunkSearch are handled by the search bridge
|
|
437
|
+
|
|
438
|
+
return globals;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Export code tool
|
|
443
|
+
*/
|
|
444
|
+
export const CodeTools: Tool[] = [CodeExecutionTool];
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Tools - Read, write, list, delete files from Buckets and SmartBuckets
|
|
3
|
+
* Mount-aware: operates on multiple buckets via path prefixes
|
|
4
|
+
* Supports both Bucket and SmartBucket (file operations only - use SmartBucket tools for AI search)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Tool } from '../types.js';
|
|
8
|
+
import type { Bucket } from '@liquidmetal-ai/raindrop-framework';
|
|
9
|
+
import { createPathBasedTool } from './tool-factory.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read a file from a mounted bucket or SmartBucket
|
|
13
|
+
*
|
|
14
|
+
* Example: read_file("/uploads/profile.json")
|
|
15
|
+
*/
|
|
16
|
+
export const ReadFileTool: Tool = createPathBasedTool<{ path: string }, any>({
|
|
17
|
+
name: 'read_file',
|
|
18
|
+
description: `Read a file from storage and return its contents.
|
|
19
|
+
|
|
20
|
+
Path format: /mount-name/path/to/file.ext
|
|
21
|
+
|
|
22
|
+
Works with both Buckets and SmartBuckets. For AI-powered search on SmartBuckets, use semantic_search or chunk_search tools.
|
|
23
|
+
|
|
24
|
+
Returns: {path, content, size, uploaded, contentType}
|
|
25
|
+
|
|
26
|
+
Example: read_file("/uploads/profile.json")`,
|
|
27
|
+
parameters: {
|
|
28
|
+
path: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'File path starting with /, e.g., /uploads/profile.json',
|
|
31
|
+
required: true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
allowedMountTypes: ['bucket', 'smartbucket'],
|
|
35
|
+
mode: 'read',
|
|
36
|
+
|
|
37
|
+
async executor(mount, parsed, params) {
|
|
38
|
+
const bucket = mount.resource as Bucket;
|
|
39
|
+
const obj = await bucket.get(parsed.path);
|
|
40
|
+
|
|
41
|
+
if (!obj) {
|
|
42
|
+
throw new Error(`File not found: ${params.path}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const content = await obj.text();
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
path: params.path,
|
|
49
|
+
content,
|
|
50
|
+
size: obj.size,
|
|
51
|
+
uploaded: obj.uploaded.toISOString(),
|
|
52
|
+
contentType: obj.httpMetadata?.contentType
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Write a file to a mounted bucket or SmartBucket
|
|
59
|
+
*
|
|
60
|
+
* Example: write_file("/outputs/result.json", '{"status": "done"}')
|
|
61
|
+
*/
|
|
62
|
+
export const WriteFileTool: Tool = createPathBasedTool<{ path: string; content: string; contentType?: string }, any>({
|
|
63
|
+
name: 'write_file',
|
|
64
|
+
description: `Write content to a file in storage. Creates or overwrites the file.
|
|
65
|
+
|
|
66
|
+
Path format: /mount-name/path/to/file.ext
|
|
67
|
+
|
|
68
|
+
Works with both Buckets and SmartBuckets. For SmartBuckets, uploaded content is automatically indexed for AI search.
|
|
69
|
+
Note: Mount must have mode 'rw' to allow writes.
|
|
70
|
+
|
|
71
|
+
Returns: {path, success, size}
|
|
72
|
+
|
|
73
|
+
Example: write_file("/outputs/result.txt", "Hello World")`,
|
|
74
|
+
parameters: {
|
|
75
|
+
path: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'File path starting with /, e.g., /outputs/result.txt',
|
|
78
|
+
required: true
|
|
79
|
+
},
|
|
80
|
+
content: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Content to write to the file',
|
|
83
|
+
required: true
|
|
84
|
+
},
|
|
85
|
+
contentType: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Optional content type, e.g., application/json, text/plain',
|
|
88
|
+
required: false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
allowedMountTypes: ['bucket', 'smartbucket'],
|
|
92
|
+
mode: 'write',
|
|
93
|
+
|
|
94
|
+
async executor(mount, parsed, params) {
|
|
95
|
+
const bucket = mount.resource as Bucket;
|
|
96
|
+
|
|
97
|
+
await bucket.put(parsed.path, params.content, {
|
|
98
|
+
httpMetadata: params.contentType
|
|
99
|
+
? {
|
|
100
|
+
contentType: params.contentType
|
|
101
|
+
}
|
|
102
|
+
: undefined
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
path: params.path,
|
|
107
|
+
success: true,
|
|
108
|
+
size: params.content.length
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List files in a mounted bucket or SmartBucket
|
|
115
|
+
*
|
|
116
|
+
* Example: list_files("/uploads/") or list_files("/logs/2024/")
|
|
117
|
+
*/
|
|
118
|
+
export const ListFilesTool: Tool = createPathBasedTool<{ path: string; limit?: number }, any>({
|
|
119
|
+
name: 'list_files',
|
|
120
|
+
description: `List files in storage with optional prefix filtering.
|
|
121
|
+
|
|
122
|
+
Path format: /mount-name/ or /mount-name/prefix/
|
|
123
|
+
|
|
124
|
+
Works with both Buckets and SmartBuckets.
|
|
125
|
+
|
|
126
|
+
Returns: {files: [{path, size, uploaded}], truncated, count}
|
|
127
|
+
|
|
128
|
+
Example: list_files("/uploads/")
|
|
129
|
+
Example with prefix: list_files("/logs/2024/")`,
|
|
130
|
+
parameters: {
|
|
131
|
+
path: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'Mount path with optional prefix, e.g., /uploads/ or /logs/2024/',
|
|
134
|
+
required: true
|
|
135
|
+
},
|
|
136
|
+
limit: {
|
|
137
|
+
type: 'number',
|
|
138
|
+
description: 'Maximum number of files to return (default: 1000)',
|
|
139
|
+
required: false
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
allowedMountTypes: ['bucket', 'smartbucket'],
|
|
143
|
+
mode: 'read',
|
|
144
|
+
|
|
145
|
+
async executor(mount, parsed, params) {
|
|
146
|
+
const bucket = mount.resource as Bucket;
|
|
147
|
+
const listed = await bucket.list({
|
|
148
|
+
prefix: parsed.path || undefined,
|
|
149
|
+
limit: params.limit || 1000
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
files: listed.objects.map(obj => ({
|
|
154
|
+
path: `/${mount.name}/${obj.key}`,
|
|
155
|
+
size: obj.size,
|
|
156
|
+
uploaded: obj.uploaded.toISOString()
|
|
157
|
+
})),
|
|
158
|
+
truncated: listed.truncated,
|
|
159
|
+
count: listed.objects.length
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Delete a file from a mounted bucket or SmartBucket
|
|
166
|
+
*
|
|
167
|
+
* Example: delete_file("/logs/old-file.txt")
|
|
168
|
+
*/
|
|
169
|
+
export const DeleteFileTool: Tool = createPathBasedTool<{ path: string }, any>({
|
|
170
|
+
name: 'delete_file',
|
|
171
|
+
description: `Delete a file from storage.
|
|
172
|
+
|
|
173
|
+
Path format: /mount-name/path/to/file.ext
|
|
174
|
+
|
|
175
|
+
Works with both Buckets and SmartBuckets. Note: Deleting from SmartBucket also removes its indexed content.
|
|
176
|
+
Note: Mount must have mode 'rw' to allow deletes.
|
|
177
|
+
|
|
178
|
+
Returns: {path, success, deleted}
|
|
179
|
+
|
|
180
|
+
Example: delete_file("/logs/old-file.txt")`,
|
|
181
|
+
parameters: {
|
|
182
|
+
path: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'File path starting with /, e.g., /logs/old-file.txt',
|
|
185
|
+
required: true
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
allowedMountTypes: ['bucket', 'smartbucket'],
|
|
189
|
+
mode: 'write',
|
|
190
|
+
|
|
191
|
+
async executor(mount, parsed, params) {
|
|
192
|
+
const bucket = mount.resource as Bucket;
|
|
193
|
+
await bucket.delete(parsed.path);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
path: params.path,
|
|
197
|
+
success: true,
|
|
198
|
+
deleted: true
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* All file tools
|
|
205
|
+
*/
|
|
206
|
+
export const FileTools: Tool[] = [ReadFileTool, WriteFileTool, ListFilesTool, DeleteFileTool];
|