primocode 9.8.0-beta.6 → 9.8.0-beta.8

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/lib/act.js CHANGED
@@ -24,6 +24,8 @@ const permissao = require('./permissao');
24
24
  const perigo = require('./perigo');
25
25
  const skills = require('./skills');
26
26
  const checkpoint = require('./checkpoint');
27
+ const tempos = require('./tempos');
28
+ const preLeitura = require('./pre-leitura');
27
29
 
28
30
  // 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
29
31
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
@@ -35,6 +37,8 @@ const MAX_EMPURROES = 3;
35
37
  // Voltas seguidas sem executar nada antes de desistir.
36
38
  const MAX_RODADAS_PARADAS = 3;
37
39
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
40
+ // As leituras que, repetidas sem mudança no meio, devolvem o guardado.
41
+ const RELEITURA = new Set(['read_file', 'list_dir']);
38
42
 
39
43
  // ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
40
44
  // O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
@@ -134,6 +138,9 @@ function limparParaOModelo(result) {
134
138
 
135
139
  function logResult(kind, result, profundidade) {
136
140
  const p = fala.resultado(profundidade);
141
+ // A linha da janela (lib/app.js) fecha aqui: o mesmo resultado que o
142
+ // terminal desenha embaixo, resumido. Ver o canal em lib/fala.js.
143
+ fala.avisar('resultado', fala.resumoDoResultado(kind, result));
137
144
  if (!result) return;
138
145
  // Uma tool que falhou é informação para o AGENTE, que vai consertar na
139
146
  // volta seguinte — não é o erro do usuário. Âmbar, uma linha, sem o
@@ -382,6 +389,7 @@ async function pausar(ms, subagent) {
382
389
  const sp = subagent ? null : new Spinner(PENSANDO).start();
383
390
  await new Promise((r) => setTimeout(r, ms));
384
391
  if (sp) sp.stop();
392
+ tempos.somar('espera de cota/rede', ms);
385
393
  }
386
394
 
387
395
  /**
@@ -451,10 +459,10 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
451
459
  const PULAR_NO_MAPA = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'out',
452
460
  '.cache', 'coverage', '__pycache__', '.venv', 'venv', '.turbo', '.vercel', '.idea', '.vscode']);
453
461
 
454
- function mapaDoProjeto(dir, limite = 80) {
462
+ function arquivosDoProjeto(dir, limite = 80) {
455
463
  const path = require('path');
456
464
  const fs = require('fs');
457
- if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return '';
465
+ if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return [];
458
466
  const achados = [];
459
467
  const andar = (atual, fundo) => {
460
468
  if (fundo > 4 || achados.length >= limite) return;
@@ -470,12 +478,18 @@ function mapaDoProjeto(dir, limite = 80) {
470
478
  }
471
479
  };
472
480
  andar(dir, 0);
481
+ return achados;
482
+ }
483
+
484
+ function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite)) {
473
485
  if (!achados.length) return '';
474
486
  const mais = achados.length >= limite ? `\n… (mais arquivos — use glob para achar)` : '';
475
487
  return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
476
488
  }
477
489
 
478
- function buildMemoryContext(projectDir, projectName) {
490
+ /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
491
+ entrar já lido (ver lib/pre-leitura.js). */
492
+ function buildMemoryContext(projectDir, projectName, pedido = '') {
479
493
  const parts = [];
480
494
  // As instruções vêm primeiro: são regra de quem manda no projeto, e a
481
495
  // memória é o que o agente anotou. Entram aqui (e não no bin) para valer
@@ -486,8 +500,11 @@ function buildMemoryContext(projectDir, projectName) {
486
500
  if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
487
501
  const projeto = cfg.readProjectMemory(projectDir);
488
502
  if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
489
- const mapa = mapaDoProjeto(projectDir);
490
- if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}`);
503
+ const arquivos = arquivosDoProjeto(projectDir);
504
+ const mapa = mapaDoProjeto(projectDir, 80, arquivos);
505
+ if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
506
+ const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
507
+ if (lidos) parts.push(lidos);
491
508
  return parts.join('\n\n');
492
509
  }
493
510
 
@@ -520,6 +537,11 @@ async function runAct(userPrompt, opts) {
520
537
  /* Quem abre a página para conferir antes do "pronto". Injetável pelo
521
538
  mesmo motivo do `motor`: o teste mede a regra sem abrir um Chrome. */
522
539
  medirPagina = (dir) => require('./pipeline.js').medirNoNavegador(dir),
540
+ /* Quem LIGA o navegador escondido antes da hora. Injetável pelo mesmo
541
+ motivo do `medirPagina` — e só vale junto com o padrão dele: um
542
+ teste que troca o medidor não pode subir um Chrome de verdade. */
543
+ aquecerPagina = opts && opts.medirPagina ? null
544
+ : () => require('./browser.js').aquecerRevisor(),
523
545
  } = opts || {};
524
546
 
525
547
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
@@ -550,6 +572,7 @@ async function runAct(userPrompt, opts) {
550
572
  let mexeuNaPagina = false; // mexeu desde a última conferência?
551
573
  let conferencias = 0; // até duas: entregar, e conferir o conserto
552
574
  let avisoDaPagina = ''; // o que ainda estava quebrado na última
575
+ let msConferindo = 0; // para o PRIMOCODE_TEMPOS separar as fases
553
576
  /* Abre a página e devolve o relato quando é para VOLTAR (achou problema e
554
577
  ainda cabe uma volta); null quando é para entregar. O que ficar
555
578
  quebrado na última conferência fica em `avisoDaPagina`, e a entrega sai
@@ -560,10 +583,13 @@ async function runAct(userPrompt, opts) {
560
583
  conferencias++;
561
584
  mexeuNaPagina = false;
562
585
  let medida = null;
586
+ const t = Date.now();
563
587
  try {
564
588
  fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
565
589
  medida = await medirPagina(pastaAtual);
566
590
  } catch { medida = null; }
591
+ msConferindo += Date.now() - t;
592
+ tempos.somar('conferir página', Date.now() - t);
567
593
  if (!medida || !medida.testou) return null;
568
594
  avisoDaPagina = medida.limpo ? ''
569
595
  : String(medida.relato || '').split('\n').filter((l) => /·|BRANCO/.test(l)).slice(0, 3).join(' ');
@@ -574,6 +600,8 @@ async function runAct(userPrompt, opts) {
574
600
  ? `${texto}\n\nAtenção: abri a página e ela ainda tem problema — ${avisoDaPagina.trim()}` : texto);
575
601
  // Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
576
602
  const jaChamado = repeticao.novo();
603
+ // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
604
+ const leiturasGuardadas = new Map();
577
605
  /* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
578
606
  no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
579
607
  não se mistura com o do principal — mas o que ele fez também precisa
@@ -623,7 +651,7 @@ async function runAct(userPrompt, opts) {
623
651
 
624
652
  const messages = [];
625
653
  if (!subagent) {
626
- const memory = buildMemoryContext(projectDir, projectName);
654
+ const memory = buildMemoryContext(projectDir, projectName, userPrompt);
627
655
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
628
656
  for (const m of (history || []).slice(-20)) {
629
657
  if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
@@ -735,6 +763,7 @@ async function runAct(userPrompt, opts) {
735
763
  let fluxo = null;
736
764
 
737
765
  emVoo = new AbortController();
766
+ const tRodada = Date.now();
738
767
  try {
739
768
  await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
740
769
  if (evt.erro) { streamError = evt.erro; return; }
@@ -781,6 +810,7 @@ async function runAct(userPrompt, opts) {
781
810
  }
782
811
  sp.stop();
783
812
  emVoo = null;
813
+ tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
784
814
  if (PARAR) { if (fluxo) fluxo.fechar(); break; }
785
815
 
786
816
  // Chamada de função malformada: o modelo errou a SINTAXE da chamada,
@@ -957,6 +987,8 @@ async function runAct(userPrompt, opts) {
957
987
 
958
988
  let finished = null;
959
989
  let algoRodou = false;
990
+ const tTools = Date.now();
991
+ const conferindoAntes = msConferindo;
960
992
 
961
993
  // ORQUESTRAÇÃO: subagentes da MESMA rodada rodam juntos. O modelo que
962
994
  // pede "3 agentes, um por arquivo" esperava paralelismo e recebia uma
@@ -1064,7 +1096,18 @@ async function runAct(userPrompt, opts) {
1064
1096
  const eco = repeticao.ver(jaChamado, tc.name, tc.arguments);
1065
1097
  if (eco.repetida) {
1066
1098
  voltasEmFalso++;
1067
- messages.push({ role: 'tool', tool_call_id: tc.id, content: eco.aviso });
1099
+ /* RELER SEM MUDANÇA devolve o que já foi lido, uma vez.
1100
+ O aviso seco ("o resultado está na conversa acima") mentia
1101
+ depois do `compactar`: a leitura velha já tinha virado 300
1102
+ caracteres, e o modelo gastava OUTRA ida ao servidor para
1103
+ achar o arquivo por outro caminho (um `cat`, um grep). Na
1104
+ segunda vez vai o conteúdo guardado — ler de novo sem nada
1105
+ mudar dá o mesmo texto, então não há o que executar. Da
1106
+ terceira em diante é laço, e o freio segue como era. */
1107
+ const guardado = eco.vezes === 2 && leiturasGuardadas.get(eco.chave);
1108
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: guardado
1109
+ ? `(sem mudança desde a última leitura — é o mesmo conteúdo; não precisa ler de novo)\n${guardado}`
1110
+ : eco.aviso });
1068
1111
  continue;
1069
1112
  }
1070
1113
 
@@ -1091,6 +1134,7 @@ async function runAct(userPrompt, opts) {
1091
1134
  const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
1092
1135
  if (!porta.ok) {
1093
1136
  fala.acao(tc.name, tc.arguments, profundidade);
1137
+ fala.avisar('resultado', { nome: tc.name, ok: false, resumo: 'não autorizado' });
1094
1138
  console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
1095
1139
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
1096
1140
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
@@ -1108,6 +1152,7 @@ async function runAct(userPrompt, opts) {
1108
1152
  });
1109
1153
  if (!risco.ok) {
1110
1154
  fala.acao(tc.name, tc.arguments, profundidade);
1155
+ fala.avisar('resultado', { nome: tc.name, ok: false, resumo: `não autorizado — ${risco.motivo}` });
1111
1156
  console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
1112
1157
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
1113
1158
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
@@ -1125,6 +1170,9 @@ async function runAct(userPrompt, opts) {
1125
1170
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
1126
1171
  logResult(tc.name, result, profundidade);
1127
1172
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1173
+ if (RELEITURA.has(tc.name) && result && result.ok !== false) {
1174
+ leiturasGuardadas.set(eco.chave, summarizeForModel(result));
1175
+ }
1128
1176
  if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
1129
1177
  // Além de narrar, ANOTA: no fim do pedido sai o balanço do que
1130
1178
  // nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
@@ -1133,6 +1181,19 @@ async function runAct(userPrompt, opts) {
1133
1181
  if (result && result.ok !== false && catalogo.criaProjeto(tc.name, tc.arguments)
1134
1182
  && /\.(html?|css|m?jsx?|tsx?|vue|svelte)$/i.test(String((tc.arguments && tc.arguments.path) || ''))) {
1135
1183
  mexeuNaPagina = true;
1184
+ /* O NAVEGADOR ESCONDIDO SOBE AGORA, e não no `finish`.
1185
+ A conferência só roda quando o agente diz "pronto", e até
1186
+ lá ainda há pelo menos uma rodada do modelo. Subir o Chrome
1187
+ ali, em paralelo com ela, tira do "pronto" o tempo de ligar
1188
+ o navegador e de fazer a PRIMEIRA navegação dele, que é a
1189
+ parte cara (ver `aquecerRevisor` em lib/browser.js). Medido
1190
+ em 24/09: a conferência num Chrome recém-aberto levava de
1191
+ 2,1 a 4,6s; aquecido antes, 1,24s — que é o assentar de
1192
+ 1,2s, e nada mais. Sem `await` e sem erro: se não subir, a
1193
+ conferência tenta de novo e diz o que houve. */
1194
+ if (aquecerPagina && !subagent && conferencias < 2) {
1195
+ try { Promise.resolve(aquecerPagina()).catch(() => {}); } catch { /* segue */ }
1196
+ }
1136
1197
  }
1137
1198
 
1138
1199
  // Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
@@ -1174,6 +1235,9 @@ async function runAct(userPrompt, opts) {
1174
1235
  messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
1175
1236
  }
1176
1237
 
1238
+ // A conferência da página tem linha própria: aqui fica só o resto.
1239
+ tempos.somar('ferramentas', Date.now() - tTools - (msConferindo - conferindoAntes));
1240
+
1177
1241
  // Uma volta inteira sem executar nada é o modelo preso. Insistir custa
1178
1242
  // um pedido ao servidor por volta, e as 15 voltas saíam caras para
1179
1243
  // terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.