primocode 8.5.0 → 8.7.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
@@ -542,19 +542,35 @@ async function sendPrompt(userText) {
542
542
  // que está torto, diz qual é o entregável e o que conta como pronto.
543
543
  const prompt2 = await refinarPedido(prompt);
544
544
 
545
- const r = await runAct(prompt2, {
546
- server: state.config.server,
547
- model: state.config.model,
548
- effort: state.config.effort,
549
- token: state.config.token,
550
- history: memoria.paraEnviar(state.memoria, state.history),
551
- projectDir: plan.dir,
552
- projectName: plan.name || state.currentProject,
553
- // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
554
- // não é motivo para criar pasta de projeto.
555
- ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
556
- motor: state.engine === 'claude-code' ? claudeEngine : api,
557
- });
545
+ // A tarefa grande vira passos, e cada passo é uma chamada própria — com
546
+ // conversa nova, e por isso pequena. Oculto: o usuário mandou UMA coisa.
547
+ const passos = await dividirTarefa(prompt2);
548
+
549
+ let r = null;
550
+ let jaFeito = '';
551
+ for (let i = 0; i < passos.length; i++) {
552
+ if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
553
+ const pedido = i === 0 ? passos[i].pedido
554
+ : `${passos[i].pedido}\n\nO QUE JÁ FOI FEITO ANTES (use estes fatos, não pesquise de novo):\n${jaFeito}`;
555
+
556
+ r = await runAct(pedido, {
557
+ server: state.config.server,
558
+ model: state.config.model,
559
+ effort: state.config.effort,
560
+ token: state.config.token,
561
+ // Só o primeiro passo enxerga a conversa: os seguintes recebem o
562
+ // que interessa em `jaFeito`, e conversa a mais só ocupa o teto.
563
+ history: i === 0 ? memoria.paraEnviar(state.memoria, state.history) : [],
564
+ projectDir: plan.dir,
565
+ projectName: plan.name || state.currentProject,
566
+ // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
567
+ // não é motivo para criar pasta de projeto.
568
+ ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
569
+ motor: state.engine === 'claude-code' ? claudeEngine : api,
570
+ });
571
+ jaFeito = String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200);
572
+ if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
573
+ }
558
574
  state.history.push({ role: 'user', content: userText });
559
575
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
560
576
  state.lastResponse = r.assistantMessage.content;
@@ -568,6 +584,35 @@ async function sendPrompt(userText) {
568
584
  refreshPrompt();
569
585
  }
570
586
 
587
+ /**
588
+ * Divide a tarefa grande em passos — e faz isso ESCONDIDO.
589
+ *
590
+ * O motivo é aritmético. Com 6.000 tokens por minuto, "pesquise sobre a X e
591
+ * crie um vídeo" não cabe numa conversa só: o custo fixo (prompt + tools) é
592
+ * ~3.700, a spec do formato ~1.400, os fatos da pesquisa o resto — e sobram
593
+ * 512 para o agente escrever o documento. Em DOIS pedidos cada um cabe
594
+ * folgado, porque cada um leva só as ferramentas e o prompt do seu passo.
595
+ *
596
+ * O dono queria isto automático: "o usuário manda a tarefa gigante, e a IA
597
+ * ocultamente vai quebrar a tarefa". E com razão — quem pede não sabe onde
598
+ * está a fronteira barata, e pode partir num pedaço que ainda não cabe.
599
+ *
600
+ * Falhou? Um passo só, que é o comportamento de sempre.
601
+ */
602
+ async function dividirTarefa(pedido) {
603
+ const inteiro = [{ titulo: 'tarefa', pedido }];
604
+ try {
605
+ const r = await api.requestPlano(state.config.server, {
606
+ prompt: pedido, model: state.config.model, token: state.config.token,
607
+ });
608
+ const passos = (r && Array.isArray(r.passos) && r.passos.length) ? r.passos : inteiro;
609
+ if (passos.length > 1) console.log(c.dim(` ⎿ ${passos[0].titulo}`));
610
+ return passos;
611
+ } catch {
612
+ return inteiro;
613
+ }
614
+ }
615
+
571
616
  /**
572
617
  * Estrutura o pedido antes de mandar o agente executar.
573
618
  *
@@ -576,24 +621,23 @@ async function sendPrompt(userText) {
576
621
  * em círculo porque não sabe o que é "pronto". Uma passada barata antes de
577
622
  * começar vira ordem de serviço: o que entregar, com que fatos, quando parar.
578
623
  *
579
- * Mostra o que mudou — o usuário precisa poder discordar do que foi entendido,
580
- * não descobrir no fim que o agente fez outra coisa.
624
+ * É OCULTO de propósito. A primeira versão mostrava "entendi assim:" na tela,
625
+ * e o dono cortou: encanamento não é conversa. O usuário pediu uma coisa e
626
+ * quer ver a coisa acontecendo, não um relatório do que a máquina entendeu.
627
+ * O refino também não decide nada — só desambigua o que ele já disse —, então
628
+ * não há o que ele precise aprovar.
581
629
  *
582
630
  * Nunca bloqueia: se o refino falhar ou vier estranho, vale o pedido original.
583
631
  */
584
632
  async function refinarPedido(cru) {
585
633
  if (!cru || cru.trim().length < 12) return cru;
586
- const sp = new Spinner('entendendo o pedido').start();
634
+ const sp = new Spinner('Primo Code pensando').start();
587
635
  try {
588
636
  const r = await api.requestRefino(state.config.server, {
589
637
  prompt: cru, model: state.config.model, token: state.config.token,
590
638
  });
591
639
  sp.stop();
592
640
  if (!r || !r.refinado || !r.prompt) return cru;
593
- console.log(c.dim(' ⎿ entendi assim:'));
594
- for (const linha of String(r.prompt).split('\n')) {
595
- if (linha.trim()) console.log(c.dim(' ' + linha.trim()));
596
- }
597
641
  return r.prompt;
598
642
  } catch {
599
643
  sp.stop(); // refinar é melhoria, não requisito
package/lib/act.js CHANGED
@@ -37,6 +37,45 @@ function interromper() {
37
37
  if (emVoo) { try { emVoo.abort(); } catch {} }
38
38
  }
39
39
 
40
+ /**
41
+ * Este erro é "não coube no minuto"?
42
+ *
43
+ * É o único erro que a gente sabe consertar sozinho: encolher o que já foi
44
+ * feito e continuar. Rede, cota do dia e chave inválida não se resolvem
45
+ * assim, e tentar de novo só gastaria a vez do usuário.
46
+ */
47
+ function ehTetoDeToken(e) {
48
+ const s = String((e && e.message) || e || '').toLowerCase();
49
+ 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
+ }
51
+
52
+ /**
53
+ * O /compact automático, feito no meio da tarefa.
54
+ *
55
+ * O dono: "quando bate no teto dos 6 mil, ele roda o compact ocultamente e
56
+ * continua a tarefa, pega todo o contexto que ele fez antes, como se nada
57
+ * tivesse acontecido".
58
+ *
59
+ * Troca as rodadas já feitas por UMA anotação do que se descobriu e do que
60
+ * ficou pronto. O agente perde o texto literal das ferramentas e mantém o que
61
+ * importa para seguir: onde estava, o que já sabe, o que falta.
62
+ */
63
+ function anotacaoDoQueJaFoi(messages) {
64
+ const chamadas = repeticao.chamadasPorId(messages);
65
+ const feitos = [];
66
+ for (const m of messages) {
67
+ if (!m || m.role !== 'tool') continue;
68
+ const ch = chamadas.get(m.tool_call_id);
69
+ if (!ch) continue;
70
+ const alvo = ch.args && (ch.args.path || ch.args.consulta || ch.args.ferramenta || ch.args.titulo || ch.args.id);
71
+ const corpo = String(m.content || '');
72
+ const deuErrado = /"ok"\s*:\s*false/.test(corpo);
73
+ feitos.push(`- ${ch.nome}${alvo ? `(${String(alvo).slice(0, 60)})` : ''}: `
74
+ + (deuErrado ? 'FALHOU' : corpo.slice(0, 220).replace(/\s+/g, ' ')));
75
+ }
76
+ return feitos;
77
+ }
78
+
40
79
  function ident(profundidade) {
41
80
  return ' '.repeat(profundidade);
42
81
  }
@@ -231,6 +270,29 @@ function compactar(messages) {
231
270
  * murro em ponta de faca. Dizer o motivo certo é a diferença entre o usuário
232
271
  * saber o que fazer e ficar no escuro.
233
272
  */
273
+ /**
274
+ * Troca as rodadas já feitas por uma ANOTAÇÃO do que se descobriu, no lugar.
275
+ *
276
+ * Mexe no array recebido de propósito: quem chamou continua o mesmo loop, com
277
+ * a mesma conversa, só que leve. O pedido do usuário e a mensagem de sistema
278
+ * ficam — sem eles o agente não sabe mais o que estava fazendo.
279
+ */
280
+ function encolherParaContinuar(messages) {
281
+ const anotacao = anotacaoDoQueJaFoi(messages);
282
+ const guardar = messages.filter((m) => m.role === 'system' || m.role === 'user');
283
+ messages.length = 0;
284
+ messages.push(...guardar);
285
+ if (anotacao.length) {
286
+ messages.push({
287
+ role: 'system',
288
+ content: 'O QUE VOCÊ JÁ FEZ NESTA TAREFA (a conversa foi encolhida para caber no limite; '
289
+ + 'estas são as suas anotações, continue de onde parou e NÃO refaça o que já está aqui):\n'
290
+ + anotacao.join('\n')
291
+ + '\n\nSiga para o próximo passo. Se já tem o que precisa, PRODUZA o resultado agora.',
292
+ });
293
+ }
294
+ }
295
+
234
296
  function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
235
297
  if (falhasPendentes.length) {
236
298
  return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
@@ -294,6 +356,12 @@ async function runAct(userPrompt, opts) {
294
356
  // Pedido novo zera o pedido de parada anterior.
295
357
  if (!subagent) PARAR = false;
296
358
 
359
+ // Quantas vezes já encolhemos por estouro de teto. Duas bastam: se depois
360
+ // disso ainda não couber, o problema é o custo fixo, e encolher de novo só
361
+ // apagaria o trabalho sem resolver.
362
+ let compactacoes = 0;
363
+ const MAX_COMPACTACOES = 2;
364
+
297
365
  let voltas = 0;
298
366
  for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
299
367
  voltas++;
@@ -318,6 +386,20 @@ async function runAct(userPrompt, opts) {
318
386
  });
319
387
  } catch (e) {
320
388
  sp.stop();
389
+ emVoo = null;
390
+ // Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
391
+ // de devolver erro e jogar fora o trabalho. O usuário não precisa
392
+ // saber que isso aconteceu — a tarefa é dele, o encanamento é nosso.
393
+ if (ehTetoDeToken(e) && compactacoes < MAX_COMPACTACOES && !PARAR) {
394
+ compactacoes++;
395
+ const antes = messages.length;
396
+ encolherParaContinuar(messages);
397
+ if (!subagent) {
398
+ console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
399
+ }
400
+ iteration--; // esta volta não contou: nada foi decidido
401
+ continue;
402
+ }
321
403
  const amigavel = friendlyError(e);
322
404
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
323
405
  console.log('\n' + c.err(amigavel));
@@ -327,6 +409,17 @@ async function runAct(userPrompt, opts) {
327
409
  emVoo = null;
328
410
  if (PARAR) break;
329
411
 
412
+ if (streamError && ehTetoDeToken(streamError) && compactacoes < MAX_COMPACTACOES && !PARAR) {
413
+ compactacoes++;
414
+ const antes = messages.length;
415
+ encolherParaContinuar(messages);
416
+ if (!subagent) {
417
+ console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
418
+ }
419
+ iteration--;
420
+ continue;
421
+ }
422
+
330
423
  if (streamError) {
331
424
  const amigavel = friendlyError(streamError);
332
425
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
@@ -497,6 +590,10 @@ async function runAct(userPrompt, opts) {
497
590
  return {
498
591
  userMessage: { role: 'user', content: userPrompt },
499
592
  assistantMessage: { role: 'assistant', content: finalText },
593
+ // Esc Esc no meio de um plano de vários passos precisa parar o plano
594
+ // INTEIRO, não só o passo — senão o próximo passo começa logo depois
595
+ // de o usuário ter mandado parar.
596
+ parado: PARAR,
500
597
  };
501
598
  }
502
599
 
@@ -524,4 +621,5 @@ async function runSubagent(args, ctx) {
524
621
  return resumo;
525
622
  }
526
623
 
527
- module.exports = { runAct, interromper, compactar, motivoDaParada, MAX_ITERATIONS };
624
+ module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken,
625
+ anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
package/lib/api.js CHANGED
@@ -162,6 +162,12 @@ function requestRefino(serverUrl, payload) {
162
162
  return request(serverUrl, '/api/refinar', body, token);
163
163
  }
164
164
 
165
+ // O plano: a tarefa grande dividida em passos, para uma chamada por pedaço.
166
+ function requestPlano(serverUrl, payload) {
167
+ const { token, ...body } = payload;
168
+ return request(serverUrl, '/api/plano', body, token);
169
+ }
170
+
165
171
  function getHealth(serverUrl, token) {
166
172
  return new Promise((resolve, reject) => {
167
173
  const url = new URL('/health', serverUrl);
@@ -178,4 +184,4 @@ function getHealth(serverUrl, token) {
178
184
  });
179
185
  }
180
186
 
181
- module.exports = { request, requestStream, requestAct, requestChat, requestResumo, requestRefino, getHealth };
187
+ module.exports = { request, requestStream, requestAct, requestChat, requestResumo, requestRefino, requestPlano, getHealth };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.5.0",
3
+ "version": "8.7.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"