ucode-agent 1.2.0 → 1.3.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/README.md +42 -8
- package/package.json +1 -1
- package/src/core/context.js +151 -0
- package/src/core/loop.js +376 -25
- package/src/core/provider.js +18 -3
- package/src/tools/files.js +173 -16
- package/src/tools/index.js +445 -394
- package/src/tools/shell.js +141 -0
- package/src/ui/plain.js +330 -325
- package/src/ui/screen.js +8 -2
- package/src/ui/theme.js +18 -0
package/src/tools/index.js
CHANGED
|
@@ -1,394 +1,445 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* index.js — the tool registry: schemas, argument checking, dispatch, and the
|
|
3
|
-
* line the user reads while each one runs.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { ToolFailure } from '../core/failure.js';
|
|
7
|
-
import { readFile, readFiles, writeFile, batchWrite, editFile, multiEdit } from './files.js';
|
|
8
|
-
import { listDir, glob, grep } from './search.js';
|
|
9
|
-
import { runCommand, runCommands } from './shell.js';
|
|
10
|
-
import { webSearch } from './web.js';
|
|
11
|
-
import { clip, READ_LINES } from './shared.js';
|
|
12
|
-
|
|
13
|
-
export { setRoot, setConfirm, getRoot } from './shared.js';
|
|
14
|
-
|
|
15
|
-
const str = (description) => ({ type: 'string', description });
|
|
16
|
-
const int = (description) => ({ type: 'integer', description });
|
|
17
|
-
const bool = (description) => ({ type: 'boolean', description });
|
|
18
|
-
|
|
19
|
-
export const tools = [
|
|
20
|
-
{
|
|
21
|
-
name: 'read_file',
|
|
22
|
-
description:
|
|
23
|
-
'Read a text file. Comes back as numbered lines — the numbers are for you to ' +
|
|
24
|
-
'refer to and must never appear in an edit_file argument. Long files arrive in ' +
|
|
25
|
-
'pages; pass offset to keep going.',
|
|
26
|
-
parameters: {
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
path: str('File path, relative to the project root.'),
|
|
30
|
-
offset: int('First line to read, 1-based. Defaults to 1.'),
|
|
31
|
-
limit: int(`How many lines. Defaults to ${READ_LINES}.`),
|
|
32
|
-
},
|
|
33
|
-
required: ['path'],
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: 'read_files',
|
|
38
|
-
description:
|
|
39
|
-
'Read several text files in one call. Use this whenever you need more than one ' +
|
|
40
|
-
'file - it is one round trip instead of one per file, so it is much faster than ' +
|
|
41
|
-
'calling read_file repeatedly. Same numbered-line output as read_file, one block ' +
|
|
42
|
-
'per file. A missing file is reported in its place without failing the others.',
|
|
43
|
-
parameters: {
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
paths: {
|
|
47
|
-
type: 'array',
|
|
48
|
-
description: 'File paths, relative to the project root. Up to 20.',
|
|
49
|
-
items: { type: 'string' },
|
|
50
|
-
},
|
|
51
|
-
limit: int(`Lines per file. Defaults to ${READ_LINES}.`),
|
|
52
|
-
},
|
|
53
|
-
required: ['paths'],
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'write_file',
|
|
58
|
-
description:
|
|
59
|
-
'Create a file, or replace all of its contents. For a change to part of an ' +
|
|
60
|
-
'existing file use edit_file instead — this one throws away everything that was ' +
|
|
61
|
-
'there. Missing parent directories are created.',
|
|
62
|
-
parameters: {
|
|
63
|
-
type: 'object',
|
|
64
|
-
properties: {
|
|
65
|
-
path: str('File path, relative to the project root.'),
|
|
66
|
-
content: str('The complete text of the file.'),
|
|
67
|
-
},
|
|
68
|
-
required: ['path', 'content'],
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'batch_write',
|
|
73
|
-
description:
|
|
74
|
-
'Create or replace several files in one call. Use this to lay out a whole ' +
|
|
75
|
-
'project at once instead of calling write_file over and over — it is the ' +
|
|
76
|
-
'difference between one round trip and twenty.',
|
|
77
|
-
parameters: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {
|
|
80
|
-
files: {
|
|
81
|
-
type: 'array',
|
|
82
|
-
description: 'The files to write.',
|
|
83
|
-
items: {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
path: str('File path, relative to the project root.'),
|
|
87
|
-
content: str('The complete text of the file.'),
|
|
88
|
-
},
|
|
89
|
-
required: ['path', 'content'],
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
required: ['files'],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: 'edit_file',
|
|
98
|
-
description:
|
|
99
|
-
'Replace one exact piece of text in a file. old_string must match the file ' +
|
|
100
|
-
'character for character, including indentation, and must occur exactly once — ' +
|
|
101
|
-
'the edit is refused on zero matches and on two. This is the normal way to ' +
|
|
102
|
-
'change existing code.',
|
|
103
|
-
parameters: {
|
|
104
|
-
type: 'object',
|
|
105
|
-
properties: {
|
|
106
|
-
path: str('File path, relative to the project root.'),
|
|
107
|
-
old_string: str('The exact text to replace. Must be unique in the file.'),
|
|
108
|
-
new_string: str('What to put there instead.'),
|
|
109
|
-
},
|
|
110
|
-
required: ['path', 'old_string', 'new_string'],
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
name: 'multi_edit',
|
|
115
|
-
description:
|
|
116
|
-
'Several exact replacements in one file, applied in order, each seeing the ' +
|
|
117
|
-
'result of the last. Same rules as edit_file for each one. If any of them is ' +
|
|
118
|
-
'ambiguous or missing, none are written at all. Prefer this to calling ' +
|
|
119
|
-
'edit_file repeatedly on the same file.',
|
|
120
|
-
parameters: {
|
|
121
|
-
type: 'object',
|
|
122
|
-
properties: {
|
|
123
|
-
path: str('File path, relative to the project root.'),
|
|
124
|
-
edits: {
|
|
125
|
-
type: 'array',
|
|
126
|
-
description: 'The replacements, in the order they should be applied.',
|
|
127
|
-
items: {
|
|
128
|
-
type: 'object',
|
|
129
|
-
properties: {
|
|
130
|
-
old_string: str('The exact text to replace. Must be unique at that point.'),
|
|
131
|
-
new_string: str('What to put there instead.'),
|
|
132
|
-
},
|
|
133
|
-
required: ['old_string', 'new_string'],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
required: ['path', 'edits'],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: '
|
|
142
|
-
description:
|
|
143
|
-
|
|
144
|
-
type
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
'
|
|
205
|
-
'
|
|
206
|
-
parameters: {
|
|
207
|
-
type: 'object',
|
|
208
|
-
properties: {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1
|
+
/**
|
|
2
|
+
* index.js — the tool registry: schemas, argument checking, dispatch, and the
|
|
3
|
+
* line the user reads while each one runs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ToolFailure } from '../core/failure.js';
|
|
7
|
+
import { readFile, readFiles, writeFile, batchWrite, editFile, multiEdit, editFiles } from './files.js';
|
|
8
|
+
import { listDir, glob, grep } from './search.js';
|
|
9
|
+
import { runCommand, runCommands } from './shell.js';
|
|
10
|
+
import { webSearch } from './web.js';
|
|
11
|
+
import { clip, READ_LINES } from './shared.js';
|
|
12
|
+
|
|
13
|
+
export { setRoot, setConfirm, getRoot } from './shared.js';
|
|
14
|
+
|
|
15
|
+
const str = (description) => ({ type: 'string', description });
|
|
16
|
+
const int = (description) => ({ type: 'integer', description });
|
|
17
|
+
const bool = (description) => ({ type: 'boolean', description });
|
|
18
|
+
|
|
19
|
+
export const tools = [
|
|
20
|
+
{
|
|
21
|
+
name: 'read_file',
|
|
22
|
+
description:
|
|
23
|
+
'Read a text file. Comes back as numbered lines — the numbers are for you to ' +
|
|
24
|
+
'refer to and must never appear in an edit_file argument. Long files arrive in ' +
|
|
25
|
+
'pages; pass offset to keep going.',
|
|
26
|
+
parameters: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
path: str('File path, relative to the project root.'),
|
|
30
|
+
offset: int('First line to read, 1-based. Defaults to 1.'),
|
|
31
|
+
limit: int(`How many lines. Defaults to ${READ_LINES}.`),
|
|
32
|
+
},
|
|
33
|
+
required: ['path'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'read_files',
|
|
38
|
+
description:
|
|
39
|
+
'Read several text files in one call. Use this whenever you need more than one ' +
|
|
40
|
+
'file - it is one round trip instead of one per file, so it is much faster than ' +
|
|
41
|
+
'calling read_file repeatedly. Same numbered-line output as read_file, one block ' +
|
|
42
|
+
'per file. A missing file is reported in its place without failing the others.',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
paths: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description: 'File paths, relative to the project root. Up to 20.',
|
|
49
|
+
items: { type: 'string' },
|
|
50
|
+
},
|
|
51
|
+
limit: int(`Lines per file. Defaults to ${READ_LINES}.`),
|
|
52
|
+
},
|
|
53
|
+
required: ['paths'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'write_file',
|
|
58
|
+
description:
|
|
59
|
+
'Create a file, or replace all of its contents. For a change to part of an ' +
|
|
60
|
+
'existing file use edit_file instead — this one throws away everything that was ' +
|
|
61
|
+
'there. Missing parent directories are created.',
|
|
62
|
+
parameters: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
path: str('File path, relative to the project root.'),
|
|
66
|
+
content: str('The complete text of the file.'),
|
|
67
|
+
},
|
|
68
|
+
required: ['path', 'content'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'batch_write',
|
|
73
|
+
description:
|
|
74
|
+
'Create or replace several files in one call. Use this to lay out a whole ' +
|
|
75
|
+
'project at once instead of calling write_file over and over — it is the ' +
|
|
76
|
+
'difference between one round trip and twenty.',
|
|
77
|
+
parameters: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
files: {
|
|
81
|
+
type: 'array',
|
|
82
|
+
description: 'The files to write.',
|
|
83
|
+
items: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
path: str('File path, relative to the project root.'),
|
|
87
|
+
content: str('The complete text of the file.'),
|
|
88
|
+
},
|
|
89
|
+
required: ['path', 'content'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: ['files'],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'edit_file',
|
|
98
|
+
description:
|
|
99
|
+
'Replace one exact piece of text in a file. old_string must match the file ' +
|
|
100
|
+
'character for character, including indentation, and must occur exactly once — ' +
|
|
101
|
+
'the edit is refused on zero matches and on two. This is the normal way to ' +
|
|
102
|
+
'change existing code.',
|
|
103
|
+
parameters: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
path: str('File path, relative to the project root.'),
|
|
107
|
+
old_string: str('The exact text to replace. Must be unique in the file.'),
|
|
108
|
+
new_string: str('What to put there instead.'),
|
|
109
|
+
},
|
|
110
|
+
required: ['path', 'old_string', 'new_string'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'multi_edit',
|
|
115
|
+
description:
|
|
116
|
+
'Several exact replacements in one file, applied in order, each seeing the ' +
|
|
117
|
+
'result of the last. Same rules as edit_file for each one. If any of them is ' +
|
|
118
|
+
'ambiguous or missing, none are written at all. Prefer this to calling ' +
|
|
119
|
+
'edit_file repeatedly on the same file.',
|
|
120
|
+
parameters: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
path: str('File path, relative to the project root.'),
|
|
124
|
+
edits: {
|
|
125
|
+
type: 'array',
|
|
126
|
+
description: 'The replacements, in the order they should be applied.',
|
|
127
|
+
items: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
old_string: str('The exact text to replace. Must be unique at that point.'),
|
|
131
|
+
new_string: str('What to put there instead.'),
|
|
132
|
+
},
|
|
133
|
+
required: ['old_string', 'new_string'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ['path', 'edits'],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'edit_files',
|
|
142
|
+
description:
|
|
143
|
+
'Exact replacements across several files in one call - the fastest way to make ' +
|
|
144
|
+
'a change that touches a route, a component and a type together. Same matching ' +
|
|
145
|
+
'rules as edit_file for every edit. If any edit in any file fails, nothing is ' +
|
|
146
|
+
'written anywhere.',
|
|
147
|
+
parameters: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
files: {
|
|
151
|
+
type: 'array',
|
|
152
|
+
description: 'One entry per file, each listed once.',
|
|
153
|
+
items: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
path: str('File path, relative to the project root.'),
|
|
157
|
+
edits: {
|
|
158
|
+
type: 'array',
|
|
159
|
+
description: 'Replacements for this file, in order.',
|
|
160
|
+
items: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
old_string: str('The exact text to replace.'),
|
|
164
|
+
new_string: str('What to put there instead.'),
|
|
165
|
+
},
|
|
166
|
+
required: ['old_string', 'new_string'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ['path', 'edits'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ['files'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'list_dir',
|
|
179
|
+
description: 'List what is in one directory, with file sizes.',
|
|
180
|
+
parameters: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: { path: str('Directory path. Defaults to the project root.') },
|
|
183
|
+
required: [],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'glob',
|
|
188
|
+
description:
|
|
189
|
+
'Find files by name pattern, most recently changed first. Understands **, *, ? ' +
|
|
190
|
+
'and {a,b}. node_modules, .git, dist and similar are skipped unless the pattern ' +
|
|
191
|
+
'names one of them.',
|
|
192
|
+
parameters: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
pattern: str('Glob pattern, e.g. "src/**/*.{ts,tsx}".'),
|
|
196
|
+
path: str('Directory to look under. Defaults to the project root.'),
|
|
197
|
+
},
|
|
198
|
+
required: ['pattern'],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'grep',
|
|
203
|
+
description:
|
|
204
|
+
'Search inside files with a regular expression. Returns file:line: text for ' +
|
|
205
|
+
'every match. Pass glob to limit which files get read.',
|
|
206
|
+
parameters: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
pattern: str('A JavaScript regular expression.'),
|
|
210
|
+
path: str('File or directory to search. Defaults to the project root.'),
|
|
211
|
+
glob: str('Optional filename filter, e.g. "**/*.js".'),
|
|
212
|
+
ignore_case: bool('Match case-insensitively. Defaults to false.'),
|
|
213
|
+
},
|
|
214
|
+
required: ['pattern'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'run_command',
|
|
219
|
+
description:
|
|
220
|
+
'Run a shell command and get back its output and exit code. It runs without ' +
|
|
221
|
+
'asking, so never run something destructive the user did not ask for. There is ' +
|
|
222
|
+
'no keyboard: pass the non-interactive flag to anything that would ask a question. ' +
|
|
223
|
+
'Dev servers (npm run dev, vite, next dev, uvicorn...) are started in the ' +
|
|
224
|
+
'background automatically and the result comes back as soon as the server says ' +
|
|
225
|
+
'it is ready, with the URL it is listening on - do not start one twice.',
|
|
226
|
+
parameters: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
command: str('The whole command line.'),
|
|
230
|
+
cwd: str('Directory to run it in. Defaults to the project root.'),
|
|
231
|
+
timeout_ms: int('Kill it after this many milliseconds. Default 120000.'),
|
|
232
|
+
background: bool('Start it detached and return its PID. For servers.'),
|
|
233
|
+
},
|
|
234
|
+
required: ['command'],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'run_commands',
|
|
239
|
+
description:
|
|
240
|
+
'Run several shell commands at once, up to max_parallel at a time. Good for ' +
|
|
241
|
+
'independent work — install, lint and test together rather than one after ' +
|
|
242
|
+
'another. Each entry takes the same fields as run_command.',
|
|
243
|
+
parameters: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
commands: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
description: 'The commands to run.',
|
|
249
|
+
items: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
command: str('The whole command line.'),
|
|
253
|
+
cwd: str('Directory to run it in. Defaults to the project root.'),
|
|
254
|
+
timeout_ms: int('Kill it after this many milliseconds. Default 120000.'),
|
|
255
|
+
background: bool('Start it detached and return its PID.'),
|
|
256
|
+
},
|
|
257
|
+
required: ['command'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
max_parallel: {
|
|
261
|
+
type: 'integer',
|
|
262
|
+
description: 'How many may run at once. Default 3.',
|
|
263
|
+
minimum: 1,
|
|
264
|
+
maximum: 10,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
required: ['commands'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'web_search',
|
|
272
|
+
description:
|
|
273
|
+
'Search the web and get back titles, links and summaries. For anything the ' +
|
|
274
|
+
'project files and your own knowledge cannot settle: current versions, recent ' +
|
|
275
|
+
'releases, an unfamiliar error, documentation for an API you do not know. Cite ' +
|
|
276
|
+
'the URLs you actually used.',
|
|
277
|
+
parameters: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
query: str('What to look up.'),
|
|
281
|
+
max_results: int('How many results, 1-10. Defaults to 5.'),
|
|
282
|
+
},
|
|
283
|
+
required: ['query'],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
const run = {
|
|
289
|
+
read_file: readFile,
|
|
290
|
+
read_files: readFiles,
|
|
291
|
+
write_file: writeFile,
|
|
292
|
+
batch_write: batchWrite,
|
|
293
|
+
edit_file: editFile,
|
|
294
|
+
multi_edit: multiEdit,
|
|
295
|
+
edit_files: editFiles,
|
|
296
|
+
list_dir: listDir,
|
|
297
|
+
glob,
|
|
298
|
+
grep,
|
|
299
|
+
run_command: runCommand,
|
|
300
|
+
run_commands: runCommands,
|
|
301
|
+
web_search: webSearch,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** Tools that change the project or execute code. */
|
|
305
|
+
export const MUTATING = new Set([
|
|
306
|
+
'write_file', 'batch_write', 'edit_file', 'multi_edit', 'edit_files', 'run_command', 'run_commands',
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
/** Tools with no side effects, so several may run at the same time. */
|
|
310
|
+
export const PARALLEL_SAFE = new Set(['read_file', 'read_files', 'list_dir', 'glob', 'grep', 'web_search']);
|
|
311
|
+
|
|
312
|
+
/** Tools withheld in plan mode. Withholding beats asking a model not to. */
|
|
313
|
+
export const WRITES = new Set([
|
|
314
|
+
'write_file', 'batch_write', 'edit_file', 'multi_edit', 'edit_files', 'run_command', 'run_commands',
|
|
315
|
+
'delegate',
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
/** Tools that change files on disk, which parallel workers take turns at. */
|
|
319
|
+
export const FILE_WRITES = new Set(['write_file', 'batch_write', 'edit_file', 'multi_edit', 'edit_files']);
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Argument checking
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check the model's arguments against the schema before anything runs.
|
|
327
|
+
*
|
|
328
|
+
* Catching it here means the model gets a precise sentence about what it got
|
|
329
|
+
* wrong and can correct itself, instead of a TypeError thrown from somewhere
|
|
330
|
+
* inside fs that means nothing to anybody.
|
|
331
|
+
*/
|
|
332
|
+
function check(name, args) {
|
|
333
|
+
const schema = tools.find((t) => t.name === name).parameters;
|
|
334
|
+
const problems = [];
|
|
335
|
+
|
|
336
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) {
|
|
337
|
+
return ['the arguments must be a JSON object'];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const key of schema.required ?? []) {
|
|
341
|
+
if (args[key] === undefined || args[key] === null) problems.push(`"${key}" is required and missing`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const [key, value] of Object.entries(args)) {
|
|
345
|
+
const spec = schema.properties[key];
|
|
346
|
+
if (!spec) {
|
|
347
|
+
problems.push(`"${key}" is not an argument of ${name} (it takes: ${Object.keys(schema.properties).join(', ')})`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (value === undefined || value === null) continue;
|
|
351
|
+
|
|
352
|
+
const actual = Array.isArray(value) ? 'array' : typeof value;
|
|
353
|
+
const wanted = spec.type === 'integer' ? 'number' : spec.type;
|
|
354
|
+
// A number sent as a string is close enough — the tool coerces it anyway.
|
|
355
|
+
if (wanted === 'number' && actual === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) continue;
|
|
356
|
+
if (actual !== wanted) problems.push(`"${key}" should be ${spec.type} but was ${actual}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return problems;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function runTool(name, args = {}, opts = {}) {
|
|
363
|
+
const impl = run[name];
|
|
364
|
+
if (!impl) {
|
|
365
|
+
throw new ToolFailure({
|
|
366
|
+
kind: 'no_such_tool',
|
|
367
|
+
attempted: `calling ${name}`,
|
|
368
|
+
failed: `There is no tool called "${name}".`,
|
|
369
|
+
fix: `The tools you have are: ${tools.map((t) => t.name).join(', ')}.`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const problems = check(name, args);
|
|
374
|
+
if (problems.length) {
|
|
375
|
+
throw new ToolFailure({
|
|
376
|
+
kind: 'bad_args',
|
|
377
|
+
attempted: `calling ${name}`,
|
|
378
|
+
failed: `The arguments were wrong: ${problems.join('; ')}.`,
|
|
379
|
+
fix: `Call ${name} again with them corrected. Its schema is: ${JSON.stringify(
|
|
380
|
+
tools.find((t) => t.name === name).parameters
|
|
381
|
+
)}`,
|
|
382
|
+
detail: { problems },
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return impl(args, opts);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The line shown while a call runs: "Listing src", "Running npm test".
|
|
391
|
+
*
|
|
392
|
+
* Present tense, no trailing full stop — it is a label on something happening
|
|
393
|
+
* now, not a sentence about something that happened. It is built from the call
|
|
394
|
+
* itself rather than from what the model said it would do, so it is always an
|
|
395
|
+
* account of the real work.
|
|
396
|
+
*/
|
|
397
|
+
export function describe(name, args = {}) {
|
|
398
|
+
switch (name) {
|
|
399
|
+
case 'read_file':
|
|
400
|
+
return `Reading ${clip(args.path)}${args.offset > 1 ? ` from line ${args.offset}` : ''}`;
|
|
401
|
+
case 'read_files': {
|
|
402
|
+
const names = (args.paths ?? []).map((p) => String(p));
|
|
403
|
+
const joined = names.join(', ');
|
|
404
|
+
return names.length && joined.length <= 60 ? `Reading ${joined}` : `Reading ${names.length} files`;
|
|
405
|
+
}
|
|
406
|
+
case 'write_file':
|
|
407
|
+
return `Writing ${clip(args.path)}`;
|
|
408
|
+
case 'batch_write': {
|
|
409
|
+
const n = args.files?.length ?? 0;
|
|
410
|
+
const first = args.files?.[0]?.path;
|
|
411
|
+
return n === 1 && first ? `Writing ${clip(first)}` : `Writing ${n} files`;
|
|
412
|
+
}
|
|
413
|
+
case 'edit_file':
|
|
414
|
+
return `Editing ${clip(args.path)}`;
|
|
415
|
+
case 'multi_edit':
|
|
416
|
+
return `Editing ${clip(args.path)}, ${args.edits?.length ?? 0} changes`;
|
|
417
|
+
case 'edit_files': {
|
|
418
|
+
const n = args.files?.length ?? 0;
|
|
419
|
+
const first = args.files?.[0]?.path;
|
|
420
|
+
return n === 1 && first ? `Editing ${clip(first)}` : `Editing ${n} files`;
|
|
421
|
+
}
|
|
422
|
+
case 'update_plan':
|
|
423
|
+
return 'Updating the plan';
|
|
424
|
+
case 'delegate':
|
|
425
|
+
return `Starting ${args.tasks?.length ?? 0} workers in parallel`;
|
|
426
|
+
case 'list_dir':
|
|
427
|
+
return !args.path || args.path === '.'
|
|
428
|
+
? 'Listing the project root'
|
|
429
|
+
: `Listing ${clip(args.path)}`;
|
|
430
|
+
case 'glob':
|
|
431
|
+
return `Finding ${clip(args.pattern)}`;
|
|
432
|
+
case 'grep':
|
|
433
|
+
return `Searching for ${clip(args.pattern, 40)}${args.glob ? ` in ${clip(args.glob, 20)}` : ''}`;
|
|
434
|
+
case 'run_command':
|
|
435
|
+
return `Running ${clip(args.command, 70)}${args.background ? ' in the background' : ''}`;
|
|
436
|
+
case 'run_commands':
|
|
437
|
+
return `Running ${args.commands?.length ?? 0} commands together`;
|
|
438
|
+
case 'web_search':
|
|
439
|
+
return `Searching the web for ${clip(args.query, 60)}`;
|
|
440
|
+
case 'load_skill':
|
|
441
|
+
return `Loading the ${clip(args.name, 40)} skill`;
|
|
442
|
+
default:
|
|
443
|
+
return `${name} ${clip(JSON.stringify(args), 60)}`;
|
|
444
|
+
}
|
|
445
|
+
}
|