primocode 8.36.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 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
- console.log('\n' + c.err(ui.friendlyError(e)));
42
+ fala.problema(e);
42
43
  });
43
44
  process.on('unhandledRejection', (e) => {
44
- console.log('\n' + c.err(ui.friendlyError(e instanceof Error ? e : new Error(String(e)))));
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('pensando').start();
694
- let started = false;
694
+ const sp = new Spinner(ui.PENSANDO).start();
695
+ const saida = fala.fluxo();
695
696
  let text = '';
696
- try {
697
- await api.requestChat(state.config.server, {
698
- prompt: promptForModel,
699
- history: memoria.paraEnviar(state.memoria, state.history),
700
- model: state.config.model,
701
- token: state.config.token,
702
- sinal: chatEmVoo.signal,
703
- }, (evt) => {
704
- if (evt.erro) { if (!started) sp.stop(); console.log('\n' + c.err(ui.friendlyError(evt.erro))); return; }
705
- if (evt.delta) {
706
- if (!started) { sp.stop(); process.stdout.write('\n'); started = true; }
707
- text += evt.delta;
708
- process.stdout.write(c.white(evt.delta));
709
- }
710
- if (evt.done && !started) sp.stop();
711
- });
712
- } catch (e) {
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
- console.log('\n' + c.err(ui.friendlyError(e)));
715
- return;
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: (msg) => {
1255
- if (primeiro) {
1256
- sp.stop();
1257
- console.log(c.muted(' Abrindo no navegador:'));
1258
- console.log(' ' + c.brand(msg));
1259
- primeiro = false;
1260
- } else {
1261
- /* O código é para DIGITAR na página, e é ele que
1262
- protege: como não vai no link, quem não está olhando
1263
- este terminal não tem o que digitar — e um link
1264
- mandado por outra pessoa não conecta o computador
1265
- dela à sua conta.
1266
-
1267
- Por isso ele aparece grande e sozinho, com o traço
1268
- no meio: o que se lê de uma tela e se digita noutra
1269
- erra menos partido em dois. */
1270
- const meio = msg.length === 8 ? msg.slice(0, 4) + '-' + msg.slice(4) : msg;
1271
- console.log('');
1272
- console.log(c.muted(' Digite este código na página:'));
1273
- console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
1274
- console.log('');
1275
- console.log(c.dim(' Esperando você aprovar…'));
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
- console.log('\n' + c.err(ui.friendlyError(e)));
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, actionHeader, Spinner, friendlyError } = require('./ui');
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 = ident(profundidade) + c.muted(' ⎿ ');
101
+ const p = fala.resultado(profundidade);
85
102
  if (!result) return;
86
- if (result.ok === false) { console.log(p + c.err(result.error || `exit ${result.code ?? 'erro'}`)); return; }
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
- const s = m ? Math.ceil(parseFloat(m[1])) + 2 : Math.min(70, 30 + tentativa * 10);
303
- return Math.min(70, Math.max(5, s)) * 1000;
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' : 'Primo Code pensando').start();
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
- let streaming = false;
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) process.stdout.write(c.white(evt.delta));
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 é nosso.
428
- if (ehTetoDeToken(e) && !PARAR) {
429
- if (messages.length > 8 && compactacoes < MAX_COMPACTACOES) {
430
- compactacoes++;
431
- const antes = messages.length;
432
- encolherParaContinuar(messages);
433
- if (!subagent) {
434
- console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
435
- }
436
- iteration--; // esta volta não contou: nada foi decidido
437
- continue;
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
- const ms = esperaDoErro(e, esperas);
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
- const amigavel = friendlyError(e);
454
- if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
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 (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo tropeçou na chamada — tentando de novo'));
521
+ if (fluxo) fluxo.fechar();
475
522
  iteration--;
476
523
  continue;
477
524
  }
478
525
 
479
- if (streamError && ehTetoDeToken(streamError) && !PARAR) {
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
- if (esperas < MAX_ESPERAS) {
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
- const ms = esperaDoErro(streamError, esperas);
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
- const amigavel = friendlyError(streamError);
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
- console.log('\n' + c.err(amigavel));
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 (finalText && !streaming) console.log('\n' + c.white(finalText));
545
- else if (streaming) process.stdout.write('\n');
583
+ if (fluxo) fluxo.fechar();
584
+ else fala.resposta(finalText);
546
585
  }
547
586
  break;
548
587
  }
549
588
  respostasVazias = 0;
550
589
 
551
- if (streaming && !subagent) process.stdout.write('\n');
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
- console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agentes'))} ${c.muted(`${juntos.length} em paralelo`)}`);
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
- console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
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
- console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
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
- if (rodadasParadas >= MAX_RODADAS_PARADAS) {
722
- if (!subagent) console.log('\n' + c.warn(`Travei repetindo as mesmas chamadas ${rodadasParadas} voltas seguidas — parei aqui.`));
723
- break;
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) console.log('\n' + c.white(finished));
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) console.log('\n' + c.warn('⏹ ' + finalText));
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) console.log('\n' + c.warn(finalText));
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
- console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(descricao)}`);
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
- console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.muted(resumo.slice(0, 120))}`);
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
- console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(rotulo)}`);
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(ident(profundidade) + ` ${c.muted('⎿ ' + linha)}`),
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 };