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/README.md +5 -5
- package/app/app.css +482 -281
- package/app/app.js +888 -315
- package/app/index.html +176 -101
- package/app/primo-marca.js +58 -1
- package/bin/primocode.js +77 -26
- package/lib/act.js +71 -7
- package/lib/app.js +297 -89
- package/lib/browser.js +65 -2
- package/lib/fala.js +95 -1
- package/lib/{ide.js → painel.js} +41 -87
- package/lib/pre-leitura.js +118 -0
- package/lib/repeticao.js +4 -2
- package/lib/tempos.js +62 -0
- package/package.json +2 -2
- package/app/ide.css +0 -345
- package/app/ide.html +0 -183
- package/app/ide.js +0 -639
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
|
|
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
|
-
|
|
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
|
|
490
|
-
|
|
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
|
-
|
|
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.
|