primocode 1.0.1 → 4.1.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 +146 -88
- package/bin/primocode.js +574 -390
- package/lib/act.js +500 -495
- package/lib/api.js +39 -39
- package/lib/browser.js +492 -492
- package/lib/config.js +76 -71
- package/lib/mascot.js +121 -0
- package/lib/memory.js +179 -0
- package/lib/mouse.js +144 -0
- package/lib/platform.js +245 -0
- package/lib/tools.js +503 -503
- package/lib/ui.js +104 -132
- package/package.json +32 -31
- package/lib/jarvis.js +0 -666
package/lib/act.js
CHANGED
|
@@ -1,495 +1,500 @@
|
|
|
1
|
-
// Modo
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
case '
|
|
21
|
-
case '
|
|
22
|
-
case '
|
|
23
|
-
case '
|
|
24
|
-
case '
|
|
25
|
-
case '
|
|
26
|
-
case '
|
|
27
|
-
case '
|
|
28
|
-
case '
|
|
29
|
-
case '
|
|
30
|
-
case '
|
|
31
|
-
case '
|
|
32
|
-
case '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
let
|
|
125
|
-
let
|
|
126
|
-
let
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let
|
|
138
|
-
let
|
|
139
|
-
let
|
|
140
|
-
let
|
|
141
|
-
let
|
|
142
|
-
let
|
|
143
|
-
let
|
|
144
|
-
let
|
|
145
|
-
let
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
if (evt.
|
|
168
|
-
if (sp) sp.stop();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (sp)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
}
|
|
196
|
-
if (evt.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
if (evt.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.replace(
|
|
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
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
let
|
|
380
|
-
let
|
|
381
|
-
let
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1
|
+
// Modo Primo Code — faz tudo sozinho: código, arquivos, terminal, navegador
|
|
2
|
+
// REGRA FUNDAMENTAL: NUNCA entregue código no CMD/terminal.
|
|
3
|
+
// SEMPRE use write_file para criar arquivos.
|
|
4
|
+
// Se for executável (HTML, etc.), use open_in_browser para abrir.
|
|
5
|
+
// Se for Python/JS, use run_in_new_terminal para executar.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { c, actionHeader, toolLine, Spinner } = require('./ui');
|
|
12
|
+
const tools = require('./tools');
|
|
13
|
+
const api = require('./api');
|
|
14
|
+
|
|
15
|
+
const MAX_ITERATIONS = 15;
|
|
16
|
+
|
|
17
|
+
function fmtArgs(name, args) {
|
|
18
|
+
if (!args) return '';
|
|
19
|
+
switch (name) {
|
|
20
|
+
case 'read_file': return args.path || '';
|
|
21
|
+
case 'write_file': return `${args.path || ''} · ${(args.content || '').length} chars`;
|
|
22
|
+
case 'list_dir': return args.path || '.';
|
|
23
|
+
case 'run_shell': return args.command || '';
|
|
24
|
+
case 'run_in_new_terminal':return `${args.command || ''}${args.cwd ? ' @ ' + args.cwd : ''}`;
|
|
25
|
+
case 'open_in_browser': return args.target || '';
|
|
26
|
+
case 'browser_open': return args.url || '';
|
|
27
|
+
case 'browser_navigate': return args.url || '';
|
|
28
|
+
case 'browser_click': return args.selector || args.text || (args.x != null ? `${args.x},${args.y}` : '');
|
|
29
|
+
case 'browser_type': return `${args.selector ? args.selector + ' · ' : ''}${(args.text || '').length} chars`;
|
|
30
|
+
case 'browser_key': return args.key || '';
|
|
31
|
+
case 'browser_wait_for': return args.selector || '';
|
|
32
|
+
case 'browser_wait': return `${args.ms || 1000}ms`;
|
|
33
|
+
case 'browser_screenshot': return args.path || '(auto)';
|
|
34
|
+
case 'browser_eval': return (args.js || '').slice(0, 60);
|
|
35
|
+
case 'browser_scroll': return `dy=${args.dy || 400}`;
|
|
36
|
+
case 'browser_url': return '';
|
|
37
|
+
case 'finish': return args.summary || '';
|
|
38
|
+
default: return JSON.stringify(args).slice(0, 80);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Nomes simples das ações
|
|
43
|
+
const NICE = {
|
|
44
|
+
read_file: 'Read', write_file: 'Write', list_dir: 'List',
|
|
45
|
+
run_shell: 'Bash', run_in_new_terminal: 'Terminal', open_in_browser: 'Open',
|
|
46
|
+
browser_open: 'Browser', browser_navigate: 'Navigate', browser_click: 'Click',
|
|
47
|
+
browser_clickText: 'ClickText', browser_type: 'Type', browser_key: 'Key',
|
|
48
|
+
browser_wait_for: 'Wait', browser_wait: 'Wait', browser_screenshot: 'Screenshot',
|
|
49
|
+
browser_eval: 'Eval', browser_scroll: 'Scroll', browser_url: 'URL', finish: 'Finish',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function makeLogger() {
|
|
53
|
+
return {
|
|
54
|
+
event(kind, arg, result) {
|
|
55
|
+
if (kind === 'run_shell') return;
|
|
56
|
+
if (kind === 'run_shell_done') {
|
|
57
|
+
if (result) {
|
|
58
|
+
const s = result.ok ? c.ok(' exit 0') : c.err(` exit ${result.code}${result.killed ? ' timeout' : ''}`);
|
|
59
|
+
process.stdout.write('\n' + s + '\n');
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const prefix = c.muted(' ⎿ ');
|
|
64
|
+
if (kind === 'write_file' && result && result.ok) {
|
|
65
|
+
console.log(prefix + c.muted(`${result.created ? 'created' : 'updated'} · ${tools.fmtBytes(result.bytes)}`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (kind === 'read_file' && result && result.ok) {
|
|
69
|
+
console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes || 0)}${result.truncated ? ' (truncated)' : ''}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (kind === 'list_dir' && result && result.ok) {
|
|
73
|
+
const lines = result.tree.split('\n').slice(0, 8);
|
|
74
|
+
for (const l of lines) console.log(prefix + c.muted(l));
|
|
75
|
+
const total = result.tree.split('\n').length;
|
|
76
|
+
if (total > 8) console.log(prefix + c.muted(`… +${total - 8}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (kind === 'open_in_browser' && result && result.ok) {
|
|
80
|
+
console.log(prefix + c.muted(`${result.opener || 'browser'}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (kind === 'run_in_new_terminal' && result && result.ok) {
|
|
84
|
+
console.log(prefix + c.muted(`${result.opener}`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (kind === 'finish') {
|
|
88
|
+
console.log(prefix + c.muted(arg || 'ok'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (result && result.ok === false) {
|
|
92
|
+
console.log(prefix + c.err(result.error || 'failed'));
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Executa uma tarefa em modo Act (agente autônomo com tools).
|
|
100
|
+
* @param {string} userPrompt - texto do usuário
|
|
101
|
+
* @param {object} opts - { server, model, history: [{role,content}, ...] }
|
|
102
|
+
* @returns {Promise<{userMessage, assistantMessage}>} para o caller atualizar seu histórico
|
|
103
|
+
*/
|
|
104
|
+
async function runAct(userPrompt, opts) {
|
|
105
|
+
const { server, model, history: chatHistory = [] } = opts;
|
|
106
|
+
|
|
107
|
+
// Injeta contexto do CWD e inclui histórico curto de conversa
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
const cwdBase = path.basename(cwd);
|
|
110
|
+
const enriched = `[contexto: diretório atual = ${cwd} (basename: '${cwdBase}')]\n\n${userPrompt}`;
|
|
111
|
+
|
|
112
|
+
// Estado do loop
|
|
113
|
+
// Começamos com as mensagens do chat (para memória) + o novo user prompt.
|
|
114
|
+
let messages = [];
|
|
115
|
+
if (Array.isArray(chatHistory) && chatHistory.length) {
|
|
116
|
+
// Só passamos user/assistant sem tool_calls (o histórico de tool calls foi episódico).
|
|
117
|
+
for (const m of chatHistory) {
|
|
118
|
+
if (m && (m.role === 'user' || m.role === 'assistant') && m.content) {
|
|
119
|
+
messages.push({ role: m.role, content: m.content });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let promptForCall = enriched;
|
|
125
|
+
let toolResults = null;
|
|
126
|
+
let finished = false;
|
|
127
|
+
const log = makeLogger();
|
|
128
|
+
const recentCalls = [];
|
|
129
|
+
let finalAssistantText = '';
|
|
130
|
+
let sp = null;
|
|
131
|
+
let planCache = ''; // cache do plano do Planner entre turnos
|
|
132
|
+
let planShown = false; // evita mostrar plano múltiplas vezes
|
|
133
|
+
const filesWritten = new Set(); // arquivos criados/modificados pelo Construtor 1 (para refino)
|
|
134
|
+
const deferredOpens = []; // open_in_browser adiados até depois do refino
|
|
135
|
+
|
|
136
|
+
for (let iter = 1; iter <= MAX_ITERATIONS && !finished; iter++) {
|
|
137
|
+
let assistantContent = '';
|
|
138
|
+
let assistantMessage = null;
|
|
139
|
+
let toolCalls = [];
|
|
140
|
+
let serverErr = null;
|
|
141
|
+
let hasToolCalls = false;
|
|
142
|
+
let streamingStarted = false;
|
|
143
|
+
let printedUpTo = 0; // quantos chars de assistantContent já foram impressos
|
|
144
|
+
let suppressRest = false; // uma vez detectado marker, para de imprimir
|
|
145
|
+
let retryNeeded = false; // server pedidu retry (ex: finish rejeitado)
|
|
146
|
+
let rejectedNote = null; // motivo da rejeição, injetado como tool_result no próximo turno
|
|
147
|
+
let subagentsPayload = null; // resultado de spawn_parallel (injetado como tool_result)
|
|
148
|
+
let subagentsToolCallId = null;
|
|
149
|
+
let subagentsTotal = 0;
|
|
150
|
+
let subagentsDone = 0;
|
|
151
|
+
|
|
152
|
+
// Spinner enquanto aguarda primeiro byte
|
|
153
|
+
sp = new Spinner('Pensando').start();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await api.requestStream(server, '/api/act/stream', {
|
|
157
|
+
prompt: promptForCall,
|
|
158
|
+
history: messages,
|
|
159
|
+
tool_results: toolResults,
|
|
160
|
+
model,
|
|
161
|
+
plan_cache: planCache,
|
|
162
|
+
}, (evt) => {
|
|
163
|
+
if (evt.phase === 'inspiring') {
|
|
164
|
+
if (sp) sp.stop();
|
|
165
|
+
sp = new Spinner('Buscando referências').start();
|
|
166
|
+
}
|
|
167
|
+
if (evt.inspiration_urls && Array.isArray(evt.inspiration_urls) && evt.inspiration_urls.length > 0) {
|
|
168
|
+
if (sp) { sp.stop(); sp = null; }
|
|
169
|
+
console.log('\n' + c.muted('◆ Inspirando-se em:'));
|
|
170
|
+
for (const u of evt.inspiration_urls) console.log(c.muted(' · ') + c.accent(u));
|
|
171
|
+
}
|
|
172
|
+
if (evt.phase === 'planning' && !planShown) {
|
|
173
|
+
if (sp) sp.stop();
|
|
174
|
+
sp = new Spinner('Pensando').start();
|
|
175
|
+
}
|
|
176
|
+
if (evt.plan) {
|
|
177
|
+
planCache = evt.plan;
|
|
178
|
+
if (sp) { sp.stop(); sp = null; }
|
|
179
|
+
planShown = true;
|
|
180
|
+
sp = new Spinner('Executando').start();
|
|
181
|
+
}
|
|
182
|
+
if (evt.subagents_spawning) {
|
|
183
|
+
subagentsTotal = evt.subagents_spawning;
|
|
184
|
+
if (sp) sp.stop();
|
|
185
|
+
console.log('\n' + c.muted(`◆ Spawn de ${subagentsTotal} subagentes em paralelo`));
|
|
186
|
+
sp = new Spinner(`Subagentes 0/${subagentsTotal}`).start();
|
|
187
|
+
}
|
|
188
|
+
if (evt.subagent_done) {
|
|
189
|
+
subagentsDone++;
|
|
190
|
+
const d = evt.subagent_done;
|
|
191
|
+
if (sp) { sp.stop(); sp = null; }
|
|
192
|
+
const status = d.ok ? c.ok('✓') : c.err('✗');
|
|
193
|
+
console.log(` ${status} ${c.muted(d.title)} ${c.muted('· ' + (d.bytes || 0) + ' bytes')}`);
|
|
194
|
+
if (subagentsDone < subagentsTotal) sp = new Spinner(`Subagentes ${subagentsDone}/${subagentsTotal}`).start();
|
|
195
|
+
}
|
|
196
|
+
if (evt.subagents_result) {
|
|
197
|
+
subagentsPayload = evt.subagents_result;
|
|
198
|
+
subagentsToolCallId = evt.tool_call_id;
|
|
199
|
+
if (sp) { sp.stop(); sp = null; }
|
|
200
|
+
}
|
|
201
|
+
if (evt.erro) { serverErr = evt.erro; return; }
|
|
202
|
+
if (evt.delta) {
|
|
203
|
+
if (!streamingStarted) {
|
|
204
|
+
sp.stop();
|
|
205
|
+
process.stdout.write('\n');
|
|
206
|
+
streamingStarted = true;
|
|
207
|
+
}
|
|
208
|
+
assistantContent += evt.delta;
|
|
209
|
+
|
|
210
|
+
if (suppressRest) return;
|
|
211
|
+
|
|
212
|
+
// Se aparece começo de marker suspeito, marca e para de imprimir daqui em diante.
|
|
213
|
+
// Também "rebobina" cortando o que já foi acumulado até antes do marker
|
|
214
|
+
// (não conseguimos apagar do terminal, mas o texto final ficará limpo).
|
|
215
|
+
const leakRe = /<\||【|\n\s*\{\s*"(name|type|summary|arguments|command|path)"|\bfinish\s*\(|\bwrite_file\s*\(|\brun_shell\s*\(|\bopen_in_browser\s*\(|\bbrowser_\w+\s*\(/;
|
|
216
|
+
const m = assistantContent.match(leakRe);
|
|
217
|
+
if (m && m.index !== undefined) {
|
|
218
|
+
const safeEnd = m.index;
|
|
219
|
+
if (safeEnd > printedUpTo) {
|
|
220
|
+
process.stdout.write(c.white(assistantContent.slice(printedUpTo, safeEnd)));
|
|
221
|
+
printedUpTo = safeEnd;
|
|
222
|
+
}
|
|
223
|
+
suppressRest = true;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Imprime até len-2 para dar margem se um marker começar a formar
|
|
228
|
+
const target = Math.max(printedUpTo, assistantContent.length - 2);
|
|
229
|
+
if (target > printedUpTo) {
|
|
230
|
+
process.stdout.write(c.white(assistantContent.slice(printedUpTo, target)));
|
|
231
|
+
printedUpTo = target;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (evt.tool_call) toolCalls.push(evt.tool_call);
|
|
235
|
+
if (evt.assistant_message) assistantMessage = evt.assistant_message;
|
|
236
|
+
if (evt.tool_rejected) rejectedNote = evt.tool_rejected;
|
|
237
|
+
if (evt.retry_needed) retryNeeded = true;
|
|
238
|
+
if (evt.done) hasToolCalls = !!evt.has_tool_calls;
|
|
239
|
+
if (evt.instability) { serverErr = 'servidor com rate-limit; tente novamente em alguns minutos'; return; }
|
|
240
|
+
});
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (sp) sp.stop();
|
|
243
|
+
console.log(c.err(`\nerro: ${e.message}`));
|
|
244
|
+
return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: `[erro: ${e.message}]` } };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (sp && !streamingStarted) sp.stop();
|
|
248
|
+
|
|
249
|
+
// Flush do buffer de 2 chars retido para evitar corte de markers
|
|
250
|
+
if (streamingStarted && !suppressRest && printedUpTo < assistantContent.length) {
|
|
251
|
+
process.stdout.write(c.white(assistantContent.slice(printedUpTo)));
|
|
252
|
+
printedUpTo = assistantContent.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (serverErr) {
|
|
256
|
+
console.log(c.err(`\nerro: ${serverErr}`));
|
|
257
|
+
return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: `[erro: ${serverErr}]` } };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (streamingStarted) process.stdout.write('\n');
|
|
261
|
+
|
|
262
|
+
// Atualiza messages internas do loop (com tool_calls, para o modelo saber o que pediu)
|
|
263
|
+
if (assistantMessage) messages.push(assistantMessage);
|
|
264
|
+
else messages.push({ role: 'assistant', content: assistantContent });
|
|
265
|
+
|
|
266
|
+
// No primeiro turno, inserimos o user antes do assistant (foi só passado como prompt)
|
|
267
|
+
if (iter === 1) {
|
|
268
|
+
const last = messages.pop();
|
|
269
|
+
messages.push({ role: 'user', content: enriched });
|
|
270
|
+
messages.push(last);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
promptForCall = null;
|
|
274
|
+
|
|
275
|
+
if (assistantContent) {
|
|
276
|
+
let cleaned = assistantContent
|
|
277
|
+
.replace(/<\|[\s\S]*$/g, '')
|
|
278
|
+
.replace(/【[\s\S]*$/g, '')
|
|
279
|
+
.replace(/\n\s*\{\s*"(name|type|summary|arguments|command|path)"[\s\S]*$/, '')
|
|
280
|
+
.replace(/^\s*\{\s*"(name|type|summary|arguments)"[\s\S]*$/, '')
|
|
281
|
+
// Corta chamadas de função vazadas em texto
|
|
282
|
+
.replace(/\b(finish|write_file|read_file|list_dir|run_shell|run_in_new_terminal|open_in_browser|browser_\w+)\s*\([\s\S]*$/g, '')
|
|
283
|
+
.trim();
|
|
284
|
+
if (cleaned) finalAssistantText = cleaned;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Subagentes rodaram — injeta resultado como tool_result e segue loop
|
|
288
|
+
if (subagentsPayload && subagentsToolCallId) {
|
|
289
|
+
toolResults = [{ tool_call_id: subagentsToolCallId, result: JSON.stringify(subagentsPayload) }];
|
|
290
|
+
promptForCall = null;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Server pediu retry (ex: finish sem trabalho no primeiro turno)
|
|
295
|
+
if (retryNeeded && rejectedNote) {
|
|
296
|
+
// Injeta pseudo tool_result educativo pro próximo turno
|
|
297
|
+
const rejectedId = assistantMessage?.tool_calls?.[0]?.id;
|
|
298
|
+
if (rejectedId) {
|
|
299
|
+
toolResults = [{ tool_call_id: rejectedId, result: JSON.stringify({ ok: false, error: rejectedNote.reason }) }];
|
|
300
|
+
}
|
|
301
|
+
// Novo prompt reforçando a ordem
|
|
302
|
+
promptForCall = `[sistema] Seu último ${rejectedNote.name}() foi rejeitado: ${rejectedNote.reason}\n\nExecute o pedido original agora, usando as ferramentas corretas antes de qualquer finish.`;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!hasToolCalls || toolCalls.length === 0) {
|
|
307
|
+
finished = true;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Executa cada tool
|
|
312
|
+
const results = [];
|
|
313
|
+
for (const tc of toolCalls) {
|
|
314
|
+
const name = tc.name;
|
|
315
|
+
const args = tc.arguments || {};
|
|
316
|
+
const nice = NICE[name] || name;
|
|
317
|
+
const detail = fmtArgs(name, args);
|
|
318
|
+
console.log('\n' + toolLine(nice, detail));
|
|
319
|
+
|
|
320
|
+
let res;
|
|
321
|
+
// Interceptação: se o Construtor 1 quer abrir arquivo E há refino planejado,
|
|
322
|
+
// adia a abertura pra DEPOIS do refino (não mostra ao usuário versão bugada).
|
|
323
|
+
// Só adiamos se já houver pelo menos 1 arquivo escrito (senão não faz sentido refinar).
|
|
324
|
+
const shouldDeferOpen = (name === 'open_in_browser' && args.target && filesWritten.size > 0);
|
|
325
|
+
if (shouldDeferOpen) {
|
|
326
|
+
deferredOpens.push(args.target);
|
|
327
|
+
res = { ok: true, text_for_model: JSON.stringify({ ok: true, deferred: true, note: 'abertura adiada para depois do refino' }) };
|
|
328
|
+
console.log(c.muted(' ⎿ adiado até depois do refino'));
|
|
329
|
+
} else {
|
|
330
|
+
try {
|
|
331
|
+
res = await tools.executeTool(name, args, log);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
res = { ok: false, text_for_model: JSON.stringify({ ok: false, error: e.message }) };
|
|
334
|
+
console.log(c.err(` exception: ${e.message}`));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
results.push({ tool_call_id: tc.id, result: res.text_for_model });
|
|
338
|
+
|
|
339
|
+
// Rastreia arquivos escritos para depois passar ao Construtor 2 (refino)
|
|
340
|
+
if (name === 'write_file' && args.path) {
|
|
341
|
+
try { filesWritten.add(String(args.path)); } catch { /* noop */ }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (res.finish) finished = true;
|
|
345
|
+
|
|
346
|
+
const sig = name + '|' + JSON.stringify(args);
|
|
347
|
+
recentCalls.push(sig);
|
|
348
|
+
if (recentCalls.length > 4) recentCalls.shift();
|
|
349
|
+
if (recentCalls.filter(s => s === sig).length >= 3) {
|
|
350
|
+
console.log('\n' + c.warn('loop detectado — encerrando.'));
|
|
351
|
+
finished = true;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
toolResults = results;
|
|
357
|
+
if (finished) break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!finished) console.log('\n' + c.warn(`limite de ${MAX_ITERATIONS} iterações atingido.`));
|
|
361
|
+
|
|
362
|
+
// ═══ FASE C — Refino (Construtor 2) ═══
|
|
363
|
+
// Só refina se o Construtor 1 realmente criou/modificou arquivos.
|
|
364
|
+
// Não refina para saudações/perguntas conceituais.
|
|
365
|
+
if (filesWritten.size > 0) {
|
|
366
|
+
const refineFiles = Array.from(filesWritten).slice(0, 8); // cap defensivo
|
|
367
|
+
console.log('\n' + c.muted('◆ Refinando (Construtor 2)'));
|
|
368
|
+
let refineSp = new Spinner('Melhorando').start();
|
|
369
|
+
let refineIter = 0;
|
|
370
|
+
let refineDone = false;
|
|
371
|
+
let refineToolResults = null;
|
|
372
|
+
const REFINE_MAX = 6;
|
|
373
|
+
// Histórico ISOLADO do refino — nunca inclui o histórico do executor
|
|
374
|
+
// (isso quebra o Groq porque as tool_calls antigas não têm tool_results correspondentes).
|
|
375
|
+
const refineMessages = [];
|
|
376
|
+
|
|
377
|
+
while (!refineDone && refineIter < REFINE_MAX) {
|
|
378
|
+
refineIter++;
|
|
379
|
+
let assistantMessage = null;
|
|
380
|
+
let toolCalls = [];
|
|
381
|
+
let serverErr = null;
|
|
382
|
+
let hasToolCalls = false;
|
|
383
|
+
let streamingStarted = false;
|
|
384
|
+
let printedUpTo = 0;
|
|
385
|
+
let suppressRest = false;
|
|
386
|
+
let assistantContent = '';
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
await api.requestStream(server, '/api/act/stream', {
|
|
390
|
+
refine: true,
|
|
391
|
+
refine_files: refineFiles,
|
|
392
|
+
refine_original_prompt: userPrompt,
|
|
393
|
+
refine_history: refineMessages,
|
|
394
|
+
tool_results: refineToolResults,
|
|
395
|
+
model,
|
|
396
|
+
}, (evt) => {
|
|
397
|
+
if (evt.erro) { serverErr = evt.erro; return; }
|
|
398
|
+
if (evt.delta) {
|
|
399
|
+
if (!streamingStarted) {
|
|
400
|
+
if (refineSp) { refineSp.stop(); refineSp = null; }
|
|
401
|
+
process.stdout.write('\n');
|
|
402
|
+
streamingStarted = true;
|
|
403
|
+
}
|
|
404
|
+
assistantContent += evt.delta;
|
|
405
|
+
if (suppressRest) return;
|
|
406
|
+
const leakRe = /<\||【|\n\s*\{\s*"(name|type|summary|arguments|command|path)"|\bfinish\s*\(|\bwrite_file\s*\(|\brun_shell\s*\(|\bopen_in_browser\s*\(|\bbrowser_\w+\s*\(/;
|
|
407
|
+
const mm = assistantContent.match(leakRe);
|
|
408
|
+
if (mm && mm.index !== undefined) {
|
|
409
|
+
const safeEnd = mm.index;
|
|
410
|
+
if (safeEnd > printedUpTo) {
|
|
411
|
+
process.stdout.write(c.white(assistantContent.slice(printedUpTo, safeEnd)));
|
|
412
|
+
printedUpTo = safeEnd;
|
|
413
|
+
}
|
|
414
|
+
suppressRest = true;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const target = Math.max(printedUpTo, assistantContent.length - 2);
|
|
418
|
+
if (target > printedUpTo) {
|
|
419
|
+
process.stdout.write(c.white(assistantContent.slice(printedUpTo, target)));
|
|
420
|
+
printedUpTo = target;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (evt.tool_call) toolCalls.push(evt.tool_call);
|
|
424
|
+
if (evt.assistant_message) assistantMessage = evt.assistant_message;
|
|
425
|
+
if (evt.done) hasToolCalls = !!evt.has_tool_calls;
|
|
426
|
+
if (evt.instability) { serverErr = 'refino indisponível (rate-limit)'; return; }
|
|
427
|
+
if (evt.refine_skipped) { refineDone = true; return; }
|
|
428
|
+
});
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (refineSp) refineSp.stop();
|
|
431
|
+
console.log(c.warn(`\n(refino pulado: ${e.message})`));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (refineSp && !streamingStarted) refineSp.stop();
|
|
436
|
+
if (streamingStarted && !suppressRest && printedUpTo < assistantContent.length) {
|
|
437
|
+
process.stdout.write(c.white(assistantContent.slice(printedUpTo)));
|
|
438
|
+
}
|
|
439
|
+
if (streamingStarted) process.stdout.write('\n');
|
|
440
|
+
|
|
441
|
+
if (serverErr) { console.log(c.warn(`\n(refino falhou: ${serverErr})`)); break; }
|
|
442
|
+
|
|
443
|
+
// Guarda o assistant_message NO HISTÓRICO ISOLADO do refino (não no messages global)
|
|
444
|
+
if (assistantMessage) refineMessages.push(assistantMessage);
|
|
445
|
+
if (!hasToolCalls || toolCalls.length === 0) { refineDone = true; break; }
|
|
446
|
+
|
|
447
|
+
const results = [];
|
|
448
|
+
for (const tc of toolCalls) {
|
|
449
|
+
const name = tc.name;
|
|
450
|
+
const args = tc.arguments || {};
|
|
451
|
+
const nice = NICE[name] || name;
|
|
452
|
+
const detail = fmtArgs(name, args);
|
|
453
|
+
console.log('\n' + toolLine(nice, detail));
|
|
454
|
+
let res;
|
|
455
|
+
try { res = await tools.executeTool(name, args, log); }
|
|
456
|
+
catch (e) { res = { ok: false, text_for_model: JSON.stringify({ ok: false, error: e.message }) }; console.log(c.err(` exception: ${e.message}`)); }
|
|
457
|
+
results.push({ tool_call_id: tc.id, result: res.text_for_model });
|
|
458
|
+
// O refino também precisa adicionar tool result no histórico isolado
|
|
459
|
+
refineMessages.push({ role: 'tool', tool_call_id: tc.id, content: res.text_for_model });
|
|
460
|
+
if (res.finish) refineDone = true;
|
|
461
|
+
}
|
|
462
|
+
refineToolResults = null; // já injetamos no refineMessages, não duplicar
|
|
463
|
+
|
|
464
|
+
// Prepara spinner do próximo turno de refino se ainda não terminou
|
|
465
|
+
if (!refineDone) refineSp = new Spinner('Melhorando').start();
|
|
466
|
+
}
|
|
467
|
+
if (refineSp) refineSp.stop();
|
|
468
|
+
|
|
469
|
+
// Executa aberturas adiadas agora que o refino terminou
|
|
470
|
+
if (deferredOpens.length > 0) {
|
|
471
|
+
for (const target of deferredOpens) {
|
|
472
|
+
console.log('\n' + toolLine('Open', target));
|
|
473
|
+
try {
|
|
474
|
+
const res = await tools.executeTool('open_in_browser', { target }, log);
|
|
475
|
+
if (!res.ok && res.text_for_model) {
|
|
476
|
+
console.log(c.muted(' ⎿ ' + (res.error || 'ok')));
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
console.log(c.err(` ⎿ falha: ${e.message}`));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} else if (deferredOpens.length > 0) {
|
|
484
|
+
// Não houve refino mas há aberturas adiadas — executa direto (fallback)
|
|
485
|
+
for (const target of deferredOpens) {
|
|
486
|
+
console.log('\n' + toolLine('Open', target));
|
|
487
|
+
try { await tools.executeTool('open_in_browser', { target }, log); }
|
|
488
|
+
catch (e) { console.log(c.err(` ⎿ falha: ${e.message}`)); }
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Retorna a mensagem final para o caller anexar ao histórico do chat
|
|
493
|
+
// Consolidamos toda a atividade + texto final para o usuário poder referenciar depois
|
|
494
|
+
return {
|
|
495
|
+
userMessage: { role: 'user', content: userPrompt },
|
|
496
|
+
assistantMessage: { role: 'assistant', content: finalAssistantText || '[ação concluída]' },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
module.exports = { runAct };
|