@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,250 @@
1
+ /**
2
+ * Actor Storage Tools - Read, write, list, delete entries from ActorStorage mounts
3
+ * Mount-aware: operates on actor-storage mounts via path prefixes
4
+ *
5
+ * ActorStorage is strongly consistent, transactional KV local to an Actor instance.
6
+ * Values are stored as UTF-8 bytes (Uint8Array).
7
+ */
8
+
9
+ import type { Tool } from '../types.js';
10
+ import type { ActorStorage } from '@liquidmetal-ai/raindrop-framework';
11
+ import { createPathBasedTool } from './tool-factory.js';
12
+
13
+ /**
14
+ * Read a value from actor storage
15
+ *
16
+ * Example: read_state("/local/user-preferences")
17
+ */
18
+ export const ReadStateTool: Tool = createPathBasedTool<{ path: string }, any>({
19
+ name: 'read_state',
20
+ description: `Read a value from actor storage (strongly consistent, transactional KV).
21
+
22
+ Path format: /mount-name/key
23
+
24
+ Returns: {path, content} or throws if key not found.
25
+
26
+ Example: read_state("/local/user-preferences")`,
27
+ parameters: {
28
+ path: {
29
+ type: 'string',
30
+ description: 'Key path starting with /, e.g., /local/user-preferences',
31
+ required: true
32
+ }
33
+ },
34
+ allowedMountTypes: ['actor-storage'],
35
+ mode: 'read',
36
+
37
+ async executor(mount, parsed, params) {
38
+ if (!parsed.path) {
39
+ throw new Error('Key is required. Path format: /mount-name/key');
40
+ }
41
+
42
+ const storage = mount.resource as ActorStorage;
43
+ const value = await storage.get(parsed.path);
44
+
45
+ if (value === undefined) {
46
+ throw new Error(`Key not found: ${params.path}`);
47
+ }
48
+
49
+ // Decode bytes back to string — values may be stored as Uint8Array (by sandbox bridge)
50
+ // or as plain strings/objects (by other callers)
51
+ let content: string;
52
+ if (typeof value === 'string') {
53
+ content = value;
54
+ } else if (value instanceof Uint8Array) {
55
+ content = new TextDecoder().decode(value);
56
+ } else if (value instanceof ArrayBuffer) {
57
+ content = new TextDecoder().decode(new Uint8Array(value));
58
+ } else {
59
+ content = JSON.stringify(value);
60
+ }
61
+
62
+ return {
63
+ path: params.path,
64
+ content
65
+ };
66
+ }
67
+ });
68
+
69
+ /**
70
+ * Write a value to actor storage
71
+ *
72
+ * Example: write_state("/local/user-preferences", '{"theme": "dark"}')
73
+ */
74
+ export const WriteStateTool: Tool = createPathBasedTool<{ path: string; content: string }, any>({
75
+ name: 'write_state',
76
+ description: `Write a value to actor storage (strongly consistent, transactional KV).
77
+
78
+ Path format: /mount-name/key
79
+
80
+ Values are stored as UTF-8 bytes. Non-string content is JSON-serialized.
81
+ Note: Mount must have mode 'rw' to allow writes.
82
+
83
+ Returns: {path, success, size}
84
+
85
+ Example: write_state("/local/user-preferences", '{"theme": "dark"}')`,
86
+ parameters: {
87
+ path: {
88
+ type: 'string',
89
+ description: 'Key path starting with /, e.g., /local/user-preferences',
90
+ required: true
91
+ },
92
+ content: {
93
+ type: 'string',
94
+ description: 'Content to store (string or JSON)',
95
+ required: true
96
+ }
97
+ },
98
+ allowedMountTypes: ['actor-storage'],
99
+ mode: 'write',
100
+
101
+ async executor(mount, parsed, params) {
102
+ if (!parsed.path) {
103
+ throw new Error('Key is required. Path format: /mount-name/key');
104
+ }
105
+
106
+ const storage = mount.resource as ActorStorage;
107
+ const value = typeof params.content === 'string' ? params.content : JSON.stringify(params.content);
108
+ const bytes = new TextEncoder().encode(value);
109
+ await storage.put(parsed.path, bytes);
110
+
111
+ return {
112
+ path: params.path,
113
+ success: true,
114
+ size: bytes.length
115
+ };
116
+ }
117
+ });
118
+
119
+ /**
120
+ * List keys in actor storage
121
+ *
122
+ * Example: list_state("/local/") or list_state("/local/conversations/")
123
+ */
124
+ export const ListStateTool: Tool = createPathBasedTool<{
125
+ path: string;
126
+ limit?: number;
127
+ start?: string;
128
+ end?: string;
129
+ reverse?: boolean;
130
+ }, any>({
131
+ name: 'list_state',
132
+ description: `List keys in actor storage with optional prefix, range, and ordering.
133
+
134
+ Path format: /mount-name/ or /mount-name/prefix/
135
+
136
+ Supports range queries via options. ActorStorage list is strongly consistent.
137
+
138
+ Returns: {keys: [{key, path}], count}
139
+
140
+ Example: list_state("/local/")
141
+ Example with prefix: list_state("/local/conversations/")`,
142
+ parameters: {
143
+ path: {
144
+ type: 'string',
145
+ description: 'Mount path with optional prefix, e.g., /local/ or /local/conversations/',
146
+ required: true
147
+ },
148
+ limit: {
149
+ type: 'number',
150
+ description: 'Maximum number of keys to return',
151
+ required: false
152
+ },
153
+ start: {
154
+ type: 'string',
155
+ description: 'Start key (inclusive) for range queries',
156
+ required: false
157
+ },
158
+ end: {
159
+ type: 'string',
160
+ description: 'End key (exclusive) for range queries',
161
+ required: false
162
+ },
163
+ reverse: {
164
+ type: 'boolean',
165
+ description: 'List in reverse order',
166
+ required: false
167
+ }
168
+ },
169
+ allowedMountTypes: ['actor-storage'],
170
+ mode: 'read',
171
+
172
+ async executor(mount, parsed, params) {
173
+ const storage = mount.resource as ActorStorage;
174
+ const listOptions: Record<string, any> = {};
175
+
176
+ if (parsed.path) {
177
+ listOptions.prefix = parsed.path;
178
+ }
179
+ if (params.start !== undefined) listOptions.start = params.start;
180
+ if (params.end !== undefined) listOptions.end = params.end;
181
+ if (params.reverse !== undefined) listOptions.reverse = params.reverse;
182
+ if (params.limit !== undefined) listOptions.limit = params.limit;
183
+
184
+ const entries = await storage.list(
185
+ Object.keys(listOptions).length > 0 ? listOptions : undefined
186
+ );
187
+
188
+ const keys = Array.from(entries).map(([key]) => ({
189
+ key,
190
+ path: `/${mount.name}/${key}`
191
+ }));
192
+
193
+ return {
194
+ keys,
195
+ count: keys.length
196
+ };
197
+ }
198
+ });
199
+
200
+ /**
201
+ * Delete a key from actor storage
202
+ *
203
+ * Example: delete_state("/local/old-session")
204
+ */
205
+ export const DeleteStateTool: Tool = createPathBasedTool<{ path: string }, any>({
206
+ name: 'delete_state',
207
+ description: `Delete a key from actor storage.
208
+
209
+ Path format: /mount-name/key
210
+
211
+ Note: Mount must have mode 'rw' to allow deletes.
212
+
213
+ Returns: {path, success, deleted}
214
+
215
+ Example: delete_state("/local/old-session")`,
216
+ parameters: {
217
+ path: {
218
+ type: 'string',
219
+ description: 'Key path starting with /, e.g., /local/old-session',
220
+ required: true
221
+ }
222
+ },
223
+ allowedMountTypes: ['actor-storage'],
224
+ mode: 'write',
225
+
226
+ async executor(mount, parsed, params) {
227
+ if (!parsed.path) {
228
+ throw new Error('Key is required. Path format: /mount-name/key');
229
+ }
230
+
231
+ const storage = mount.resource as ActorStorage;
232
+ const deleted = await storage.delete(parsed.path);
233
+
234
+ return {
235
+ path: params.path,
236
+ success: true,
237
+ deleted
238
+ };
239
+ }
240
+ });
241
+
242
+ /**
243
+ * All actor storage tools
244
+ */
245
+ export const ActorStorageTools: Tool[] = [
246
+ ReadStateTool,
247
+ WriteStateTool,
248
+ ListStateTool,
249
+ DeleteStateTool
250
+ ];
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatToolOutput } from './code-tools.js';
3
+
4
+ describe('Code Tools - Output Formatting', () => {
5
+ it('should format output with only console.log', () => {
6
+ const result = {
7
+ success: true,
8
+ consoleOutput: ['Line 1', 'Line 2', 'Line 3']
9
+ };
10
+
11
+ const formatted = formatToolOutput(result);
12
+
13
+ // No [result] section when there's no result value
14
+ expect(formatted.output).toContain('[stdout]');
15
+ expect(formatted.output).toContain('Line 1');
16
+ expect(formatted.output).toContain('Line 2');
17
+ expect(formatted.output).toContain('Line 3');
18
+ });
19
+
20
+ it('should format output with only return value', () => {
21
+ const result = {
22
+ success: true,
23
+ result: { count: 42, status: 'ok' }
24
+ };
25
+
26
+ const formatted = formatToolOutput(result);
27
+
28
+ expect(formatted.output).toContain('[result]');
29
+ expect(formatted.output).toContain('"count": 42');
30
+ expect(formatted.output).toContain('"status": "ok"');
31
+ });
32
+
33
+ it('should format output with both console.log and return value', () => {
34
+ const result = {
35
+ success: true,
36
+ consoleOutput: ['Starting process...', 'Processing complete'],
37
+ result: { success: true, count: 100 }
38
+ };
39
+
40
+ const formatted = formatToolOutput(result);
41
+
42
+ expect(formatted.output).toContain('[stdout]');
43
+ expect(formatted.output).toContain('Starting process...');
44
+ expect(formatted.output).toContain('Processing complete');
45
+ expect(formatted.output).toContain('[result]');
46
+ expect(formatted.output).toContain('"success": true');
47
+ expect(formatted.output).toContain('"count": 100');
48
+ });
49
+
50
+ it('should handle no output', () => {
51
+ const result = {
52
+ success: true
53
+ };
54
+
55
+ const formatted = formatToolOutput(result);
56
+
57
+ // Always return output field, even if empty
58
+ expect(formatted).toEqual({ output: '(no output)' });
59
+ });
60
+
61
+ it('should handle empty console output', () => {
62
+ const result = {
63
+ success: true,
64
+ consoleOutput: [],
65
+ result: 'done'
66
+ };
67
+
68
+ const formatted = formatToolOutput(result);
69
+
70
+ expect(formatted.output).toContain('[result]');
71
+ expect(formatted.output).toContain('done');
72
+ // Only return output field for consistency
73
+ expect(formatted.success).toBeUndefined();
74
+ });
75
+
76
+ it('should only return output field', () => {
77
+ const result = {
78
+ success: true,
79
+ consoleOutput: ['debug'],
80
+ result: { data: 'test' },
81
+ executionTime: 123
82
+ };
83
+
84
+ const formatted = formatToolOutput(result);
85
+
86
+ // Only return output field for OpenAI chat completion compatibility
87
+ expect(formatted).toHaveProperty('output');
88
+ expect(formatted.output).toContain('[stdout]');
89
+ expect(formatted.output).toContain('debug');
90
+ expect(formatted.output).toContain('[result]');
91
+ expect(formatted.output).toContain('"data": "test"');
92
+ // Original properties should not be spread
93
+ expect(formatted.success).toBeUndefined();
94
+ expect(formatted.consoleOutput).toBeUndefined();
95
+ expect(formatted.result).toBeUndefined();
96
+ expect(formatted.executionTime).toBeUndefined();
97
+ });
98
+
99
+ it('should format string return value', () => {
100
+ const result = {
101
+ success: true,
102
+ consoleOutput: ['Log message'],
103
+ result: 'Return value'
104
+ };
105
+
106
+ const formatted = formatToolOutput(result);
107
+
108
+ expect(formatted.output).toContain('[stdout]');
109
+ expect(formatted.output).toContain('Log message');
110
+ expect(formatted.output).toContain('[result]');
111
+ expect(formatted.output).toContain('Return value');
112
+ });
113
+
114
+ it('should format null return value', () => {
115
+ const result = {
116
+ success: true,
117
+ consoleOutput: ['Processing'],
118
+ result: null
119
+ };
120
+
121
+ const formatted = formatToolOutput(result);
122
+
123
+ expect(formatted.output).toContain('[stdout]');
124
+ expect(formatted.output).toContain('Processing');
125
+ expect(formatted.output).toContain('[result]');
126
+ expect(formatted.output).toContain('null');
127
+ });
128
+
129
+ it('should format undefined return value', () => {
130
+ const result = {
131
+ success: true,
132
+ consoleOutput: ['Done'],
133
+ result: undefined
134
+ };
135
+
136
+ const formatted = formatToolOutput(result);
137
+
138
+ // When result is undefined, we don't show [result] section
139
+ expect(formatted.output).toContain('[stdout]');
140
+ expect(formatted.output).toContain('Done');
141
+ });
142
+
143
+ it('should format error output', () => {
144
+ const result = {
145
+ success: false,
146
+ error: 'ReferenceError: x is not defined',
147
+ consoleOutput: ['Before error']
148
+ };
149
+
150
+ const formatted = formatToolOutput(result);
151
+
152
+ expect(formatted.output).toContain('[error]');
153
+ expect(formatted.output).toContain('ReferenceError: x is not defined');
154
+ expect(formatted.output).toContain('[stdout]');
155
+ expect(formatted.output).toContain('Before error');
156
+ });
157
+
158
+ it('should truncate very large result objects', () => {
159
+ const largeArray = Array.from({ length: 10000 }, (_, i) => ({ id: i, data: 'x'.repeat(100) }));
160
+ const result = {
161
+ success: true,
162
+ result: largeArray
163
+ };
164
+
165
+ const formatted = formatToolOutput(result);
166
+
167
+ expect(formatted.output).toContain('[result]');
168
+ expect(formatted.output).toContain('...(truncated)');
169
+ // Should be capped at roughly 50k chars
170
+ expect(formatted.output.length).toBeLessThan(60000);
171
+ });
172
+
173
+ it('should handle circular reference in result gracefully', () => {
174
+ const obj: any = { a: 1 };
175
+ obj.self = obj;
176
+ const result = {
177
+ success: true,
178
+ result: obj
179
+ };
180
+
181
+ const formatted = formatToolOutput(result);
182
+
183
+ expect(formatted.output).toContain('[result]');
184
+ // Should not throw, falls back to String()
185
+ expect(formatted.output).toBeDefined();
186
+ });
187
+
188
+ it('should format number return value', () => {
189
+ const result = {
190
+ success: true,
191
+ result: 42
192
+ };
193
+
194
+ const formatted = formatToolOutput(result);
195
+
196
+ expect(formatted.output).toContain('[result]');
197
+ expect(formatted.output).toContain('42');
198
+ });
199
+ });