primocode 9.10.0 → 10.0.9
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 +30 -6
- package/bin/primocode.js +169 -342
- package/lib/act.js +360 -39
- package/lib/browser.js +105 -4
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +77 -11
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +21 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +66 -0
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +284 -53
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +13 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +78 -0
- package/lib/estudio/lancamento.js +441 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +139 -51
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +414 -0
- package/lib/fala.js +1 -94
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +44 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +100 -22
- package/lib/ui.js +50 -8
- package/lib/video/analise.js +89 -0
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +280 -0
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +347 -0
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +274 -0
- package/lib/video/texto.js +144 -0
- package/lib/video/timeline.js +318 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +5 -5
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/act.js
CHANGED
|
@@ -31,6 +31,7 @@ const preLeitura = require('./pre-leitura');
|
|
|
31
31
|
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
32
32
|
// de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
|
|
33
33
|
const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
|
|
34
|
+
const MAX_VOLTAS_AGENTE = Number(process.env.PRIMOCODE_MAX_VOLTAS_AGENTE || 20);
|
|
34
35
|
// Quantas vezes o agente é EMPURRADO quando para no meio para narrar
|
|
35
36
|
// ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
|
|
36
37
|
const MAX_EMPURROES = 3;
|
|
@@ -39,6 +40,10 @@ const MAX_RODADAS_PARADAS = 3;
|
|
|
39
40
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
40
41
|
// As leituras que, repetidas sem mudança no meio, devolvem o guardado.
|
|
41
42
|
const RELEITURA = new Set(['read_file', 'list_dir']);
|
|
43
|
+
/* O que só lê: pode rodar junto com as outras leituras da mesma rodada. */
|
|
44
|
+
const SO_LEITURA = new Set(['read_file', 'list_dir', 'glob', 'grep', 'pesquisar_referencia', 'estudio_site']);
|
|
45
|
+
const ehLeitura = (tc) => SO_LEITURA.has(tc.name)
|
|
46
|
+
|| (tc.name === 'run_shell' && /^\s*primocode\s+(skill|mcp\s+list|video\s+(help|get_timeline|get_transcript))\b/.test(String((tc.arguments && tc.arguments.command) || '')));
|
|
42
47
|
|
|
43
48
|
// ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
|
|
44
49
|
// O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
|
|
@@ -55,6 +60,44 @@ function interromper() {
|
|
|
55
60
|
if (emVoo) { try { emVoo.abort(); } catch {} }
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/* ── A CAIXA DE ENTRADA: o que a pessoa digita enquanto ele trabalha ────
|
|
64
|
+
* Antes, a linha digitada durante uma tarefa ia para a fila do terminal e só
|
|
65
|
+
* rodava DEPOIS — como um pedido novo, que não sabia nada do que estava em
|
|
66
|
+
* curso. "Mesmo quando ela estiver trabalhando poderá enviar mensagem; ela
|
|
67
|
+
* vai à fila e, quando o PrimoCode termina um passo, recebe a mensagem e
|
|
68
|
+
* integra na tarefa." É o que o Claude Code e o Codex fazem: o recado entra
|
|
69
|
+
* na conversa na fronteira seguinte — depois do resultado das ferramentas,
|
|
70
|
+
* antes da próxima ida ao modelo. Nenhuma ferramenta é cortada no meio.
|
|
71
|
+
*
|
|
72
|
+
* Só o pedido PRINCIPAL lê a caixa (o subagente trabalha para ele). O que
|
|
73
|
+
* sobrar quando a tarefa acabar volta para o terminal como pedido normal. */
|
|
74
|
+
const CAIXA = [];
|
|
75
|
+
let TRABALHANDO = false;
|
|
76
|
+
function mandarDuranteATarefa(texto) {
|
|
77
|
+
const t = String(texto || '').trim();
|
|
78
|
+
if (!t || !TRABALHANDO) return false;
|
|
79
|
+
CAIXA.push(t);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
function trabalhando() { return TRABALHANDO; }
|
|
83
|
+
/** O que ficou na caixa sem ser lido — o terminal roda como pedido novo. */
|
|
84
|
+
function sobrasDaCaixa() { return CAIXA.splice(0); }
|
|
85
|
+
/* Esvazia a caixa dentro da conversa. Devolve quantos recados entraram. */
|
|
86
|
+
function entregarRecados(messages, profundidade) {
|
|
87
|
+
if (!CAIXA.length) return 0;
|
|
88
|
+
const recados = CAIXA.splice(0);
|
|
89
|
+
for (const r of recados) fala.pensamento(`recado recebido — entrando na tarefa: ${r}`, profundidade);
|
|
90
|
+
messages.push({
|
|
91
|
+
role: 'user',
|
|
92
|
+
content: (recados.length === 1 ? 'MENSAGEM NOVA do usuário, enviada enquanto você trabalhava:\n'
|
|
93
|
+
: 'MENSAGENS NOVAS do usuário, enviadas enquanto você trabalhava:\n')
|
|
94
|
+
+ recados.map((r) => `- ${r}`).join('\n')
|
|
95
|
+
+ '\n\nIntegre isto na tarefa em curso: se é um ajuste, aplique; se muda o rumo, siga o novo rumo; '
|
|
96
|
+
+ 'se é uma pergunta, responda em uma frase e continue. Não recomece o que já está feito.',
|
|
97
|
+
});
|
|
98
|
+
return recados.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
58
101
|
/**
|
|
59
102
|
* Este erro é "não coube no minuto"?
|
|
60
103
|
*
|
|
@@ -138,9 +181,6 @@ function limparParaOModelo(result) {
|
|
|
138
181
|
|
|
139
182
|
function logResult(kind, result, profundidade) {
|
|
140
183
|
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));
|
|
144
184
|
if (!result) return;
|
|
145
185
|
// Uma tool que falhou é informação para o AGENTE, que vai consertar na
|
|
146
186
|
// volta seguinte — não é o erro do usuário. Âmbar, uma linha, sem o
|
|
@@ -221,9 +261,12 @@ function logResult(kind, result, profundidade) {
|
|
|
221
261
|
case 'browser_open':
|
|
222
262
|
console.log(p + c.muted(result.url || 'aberto'));
|
|
223
263
|
return;
|
|
224
|
-
case 'browser_read':
|
|
225
|
-
|
|
264
|
+
case 'browser_read': {
|
|
265
|
+
const refs = (String(result.pagina || '').match(/^@e\d+/gm) || []).length;
|
|
266
|
+
console.log(p + c.muted(refs ? `${refs} elementos com ref · ${(result.text || '').length} caracteres de texto`
|
|
267
|
+
: `${(result.text || '').length} caracteres lidos da página`));
|
|
226
268
|
return;
|
|
269
|
+
}
|
|
227
270
|
case 'pesquisar_referencia': {
|
|
228
271
|
console.log(p + c.muted(`${result.fontes} fonte${result.fontes === 1 ? '' : 's'} lida${result.fontes === 1 ? '' : 's'} sobre "${result.consulta}"`));
|
|
229
272
|
for (const r of (result.referencias || []).slice(0, 4)) {
|
|
@@ -298,6 +341,50 @@ const RESTO_DO_RESULTADO = 300;
|
|
|
298
341
|
const RESTO_DA_PESQUISA = 1500;
|
|
299
342
|
const MATERIA_PRIMA = new Set(['pesquisar_referencia']);
|
|
300
343
|
|
|
344
|
+
/* ── O ARQUIVO JÁ GRAVADO NÃO VIAJA DE NOVO ─────────────────────────────
|
|
345
|
+
* O `compactar` encolhia o resultado das ferramentas e deixava intacto o que
|
|
346
|
+
* o MODELO escreveu — e o que ele escreve num site é o arquivo inteiro, dentro
|
|
347
|
+
* da chamada write_file. Medido em 29/09 ("crie uma landing de barbearia"):
|
|
348
|
+
* depois do index.html (7,6 KB) e do style.css (15 KB), cada ida ao modelo
|
|
349
|
+
* levava os dois de novo, passava do teto de tokens por minuto do plano
|
|
350
|
+
* grátis e ficava esperando a janela virar, em silêncio — "escreve dois
|
|
351
|
+
* arquivos e trava". O conteúdo já está no disco: nas voltas antigas ele vira
|
|
352
|
+
* um ponteiro. As duas últimas ficam inteiras (é nelas que o modelo decide o
|
|
353
|
+
* próximo passo, e é delas que sai a releitura sem disco — ver read_file). */
|
|
354
|
+
const ESCRITAS_INTEIRAS = 2;
|
|
355
|
+
const RESTO_DA_ESCRITA = 400;
|
|
356
|
+
function assistentesAntigas(messages) {
|
|
357
|
+
const idx = [];
|
|
358
|
+
for (let i = 0; i < messages.length; i++) {
|
|
359
|
+
const m = messages[i];
|
|
360
|
+
if (m && m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) idx.push(i);
|
|
361
|
+
}
|
|
362
|
+
return new Set(idx.slice(0, -ESCRITAS_INTEIRAS));
|
|
363
|
+
}
|
|
364
|
+
function encolherEscritas(m) {
|
|
365
|
+
let mudou = false;
|
|
366
|
+
const tool_calls = m.tool_calls.map((tc) => {
|
|
367
|
+
const f = tc && tc.function;
|
|
368
|
+
if (!f || !/^(write_file|edit_file|multi_edit)$/.test(f.name)) return tc;
|
|
369
|
+
const bruto = String(f.arguments || '');
|
|
370
|
+
if (bruto.length <= RESTO_DA_ESCRITA) return tc;
|
|
371
|
+
let a;
|
|
372
|
+
try { a = JSON.parse(bruto); } catch { return tc; }
|
|
373
|
+
const ponteiro = (n) => `(${n} caracteres — já gravado no disco; para mudar, leia com read_file e use edit_file)`;
|
|
374
|
+
if (f.name === 'write_file' && typeof a.content === 'string' && a.content.length > RESTO_DA_ESCRITA) a.content = ponteiro(a.content.length);
|
|
375
|
+
else if (f.name === 'edit_file' && typeof a.new_string === 'string' && a.new_string.length > RESTO_DA_ESCRITA) {
|
|
376
|
+
a.new_string = ponteiro(a.new_string.length);
|
|
377
|
+
if (typeof a.old_string === 'string' && a.old_string.length > 200) a.old_string = a.old_string.slice(0, 200) + '…';
|
|
378
|
+
} else if (f.name === 'multi_edit' && Array.isArray(a.edits)) {
|
|
379
|
+
a.edits = a.edits.map((e) => (e && typeof e.new_string === 'string' && e.new_string.length > RESTO_DA_ESCRITA
|
|
380
|
+
? { ...e, new_string: ponteiro(e.new_string.length), old_string: String(e.old_string || '').slice(0, 200) } : e));
|
|
381
|
+
} else return tc;
|
|
382
|
+
mudou = true;
|
|
383
|
+
return { ...tc, function: { ...f, arguments: JSON.stringify(a) } };
|
|
384
|
+
});
|
|
385
|
+
return mudou ? { ...m, tool_calls } : m;
|
|
386
|
+
}
|
|
387
|
+
|
|
301
388
|
/**
|
|
302
389
|
* Encolhe os resultados de tool ANTIGOS antes de mandar a conversa.
|
|
303
390
|
*
|
|
@@ -322,11 +409,13 @@ function compactar(messages) {
|
|
|
322
409
|
|
|
323
410
|
const indices = [];
|
|
324
411
|
for (let i = 0; i < messages.length; i++) if (messages[i].role === 'tool') indices.push(i);
|
|
325
|
-
if (indices.length <= RESULTADOS_INTEIROS && !duplicadas.size) return messages;
|
|
412
|
+
if (indices.length <= RESULTADOS_INTEIROS && !duplicadas.size && !assistentesAntigas(messages).size) return messages;
|
|
326
413
|
|
|
327
414
|
const chamadas = repeticao.chamadasPorId(messages);
|
|
328
415
|
const encolher = new Set(indices.slice(0, -RESULTADOS_INTEIROS));
|
|
416
|
+
const antigas = assistentesAntigas(messages);
|
|
329
417
|
return messages.map((m, i) => {
|
|
418
|
+
if (antigas.has(i)) return encolherEscritas(m);
|
|
330
419
|
if (duplicadas.has(i)) {
|
|
331
420
|
return { ...m, content: '(a mesma referência aparece inteira mais adiante nesta conversa — use aquela)' };
|
|
332
421
|
}
|
|
@@ -383,10 +472,21 @@ function esperaDoErro(e, tentativa = 0) {
|
|
|
383
472
|
return Math.min(20, 3 * 2 ** Math.max(0, tentativa - 1)) * 1000;
|
|
384
473
|
}
|
|
385
474
|
|
|
386
|
-
/** Espera
|
|
387
|
-
*
|
|
475
|
+
/** Espera a vez no modelo — À VISTA, com os segundos, também no subagente.
|
|
476
|
+
* Antes girava o spinner de "pensando" (e no subagente, nada): eram minutos
|
|
477
|
+
* de tela parada que pareciam trava. "As coisas estão acontecendo, só não
|
|
478
|
+
* estão sendo mostradas." Dizer que é a vez do modelo, sem alarme, é a
|
|
479
|
+
* diferença entre esperar e desistir. */
|
|
480
|
+
/* PRIMOCODE_TRACO=1: uma linha por ida ao modelo no stderr — o tamanho da
|
|
481
|
+
conversa, o erro e a espera. É o que diz ONDE uma tarefa travou quando a
|
|
482
|
+
tela (ou um log sem terminal) só mostra o último arquivo escrito. */
|
|
483
|
+
function traco(texto) {
|
|
484
|
+
if (process.env.PRIMOCODE_TRACO === '1') process.stderr.write(` [traço ${new Date().toTimeString().slice(0, 8)}] ${texto}\n`);
|
|
485
|
+
}
|
|
486
|
+
|
|
388
487
|
async function pausar(ms, subagent) {
|
|
389
|
-
|
|
488
|
+
traco(`esperando ${Math.round(ms / 1000)}s pela vez no modelo${subagent ? ' (agente)' : ''}`);
|
|
489
|
+
const sp = new Spinner(subagent ? 'o agente espera a vez no modelo' : 'esperando a vez no modelo').start();
|
|
390
490
|
await new Promise((r) => setTimeout(r, ms));
|
|
391
491
|
if (sp) sp.stop();
|
|
392
492
|
tempos.somar('espera de cota/rede', ms);
|
|
@@ -431,7 +531,7 @@ const TROCA_DE_ROTA = 'Você ficou três voltas sem conseguir rodar nada, repeti
|
|
|
431
531
|
+ '(write_todos) e faça AGORA o primeiro passo, por um caminho diferente: outra ferramenta, outro arquivo, '
|
|
432
532
|
+ 'outro comando. Se não houver caminho, chame finish e diga em uma frase o que impediu.';
|
|
433
533
|
|
|
434
|
-
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
534
|
+
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou, teto = MAX_ITERATIONS }) {
|
|
435
535
|
if (falhasPendentes.length) {
|
|
436
536
|
return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
|
|
437
537
|
}
|
|
@@ -439,11 +539,11 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
|
439
539
|
return 'Fiquei preso: tentei dois caminhos e os dois voltaram ao mesmo lugar, então parei antes de gastar sua cota à toa. '
|
|
440
540
|
+ 'O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
|
|
441
541
|
}
|
|
442
|
-
if (voltas >=
|
|
542
|
+
if (voltas >= teto) {
|
|
443
543
|
const emFalso = voltasEmFalso
|
|
444
544
|
? ` ${voltasEmFalso} ${voltasEmFalso === 1 ? 'chamada foi repetida' : 'chamadas foram repetidas'} e não rodaram de novo.`
|
|
445
545
|
: '';
|
|
446
|
-
return `Usei as ${
|
|
546
|
+
return `Usei as ${teto} voltas deste pedido sem chegar ao fim.${emFalso}`
|
|
447
547
|
+ ' O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
|
|
448
548
|
}
|
|
449
549
|
return 'Parei sem concluir e sem explicação do modelo. Diga "continua" que eu tento de novo daqui.';
|
|
@@ -487,6 +587,69 @@ function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite
|
|
|
487
587
|
return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
|
|
488
588
|
}
|
|
489
589
|
|
|
590
|
+
/* O comando de shell que só LÊ um arquivo — `cat x`, `sed -n '10,40p' x`,
|
|
591
|
+
`head -50 x`, `tail x`, `nl x`, com ou sem `cd pasta &&` na frente. Medido
|
|
592
|
+
num pedido de site (29/09): depois de escrever o script.js, o agente o
|
|
593
|
+
releu em quatro fatias de `sed -n`, uma volta de modelo cada. Devolve o
|
|
594
|
+
caminho lido (absoluto quando houve cd), ou null. */
|
|
595
|
+
function leituraPeloShell(comando) {
|
|
596
|
+
const m = /^\s*(?:cd\s+("[^"]+"|'[^']+'|\S+)\s*&&\s*)?(?:cat|nl|sed\s+-n\s+(?:'[^']*'|"[^"]*"|\S+)|head(?:\s+-n?\s*\d+)?|tail(?:\s+-n?\s*\d+)?)\s+("[^"]+"|'[^']+'|[^\s|;&><]+)\s*$/.exec(String(comando || ''));
|
|
597
|
+
if (!m) return null;
|
|
598
|
+
const tira = (x) => String(x || '').replace(/^["']|["']$/g, '');
|
|
599
|
+
const arquivo = tira(m[2]);
|
|
600
|
+
return m[1] ? require('path').resolve(tira(m[1]), arquivo) : arquivo;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/* ── O HISTÓRICO QUE VIAJA ─────────────────────────────────────────────
|
|
604
|
+
* Vinte mensagens INTEIRAS iam em toda volta do agente principal. Medido em
|
|
605
|
+
* 29/09: num pedido de site, 28 mil dos 38 mil caracteres da primeira ida
|
|
606
|
+
* eram respostas de pedidos anteriores — sozinhas, acima do teto por minuto
|
|
607
|
+
* do plano grátis. O que a conversa precisa para continuar é o fio (o que
|
|
608
|
+
* foi pedido, o que ficou pronto), não o texto inteiro: cada mensagem entra
|
|
609
|
+
* até um teto, e o total também, das mais novas para as mais velhas. */
|
|
610
|
+
const HISTORICO_POR_MENSAGEM = 1200;
|
|
611
|
+
const HISTORICO_TOTAL = 6000;
|
|
612
|
+
function historicoEnxuto(history) {
|
|
613
|
+
const fora = [];
|
|
614
|
+
let total = 0;
|
|
615
|
+
for (const m of (history || []).slice(-20).reverse()) {
|
|
616
|
+
if (!m || !m.role || !m.content) continue;
|
|
617
|
+
let t = String(m.content);
|
|
618
|
+
if (t.length > HISTORICO_POR_MENSAGEM) t = t.slice(0, HISTORICO_POR_MENSAGEM) + '…(encurtado)';
|
|
619
|
+
if (total + t.length > HISTORICO_TOTAL && fora.length) break;
|
|
620
|
+
total += t.length;
|
|
621
|
+
fora.unshift({ role: m.role, content: t });
|
|
622
|
+
}
|
|
623
|
+
// A API quer a conversa começando pelo usuário.
|
|
624
|
+
while (fora.length && fora[0].role !== 'user') fora.shift();
|
|
625
|
+
return fora;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/* ── O RITMO ─────────────────────────────────────────────────────────
|
|
629
|
+
* "A potência não deve vir do modelo, deve vir do sistema." O prompt do
|
|
630
|
+
* servidor manda "leia antes de editar" e "confira antes de dizer que
|
|
631
|
+
* terminou" — certo para editar código dos outros, e a receita do laço
|
|
632
|
+
* quando o arquivo foi ele mesmo que escreveu um minuto antes. Estas regras
|
|
633
|
+
* vêm do que o opencode e o Codex põem nos prompts deles para o modelo não
|
|
634
|
+
* reler o que fez; entram em TODO pedido, curtas, porque o plano conta token.
|
|
635
|
+
* A conferência de verdade é nossa (a página aberta no Chrome escondido). */
|
|
636
|
+
const RITMO = '[ritmo]\n'
|
|
637
|
+
+ '- O resultado de write_file/edit_file é a verdade. Não releia um arquivo que você acabou de escrever para conferir (nem com cat/sed/head): a conferência é do PrimoCode.\n'
|
|
638
|
+
+ '- Várias ações independentes vão na MESMA resposta (ex.: escrever index.html, style.css e script.js juntos).\n'
|
|
639
|
+
+ '- Leia um arquivo inteiro uma vez, não em pedaços pequenos; não leia de novo o que não mudou.\n'
|
|
640
|
+
+ '- Não narre o próximo passo: faça. Termine com finish("o que foi feito").\n'
|
|
641
|
+
+ '- VOCÊ DECIDE O CAMINHO. Pedido simples: faça direto. Pedido grande com partes independentes: write_todos com as partes, '
|
|
642
|
+
+ 'depois spawn_agent — um por parte, até 8 NA MESMA resposta (rodam juntos), cada um com o especialista e tudo que ele precisa saber. '
|
|
643
|
+
+ 'Cada um volta com uma ENTREGA: confira; o que falhou volta para conserto; a parte que depende de outra recebe a entrega dela no prompt.\n'
|
|
644
|
+
+ '- Antes de dizer pronto, PROVE: rode o teste ou o comando, abra o resultado, olhe a imagem. Sem prova, não está pronto.';
|
|
645
|
+
|
|
646
|
+
/* O ritmo do subagente: o mesmo, sem a parte de orquestrar (ele não abre outro agente). */
|
|
647
|
+
const RITMO_AGENTE = '[ritmo]\n'
|
|
648
|
+
+ '- O resultado de write_file/edit_file é a verdade. Não releia o que acabou de escrever.\n'
|
|
649
|
+
+ '- Leia um arquivo INTEIRO uma vez (read_file sem offset). Não leia aos pedaços, não leia de novo o que não mudou.\n'
|
|
650
|
+
+ '- Várias ações independentes na MESMA resposta.\n'
|
|
651
|
+
+ '- Faça o que a sua parte pede, prove (teste ou comando) e chame finish. Polimento além do pedido não é sua parte.';
|
|
652
|
+
|
|
490
653
|
/* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
|
|
491
654
|
entrar já lido (ver lib/pre-leitura.js). */
|
|
492
655
|
function buildMemoryContext(projectDir, projectName, pedido = '') {
|
|
@@ -505,6 +668,12 @@ function buildMemoryContext(projectDir, projectName, pedido = '') {
|
|
|
505
668
|
if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
|
|
506
669
|
const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
|
|
507
670
|
if (lidos) parts.push(lidos);
|
|
671
|
+
parts.push(RITMO);
|
|
672
|
+
// Os servidores MCP configurados, numa linha (lib/mcp.js).
|
|
673
|
+
try { const m = require('./mcp.js').dica(projectDir); if (m) parts.push(m); } catch { /* sem mcp */ }
|
|
674
|
+
// A equipe calada (lib/personas.js): o olhar do especialista que o pedido pede.
|
|
675
|
+
const equipe = require('./personas.js').bloco(pedido);
|
|
676
|
+
if (equipe) parts.push(equipe);
|
|
508
677
|
return parts.join('\n\n');
|
|
509
678
|
}
|
|
510
679
|
|
|
@@ -584,10 +753,12 @@ async function runAct(userPrompt, opts) {
|
|
|
584
753
|
mexeuNaPagina = false;
|
|
585
754
|
let medida = null;
|
|
586
755
|
const t = Date.now();
|
|
756
|
+
// Com spinner: eram segundos de tela parada entre o último arquivo e o
|
|
757
|
+
// "pronto", e parecia que tinha travado ali.
|
|
758
|
+
const sp = new Spinner('abrindo a página escondido para conferir').start();
|
|
587
759
|
try {
|
|
588
|
-
fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
|
|
589
760
|
medida = await medirPagina(pastaAtual);
|
|
590
|
-
} catch { medida = null; }
|
|
761
|
+
} catch { medida = null; } finally { sp.stop(); }
|
|
591
762
|
msConferindo += Date.now() - t;
|
|
592
763
|
tempos.somar('conferir página', Date.now() - t);
|
|
593
764
|
if (!medida || !medida.testou) return null;
|
|
@@ -602,6 +773,9 @@ async function runAct(userPrompt, opts) {
|
|
|
602
773
|
const jaChamado = repeticao.novo();
|
|
603
774
|
// O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
|
|
604
775
|
const leiturasGuardadas = new Map();
|
|
776
|
+
// O que ELE escreveu inteiro nesta tarefa, por caminho absoluto — para
|
|
777
|
+
// não gastar uma volta relendo o próprio texto (ver read_file abaixo).
|
|
778
|
+
const escritosNaTarefa = new Map();
|
|
605
779
|
/* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
|
|
606
780
|
no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
|
|
607
781
|
não se mistura com o do principal — mas o que ele fez também precisa
|
|
@@ -625,12 +799,9 @@ async function runAct(userPrompt, opts) {
|
|
|
625
799
|
* não é uma opção num plano limitado por token. */
|
|
626
800
|
let pedido = userPrompt;
|
|
627
801
|
if (!subagent) {
|
|
628
|
-
const comSkills = skills.
|
|
802
|
+
const comSkills = skills.cardapio(userPrompt);
|
|
629
803
|
pedido = comSkills.texto;
|
|
630
804
|
if (instrucao) pedido = `${instrucao}\n\nPEDIDO: ${pedido}`;
|
|
631
|
-
if (comSkills.skills.length) {
|
|
632
|
-
fala.pensamento(`seguindo: ${comSkills.skills.map((s) => s.titulo).join(', ')}`, profundidade);
|
|
633
|
-
}
|
|
634
805
|
}
|
|
635
806
|
let voltasEmFalso = 0;
|
|
636
807
|
// Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
|
|
@@ -653,9 +824,10 @@ async function runAct(userPrompt, opts) {
|
|
|
653
824
|
if (!subagent) {
|
|
654
825
|
const memory = buildMemoryContext(projectDir, projectName, userPrompt);
|
|
655
826
|
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
656
|
-
for (const m of (history
|
|
657
|
-
|
|
658
|
-
|
|
827
|
+
for (const m of historicoEnxuto(history)) messages.push(m);
|
|
828
|
+
} else {
|
|
829
|
+
// O subagente também trabalha no ritmo: sem isto ele relia o mesmo arquivo aos pedaços.
|
|
830
|
+
messages.push({ role: 'system', content: RITMO_AGENTE });
|
|
659
831
|
}
|
|
660
832
|
/* O que VAI para o modelo é `pedido` (com as skills na frente); o que
|
|
661
833
|
fica guardado na memória e no histórico é `userPrompt`, o que a pessoa
|
|
@@ -676,8 +848,20 @@ async function runAct(userPrompt, opts) {
|
|
|
676
848
|
* Num subagente não entra: ele já está fazendo uma parte de um plano, e
|
|
677
849
|
* um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
|
|
678
850
|
if (!subagent && profundidade === 0) {
|
|
679
|
-
const
|
|
680
|
-
|
|
851
|
+
const diretor = require('./estudio/diretor.js');
|
|
852
|
+
const producao = diretor.precisaDeProducao(pedido);
|
|
853
|
+
if (diretor.precisaDeEdicao(pedido)) {
|
|
854
|
+
// A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
|
|
855
|
+
messages.push({ role: 'system', content: 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
856
|
+
+ 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
|
|
857
|
+
+ 'depois `primocode video auto --json \'{}\'` (lê o que ela FALA na gravação e aplica cada efeito no momento em que diz), inspect, export e abra o mp4. '
|
|
858
|
+
+ 'Senão: '
|
|
859
|
+
+ 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
|
|
860
|
+
+ 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.\n'
|
|
861
|
+
+ 'EFEITOS que seguem a pessoa: separate_layers (recorte + momento 3D), replace_background (fundo novo no estalo), '
|
|
862
|
+
+ 'attach_3d (logo 3D na mão, segue a mão), merge_on_join (mãos juntas → fusão → cérebro), cut_silence_at (silêncio depois do estalo), '
|
|
863
|
+
+ 'style_captions (cor, legenda em cima da cabeça) — detalhe em `primocode video help efeitos`. Atalho: `primocode video do --json \'{"pedido":"<o pedido em português>"}\'`.' });
|
|
864
|
+
} else if (producao.producao) {
|
|
681
865
|
/* "SE ESTIVER NA SUA LISTA" não é hesitação — é a diferença entre
|
|
682
866
|
a tarefa seguir e a tarefa morrer.
|
|
683
867
|
|
|
@@ -697,8 +881,14 @@ async function runAct(userPrompt, opts) {
|
|
|
697
881
|
com as primitivas" — o modelo escrevia posição em pixel e o
|
|
698
882
|
vídeo saía quebrado, além de custar três ou quatro rodadas a
|
|
699
883
|
mais de modelo (medido em 25/09). */
|
|
884
|
+
/* A IA DECIDE (01/10): "pedi um roteiro de vídeo e ela já ia fazer o
|
|
885
|
+
motion". A regra só sabe que a palavra "vídeo" apareceu; quem sabe
|
|
886
|
+
se a pessoa quer o vídeo PRONTO ou só o roteiro, a ideia ou uma
|
|
887
|
+
resposta é o modelo. Então isto é o caminho SE for produzir, e não
|
|
888
|
+
mais uma ordem de produzir. */
|
|
700
889
|
messages.push({ role: 'system', content:
|
|
701
|
-
|
|
890
|
+
'VOCÊ DECIDE o que entregar, pelo que a pessoa pediu: se ela quer só o roteiro, a ideia, um texto ou uma resposta, ESCREVA isso e pare — não produza vídeo. '
|
|
891
|
+
+ `SE ela quer a peça pronta (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"), faça em três passos: `
|
|
702
892
|
+ `estudio_projeto acao:"criar" com formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}" → `
|
|
703
893
|
+ 'estudio_plano com "roteiro" (partes com tipo e fala; o PrimoCode faz layout, voz, ícones, música e sons) → '
|
|
704
894
|
+ 'estudio_render. Se a peça for de uma marca de verdade, estudio_site antes, e use as cores e a logo em roteiro.marca. '
|
|
@@ -749,8 +939,25 @@ async function runAct(userPrompt, opts) {
|
|
|
749
939
|
const MAX_ESPERAS_REDE = Number(process.env.PRIMOCODE_MAX_ESPERAS_REDE || 6);
|
|
750
940
|
|
|
751
941
|
let voltas = 0;
|
|
752
|
-
|
|
942
|
+
const teto = subagent ? MAX_VOLTAS_AGENTE : MAX_ITERATIONS;
|
|
943
|
+
let avisouTeto = false;
|
|
944
|
+
if (!subagent) TRABALHANDO = true;
|
|
945
|
+
try {
|
|
946
|
+
/* O AGENTE TEM TETO PRÓPRIO. No teste da loja (01/10) o "fechar
|
|
947
|
+
pendências do painel" passou de 38 voltas lendo o mesmo app.js aos
|
|
948
|
+
pedaços — a conversa dele inchou para 45 mil caracteres e uma resposta
|
|
949
|
+
levou 22 minutos. Uma parte de um plano cabe em 20 voltas; na 15ª ele
|
|
950
|
+
é avisado para fechar, e no teto volta com a entrega do que fez. */
|
|
951
|
+
for (let iteration = 0; iteration < teto && !PARAR; iteration++) {
|
|
753
952
|
voltas++;
|
|
953
|
+
if (subagent && !avisouTeto && voltas >= teto - 5) {
|
|
954
|
+
avisouTeto = true;
|
|
955
|
+
messages.push({ role: 'user', content: `Faltam ${teto - voltas + 1} voltas para o seu limite. Feche o essencial do que foi pedido agora, `
|
|
956
|
+
+ 'prove (rode o teste ou o comando) e chame finish dizendo o que ficou de fora. Nada de polimento extra.' });
|
|
957
|
+
}
|
|
958
|
+
// Recado digitado durante o passo anterior entra AGORA, antes da ida
|
|
959
|
+
// ao modelo — o resultado das ferramentas já está na conversa.
|
|
960
|
+
if (!subagent) entregarRecados(messages, profundidade);
|
|
754
961
|
const sp = new Spinner(subagent ? 'subagente trabalhando' : PENSANDO).start();
|
|
755
962
|
|
|
756
963
|
const toolCalls = [];
|
|
@@ -769,11 +976,19 @@ async function runAct(userPrompt, opts) {
|
|
|
769
976
|
emVoo = new AbortController();
|
|
770
977
|
const tRodada = Date.now();
|
|
771
978
|
try {
|
|
772
|
-
|
|
979
|
+
const enviadas = compactar(messages);
|
|
980
|
+
traco(`volta ${voltas}${subagent ? ' (agente)' : ''} · ${enviadas.length} mensagens · ${JSON.stringify(enviadas).length} caracteres`);
|
|
981
|
+
await motor.requestAct(server, { messages: enviadas, model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
|
|
773
982
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
774
|
-
if (evt.tool_call) {
|
|
983
|
+
if (evt.tool_call) {
|
|
984
|
+
toolCalls.push(evt.tool_call);
|
|
985
|
+
const a = evt.tool_call.arguments || {};
|
|
986
|
+
sp.detalhe = `${toolCalls.length} ${toolCalls.length > 1 ? 'ações prontas' : 'ação pronta'} · ${evt.tool_call.name}${a.path ? ' ' + a.path : ''}`;
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
775
989
|
if (evt.delta) {
|
|
776
990
|
assistantText += evt.delta;
|
|
991
|
+
sp.detalhe = assistantText.length > 200 ? `${Math.round(assistantText.length / 100) / 10} mil caracteres` : '';
|
|
777
992
|
if (!subagent && !toolCalls.length) {
|
|
778
993
|
if (!fluxo && assistantText.length > fala.LIMIAR_PENSAMENTO) {
|
|
779
994
|
sp.stop();
|
|
@@ -787,6 +1002,7 @@ async function runAct(userPrompt, opts) {
|
|
|
787
1002
|
} catch (e) {
|
|
788
1003
|
sp.stop();
|
|
789
1004
|
emVoo = null;
|
|
1005
|
+
traco(`erro na volta ${voltas}: ${String((e && e.message) || e).slice(0, 200)}`);
|
|
790
1006
|
if (fluxo) fluxo.fechar();
|
|
791
1007
|
// Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
|
|
792
1008
|
// de devolver erro e jogar fora o trabalho. O usuário não precisa
|
|
@@ -814,6 +1030,7 @@ async function runAct(userPrompt, opts) {
|
|
|
814
1030
|
}
|
|
815
1031
|
sp.stop();
|
|
816
1032
|
emVoo = null;
|
|
1033
|
+
traco(`volta ${voltas} voltou em ${Math.round((Date.now() - tRodada) / 1000)}s · ${toolCalls.map((t) => t.name).join(', ') || 'texto'}${streamError ? ' · erro: ' + String(streamError).slice(0, 160) : ''}`);
|
|
817
1034
|
tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
|
|
818
1035
|
if (PARAR) { if (fluxo) fluxo.fechar(); break; }
|
|
819
1036
|
|
|
@@ -956,7 +1173,7 @@ async function runAct(userPrompt, opts) {
|
|
|
956
1173
|
messages.push({
|
|
957
1174
|
role: 'user',
|
|
958
1175
|
content: `AINDA NÃO. Abri a página para conferir e ela tem problema:\n${relato}\n\n`
|
|
959
|
-
+ 'Conserte
|
|
1176
|
+
+ 'Conserte SÓ o que está apontado, com edit_file no arquivo do erro (leia só ele, se precisar), e finalize de novo.',
|
|
960
1177
|
});
|
|
961
1178
|
finalText = '';
|
|
962
1179
|
continue;
|
|
@@ -967,6 +1184,13 @@ async function runAct(userPrompt, opts) {
|
|
|
967
1184
|
if (fluxo) fluxo.fechar();
|
|
968
1185
|
else fala.resposta(finalText);
|
|
969
1186
|
}
|
|
1187
|
+
/* Ia terminar, mas chegou recado no meio: a tarefa não acaba com
|
|
1188
|
+
a pessoa esperando resposta. A volta seguinte entrega o recado. */
|
|
1189
|
+
if (!subagent && CAIXA.length && !PARAR) {
|
|
1190
|
+
messages.push({ role: 'assistant', content: finalText });
|
|
1191
|
+
finalText = '';
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
970
1194
|
break;
|
|
971
1195
|
}
|
|
972
1196
|
respostasVazias = 0;
|
|
@@ -999,13 +1223,13 @@ async function runAct(userPrompt, opts) {
|
|
|
999
1223
|
// fila — cada subagente só começava quando o anterior terminava, e a
|
|
1000
1224
|
// rodada custava a soma dos três. Agora as chamadas spawn_agent da
|
|
1001
1225
|
// rodada disparam de uma vez (Promise.all) e cada resultado volta na
|
|
1002
|
-
// posição certa da conversa. Limite de
|
|
1003
|
-
//
|
|
1226
|
+
// posição certa da conversa. Limite de 8 por rodada (era 4: o laço do
|
|
1227
|
+
// Qwen despacha várias direções de uma vez); o excedente roda em série.
|
|
1004
1228
|
const spawns = toolCalls.filter((tc) => tc.name === 'spawn_agent');
|
|
1005
1229
|
const resultadosSpawn = new Map();
|
|
1006
1230
|
if (spawns.length && !PARAR) garantirPasta();
|
|
1007
1231
|
if (spawns.length > 1 && !PARAR) {
|
|
1008
|
-
const juntos = spawns.slice(0,
|
|
1232
|
+
const juntos = spawns.slice(0, 8);
|
|
1009
1233
|
fala.paralelo(juntos.length, profundidade);
|
|
1010
1234
|
const rs = await Promise.all(juntos.map((tc) =>
|
|
1011
1235
|
runSubagent(tc.arguments || {}, {
|
|
@@ -1014,6 +1238,22 @@ async function runAct(userPrompt, opts) {
|
|
|
1014
1238
|
juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
|
|
1015
1239
|
}
|
|
1016
1240
|
|
|
1241
|
+
/* LEITURAS JUNTAS. O RITMO pede ao modelo para juntar as ações
|
|
1242
|
+
independentes numa resposta só — e ele junta: três read_file, um
|
|
1243
|
+
grep e uma pesquisa. Só que o laço abaixo rodava uma de cada vez, e
|
|
1244
|
+
a rodada custava a soma. O que só LÊ (não muda arquivo, tela nem
|
|
1245
|
+
conta) roda tudo de uma vez aqui; o laço usa o resultado pronto,
|
|
1246
|
+
passando pelos mesmos freios de sempre (repetição, registro). */
|
|
1247
|
+
const adiantadas = new Map();
|
|
1248
|
+
const leituras = toolCalls.filter(ehLeitura);
|
|
1249
|
+
if (leituras.length > 1 && !PARAR) {
|
|
1250
|
+
const rs = await Promise.all(leituras.slice(0, 8).map((tc) =>
|
|
1251
|
+
tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api })
|
|
1252
|
+
.catch((e) => ({ ok: false, error: e && e.message }))));
|
|
1253
|
+
leituras.slice(0, 8).forEach((tc, i) => adiantadas.set(tc.id, rs[i]));
|
|
1254
|
+
traco(`${adiantadas.size} leituras em paralelo`);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1017
1257
|
for (const tc of toolCalls) {
|
|
1018
1258
|
if (PARAR) break;
|
|
1019
1259
|
if (tc.name === 'finish') {
|
|
@@ -1037,7 +1277,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1037
1277
|
role: 'tool',
|
|
1038
1278
|
tool_call_id: tc.id,
|
|
1039
1279
|
content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.map((f) => f.texto).join('; ')}. `
|
|
1040
|
-
+ 'Conserte agora
|
|
1280
|
+
+ 'Conserte agora com edit_file no arquivo que falhou ou, se for impossível, diga claramente ao usuário o que falhou.',
|
|
1041
1281
|
});
|
|
1042
1282
|
continue;
|
|
1043
1283
|
}
|
|
@@ -1055,7 +1295,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1055
1295
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
1056
1296
|
});
|
|
1057
1297
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1058
|
-
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
1298
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: r + lembreteDeParalelo(todosDaTarefa, spawns.length) });
|
|
1059
1299
|
continue;
|
|
1060
1300
|
}
|
|
1061
1301
|
|
|
@@ -1117,6 +1357,38 @@ async function runAct(userPrompt, opts) {
|
|
|
1117
1357
|
|
|
1118
1358
|
if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
|
|
1119
1359
|
|
|
1360
|
+
/* RELER O QUE ACABOU DE ESCREVER. O modelo grátis escreve o
|
|
1361
|
+
index.html e, na volta seguinte, pede para lê-lo "para
|
|
1362
|
+
conferir" — uma ida ao servidor para receber o texto que ele
|
|
1363
|
+
mesmo mandou, e que continua inteiro na chamada write_file dele
|
|
1364
|
+
(o `compactar` só encolhe resultado de ferramenta, nunca o que
|
|
1365
|
+
o modelo escreveu). Se o arquivo está byte a byte como ele
|
|
1366
|
+
deixou, a resposta é essa, sem abrir nada. Depois de um
|
|
1367
|
+
edit_file não vale: aí o texto na conversa está em pedaços. */
|
|
1368
|
+
const relido = tc.name === 'read_file' && tc.arguments && tc.arguments.path
|
|
1369
|
+
? tc.arguments.path
|
|
1370
|
+
: tc.name === 'run_shell' ? leituraPeloShell(tc.arguments && tc.arguments.command) : null;
|
|
1371
|
+
if (relido && escritosNaTarefa.size) {
|
|
1372
|
+
let igual = false;
|
|
1373
|
+
try {
|
|
1374
|
+
const abs = require('path').isAbsolute(relido) ? relido : tools.resolveInProject(relido, pastaAtual);
|
|
1375
|
+
const e = escritosNaTarefa.get(abs);
|
|
1376
|
+
// Só enquanto a escrita ainda viaja inteira na conversa
|
|
1377
|
+
// (as duas últimas voltas — ver ESCRITAS_INTEIRAS).
|
|
1378
|
+
igual = !!e && voltas - e.volta <= 1 && require('fs').readFileSync(abs, 'utf8') === e.texto;
|
|
1379
|
+
} catch { igual = false; }
|
|
1380
|
+
if (igual) {
|
|
1381
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1382
|
+
console.log(fala.resultado(profundidade) + c.muted('igual ao que ele escreveu — não precisou ler'));
|
|
1383
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({
|
|
1384
|
+
ok: true, igualAoQueVoceEscreveu: true,
|
|
1385
|
+
aviso: 'Este arquivo está EXATAMENTE como você o escreveu nesta tarefa — o conteúdo é o da sua '
|
|
1386
|
+
+ 'chamada write_file acima. Não precisa ler. Para mudar, use edit_file direto; se terminou, siga.',
|
|
1387
|
+
}) });
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1120
1392
|
if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
|
|
1121
1393
|
voltasEmFalso++;
|
|
1122
1394
|
messages.push({
|
|
@@ -1138,7 +1410,6 @@ async function runAct(userPrompt, opts) {
|
|
|
1138
1410
|
const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
|
|
1139
1411
|
if (!porta.ok) {
|
|
1140
1412
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1141
|
-
fala.avisar('resultado', { nome: tc.name, ok: false, resumo: 'não autorizado' });
|
|
1142
1413
|
console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
|
|
1143
1414
|
registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
|
|
1144
1415
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
|
|
@@ -1156,7 +1427,6 @@ async function runAct(userPrompt, opts) {
|
|
|
1156
1427
|
});
|
|
1157
1428
|
if (!risco.ok) {
|
|
1158
1429
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1159
|
-
fala.avisar('resultado', { nome: tc.name, ok: false, resumo: `não autorizado — ${risco.motivo}` });
|
|
1160
1430
|
console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
|
|
1161
1431
|
registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
|
|
1162
1432
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
|
|
@@ -1174,13 +1444,24 @@ async function runAct(userPrompt, opts) {
|
|
|
1174
1444
|
/* O servidor, o token e o cliente da API vão junto: a legenda do
|
|
1175
1445
|
estudio_video transcreve no servidor, e sem eles a ferramenta
|
|
1176
1446
|
respondia "a transcrição precisa do servidor". */
|
|
1177
|
-
|
|
1447
|
+
traco(`executando ${tc.name}`);
|
|
1448
|
+
const result = adiantadas.has(tc.id) ? adiantadas.get(tc.id)
|
|
1449
|
+
: await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
|
|
1450
|
+
traco(`${tc.name} devolveu ok=${result && result.ok}`);
|
|
1178
1451
|
logResult(tc.name, result, profundidade);
|
|
1179
1452
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1180
1453
|
if (RELEITURA.has(tc.name) && result && result.ok !== false) {
|
|
1181
1454
|
leiturasGuardadas.set(eco.chave, summarizeForModel(result));
|
|
1182
1455
|
}
|
|
1183
1456
|
if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
|
|
1457
|
+
if (/^(write_file|edit_file|multi_edit)$/.test(tc.name) && tc.arguments && tc.arguments.path) {
|
|
1458
|
+
try {
|
|
1459
|
+
const abs = tools.resolveInProject(tc.arguments.path, pastaAtual);
|
|
1460
|
+
if (tc.name === 'write_file' && result && result.ok !== false && typeof tc.arguments.content === 'string') {
|
|
1461
|
+
escritosNaTarefa.set(abs, { texto: tc.arguments.content, volta: voltas });
|
|
1462
|
+
} else escritosNaTarefa.delete(abs);
|
|
1463
|
+
} catch { /* caminho inválido: a tool já recusou */ }
|
|
1464
|
+
}
|
|
1184
1465
|
// Além de narrar, ANOTA: no fim do pedido sai o balanço do que
|
|
1185
1466
|
// nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
|
|
1186
1467
|
// tarefa de trinta ações, a primeira já saiu da tela.
|
|
@@ -1270,9 +1551,14 @@ async function runAct(userPrompt, opts) {
|
|
|
1270
1551
|
if (finished) {
|
|
1271
1552
|
finalText = finished;
|
|
1272
1553
|
if (!subagent) fala.resposta(finished);
|
|
1554
|
+
// Recado chegou enquanto ele fechava: entrega e segue.
|
|
1555
|
+
if (!subagent && CAIXA.length && !PARAR) { finalText = ''; continue; }
|
|
1273
1556
|
break;
|
|
1274
1557
|
}
|
|
1275
1558
|
}
|
|
1559
|
+
} finally {
|
|
1560
|
+
if (!subagent) TRABALHANDO = false;
|
|
1561
|
+
}
|
|
1276
1562
|
|
|
1277
1563
|
// O que o modelo entregou por vontade própria, ANTES dos textos de
|
|
1278
1564
|
// desistência. É esta a diferença que o plano de passos precisa: um passo
|
|
@@ -1289,7 +1575,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1289
1575
|
// não dá nem para saber que acabou. Medido: um pedido de navegador morria assim
|
|
1290
1576
|
// depois de a primeira tentativa de seletor falhar.
|
|
1291
1577
|
if (!finalText) {
|
|
1292
|
-
finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS });
|
|
1578
|
+
finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS, teto });
|
|
1293
1579
|
if (!subagent) fala.aviso(finalText);
|
|
1294
1580
|
}
|
|
1295
1581
|
|
|
@@ -1367,7 +1653,42 @@ async function runSubagent(args, ctx) {
|
|
|
1367
1653
|
|
|
1368
1654
|
const resumo = r.assistantMessage.content || 'sem resultado';
|
|
1369
1655
|
fala.agenteVoltou(resumo, profundidade);
|
|
1370
|
-
return resumo;
|
|
1656
|
+
return entregaDoAgente(especialistas.rotulo(esp), resumo, r.registro);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/* UM AGENTE DE CADA VEZ é o padrão do modelo grátis: no teste da loja
|
|
1660
|
+
(API + site + painel, 01/10) ele despachou os três em fila — 6 + 8 + 5
|
|
1661
|
+
minutos — quando o site e o painel só dependiam do contrato, que já
|
|
1662
|
+
existia. Quem decide continua sendo ele; a volta só lembra que as partes
|
|
1663
|
+
que faltam podem ir JUNTAS. Pura. */
|
|
1664
|
+
function lembreteDeParalelo(todos, naRodada) {
|
|
1665
|
+
const faltam = (todos || []).filter((t) => t && t.status !== 'completed' && t.status !== 'concluida');
|
|
1666
|
+
if (naRodada > 1 || faltam.length < 2) return '';
|
|
1667
|
+
return `\n\n(Faltam ${faltam.length} partes do plano. As que não dependem uma da outra vão JUNTAS: `
|
|
1668
|
+
+ 'vários spawn_agent na MESMA resposta rodam em paralelo.)';
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/* O SALTO ENTRE AGENTES. O pai recebia só o texto final do especialista —
|
|
1672
|
+
"pronto, fiz a API" — e, para passar a vez ao próximo (o do front, o
|
|
1673
|
+
revisor), tinha de adivinhar o que existia. Agora a volta é uma ENTREGA:
|
|
1674
|
+
quem fez, o que disse, os arquivos que nasceram e mudaram, os comandos, o
|
|
1675
|
+
que falhou. É o contexto que o próximo especialista precisa no prompt
|
|
1676
|
+
dele, e é o que deixa o pai mandar a falha de volta para conserto (o laço
|
|
1677
|
+
do Qwen: planeja → despacha → confere → conserta). Pura, para o teste. */
|
|
1678
|
+
function entregaDoAgente(quem, texto, reg) {
|
|
1679
|
+
const linhas = [`ENTREGA DO ${String(quem).toUpperCase()}`, String(texto).trim().slice(0, 4000)];
|
|
1680
|
+
if (reg) {
|
|
1681
|
+
const lista = (nome, xs) => { if (xs && xs.length) linhas.push(`${nome}: ${xs.slice(0, 15).join(', ')}`); };
|
|
1682
|
+
lista('criou', reg.criados);
|
|
1683
|
+
lista('alterou', reg.alterados);
|
|
1684
|
+
lista('rodou', (reg.comandos || []).map((x) => `${x.comando}${x.ok === false ? ` (saiu ${x.code})` : ''}`));
|
|
1685
|
+
lista('testes', (reg.testes || []).map((x) => `${x.comando}: ${x.ok ? 'passou' : 'FALHOU'}`));
|
|
1686
|
+
if (reg.falhas && reg.falhas.length) linhas.push('FALHOU: ' + reg.falhas.slice(0, 6).map((f) => `${f.acao} ${f.alvo}: ${f.motivo}`.slice(0, 200)).join(' | '));
|
|
1687
|
+
}
|
|
1688
|
+
linhas.push(reg && ((reg.falhas && reg.falhas.length) || (reg.testes || []).some((x) => !x.ok))
|
|
1689
|
+
? 'PRÓXIMO: há falha acima — mande de volta para conserto (spawn_agent com o erro no prompt) ou conserte você, antes de seguir.'
|
|
1690
|
+
: 'PRÓXIMO: se outra parte depende desta, passe a vez (spawn_agent) com esta entrega no prompt; senão, confira e siga.');
|
|
1691
|
+
return linhas.join('\n');
|
|
1371
1692
|
}
|
|
1372
1693
|
|
|
1373
1694
|
/* ══ O PIPELINE DE QUATRO PAPÉIS ═══════════════════════════════════════════
|
|
@@ -1480,5 +1801,5 @@ async function rodarPipeline(args, ctx) {
|
|
|
1480
1801
|
});
|
|
1481
1802
|
}
|
|
1482
1803
|
|
|
1483
|
-
module.exports = { runAct, buildMemoryContext, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1804
|
+
module.exports = { runAct, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1484
1805
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|