primocode 8.2.2 → 8.4.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
@@ -24,6 +24,7 @@ const claudeEngine = require('../lib/claude-engine');
24
24
  const pasta = require('../lib/pasta');
25
25
  const catalogo = require('../lib/catalogo');
26
26
  const effort = require('../lib/effort');
27
+ const memoria = require('../lib/memoria-conversa');
27
28
 
28
29
  // Rede de segurança de último recurso: sem isto, uma exceção fora de um
29
30
  // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
@@ -50,6 +51,9 @@ const state = {
50
51
  mode: null, // 'primocode' | 'chat' | 'auto' — vem da config em main()
51
52
  modoEfetivo: null, // no auto, o modo que o último pedido usou de fato
52
53
  attachedFiles: [],
54
+ // O resumo incremental da conversa — um /compact automático. O porquê
55
+ // está em lib/memoria-conversa.js.
56
+ memoria: memoria.nova(),
53
57
  lastResponse: '',
54
58
  currentProject: null,
55
59
  workDir: null, // pasta onde o agente trabalha nesta sessão
@@ -529,12 +533,17 @@ async function sendPrompt(userText) {
529
533
  if (modo === 'primocode') {
530
534
  const plan = prepararPasta(userText);
531
535
 
536
+ // Antes de gastar com a tarefa, dobra no resumo o que já passou. A
537
+ // conta é pequena: entra "resumo de antes + o que é novo", não a
538
+ // conversa inteira.
539
+ await comprimirConversa();
540
+
532
541
  const r = await runAct(prompt, {
533
542
  server: state.config.server,
534
543
  model: state.config.model,
535
544
  effort: state.config.effort,
536
545
  token: state.config.token,
537
- history: state.history,
546
+ history: memoria.paraEnviar(state.memoria, state.history),
538
547
  projectDir: plan.dir,
539
548
  projectName: plan.name || state.currentProject,
540
549
  // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
@@ -547,6 +556,7 @@ async function sendPrompt(userText) {
547
556
  state.lastResponse = r.assistantMessage.content;
548
557
  cfg.appendHistory({ mode: 'primocode', project: state.currentProject, prompt: userText });
549
558
  } else {
559
+ await comprimirConversa();
550
560
  await runChat(prompt, userText);
551
561
  }
552
562
 
@@ -554,6 +564,27 @@ async function sendPrompt(userText) {
554
564
  refreshPrompt();
555
565
  }
556
566
 
567
+ /**
568
+ * Atualiza o resumo da conversa, se já compensar.
569
+ *
570
+ * Silencioso quando não há o que fazer: nas primeiras rodadas a conversa é
571
+ * curta e o resumo não pagaria a chamada.
572
+ */
573
+ async function comprimirConversa() {
574
+ if (!memoria.precisaResumir(state.memoria, state.history)) return;
575
+ const sp = new Spinner('resumindo a conversa').start();
576
+ try {
577
+ state.memoria = await memoria.atualizar(state.memoria, state.history, ({ resumo, mensagens }) =>
578
+ api.requestResumo(state.config.server, { resumo, mensagens, token: state.config.token })
579
+ .then((r) => r.resumo));
580
+ sp.stop();
581
+ const e = memoria.economia(state.memoria, state.history);
582
+ if (e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
583
+ } catch {
584
+ sp.stop(); // resumo é economia, não requisito: falhou, segue a vida
585
+ }
586
+ }
587
+
557
588
  async function runChat(promptForModel, userTextForMemory) {
558
589
  const sp = new Spinner('pensando').start();
559
590
  let started = false;
@@ -561,7 +592,7 @@ async function runChat(promptForModel, userTextForMemory) {
561
592
  try {
562
593
  await api.requestChat(state.config.server, {
563
594
  prompt: promptForModel,
564
- history: state.history.slice(-20),
595
+ history: memoria.paraEnviar(state.memoria, state.history),
565
596
  model: state.config.model,
566
597
  token: state.config.token,
567
598
  }, (evt) => {
@@ -854,7 +885,7 @@ async function handleSlash(text) {
854
885
  break;
855
886
  }
856
887
 
857
- case 'clear': state.history = []; console.log(c.ok('Conversa limpa.')); break;
888
+ case 'clear': state.history = []; state.memoria = memoria.nova(); console.log(c.ok('Conversa limpa.')); break;
858
889
  case 'cls': process.stdout.write('\x1Bc'); break;
859
890
 
860
891
  case 'status': {
@@ -872,6 +903,14 @@ async function handleSlash(text) {
872
903
  console.log(c.dim(` Reservas: ${fila.length - 1}`));
873
904
  }
874
905
  } catch (e) { sp.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
906
+ // A memória da conversa: quanto ela está poupando por pedido.
907
+ if (state.history.length) {
908
+ const e = memoria.economia(state.memoria, state.history);
909
+ console.log(c.muted(` Conversa: ${state.history.length} mensagens`)
910
+ + (state.memoria.resumo
911
+ ? c.dim(` · resumida, ${e.enxuto} tokens por pedido em vez de ${e.cheio}`)
912
+ : c.dim(` · ${e.enxuto} tokens por pedido`)));
913
+ }
875
914
  break;
876
915
  }
877
916
 
package/lib/api.js CHANGED
@@ -128,6 +128,13 @@ function requestChat(serverUrl, payload, onEvent) {
128
128
  return requestStream(serverUrl, '/api/chat/stream', body, onEvent, token);
129
129
  }
130
130
 
131
+ // Resumo incremental da conversa. Vai e volta pequeno: manda o resumo de
132
+ // antes + o que é novo, recebe o resumo atualizado.
133
+ function requestResumo(serverUrl, payload) {
134
+ const { token, ...body } = payload;
135
+ return request(serverUrl, '/api/resumir', body, token);
136
+ }
137
+
131
138
  function getHealth(serverUrl, token) {
132
139
  return new Promise((resolve, reject) => {
133
140
  const url = new URL('/health', serverUrl);
@@ -144,4 +151,4 @@ function getHealth(serverUrl, token) {
144
151
  });
145
152
  }
146
153
 
147
- module.exports = { request, requestStream, requestAct, requestChat, getHealth };
154
+ module.exports = { request, requestStream, requestAct, requestChat, requestResumo, getHealth };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * memoria-conversa.js — a conversa que não engorda.
3
+ *
4
+ * ── O PROBLEMA ───────────────────────────────────────────────────────────
5
+ * O histórico ia inteiro (as últimas 20 mensagens) em todo pedido. Numa
6
+ * conversa de trabalho isso vira milhares de tokens repetidos a cada rodada, e
7
+ * num plano com teto por minuto a tarefa morre no meio — depois de o esforço
8
+ * já ter sido gasto. Cortar pelas últimas N é pior ainda: some justamente o
9
+ * começo, onde está o que o usuário pediu.
10
+ *
11
+ * ── A IDEIA (do dono) ────────────────────────────────────────────────────
12
+ * Resumo INCREMENTAL, como um /compact automático: guarda-se um resumo do que
13
+ * já passou e, a cada rodada, só o PEDAÇO NOVO é dobrado nele. A primeira vez
14
+ * custa; da segunda em diante o resumo já existe e só o que foi dito a mais
15
+ * precisa ser lido.
16
+ *
17
+ * O custo fica limitado por construção: a entrada do resumo é sempre
18
+ * "resumo de antes + o que aconteceu desde então", nunca a conversa inteira.
19
+ *
20
+ * ── O QUE NUNCA É RESUMIDO ───────────────────────────────────────────────
21
+ * As últimas rodadas vão CRUAS. Resumo é bom para lembrar do que foi feito,
22
+ * péssimo para continuar de onde parou: "agora põe música de fundo" precisa da
23
+ * frase anterior literal, não de um tópico dizendo que houve um vídeo.
24
+ *
25
+ * Este arquivo é puro — quem fala com o servidor é quem chama `atualizar`.
26
+ * Assim o teste mede a política inteira sem rede e sem chave de API.
27
+ */
28
+
29
+ 'use strict';
30
+
31
+ /** ~4 caracteres por token. Mesma conta do servidor, e boa o bastante aqui. */
32
+ function tokensDe(x) {
33
+ const texto = typeof x === 'string' ? x : JSON.stringify(x || '');
34
+ return Math.ceil(texto.length / 4);
35
+ }
36
+
37
+ // Quantas mensagens do fim ficam sempre cruas (o "de onde parei").
38
+ const CRUAS = 4;
39
+ // Acima disto, a cauda vira resumo. Abaixo, não compensa pagar a chamada.
40
+ const LIMITE_TOKENS = 700;
41
+
42
+ /** Estado inicial — o que o CLI guarda entre um pedido e outro. */
43
+ function nova() {
44
+ return { resumo: '', resumidasAte: 0 };
45
+ }
46
+
47
+ /**
48
+ * O que ainda não foi resumido e já não cabe entre as cruas.
49
+ * É exatamente o que a próxima chamada de resumo precisa ler.
50
+ */
51
+ function pendentes(memoria, history) {
52
+ const fim = Math.max(0, (history || []).length - CRUAS);
53
+ if (fim <= memoria.resumidasAte) return [];
54
+ return history.slice(memoria.resumidasAte, fim);
55
+ }
56
+
57
+ /** Vale a pena pagar uma chamada de resumo agora? */
58
+ function precisaResumir(memoria, history) {
59
+ const novas = pendentes(memoria, history);
60
+ if (!novas.length) return false;
61
+ return tokensDe(novas) >= LIMITE_TOKENS;
62
+ }
63
+
64
+ /**
65
+ * Dobra o pedaço novo no resumo. `resumir` é quem fala com o servidor:
66
+ * recebe { resumo, mensagens } e devolve o texto do resumo atualizado.
67
+ *
68
+ * Devolve SEMPRE uma memória utilizável — se o resumo falhar (rede, cota), a
69
+ * anterior continua valendo e a conversa segue com o histórico cru.
70
+ */
71
+ async function atualizar(memoria, history, resumir) {
72
+ const novas = pendentes(memoria, history);
73
+ if (!novas.length) return memoria;
74
+
75
+ let texto;
76
+ try {
77
+ texto = await resumir({ resumo: memoria.resumo, mensagens: novas });
78
+ } catch {
79
+ return memoria;
80
+ }
81
+ const limpo = String(texto || '').trim();
82
+ if (!limpo) return memoria;
83
+
84
+ return { resumo: limpo, resumidasAte: (history || []).length - CRUAS };
85
+ }
86
+
87
+ /**
88
+ * Onde a parte crua pode começar sem deixar resultado órfão.
89
+ *
90
+ * A API recusa um `role:'tool'` sem o turno do assistente que o pediu. Cortar
91
+ * "as últimas N" cai nisso na hora em que a conta não bate — então o começo
92
+ * recua até um ponto onde a rodada está inteira.
93
+ */
94
+ function inicioSeguro(todas, alvo) {
95
+ let i = Math.max(0, Math.min(alvo, todas.length));
96
+ while (i > 0 && todas[i] && todas[i].role === 'tool') i--;
97
+ return i;
98
+ }
99
+
100
+ /**
101
+ * O histórico que vai no pedido: o resumo (se houver) + as últimas cruas.
102
+ *
103
+ * O resumo entra como mensagem de sistema, não como fala do usuário — o modelo
104
+ * precisa saber que aquilo é memória, e não algo que alguém acabou de dizer.
105
+ */
106
+ function paraEnviar(memoria, history) {
107
+ const todas = history || [];
108
+ if (!memoria || !memoria.resumo) return todas.slice(inicioSeguro(todas, todas.length - 20));
109
+
110
+ const cruas = todas.slice(inicioSeguro(todas, todas.length - CRUAS));
111
+ return [
112
+ { role: 'system', content: `Resumo da conversa até aqui (memória, não é fala de ninguém):\n${memoria.resumo}` },
113
+ ...cruas,
114
+ ];
115
+ }
116
+
117
+ /** Quanto este jeito economiza — para o /status e para o teste provarem. */
118
+ function economia(memoria, history) {
119
+ const cheio = tokensDe((history || []).slice(-20));
120
+ const enxuto = tokensDe(paraEnviar(memoria, history));
121
+ return { cheio, enxuto, poupado: Math.max(0, cheio - enxuto) };
122
+ }
123
+
124
+ module.exports = { nova, pendentes, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.2.2",
3
+ "version": "8.4.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"
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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"