@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,475 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { executeWithAsyncHost } from '../test-helper.js';
3
+ import { installSearch } from './search.js';
4
+ import type { SearchMountInfo } from './search.js';
5
+
6
+ function createMockSmartBucket() {
7
+ const searches = new Map<string, any>();
8
+
9
+ const mockSmartBucket: any = {
10
+ search: vi.fn(async ({ input, requestId }: { input: string; requestId: string }) => {
11
+ // Store the search with requestId for pagination
12
+ const page = 1;
13
+ const pageSize = 5;
14
+
15
+ // Simulate 12 results total (3 pages of 5)
16
+ const allResults = Array.from({ length: 12 }, (_, i) => ({
17
+ text: `Result ${i + 1} for ${input}`,
18
+ source: `source-${i + 1}.txt`,
19
+ score: 0.9 - (i * 0.05),
20
+ chunkSignature: `chunk-${i + 1}`
21
+ }));
22
+
23
+ const results = allResults.slice(0, pageSize);
24
+ searches.set(requestId, allResults);
25
+
26
+ return {
27
+ results,
28
+ pagination: {
29
+ total: allResults.length,
30
+ page,
31
+ pageSize,
32
+ hasMore: true
33
+ }
34
+ };
35
+ }),
36
+
37
+ chunkSearch: vi.fn(async ({ input, requestId: _requestId }: { input: string; requestId: string }) => {
38
+ // RAG-style search returns all results at once
39
+ return {
40
+ results: [
41
+ {
42
+ text: `Chunk result 1 for ${input}`,
43
+ source: 'doc1.txt',
44
+ score: 0.95,
45
+ chunkSignature: 'chunk-1'
46
+ },
47
+ {
48
+ text: `Chunk result 2 for ${input}`,
49
+ source: 'doc1.txt',
50
+ score: 0.88,
51
+ chunkSignature: 'chunk-2'
52
+ }
53
+ ]
54
+ };
55
+ }),
56
+
57
+ getPaginatedResults: vi.fn(async ({ requestId, page }: { requestId: string; page: number }) => {
58
+ const allResults = searches.get(requestId);
59
+ if (!allResults) {
60
+ throw new Error('Search not found');
61
+ }
62
+
63
+ const pageSize = 5;
64
+ const startIdx = (page - 1) * pageSize;
65
+ const results = allResults.slice(startIdx, startIdx + pageSize);
66
+ const hasMore = startIdx + pageSize < allResults.length;
67
+
68
+ return {
69
+ results,
70
+ pagination: {
71
+ total: allResults.length,
72
+ page,
73
+ pageSize,
74
+ hasMore
75
+ }
76
+ };
77
+ })
78
+ };
79
+
80
+ return mockSmartBucket;
81
+ }
82
+
83
+ describe('Search Bridge', () => {
84
+ describe('Basic Search', () => {
85
+ it('should perform search and return results', async () => {
86
+ const mockSmartBucket = createMockSmartBucket();
87
+ const searchMounts = new Map<string, SearchMountInfo>([
88
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
89
+ ]);
90
+
91
+ const result = await executeWithAsyncHost(
92
+ `
93
+ const results = await search("/search/", "test query");
94
+ return {
95
+ count: results.results.length,
96
+ total: results.total,
97
+ hasMore: results.hasMore,
98
+ page: results.page,
99
+ pageSize: results.pageSize
100
+ };
101
+ `,
102
+ {},
103
+ {
104
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
105
+ }
106
+ );
107
+
108
+ expect(result.success).toBe(true);
109
+ expect(result.result.count).toBe(5);
110
+ expect(result.result.total).toBe(12);
111
+ expect(result.result.hasMore).toBe(true);
112
+ expect(result.result.page).toBe(1);
113
+ expect(result.result.pageSize).toBe(5);
114
+ });
115
+
116
+ it('should return results with correct structure', async () => {
117
+ const mockSmartBucket = createMockSmartBucket();
118
+ const searchMounts = new Map<string, SearchMountInfo>([
119
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
120
+ ]);
121
+
122
+ const result = await executeWithAsyncHost(
123
+ `
124
+ const results = await search("/search/", "test query");
125
+ const firstResult = results.results[0];
126
+ return {
127
+ hasText: typeof firstResult.text === 'string',
128
+ hasSource: typeof firstResult.source === 'string',
129
+ hasScore: typeof firstResult.score === 'number',
130
+ hasChunkSignature: typeof firstResult.chunkSignature === 'string'
131
+ };
132
+ `,
133
+ {},
134
+ {
135
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
136
+ }
137
+ );
138
+
139
+ expect(result.success).toBe(true);
140
+ expect(result.result.hasText).toBe(true);
141
+ expect(result.result.hasSource).toBe(true);
142
+ expect(result.result.hasScore).toBe(true);
143
+ expect(result.result.hasChunkSignature).toBe(true);
144
+ });
145
+
146
+ it('should handle invalid mount path', async () => {
147
+ const mockSmartBucket = createMockSmartBucket();
148
+ const searchMounts = new Map<string, SearchMountInfo>([
149
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
150
+ ]);
151
+
152
+ const result = await executeWithAsyncHost(
153
+ `
154
+ try {
155
+ await search("/invalid/", "test");
156
+ return { error: false };
157
+ } catch (e) {
158
+ return { error: true, message: String(e) };
159
+ }
160
+ `,
161
+ {},
162
+ {
163
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
164
+ }
165
+ );
166
+
167
+ expect(result.success).toBe(true);
168
+ expect(result.result.error).toBe(true);
169
+ expect(result.result.message).toContain('not found');
170
+ });
171
+ });
172
+
173
+ describe('Pagination', () => {
174
+ it('should navigate to next page', async () => {
175
+ const mockSmartBucket = createMockSmartBucket();
176
+ const searchMounts = new Map<string, SearchMountInfo>([
177
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
178
+ ]);
179
+
180
+ const result = await executeWithAsyncHost(
181
+ `
182
+ const page1 = await search("/search/", "test query");
183
+ const page2 = await page1.nextPage();
184
+ return {
185
+ page1Count: page1.results.length,
186
+ page2Count: page2.results.length,
187
+ page2Page: page2.page,
188
+ page2HasMore: page2.hasMore
189
+ };
190
+ `,
191
+ {},
192
+ {
193
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
194
+ }
195
+ );
196
+
197
+ expect(result.success).toBe(true);
198
+ expect(result.result.page1Count).toBe(5);
199
+ expect(result.result.page2Count).toBe(5);
200
+ expect(result.result.page2Page).toBe(2);
201
+ expect(result.result.page2HasMore).toBe(true);
202
+ });
203
+
204
+ it('should return empty results when no more pages', async () => {
205
+ const mockSmartBucket = createMockSmartBucket();
206
+ const searchMounts = new Map<string, SearchMountInfo>([
207
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
208
+ ]);
209
+
210
+ const result = await executeWithAsyncHost(
211
+ `
212
+ const page1 = await search("/search/", "test query");
213
+ const page2 = await page1.nextPage();
214
+ const page3 = await page2.nextPage();
215
+ const page4 = await page3.nextPage();
216
+ return {
217
+ page3Count: page3.results.length,
218
+ page4Count: page4.results.length,
219
+ page4HasMore: page4.hasMore
220
+ };
221
+ `,
222
+ {},
223
+ {
224
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
225
+ }
226
+ );
227
+
228
+ expect(result.success).toBe(true);
229
+ expect(result.result.page3Count).toBe(2); // Last page has 2 items
230
+ expect(result.result.page4Count).toBe(0);
231
+ expect(result.result.page4HasMore).toBe(false);
232
+ });
233
+
234
+ it('should paginate through all pages and iterate total results', async () => {
235
+ const mockSmartBucket = createMockSmartBucket();
236
+ const searchMounts = new Map<string, SearchMountInfo>([
237
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
238
+ ]);
239
+
240
+ const result = await executeWithAsyncHost(
241
+ `
242
+ let current = await search("/search/", "test query");
243
+ let total = 0;
244
+ while (current) {
245
+ total += current.results.length;
246
+ if (current.hasMore) {
247
+ current = await current.nextPage();
248
+ } else {
249
+ current = null;
250
+ }
251
+ }
252
+ return total;
253
+ `,
254
+ {},
255
+ {
256
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
257
+ }
258
+ );
259
+
260
+ expect(result.success).toBe(true);
261
+ expect(result.result).toBe(12);
262
+ });
263
+ });
264
+
265
+ describe('Async Iterator', () => {
266
+ it('should support for-await iteration', async () => {
267
+ const mockSmartBucket = createMockSmartBucket();
268
+ const searchMounts = new Map<string, SearchMountInfo>([
269
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
270
+ ]);
271
+
272
+ const result = await executeWithAsyncHost(
273
+ `
274
+ const results = await search("/search/", "test query");
275
+ let count = 0;
276
+ for await (const item of results) {
277
+ count++;
278
+ }
279
+ return count;
280
+ `,
281
+ {},
282
+ {
283
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
284
+ }
285
+ );
286
+
287
+ expect(result.success).toBe(true);
288
+ expect(result.result).toBe(12);
289
+ });
290
+
291
+ it('should iterate through all pages automatically', async () => {
292
+ const mockSmartBucket = createMockSmartBucket();
293
+ const searchMounts = new Map<string, SearchMountInfo>([
294
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
295
+ ]);
296
+
297
+ const result = await executeWithAsyncHost(
298
+ `
299
+ const results = await search("/search/", "test query");
300
+ const texts = [];
301
+ for await (const item of results) {
302
+ texts.push(item.text);
303
+ }
304
+ return {
305
+ count: texts.length,
306
+ firstText: texts[0],
307
+ lastText: texts[texts.length - 1]
308
+ };
309
+ `,
310
+ {},
311
+ {
312
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
313
+ }
314
+ );
315
+
316
+ expect(result.success).toBe(true);
317
+ expect(result.result.count).toBe(12);
318
+ expect(result.result.firstText).toContain('Result 1');
319
+ expect(result.result.lastText).toContain('Result 12');
320
+ });
321
+ });
322
+
323
+ describe('chunkSearch', () => {
324
+ it('should perform chunk search without pagination', async () => {
325
+ const mockSmartBucket = createMockSmartBucket();
326
+ const searchMounts = new Map<string, SearchMountInfo>([
327
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
328
+ ]);
329
+
330
+ const result = await executeWithAsyncHost(
331
+ `
332
+ const results = await chunkSearch("/search/", "rag query");
333
+ return {
334
+ count: results.results.length,
335
+ hasText: typeof results.results[0].text === 'string'
336
+ };
337
+ `,
338
+ {},
339
+ {
340
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
341
+ }
342
+ );
343
+
344
+ expect(result.success).toBe(true);
345
+ expect(result.result.count).toBe(2);
346
+ expect(result.result.hasText).toBe(true);
347
+ });
348
+
349
+ it('should return chunk results with all expected fields', async () => {
350
+ const mockSmartBucket = createMockSmartBucket();
351
+ const searchMounts = new Map<string, SearchMountInfo>([
352
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
353
+ ]);
354
+
355
+ const result = await executeWithAsyncHost(
356
+ `
357
+ const results = await chunkSearch("/search/", "rag query");
358
+ const first = results.results[0];
359
+ return {
360
+ text: first.text,
361
+ source: first.source,
362
+ score: first.score,
363
+ chunkSignature: first.chunkSignature
364
+ };
365
+ `,
366
+ {},
367
+ {
368
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
369
+ }
370
+ );
371
+
372
+ expect(result.success).toBe(true);
373
+ expect(result.result.text).toContain('Chunk result 1');
374
+ expect(result.result.source).toBe('doc1.txt');
375
+ expect(typeof result.result.score).toBe('number');
376
+ expect(result.result.chunkSignature).toBe('chunk-1');
377
+ });
378
+ });
379
+
380
+ describe('Edge Cases', () => {
381
+ it('should handle empty search results', async () => {
382
+ const mockSmartBucket = createMockSmartBucket();
383
+ // Override search to return empty results
384
+ mockSmartBucket.search = vi.fn(async () => ({
385
+ results: [],
386
+ pagination: {
387
+ total: 0,
388
+ page: 1,
389
+ pageSize: 5,
390
+ hasMore: false
391
+ }
392
+ }));
393
+
394
+ const searchMounts = new Map<string, SearchMountInfo>([
395
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
396
+ ]);
397
+
398
+ const result = await executeWithAsyncHost(
399
+ `
400
+ const results = await search("/search/", "no results query");
401
+ return {
402
+ count: results.results.length,
403
+ hasMore: results.hasMore,
404
+ total: results.total
405
+ };
406
+ `,
407
+ {},
408
+ {
409
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
410
+ }
411
+ );
412
+
413
+ expect(result.success).toBe(true);
414
+ expect(result.result.count).toBe(0);
415
+ expect(result.result.hasMore).toBe(false);
416
+ expect(result.result.total).toBe(0);
417
+ });
418
+
419
+ it('should handle single page of results', async () => {
420
+ const mockSmartBucket = createMockSmartBucket();
421
+ // Override search to return exactly one page
422
+ mockSmartBucket.search = vi.fn(async () => ({
423
+ results: [
424
+ {
425
+ text: 'Single result',
426
+ source: 'doc.txt',
427
+ score: 0.9,
428
+ chunkSignature: 'chunk-1'
429
+ }
430
+ ],
431
+ pagination: {
432
+ total: 1,
433
+ page: 1,
434
+ pageSize: 5,
435
+ hasMore: false
436
+ }
437
+ }));
438
+
439
+ mockSmartBucket.getPaginatedResults = vi.fn(async () => ({
440
+ results: [],
441
+ pagination: {
442
+ total: 1,
443
+ page: 2,
444
+ pageSize: 5,
445
+ hasMore: false
446
+ }
447
+ }));
448
+
449
+ const searchMounts = new Map<string, SearchMountInfo>([
450
+ ['search', { name: 'search', smartbucket: mockSmartBucket as any }]
451
+ ]);
452
+
453
+ const result = await executeWithAsyncHost(
454
+ `
455
+ const results = await search("/search/", "single result query");
456
+ const page2 = await results.nextPage();
457
+ return {
458
+ page1Count: results.results.length,
459
+ page1HasMore: results.hasMore,
460
+ page2Count: page2.results.length
461
+ };
462
+ `,
463
+ {},
464
+ {
465
+ bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
466
+ }
467
+ );
468
+
469
+ expect(result.success).toBe(true);
470
+ expect(result.result.page1Count).toBe(1);
471
+ expect(result.result.page1HasMore).toBe(false);
472
+ expect(result.result.page2Count).toBe(0);
473
+ });
474
+ });
475
+ });