@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,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
|
+
});
|