@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.
Files changed (78) hide show
  1. package/.prettierrc +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/eslint.config.mjs +28 -0
  4. package/package.json +53 -0
  5. package/src/engine/agent.ts +478 -0
  6. package/src/engine/llm-provider.test.ts +275 -0
  7. package/src/engine/llm-provider.ts +330 -0
  8. package/src/engine/stream-parser.ts +170 -0
  9. package/src/index.ts +142 -0
  10. package/src/mounts/mount-manager.test.ts +516 -0
  11. package/src/mounts/mount-manager.ts +327 -0
  12. package/src/mounts/mount-registry.ts +196 -0
  13. package/src/mounts/zod-to-string.test.ts +154 -0
  14. package/src/mounts/zod-to-string.ts +213 -0
  15. package/src/presets/agent-tools.ts +57 -0
  16. package/src/presets/index.ts +5 -0
  17. package/src/sandbox/README.md +1321 -0
  18. package/src/sandbox/bridges/README.md +571 -0
  19. package/src/sandbox/bridges/actor.test.ts +229 -0
  20. package/src/sandbox/bridges/actor.ts +195 -0
  21. package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
  22. package/src/sandbox/bridges/bucket.test.ts +300 -0
  23. package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
  24. package/src/sandbox/bridges/console-multiple.test.ts +187 -0
  25. package/src/sandbox/bridges/console.test.ts +157 -0
  26. package/src/sandbox/bridges/console.ts +122 -0
  27. package/src/sandbox/bridges/fetch.ts +93 -0
  28. package/src/sandbox/bridges/index.ts +78 -0
  29. package/src/sandbox/bridges/readable-stream.ts +323 -0
  30. package/src/sandbox/bridges/response.test.ts +154 -0
  31. package/src/sandbox/bridges/response.ts +123 -0
  32. package/src/sandbox/bridges/review-fixes.test.ts +331 -0
  33. package/src/sandbox/bridges/search.test.ts +475 -0
  34. package/src/sandbox/bridges/search.ts +264 -0
  35. package/src/sandbox/bridges/shared/body-methods.ts +93 -0
  36. package/src/sandbox/bridges/shared/cleanup.ts +112 -0
  37. package/src/sandbox/bridges/shared/convert.ts +76 -0
  38. package/src/sandbox/bridges/shared/headers.ts +181 -0
  39. package/src/sandbox/bridges/shared/index.ts +36 -0
  40. package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
  41. package/src/sandbox/bridges/shared/path-parser.ts +109 -0
  42. package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
  43. package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
  44. package/src/sandbox/bridges/shared/response-object.ts +280 -0
  45. package/src/sandbox/bridges/shared/result-builder.ts +130 -0
  46. package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
  47. package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
  48. package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
  49. package/src/sandbox/bridges/storage.ts +421 -0
  50. package/src/sandbox/bridges/text-decoder.ts +190 -0
  51. package/src/sandbox/bridges/text-encoder.ts +102 -0
  52. package/src/sandbox/bridges/types.ts +39 -0
  53. package/src/sandbox/bridges/utils.ts +123 -0
  54. package/src/sandbox/index.ts +6 -0
  55. package/src/sandbox/quickjs-wasm.d.ts +9 -0
  56. package/src/sandbox/sandbox.test.ts +191 -0
  57. package/src/sandbox/sandbox.ts +831 -0
  58. package/src/sandbox/test-helper.ts +43 -0
  59. package/src/sandbox/test-mocks.ts +154 -0
  60. package/src/sandbox/user-stream.test.ts +77 -0
  61. package/src/skills/frontmatter.test.ts +305 -0
  62. package/src/skills/frontmatter.ts +200 -0
  63. package/src/skills/index.ts +9 -0
  64. package/src/skills/skills-loader.test.ts +237 -0
  65. package/src/skills/skills-loader.ts +200 -0
  66. package/src/tools/actor-storage-tools.ts +250 -0
  67. package/src/tools/code-tools.test.ts +199 -0
  68. package/src/tools/code-tools.ts +444 -0
  69. package/src/tools/file-tools.ts +206 -0
  70. package/src/tools/registry.ts +125 -0
  71. package/src/tools/script-tools.ts +145 -0
  72. package/src/tools/smartbucket-tools.ts +203 -0
  73. package/src/tools/sql-tools.ts +213 -0
  74. package/src/tools/tool-factory.ts +119 -0
  75. package/src/types.ts +512 -0
  76. package/tsconfig.eslint.json +5 -0
  77. package/tsconfig.json +15 -0
  78. package/vitest.config.ts +33 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * TextEncoder bridge - Encodes strings to Uint8Array
3
+ */
4
+
5
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
6
+ import type { BridgeContext } from './types.js';
7
+ import { defineClass } from './utils.js';
8
+ import { unwrapResult } from './shared/index.js';
9
+
10
+ /**
11
+ * Install TextEncoder in the sandbox
12
+ * Bridges to runtime's native TextEncoder
13
+ */
14
+ export function installTextEncoder(ctx: BridgeContext): void {
15
+ const { context, logger } = ctx;
16
+
17
+ // Create cached Uint8Array wrapper function once - avoids evalCode on every encode()
18
+ // Store in global so it persists and can be used by the host function
19
+ {
20
+ using _uint8ArrayWrapper = unwrapResult(
21
+ context.evalCode('(globalThis.__uint8ArrayWrapper = function(ab) { return new Uint8Array(ab); })'),
22
+ context
23
+ );
24
+ // wrapper persists in globalThis; our reference is disposed by `using`
25
+ }
26
+
27
+ // Create host function that does the actual encoding
28
+ // Reusable native encoder (stateless)
29
+ const nativeEncoder = new TextEncoder();
30
+ {
31
+ using hostEncodeFunction = context.newFunction('__hostEncode', strHandle => {
32
+ try {
33
+ const str = context.dump(strHandle);
34
+ if (typeof str !== 'string') {
35
+ return context.newError('encode() requires a string');
36
+ }
37
+
38
+ const uint8Array = nativeEncoder.encode(str);
39
+
40
+ // Create a Uint8Array in QuickJS from the ArrayBuffer
41
+ // Use slice to handle any Uint8Array views into larger ArrayBuffers
42
+ const buf =
43
+ uint8Array.byteOffset === 0 && uint8Array.byteLength === uint8Array.buffer.byteLength
44
+ ? uint8Array.buffer
45
+ : uint8Array.buffer.slice(
46
+ uint8Array.byteOffset,
47
+ uint8Array.byteOffset + uint8Array.byteLength
48
+ );
49
+ using arrayBufferHandle = context.newArrayBuffer(buf);
50
+
51
+ // Get the cached wrapper from global
52
+ using wrapperHandle = context.getProp(context.global, '__uint8ArrayWrapper');
53
+
54
+ // Use cached wrapper function instead of evalCode on every encode
55
+ return unwrapResult(
56
+ context.callFunction(wrapperHandle, context.undefined, arrayBufferHandle),
57
+ context
58
+ );
59
+ } catch (e) {
60
+ logger?.error?.('TextEncoder.encode error', { error: e });
61
+ return context.newError(e instanceof Error ? e.message : String(e));
62
+ }
63
+ });
64
+
65
+ context.setProp(context.global, '__hostEncode', hostEncodeFunction);
66
+ }
67
+
68
+ // Define TextEncoder class in QuickJS
69
+ const classCode = `
70
+ class TextEncoder {
71
+ constructor() {
72
+ this.encoding = 'utf-8';
73
+ }
74
+
75
+ encode(str) {
76
+ return __hostEncode(str);
77
+ }
78
+ }
79
+ `;
80
+
81
+ const classHandle = defineClass(ctx, classCode, 'TextEncoder');
82
+ if (classHandle) {
83
+ using handle = classHandle;
84
+ context.setProp(context.global, 'TextEncoder', handle);
85
+
86
+ // Clean up internal globals — make non-enumerable/writable to hide from sandbox
87
+ // NOTE: The sandbox is not a hard security boundary. Internal helpers are hidden via
88
+ // enumerable: false but can still be called by name. This is acceptable because the bridges
89
+ // are the primary security boundary, not QuickJS containment. See Sandbox.ts for details.
90
+ const hideResult = context.evalCode(`(function() {
91
+ try { Object.defineProperty(globalThis, '__hostEncode', { enumerable: false, writable: false, configurable: false }); } catch(e) {}
92
+ try { Object.defineProperty(globalThis, '__uint8ArrayWrapper', { enumerable: false, writable: false, configurable: false }); } catch(e) {}
93
+ })()`);
94
+ if (hideResult.error) {
95
+ hideResult.error.dispose();
96
+ } else {
97
+ (hideResult as { value: QuickJSHandle }).value.dispose();
98
+ }
99
+
100
+ logger?.info?.('[Bridge] TextEncoder installed');
101
+ }
102
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Types for QuickJS bridge utilities
3
+ */
4
+
5
+ import type { QuickJSContext, QuickJSRuntime, QuickJSDeferredPromise } from 'quickjs-emscripten-core';
6
+ import type { Logger } from '../../types.js';
7
+
8
+ /**
9
+ * Promise tracker for async operations
10
+ */
11
+ export interface PromiseTracker {
12
+ pendingPromises: Set<Promise<void>>;
13
+ notifyNewPromise: () => void;
14
+ newPromiseSignal: Promise<void>;
15
+ deferredPromises: Set<QuickJSDeferredPromise>;
16
+ }
17
+
18
+ /**
19
+ * Bridge context containing QuickJS context and shared resources
20
+ */
21
+ export interface BridgeContext {
22
+ context: QuickJSContext;
23
+ runtime: QuickJSRuntime;
24
+ tracker: PromiseTracker;
25
+ logger?: Logger;
26
+ cleanupHandlers: Array<() => void | Promise<void>>;
27
+ }
28
+
29
+ /**
30
+ * Options for bridge installation
31
+ */
32
+ export interface BridgeOptions {
33
+ logger?: Logger;
34
+ }
35
+
36
+ /**
37
+ * A bridge installer function
38
+ */
39
+ export type BridgeInstaller = (ctx: BridgeContext) => void | (() => void);
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Utility functions for creating QuickJS bridges
3
+ */
4
+
5
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
6
+ import type { BridgeContext } from './types.js';
7
+ import { convertToHandle } from './shared/convert.js';
8
+
9
+ // Re-export convertToHandle from the shared module (single source of truth)
10
+ export { convertToHandle } from './shared/convert.js';
11
+
12
+ /**
13
+ * Create a synchronous function that bridges to a sync host function
14
+ */
15
+ export function createSyncBridge(
16
+ ctx: BridgeContext,
17
+ name: string,
18
+ hostFn: (...args: any[]) => any
19
+ ): QuickJSHandle {
20
+ const { context, logger } = ctx;
21
+
22
+ return context.newFunction(name, (...args: QuickJSHandle[]) => {
23
+ const jsArgs = args.map(arg => context.dump(arg));
24
+ const result = hostFn(...jsArgs);
25
+
26
+ logger?.debug?.(`[Bridge] Sync bridge called: ${name}`);
27
+
28
+ return convertToHandle(context, result);
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Create a promise-returning function that bridges to an async host function
34
+ */
35
+ export function createAsyncBridge(
36
+ ctx: BridgeContext,
37
+ name: string,
38
+ hostFn: (...args: any[]) => Promise<any>
39
+ ): QuickJSHandle {
40
+ const { context, runtime, tracker, logger } = ctx;
41
+
42
+ return context.newFunction(name, (...args: QuickJSHandle[]) => {
43
+ const jsArgs = args.map(arg => context.dump(arg));
44
+ const deferred = context.newPromise();
45
+ tracker.deferredPromises.add(deferred);
46
+
47
+ const hostPromise = hostFn(...jsArgs)
48
+ .then((result: any) => {
49
+ try {
50
+ const handle = convertToHandle(context, result);
51
+ deferred.resolve(handle);
52
+ handle.dispose();
53
+ runtime.executePendingJobs();
54
+ } catch (e) {
55
+ try {
56
+ const errHandle = context.newString(String(e));
57
+ deferred.reject(errHandle);
58
+ errHandle.dispose();
59
+ runtime.executePendingJobs();
60
+ } catch {
61
+ // Context truly disposed — deferred will be cleaned up by sandbox cleanup
62
+ }
63
+ logger?.warn?.(`[Bridge] ${name}: error after async completion: ${e}`);
64
+ }
65
+ })
66
+ .catch((err: Error) => {
67
+ try {
68
+ const errHandle = context.newString(err.message || String(err));
69
+ deferred.reject(errHandle);
70
+ errHandle.dispose();
71
+ runtime.executePendingJobs();
72
+ } catch (e2) {
73
+ // Second-level catch: context disposed or string creation failed
74
+ // Try one more time with a minimal error before giving up
75
+ try {
76
+ const fallbackHandle = context.newString('Async bridge error');
77
+ deferred.reject(fallbackHandle);
78
+ fallbackHandle.dispose();
79
+ runtime.executePendingJobs();
80
+ } catch {
81
+ // Context truly disposed
82
+ }
83
+ logger?.warn?.(`[Bridge] ${name}: error handling failed: ${err.message}, fallback error: ${e2}`);
84
+ }
85
+ })
86
+ .finally(() => {
87
+ tracker.pendingPromises.delete(hostPromise);
88
+ tracker.deferredPromises.delete(deferred);
89
+ });
90
+
91
+ tracker.pendingPromises.add(hostPromise);
92
+ tracker.notifyNewPromise();
93
+
94
+ logger?.debug?.(`[Bridge] Created async bridge for ${name}`);
95
+
96
+ return deferred.handle;
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Define an ES6 class in the sandbox using evalCode
102
+ * Returns the class handle or null if there was an error
103
+ */
104
+ export function defineClass(
105
+ ctx: BridgeContext,
106
+ classCode: string,
107
+ className: string
108
+ ): QuickJSHandle | null {
109
+ const { context, logger } = ctx;
110
+
111
+ const classDef = context.evalCode(`(${classCode})`);
112
+
113
+ if (classDef.error) {
114
+ const error = context.dump(classDef.error);
115
+ classDef.error.dispose();
116
+ logger?.error?.(`Failed to define ${className} class`, { error });
117
+ return null;
118
+ }
119
+
120
+ return (classDef as any).value;
121
+ }
122
+
123
+
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sandbox exports
3
+ */
4
+
5
+ export { Sandbox } from './sandbox.js';
6
+ export type { BridgeInstaller } from './bridges/types.js';
@@ -0,0 +1,9 @@
1
+ declare module '*.wasm' {
2
+ const content: WebAssembly.Module;
3
+ export default content;
4
+ }
5
+
6
+ declare module '@jitl/quickjs-ng-wasmfile-release-sync/dist/emscripten-module.wasm' {
7
+ const content: WebAssembly.Module;
8
+ export default content;
9
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Sandbox tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { Sandbox } from './index.js';
7
+
8
+ describe('Sandbox', () => {
9
+ beforeEach(async () => {
10
+ await Sandbox.reset();
11
+ });
12
+
13
+ afterEach(async () => {
14
+ await Sandbox.reset();
15
+ });
16
+
17
+ it('should create singleton instance', async () => {
18
+ const sandbox1 = await Sandbox.configure();
19
+ const sandbox2 = await Sandbox.getInstance();
20
+
21
+ expect(sandbox1).toBe(sandbox2);
22
+ });
23
+
24
+ it('should return existing instance when configure is called twice', async () => {
25
+ const sandbox1 = await Sandbox.configure();
26
+ const sandbox2 = await Sandbox.configure();
27
+ expect(sandbox2).toBe(sandbox1);
28
+ });
29
+
30
+ it('should reject getInstance when not configured', async () => {
31
+ await expect(Sandbox.getInstance()).rejects.toThrow('not been configured');
32
+ });
33
+
34
+ it('should execute simple code', async () => {
35
+ const sandbox = await Sandbox.configure();
36
+ const result = await sandbox.execute('const x = 1 + 2; return x;', {}, { logger: console });
37
+
38
+ if (!result.success) {
39
+ console.error('Execution failed:', result.error);
40
+ console.error('Full result:', JSON.stringify(result, null, 2));
41
+ }
42
+ expect(result.success).toBe(true);
43
+ expect(result.result).toBe(3);
44
+ });
45
+
46
+ it('should handle async operations', async () => {
47
+ const sandbox = await Sandbox.configure();
48
+ const result = await sandbox.execute(
49
+ `
50
+ const p = Promise.resolve('done');
51
+ return await p;
52
+ `,
53
+ {},
54
+ { logger: console }
55
+ );
56
+
57
+ if (!result.success) {
58
+ console.error('Execution failed:', result.error);
59
+ console.error('Full result:', JSON.stringify(result, null, 2));
60
+ }
61
+ expect(result.success).toBe(true);
62
+ expect(result.result).toBe('done');
63
+ });
64
+
65
+ it('should queue concurrent executions', async () => {
66
+ const sandbox = await Sandbox.configure();
67
+ const order: number[] = [];
68
+
69
+ // Each execution records its completion order via .then() callback.
70
+ // The queue serializes them, so they complete in submission order.
71
+ // Use computation (not setTimeout) to create varying execution times.
72
+ const promise1 = sandbox
73
+ .execute(
74
+ `
75
+ // Heavier computation to take longer
76
+ let sum = 0;
77
+ for (let i = 0; i < 10000; i++) sum += i;
78
+ return 1;
79
+ `
80
+ )
81
+ .then((result) => {
82
+ if (!result.success) console.error('Execution 1 failed:', result.error);
83
+ expect(result.success).toBe(true);
84
+ order.push(result.result as number);
85
+ });
86
+
87
+ const promise2 = sandbox
88
+ .execute(
89
+ `
90
+ let sum = 0;
91
+ for (let i = 0; i < 100; i++) sum += i;
92
+ return 2;
93
+ `
94
+ )
95
+ .then((result) => {
96
+ expect(result.success).toBe(true);
97
+ order.push(result.result as number);
98
+ });
99
+
100
+ const promise3 = sandbox
101
+ .execute(
102
+ `
103
+ return 3;
104
+ `
105
+ )
106
+ .then((result) => {
107
+ expect(result.success).toBe(true);
108
+ order.push(result.result as number);
109
+ });
110
+
111
+ await Promise.all([promise1, promise2, promise3]);
112
+
113
+ // Executions are serialized by the queue — they must complete in submission order
114
+ // even though execution 2 and 3 are faster computations than execution 1
115
+ expect(order).toEqual([1, 2, 3]);
116
+ });
117
+
118
+ it('should inject globals', async () => {
119
+ const sandbox = await Sandbox.configure();
120
+ const result = await sandbox.execute('return myGlobal;', {
121
+ myGlobal: 'hello world'
122
+ });
123
+
124
+ expect(result.success).toBe(true);
125
+ expect(result.result).toBe('hello world');
126
+ });
127
+
128
+ it('should handle errors', async () => {
129
+ const sandbox = await Sandbox.configure();
130
+ const result = await sandbox.execute('throw new Error("test error");', {}, { logger: console });
131
+
132
+ if (!result.success) {
133
+ console.error('Error result:', result.error);
134
+ }
135
+ expect(result.success).toBe(false);
136
+ expect(result.error).toContain('test error');
137
+ });
138
+
139
+ it('should enforce timeout', async () => {
140
+ const sandbox = await Sandbox.configure();
141
+ const result = await sandbox.execute(
142
+ 'await new Promise(() => {});',
143
+ {},
144
+ {
145
+ timeoutMs: 100
146
+ }
147
+ );
148
+
149
+ expect(result.success).toBe(false);
150
+ expect(result.error).toContain('timeout');
151
+ });
152
+
153
+ it('should capture console output', async () => {
154
+ const sandbox = await Sandbox.configure();
155
+ const result = await sandbox.execute('console.log("hello"); return "done";');
156
+
157
+ expect(result.success).toBe(true);
158
+ expect(result.consoleOutput).toContain('hello');
159
+ });
160
+
161
+ it('should dispose correctly', async () => {
162
+ const sandbox = await Sandbox.configure();
163
+ await sandbox.dispose();
164
+
165
+ // After dispose, configure should create a new instance
166
+ const sandbox2 = await Sandbox.configure();
167
+ expect(sandbox2).not.toBe(sandbox);
168
+ });
169
+
170
+ it('should reject queued executions after dispose', async () => {
171
+ const sandbox = await Sandbox.configure();
172
+
173
+ // Start a long execution
174
+ const longPromise = sandbox.execute(`
175
+ await new Promise(resolve => setTimeout(resolve, 100));
176
+ return 'done';
177
+ `);
178
+
179
+ // Queue another execution
180
+ const queuedPromise = sandbox.execute('return "queued";');
181
+
182
+ // Dispose while first is still running
183
+ await sandbox.dispose();
184
+
185
+ // Queued execution should be rejected
186
+ await expect(queuedPromise).rejects.toThrow('disposed');
187
+
188
+ // Clean up long promise
189
+ await longPromise.catch(() => {});
190
+ });
191
+ });