primocode 8.37.0 → 8.38.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/bin/primocode.js +65 -50
- package/lib/act.js +121 -78
- package/lib/area.js +104 -0
- package/lib/browser.js +110 -2
- package/lib/catalogo.js +7 -2
- package/lib/conta.js +122 -29
- package/lib/especialistas.js +123 -0
- package/lib/fala.js +146 -0
- package/lib/studio.js +319 -10
- package/lib/tools.js +19 -5
- package/lib/ui.js +57 -22
- package/package.json +2 -2
- package/studio/ferramentas/01-modelos.md +3 -1
- package/studio/ferramentas/corte.md +8 -0
- package/studio/ferramentas/prisma.md +8 -0
- package/studio/server.py +2 -1
- package/studio/web/js/icones.js +30 -1
- package/studio/web/js/player.js +58 -16
- package/studio/web/js/render.js +53 -1
- package/studio/web/js/video.js +44 -18
- package/studio/web/render.css +80 -0
package/bin/primocode.js
CHANGED
|
@@ -19,7 +19,8 @@ const { escolherModo } = require('../lib/modo.js');
|
|
|
19
19
|
const api = require('../lib/api');
|
|
20
20
|
const tools = require('../lib/tools');
|
|
21
21
|
const cfg = require('../lib/config');
|
|
22
|
-
const { runAct, interromper } = require('../lib/act');
|
|
22
|
+
const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
|
|
23
|
+
const fala = require('../lib/fala');
|
|
23
24
|
const claudeEngine = require('../lib/claude-engine');
|
|
24
25
|
const codexEngine = require('../lib/codex-engine');
|
|
25
26
|
const chaves = require('../lib/chaves');
|
|
@@ -38,10 +39,10 @@ const nuvem = require('../lib/nuvem');
|
|
|
38
39
|
// via a resposta parar no meio e cair de volta pro shell, sem mensagem
|
|
39
40
|
// nenhuma. Agora vira um erro visível, e o processo continua de pé.
|
|
40
41
|
process.on('uncaughtException', (e) => {
|
|
41
|
-
|
|
42
|
+
fala.problema(e);
|
|
42
43
|
});
|
|
43
44
|
process.on('unhandledRejection', (e) => {
|
|
44
|
-
|
|
45
|
+
fala.problema(e instanceof Error ? e : new Error(String(e)));
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
// A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
|
|
@@ -690,33 +691,49 @@ let chatEmVoo = null;
|
|
|
690
691
|
|
|
691
692
|
async function runChat(promptForModel, userTextForMemory) {
|
|
692
693
|
chatEmVoo = new AbortController();
|
|
693
|
-
const sp = new Spinner(
|
|
694
|
-
|
|
694
|
+
const sp = new Spinner(ui.PENSANDO).start();
|
|
695
|
+
const saida = fala.fluxo();
|
|
695
696
|
let text = '';
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
697
|
+
// O chat não tinha a paciência do agente: o primeiro 429 ou teto do
|
|
698
|
+
// minuto saía em vermelho na tela, na primeira recusa. Agora o que é
|
|
699
|
+
// passageiro (minuto, 429, 5xx, rede) espera a janela e repete calado,
|
|
700
|
+
// com o spinner de pensar girando — até seis vezes. O que NÃO é
|
|
701
|
+
// passageiro (crédito, chave, cota do dia) continua saindo, como aviso.
|
|
702
|
+
// Texto já na tela não se repete: se o erro veio no meio da resposta, ela
|
|
703
|
+
// fica como está e o aviso vem embaixo.
|
|
704
|
+
for (let tentativa = 0; ; tentativa++) {
|
|
705
|
+
let erro = null;
|
|
706
|
+
try {
|
|
707
|
+
await api.requestChat(state.config.server, {
|
|
708
|
+
prompt: promptForModel,
|
|
709
|
+
history: memoria.paraEnviar(state.memoria, state.history),
|
|
710
|
+
model: state.config.model,
|
|
711
|
+
token: state.config.token,
|
|
712
|
+
sinal: chatEmVoo.signal,
|
|
713
|
+
}, (evt) => {
|
|
714
|
+
if (evt.erro) { erro = evt.erro; return; }
|
|
715
|
+
if (evt.delta) {
|
|
716
|
+
if (!saida.escreveu()) sp.stop();
|
|
717
|
+
text += evt.delta;
|
|
718
|
+
saida.escrever(evt.delta);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
} catch (e) { erro = e; }
|
|
722
|
+
if (!erro) break;
|
|
723
|
+
const passageiro = ehTetoDeToken(erro) || ehTransitorio(erro);
|
|
724
|
+
if (passageiro && tentativa < 6 && !text && !chatEmVoo.signal.aborted) {
|
|
725
|
+
await new Promise((r) => setTimeout(r, esperaDoErro(erro, tentativa + 1)));
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
713
728
|
sp.stop();
|
|
714
|
-
|
|
715
|
-
|
|
729
|
+
saida.fechar();
|
|
730
|
+
if (!chatEmVoo.signal.aborted) fala.problema(erro);
|
|
731
|
+
if (!text) return;
|
|
732
|
+
break;
|
|
716
733
|
}
|
|
717
734
|
sp.stop();
|
|
735
|
+
saida.fechar();
|
|
718
736
|
if (text) {
|
|
719
|
-
process.stdout.write('\n');
|
|
720
737
|
state.history.push({ role: 'user', content: userTextForMemory });
|
|
721
738
|
state.history.push({ role: 'assistant', content: text });
|
|
722
739
|
state.lastResponse = text;
|
|
@@ -1248,32 +1265,30 @@ async function handleSlash(text) {
|
|
|
1248
1265
|
if (rest === 'trocar') conta.sair();
|
|
1249
1266
|
|
|
1250
1267
|
const sp = new Spinner('abrindo o login').start();
|
|
1251
|
-
let primeiro = true;
|
|
1252
1268
|
const r = await conta.entrar({
|
|
1253
1269
|
abrir: (u) => tools.openInBrowser(u),
|
|
1254
|
-
contar: (
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
}
|
|
1270
|
+
contar: (info) => {
|
|
1271
|
+
sp.stop();
|
|
1272
|
+
/* O código continua aparecendo — grande, partido em dois,
|
|
1273
|
+
porque é o que se lê de uma tela e se confere noutra. Mas
|
|
1274
|
+
agora ele já FOI para a página: o CLI serve o código em
|
|
1275
|
+
127.0.0.1 e a página pergunta ali; e foi para a área de
|
|
1276
|
+
transferência, para o "Colar" de um toque onde o
|
|
1277
|
+
navegador bloqueia o 127.0.0.1 (Safari). Digitar virou a
|
|
1278
|
+
terceira saída, não a primeira. */
|
|
1279
|
+
const meio = info.codigo.length === 8
|
|
1280
|
+
? info.codigo.slice(0, 4) + '-' + info.codigo.slice(4) : info.codigo;
|
|
1281
|
+
console.log(c.muted(' Abrindo no navegador:'));
|
|
1282
|
+
console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
|
|
1283
|
+
console.log('');
|
|
1284
|
+
console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
|
|
1285
|
+
console.log('');
|
|
1286
|
+
const como = [];
|
|
1287
|
+
if (info.porta) como.push('a página preenche sozinha');
|
|
1288
|
+
if (info.copiado) como.push('e o código já está copiado');
|
|
1289
|
+
console.log(c.dim(' ' + (como.length ? como.join(', ').replace(/^e /, '') + '.'
|
|
1290
|
+
: 'Digite o código na página.')));
|
|
1291
|
+
console.log(c.dim(' Esperando você aprovar…'));
|
|
1277
1292
|
},
|
|
1278
1293
|
});
|
|
1279
1294
|
sp.stop();
|
|
@@ -1619,7 +1634,7 @@ async function main() {
|
|
|
1619
1634
|
if (isSlash(text)) await handleSlash(text);
|
|
1620
1635
|
else await sendPrompt(text);
|
|
1621
1636
|
} catch (e) {
|
|
1622
|
-
|
|
1637
|
+
fala.problema(e);
|
|
1623
1638
|
}
|
|
1624
1639
|
}
|
|
1625
1640
|
busy = false;
|
package/lib/act.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const { c,
|
|
13
|
+
const { c, Spinner, PENSANDO } = require('./ui');
|
|
14
|
+
const fala = require('./fala');
|
|
15
|
+
const especialistas = require('./especialistas');
|
|
14
16
|
const tools = require('./tools');
|
|
15
17
|
const api = require('./api');
|
|
16
18
|
const cfg = require('./config');
|
|
@@ -49,6 +51,21 @@ function ehTetoDeToken(e) {
|
|
|
49
51
|
return /não cabe no teto|conversa passou do teto|request too large|tokens per minute|\btpm\b|reduce your message size/.test(s);
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Este erro é PASSAGEIRO — daqui a alguns segundos a mesma chamada passa?
|
|
56
|
+
*
|
|
57
|
+
* 429 por pedidos, 5xx do provedor ou do servidor, conexão que caiu no meio.
|
|
58
|
+
* Nada disso é do usuário, e nada disso ele precisa ver: espera-se um pouco
|
|
59
|
+
* e repete-se, com o spinner de pensar girando. O que NÃO entra aqui, de
|
|
60
|
+
* propósito: cota do DIA (esperar minutos não resolve), crédito, chave e o
|
|
61
|
+
* teto de tokens (esse tem a função acima e o conserto próprio, encolher).
|
|
62
|
+
*/
|
|
63
|
+
function ehTransitorio(e) {
|
|
64
|
+
const s = String((e && e.message) || e || '').toLowerCase();
|
|
65
|
+
if (/tokens per day|\btpd\b|free-models-per-day|per day|\b402\b|\b401\b|\b403\b|afford|credit/.test(s)) return false;
|
|
66
|
+
return /rate.?limit|too many requests|\b429\b|\b50[234]\b|bad gateway|service unavailable|gateway timeout|overloaded|socket hang up|econnreset|etimedout|aborted|internal server error|\b500\b/.test(s);
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
/**
|
|
53
70
|
* O /compact automático, feito no meio da tarefa.
|
|
54
71
|
*
|
|
@@ -81,9 +98,16 @@ function ident(profundidade) {
|
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
function logResult(kind, result, profundidade) {
|
|
84
|
-
const p =
|
|
101
|
+
const p = fala.resultado(profundidade);
|
|
85
102
|
if (!result) return;
|
|
86
|
-
|
|
103
|
+
// Uma tool que falhou é informação para o AGENTE, que vai consertar na
|
|
104
|
+
// volta seguinte — não é o erro do usuário. Âmbar, uma linha, sem o
|
|
105
|
+
// vermelho que faz parecer que a tarefa morreu.
|
|
106
|
+
if (result.ok === false) {
|
|
107
|
+
const texto = String(result.error || `exit ${result.code ?? 'erro'}`).replace(/\s+/g, ' ').trim();
|
|
108
|
+
console.log(p + c.warn(texto.length > 160 ? texto.slice(0, 159) + '…' : texto));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
87
111
|
|
|
88
112
|
switch (kind) {
|
|
89
113
|
case 'write_file':
|
|
@@ -297,10 +321,21 @@ function encolherParaContinuar(messages) {
|
|
|
297
321
|
* ("Please try again in 7.664s"); sem isso, meio minuto é seguro. */
|
|
298
322
|
function esperaDoErro(e, tentativa = 0) {
|
|
299
323
|
const m = /try again in ([0-9.]+)s/i.exec(String((e && e.message) || e || ''));
|
|
324
|
+
if (m) return Math.min(70, Math.max(5, Math.ceil(parseFloat(m[1])) + 2)) * 1000;
|
|
300
325
|
// Sem o tempo do provedor, cresce a passada a cada tentativa: bater na
|
|
301
326
|
// mesma porta a cada 30s por horas só esquenta o rate limit.
|
|
302
|
-
|
|
303
|
-
|
|
327
|
+
if (ehTetoDeToken(e)) return Math.min(70, Math.max(5, 30 + tentativa * 10)) * 1000;
|
|
328
|
+
// Rede e 5xx voltam em segundos, não em minutos: 3, 6, 12, 20, 20…
|
|
329
|
+
// Esperar meio minuto para um servidor que já voltou é a tela parada à toa.
|
|
330
|
+
return Math.min(20, 3 * 2 ** Math.max(0, tentativa - 1)) * 1000;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Espera com o spinner de PENSAR girando — nunca um de "limite" ou
|
|
334
|
+
* "continuando em Ns". Quem olha vê o Primo pensando um pouco mais, e só. */
|
|
335
|
+
async function pausar(ms, subagent) {
|
|
336
|
+
const sp = subagent ? null : new Spinner(PENSANDO).start();
|
|
337
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
338
|
+
if (sp) sp.stop();
|
|
304
339
|
}
|
|
305
340
|
|
|
306
341
|
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
@@ -396,16 +431,27 @@ async function runAct(userPrompt, opts) {
|
|
|
396
431
|
// Esc, é "espera até dar".
|
|
397
432
|
// O teste derruba para 2 via env; em produção são 200.
|
|
398
433
|
const MAX_ESPERAS = Number(process.env.PRIMOCODE_MAX_ESPERAS || 200);
|
|
434
|
+
// Rede e 5xx têm teto menor: seis esperas curtas (~1 min) e aí é o
|
|
435
|
+
// servidor que caiu de verdade, e a pessoa merece saber.
|
|
436
|
+
const MAX_ESPERAS_REDE = Number(process.env.PRIMOCODE_MAX_ESPERAS_REDE || 6);
|
|
399
437
|
|
|
400
438
|
let voltas = 0;
|
|
401
439
|
for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
|
|
402
440
|
voltas++;
|
|
403
|
-
const sp = new Spinner(subagent ? 'subagente trabalhando' :
|
|
441
|
+
const sp = new Spinner(subagent ? 'subagente trabalhando' : PENSANDO).start();
|
|
404
442
|
|
|
405
443
|
const toolCalls = [];
|
|
406
444
|
let assistantText = '';
|
|
407
445
|
let streamError = null;
|
|
408
|
-
|
|
446
|
+
// O texto do modelo NÃO sai letra a letra enquanto chega: fica guardado
|
|
447
|
+
// até o fim da rodada, porque só ali se sabe o que ele É. Vieram
|
|
448
|
+
// tool_calls junto? É PENSAMENTO ("o vídeo veio em .avi, vou
|
|
449
|
+
// converter") e sai recuado, em cinza, com ✦. Não veio tool nenhuma? É
|
|
450
|
+
// a RESPOSTA, e sai com ● na cor da marca. Antes tudo saía em branco
|
|
451
|
+
// no mesmo lugar, e "vou converter" e "pronto, converti" tinham a
|
|
452
|
+
// mesma cara. A exceção é texto longo: passou do limiar sem tool à
|
|
453
|
+
// vista, é resposta — e aí volta a correr na tela conforme chega.
|
|
454
|
+
let fluxo = null;
|
|
409
455
|
|
|
410
456
|
emVoo = new AbortController();
|
|
411
457
|
try {
|
|
@@ -413,51 +459,48 @@ async function runAct(userPrompt, opts) {
|
|
|
413
459
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
414
460
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
415
461
|
if (evt.delta) {
|
|
416
|
-
if (!streaming) { sp.stop(); streaming = true; }
|
|
417
462
|
assistantText += evt.delta;
|
|
418
|
-
if (!subagent
|
|
463
|
+
if (!subagent && !toolCalls.length) {
|
|
464
|
+
if (!fluxo && assistantText.length > fala.LIMIAR_PENSAMENTO) {
|
|
465
|
+
sp.stop();
|
|
466
|
+
fluxo = fala.fluxo();
|
|
467
|
+
fluxo.escrever(assistantText);
|
|
468
|
+
} else if (fluxo) fluxo.escrever(evt.delta);
|
|
469
|
+
}
|
|
419
470
|
}
|
|
420
471
|
if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
|
|
421
472
|
});
|
|
422
473
|
} catch (e) {
|
|
423
474
|
sp.stop();
|
|
424
475
|
emVoo = null;
|
|
476
|
+
if (fluxo) fluxo.fechar();
|
|
425
477
|
// Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
|
|
426
478
|
// de devolver erro e jogar fora o trabalho. O usuário não precisa
|
|
427
|
-
// saber que isso aconteceu — a tarefa é dele, o encanamento é
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (esperas < MAX_ESPERAS) {
|
|
479
|
+
// saber que isso aconteceu — a tarefa é dele, o encanamento é
|
|
480
|
+
// nosso. E não sabe MESMO: nada disto vai para a tela; o spinner de
|
|
481
|
+
// pensar segue girando com as frases de sempre.
|
|
482
|
+
if (ehTetoDeToken(e) && !PARAR && messages.length > 8 && compactacoes < MAX_COMPACTACOES) {
|
|
483
|
+
compactacoes++;
|
|
484
|
+
encolherParaContinuar(messages);
|
|
485
|
+
iteration--; // esta volta não contou: nada foi decidido
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if ((ehTetoDeToken(e) || ehTransitorio(e)) && !PARAR) {
|
|
489
|
+
const teto = ehTetoDeToken(e) ? MAX_ESPERAS : MAX_ESPERAS_REDE;
|
|
490
|
+
if (esperas < teto) {
|
|
440
491
|
esperas++;
|
|
441
|
-
|
|
442
|
-
if (!subagent) {
|
|
443
|
-
const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
|
|
444
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
445
|
-
sp2.stop();
|
|
446
|
-
} else {
|
|
447
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
448
|
-
}
|
|
492
|
+
await pausar(esperaDoErro(e, esperas), subagent);
|
|
449
493
|
iteration--;
|
|
450
494
|
continue;
|
|
451
495
|
}
|
|
452
496
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
console.log('\n' + c.err(amigavel));
|
|
497
|
+
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${fala.frase(e)}` } };
|
|
498
|
+
fala.problema(e);
|
|
456
499
|
throw e;
|
|
457
500
|
}
|
|
458
501
|
sp.stop();
|
|
459
502
|
emVoo = null;
|
|
460
|
-
if (PARAR) break;
|
|
503
|
+
if (PARAR) { if (fluxo) fluxo.fechar(); break; }
|
|
461
504
|
|
|
462
505
|
// Chamada de função malformada: o modelo errou a SINTAXE da chamada,
|
|
463
506
|
// não a tarefa. Repetir a mesma rodada quase sempre resolve — e sem
|
|
@@ -468,47 +511,44 @@ async function runAct(userPrompt, opts) {
|
|
|
468
511
|
// tropeço: o provedor recusou o JSON que o MODELO gerou. Medido em
|
|
469
512
|
// produção num "crie um vídeo" — o documento grande saía torto e o
|
|
470
513
|
// erro cru parava a tarefa com a corrente inteira ainda de pé.
|
|
514
|
+
// Tudo o que é encanamento daqui para baixo acontece EM SILÊNCIO: o
|
|
515
|
+
// tropeço, o encolhimento, a espera. Antes cada um escrevia uma linha
|
|
516
|
+
// cinza ("o modelo tropeçou", "conversa encolhida (12 → 4)"), e quem
|
|
517
|
+
// lia via a máquina engasgando no meio do trabalho.
|
|
471
518
|
if (streamError && /failed to call a function|failed to parse tool call|failed_generation|tool_use_failed/i.test(String(streamError))
|
|
472
519
|
&& funcoesQuebradas < 4 && !PARAR) {
|
|
473
520
|
funcoesQuebradas++;
|
|
474
|
-
if (
|
|
521
|
+
if (fluxo) fluxo.fechar();
|
|
475
522
|
iteration--;
|
|
476
523
|
continue;
|
|
477
524
|
}
|
|
478
525
|
|
|
479
|
-
if (streamError && ehTetoDeToken(streamError)
|
|
526
|
+
if (streamError && !PARAR && (ehTetoDeToken(streamError) || ehTransitorio(streamError))) {
|
|
527
|
+
if (fluxo) fluxo.fechar();
|
|
480
528
|
// Conversa longa: encolher resolve, porque o peso é o histórico.
|
|
481
|
-
if (messages.length > 8 && compactacoes < MAX_COMPACTACOES) {
|
|
529
|
+
if (ehTetoDeToken(streamError) && messages.length > 8 && compactacoes < MAX_COMPACTACOES) {
|
|
482
530
|
compactacoes++;
|
|
483
|
-
const antes = messages.length;
|
|
484
531
|
encolherParaContinuar(messages);
|
|
485
|
-
if (!subagent) {
|
|
486
|
-
console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
|
|
487
|
-
}
|
|
488
532
|
iteration--;
|
|
489
533
|
continue;
|
|
490
534
|
}
|
|
491
535
|
// Conversa curta: o peso é o MINUTO, não o histórico — as chamadas
|
|
492
|
-
// anteriores gastaram a cota. Espera a janela virar e repete.
|
|
493
|
-
|
|
536
|
+
// anteriores gastaram a cota. Espera a janela virar e repete. O
|
|
537
|
+
// 5xx e a rede caem no mesmo lugar, com o teto menor.
|
|
538
|
+
const teto = ehTetoDeToken(streamError) ? MAX_ESPERAS : MAX_ESPERAS_REDE;
|
|
539
|
+
if (esperas < teto) {
|
|
494
540
|
esperas++;
|
|
495
|
-
|
|
496
|
-
if (!subagent) {
|
|
497
|
-
const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
|
|
498
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
499
|
-
sp2.stop();
|
|
500
|
-
} else {
|
|
501
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
502
|
-
}
|
|
541
|
+
await pausar(esperaDoErro(streamError, esperas), subagent);
|
|
503
542
|
iteration--;
|
|
504
543
|
continue;
|
|
505
544
|
}
|
|
506
545
|
}
|
|
507
546
|
|
|
508
547
|
if (streamError) {
|
|
509
|
-
|
|
548
|
+
if (fluxo) fluxo.fechar();
|
|
549
|
+
const amigavel = fala.frase(streamError);
|
|
510
550
|
if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` }, concluiu: false };
|
|
511
|
-
|
|
551
|
+
fala.problema(streamError);
|
|
512
552
|
// concluiu: false — o plano de passos não pode tratar um erro de
|
|
513
553
|
// provedor como resultado e passá-lo adiante como "fato".
|
|
514
554
|
return { assistantMessage: { role: 'assistant', content: amigavel }, concluiu: false };
|
|
@@ -523,7 +563,6 @@ async function runAct(userPrompt, opts) {
|
|
|
523
563
|
// resolve — visto em produção no primeiro pedido de vídeo.
|
|
524
564
|
if (!finalText && respostasVazias < 3 && !PARAR) {
|
|
525
565
|
respostasVazias++;
|
|
526
|
-
if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo respondeu vazio — tentando de novo'));
|
|
527
566
|
// Repetir o MESMO contexto costuma produzir o mesmo vazio: se
|
|
528
567
|
// o turno anterior já engasgou, o que falta é um empurrão
|
|
529
568
|
// curto e concreto. A partir da segunda vez, entra um pedido
|
|
@@ -541,14 +580,18 @@ async function runAct(userPrompt, opts) {
|
|
|
541
580
|
continue;
|
|
542
581
|
}
|
|
543
582
|
if (!subagent) {
|
|
544
|
-
if (
|
|
545
|
-
else
|
|
583
|
+
if (fluxo) fluxo.fechar();
|
|
584
|
+
else fala.resposta(finalText);
|
|
546
585
|
}
|
|
547
586
|
break;
|
|
548
587
|
}
|
|
549
588
|
respostasVazias = 0;
|
|
550
589
|
|
|
551
|
-
|
|
590
|
+
// O que o modelo disse ANTES de agir é pensamento, e sai como tal.
|
|
591
|
+
if (!subagent) {
|
|
592
|
+
if (fluxo) fluxo.fechar();
|
|
593
|
+
else if (assistantText.trim()) fala.pensamento(assistantText, profundidade);
|
|
594
|
+
}
|
|
552
595
|
|
|
553
596
|
// Registra o turno do assistente COM os tool_calls. A API exige que
|
|
554
597
|
// cada mensagem role:'tool' venha logo depois deste turno.
|
|
@@ -576,7 +619,7 @@ async function runAct(userPrompt, opts) {
|
|
|
576
619
|
const resultadosSpawn = new Map();
|
|
577
620
|
if (spawns.length > 1 && !PARAR) {
|
|
578
621
|
const juntos = spawns.slice(0, 4);
|
|
579
|
-
|
|
622
|
+
fala.paralelo(juntos.length, profundidade);
|
|
580
623
|
const rs = await Promise.all(juntos.map((tc) =>
|
|
581
624
|
runSubagent(tc.arguments || {}, {
|
|
582
625
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
@@ -624,7 +667,7 @@ async function runAct(userPrompt, opts) {
|
|
|
624
667
|
esquecer de passar o token, a pasta ou o motor. */
|
|
625
668
|
if (tc.name === 'construir_app') {
|
|
626
669
|
algoRodou = true;
|
|
627
|
-
|
|
670
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
628
671
|
const r = await rodarPipeline(tc.arguments || {}, {
|
|
629
672
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
630
673
|
});
|
|
@@ -636,12 +679,12 @@ async function runAct(userPrompt, opts) {
|
|
|
636
679
|
|
|
637
680
|
// Repetiu a mesma chamada? Não executa de novo. Rodar outra vez
|
|
638
681
|
// gastaria a cota para devolver exatamente o mesmo texto, e é
|
|
639
|
-
// esse texto que convence o modelo a repetir mais uma vez.
|
|
682
|
+
// esse texto que convence o modelo a repetir mais uma vez. Sem
|
|
683
|
+
// linha na tela: o freio é nosso, não do usuário — o modelo recebe
|
|
684
|
+
// o aviso e segue, e quem olha vê o trabalho continuar.
|
|
640
685
|
const eco = repeticao.ver(jaChamado, tc.name, tc.arguments);
|
|
641
686
|
if (eco.repetida) {
|
|
642
687
|
voltasEmFalso++;
|
|
643
|
-
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
644
|
-
console.log(ident(profundidade) + c.muted(' ⎿ ') + c.warn(`repetido (${eco.vezes}ª vez) — não rodei de novo`));
|
|
645
688
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: eco.aviso });
|
|
646
689
|
continue;
|
|
647
690
|
}
|
|
@@ -655,8 +698,6 @@ async function runAct(userPrompt, opts) {
|
|
|
655
698
|
|
|
656
699
|
if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
|
|
657
700
|
voltasEmFalso++;
|
|
658
|
-
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
659
|
-
console.log(ident(profundidade) + c.muted(' ⎿ ') + c.warn('3 pesquisas sem resultado — siga com o que já tem'));
|
|
660
701
|
messages.push({
|
|
661
702
|
role: 'tool',
|
|
662
703
|
tool_call_id: tc.id,
|
|
@@ -670,7 +711,7 @@ async function runAct(userPrompt, opts) {
|
|
|
670
711
|
}
|
|
671
712
|
|
|
672
713
|
algoRodou = true;
|
|
673
|
-
|
|
714
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
674
715
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
675
716
|
logResult(tc.name, result, profundidade);
|
|
676
717
|
|
|
@@ -718,17 +759,16 @@ async function runAct(userPrompt, opts) {
|
|
|
718
759
|
// terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.
|
|
719
760
|
if (!algoRodou && toolCalls.length) {
|
|
720
761
|
rodadasParadas++;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
762
|
+
// Quem fala é o motivoDaParada, uma vez só — aqui havia uma
|
|
763
|
+
// segunda frase de "travei", e as duas saíam em sequência.
|
|
764
|
+
if (rodadasParadas >= MAX_RODADAS_PARADAS) break;
|
|
725
765
|
} else {
|
|
726
766
|
rodadasParadas = 0;
|
|
727
767
|
}
|
|
728
768
|
|
|
729
769
|
if (finished) {
|
|
730
770
|
finalText = finished;
|
|
731
|
-
if (!subagent)
|
|
771
|
+
if (!subagent) fala.resposta(finished);
|
|
732
772
|
break;
|
|
733
773
|
}
|
|
734
774
|
}
|
|
@@ -740,7 +780,7 @@ async function runAct(userPrompt, opts) {
|
|
|
740
780
|
|
|
741
781
|
if (PARAR && !finalText) {
|
|
742
782
|
finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
|
|
743
|
-
if (!subagent)
|
|
783
|
+
if (!subagent) fala.aviso(finalText);
|
|
744
784
|
}
|
|
745
785
|
|
|
746
786
|
// Nunca terminar em silêncio. Quando o modelo devolvia resposta vazia sem
|
|
@@ -749,7 +789,7 @@ async function runAct(userPrompt, opts) {
|
|
|
749
789
|
// depois de a primeira tentativa de seletor falhar.
|
|
750
790
|
if (!finalText) {
|
|
751
791
|
finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS });
|
|
752
|
-
if (!subagent)
|
|
792
|
+
if (!subagent) fala.aviso(finalText);
|
|
753
793
|
}
|
|
754
794
|
|
|
755
795
|
// Nada de arquivo mexido = nada para lembrar. E grava em `pastaAtual`, não em
|
|
@@ -801,9 +841,12 @@ async function runSubagent(args, ctx) {
|
|
|
801
841
|
}
|
|
802
842
|
if (!args.prompt) return 'Recusado: prompt do subagente vazio.';
|
|
803
843
|
|
|
804
|
-
|
|
844
|
+
// O ofício: o modelo diz quem faz (`especialista`), ou a tarefa sugere. O
|
|
845
|
+
// briefing vai na frente da tarefa, na mesma mensagem — ver especialistas.js.
|
|
846
|
+
const esp = especialistas.escolher(args.especialista, args.prompt);
|
|
847
|
+
fala.agente(especialistas.rotulo(esp), descricao, profundidade);
|
|
805
848
|
|
|
806
|
-
const r = await runAct(args.prompt, {
|
|
849
|
+
const r = await runAct(especialistas.montarTarefa(esp, args.prompt), {
|
|
807
850
|
server, model, token, projectDir, projectName,
|
|
808
851
|
history: [],
|
|
809
852
|
profundidade: profundidade + 1,
|
|
@@ -812,7 +855,7 @@ async function runSubagent(args, ctx) {
|
|
|
812
855
|
});
|
|
813
856
|
|
|
814
857
|
const resumo = r.assistantMessage.content || 'sem resultado';
|
|
815
|
-
|
|
858
|
+
fala.agenteVoltou(resumo, profundidade);
|
|
816
859
|
return resumo;
|
|
817
860
|
}
|
|
818
861
|
|
|
@@ -848,7 +891,7 @@ async function rodarPipeline(args, ctx) {
|
|
|
848
891
|
|
|
849
892
|
const correr = async (prompt, rotulo) => {
|
|
850
893
|
if (PARAR) return '';
|
|
851
|
-
|
|
894
|
+
fala.agente('Agente', rotulo, profundidade);
|
|
852
895
|
const r = await runAct(prompt, {
|
|
853
896
|
server, model, token, projectDir: pasta, projectName,
|
|
854
897
|
history: [], profundidade: profundidade + 1, subagent: true, motor,
|
|
@@ -861,10 +904,10 @@ async function rodarPipeline(args, ctx) {
|
|
|
861
904
|
pasta,
|
|
862
905
|
rodadas: args.rodadas,
|
|
863
906
|
correr,
|
|
864
|
-
mostrar: (linha) => console.log(
|
|
907
|
+
mostrar: (linha) => console.log(fala.resultado(profundidade) + c.muted(linha)),
|
|
865
908
|
abrir: tools.openInBrowser,
|
|
866
909
|
});
|
|
867
910
|
}
|
|
868
911
|
|
|
869
|
-
module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, esperaDoErro,
|
|
912
|
+
module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
870
913
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|