primocode 8.6.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
  *
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.6.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"