primocode 8.2.0 → 8.2.1

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/lib/act.js CHANGED
@@ -160,6 +160,37 @@ function summarizeForModel(result) {
160
160
  return json.length > 6000 ? json.slice(0, 6000) + '…(truncado)' : json;
161
161
  }
162
162
 
163
+ // Quantos resultados de tool ficam INTEIROS na conversa. Os mais novos são o
164
+ // que o modelo precisa para decidir o próximo passo; os velhos viram peso.
165
+ const RESULTADOS_INTEIROS = 3;
166
+ const RESTO_DO_RESULTADO = 300;
167
+
168
+ /**
169
+ * Encolhe os resultados de tool ANTIGOS antes de mandar a conversa.
170
+ *
171
+ * Sem isto, cada página lida e cada pesquisa ficava inteira no histórico para
172
+ * sempre. Medido num pedido real ("pesquise sobre a empresa X e faça um
173
+ * vídeo"): cinco pesquisas e seis leituras de página levaram a conversa a
174
+ * 27.454 tokens contra um teto de 6.000 por minuto do plano grátis — a tarefa
175
+ * morreu no meio, depois de todo o trabalho de pesquisa já feito.
176
+ *
177
+ * Nenhuma mensagem é REMOVIDA: a API exige que cada role:'tool' continue
178
+ * pareada com o tool_call que a pediu. Só o texto encolhe.
179
+ */
180
+ function compactar(messages) {
181
+ const indices = [];
182
+ for (let i = 0; i < messages.length; i++) if (messages[i].role === 'tool') indices.push(i);
183
+ if (indices.length <= RESULTADOS_INTEIROS) return messages;
184
+
185
+ const encolher = new Set(indices.slice(0, -RESULTADOS_INTEIROS));
186
+ return messages.map((m, i) => {
187
+ if (!encolher.has(i)) return m;
188
+ const texto = String(m.content || '');
189
+ if (texto.length <= RESTO_DO_RESULTADO) return m;
190
+ return { ...m, content: texto.slice(0, RESTO_DO_RESULTADO) + `…(resultado antigo encurtado; eram ${texto.length} caracteres)` };
191
+ });
192
+ }
193
+
163
194
  function buildMemoryContext(projectDir, projectName) {
164
195
  const parts = [];
165
196
  const geral = cfg.readGeneralMemory();
@@ -210,7 +241,7 @@ async function runAct(userPrompt, opts) {
210
241
  let streaming = false;
211
242
 
212
243
  try {
213
- await motor.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
244
+ await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir, projectName, subagent }, (evt) => {
214
245
  if (evt.erro) { streamError = evt.erro; return; }
215
246
  if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
216
247
  if (evt.delta) {
@@ -271,7 +302,7 @@ async function runAct(userPrompt, opts) {
271
302
  messages.push({
272
303
  role: 'tool',
273
304
  tool_call_id: tc.id,
274
- content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.join('; ')}. `
305
+ content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.map((f) => f.texto).join('; ')}. `
275
306
  + 'Conserte agora (leia o arquivo antes de editar) ou, se for impossível, diga claramente ao usuário o que falhou.',
276
307
  });
277
308
  continue;
@@ -306,12 +337,29 @@ async function runAct(userPrompt, opts) {
306
337
  // a lista inteira e o agente conseguia dizer "concluído" com uma
307
338
  // falha real ainda pendente.
308
339
  if (result && result.ok === false) {
309
- falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
310
- } else if (['write_file', 'edit_file'].includes(tc.name) && tc.arguments?.path) {
311
- const alvo = tc.arguments.path;
312
- const mantidas = falhasPendentes.filter((f) => !f.includes(`(${alvo})`));
340
+ falhasPendentes.push({
341
+ tool: tc.name,
342
+ alvo: tc.arguments?.path || '',
343
+ texto: `${tc.name}(${tc.arguments?.path || ''}): ${result.error}`,
344
+ });
345
+ } else {
346
+ // Deu certo: some com o que ESTA tool tinha deixado pendente.
347
+ //
348
+ // Antes só write_file/edit_file limpavam, e por caminho. Uma
349
+ // falha já superada continuava bloqueando o finish para sempre:
350
+ // medido num pedido real, o browser_read errou o seletor
351
+ // ("main"), acertou na tentativa seguinte lendo 5.240 chars — e
352
+ // mesmo assim a tarefa morreu com "Parei sem concluir. A última
353
+ // falha foi: browser_read()". Acertar depois de errar é
354
+ // exatamente o que esta guarda quer premiar, não punir.
355
+ const alvo = tc.arguments?.path;
356
+ const sobrou = falhasPendentes.filter((f) => {
357
+ if (f.tool === tc.name) return false; // a mesma tool funcionou
358
+ if (alvo && f.alvo === alvo) return false; // o mesmo arquivo foi resolvido
359
+ return true;
360
+ });
313
361
  falhasPendentes.length = 0;
314
- falhasPendentes.push(...mantidas);
362
+ falhasPendentes.push(...sobrou);
315
363
  }
316
364
 
317
365
  messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
@@ -335,7 +383,7 @@ async function runAct(userPrompt, opts) {
335
383
  // depois de a primeira tentativa de seletor falhar.
336
384
  if (!finalText) {
337
385
  finalText = falhasPendentes.length
338
- ? `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1]}`
386
+ ? `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`
339
387
  : 'Parei sem concluir e sem explicação do modelo. Tente pedir de novo, em partes menores.';
340
388
  if (!subagent) console.log('\n' + c.warn(finalText));
341
389
  }
@@ -382,4 +430,4 @@ async function runSubagent(args, ctx) {
382
430
  return resumo;
383
431
  }
384
432
 
385
- module.exports = { runAct, interromper };
433
+ module.exports = { runAct, interromper, compactar };
@@ -24,16 +24,55 @@ const MARCA_FIM = '<<<FIM>>>';
24
24
  */
25
25
  function extrairDecisao(texto) {
26
26
  const t = String(texto || '');
27
- const inicio = t.lastIndexOf(MARCA_INICIO);
28
- if (inicio === -1) return null;
29
- const fim = t.indexOf(MARCA_FIM, inicio);
30
- if (fim === -1) return null;
31
- const bloco = t.slice(inicio + MARCA_INICIO.length, fim).trim();
32
- try {
33
- return JSON.parse(bloco);
34
- } catch {
35
- return null;
27
+
28
+ // Varre TODOS os blocos completos e fica com o último que dá para ler.
29
+ //
30
+ // A versão anterior fazia lastIndexOf(INICIO) e depois procurava o FIM
31
+ // para frente. Bastava o modelo escrever a marca de abertura mais uma vez
32
+ // — explicando o formato, ou começando um bloco e desistindo — para o FIM
33
+ // não existir depois dela: a extração devolvia null e o protocolo CRU ia
34
+ // parar na tela do usuário, com tool_call e tudo. Foi o que aconteceu num
35
+ // pedido real de vídeo.
36
+ const blocos = [];
37
+ let i = 0;
38
+ while (true) {
39
+ const inicio = t.indexOf(MARCA_INICIO, i);
40
+ if (inicio === -1) break;
41
+ const fim = t.indexOf(MARCA_FIM, inicio + MARCA_INICIO.length);
42
+ if (fim === -1) break; // bloco aberto e não fechado: ignora
43
+ blocos.push(t.slice(inicio + MARCA_INICIO.length, fim));
44
+ i = fim + MARCA_FIM.length;
36
45
  }
46
+
47
+ for (let k = blocos.length - 1; k >= 0; k--) {
48
+ const lido = lerJSON(blocos[k]);
49
+ if (lido) return lido;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** O JSON do bloco, tolerando o que o modelo costuma acrescentar sem querer. */
55
+ function lerJSON(bruto) {
56
+ let s = String(bruto || '').trim();
57
+ // Cerca de markdown em volta do JSON — o modelo põe por hábito.
58
+ s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
59
+ try { return JSON.parse(s); } catch {}
60
+ // Sobrou texto depois do objeto? Corta no fecha-chaves que casa com o primeiro.
61
+ const abre = s.indexOf('{');
62
+ if (abre === -1) return null;
63
+ let nivel = 0, dentro = false, escapa = false;
64
+ for (let k = abre; k < s.length; k++) {
65
+ const c = s[k];
66
+ if (escapa) { escapa = false; continue; }
67
+ if (c === '\\') { escapa = true; continue; }
68
+ if (c === '"') { dentro = !dentro; continue; }
69
+ if (dentro) continue;
70
+ if (c === '{') nivel++;
71
+ else if (c === '}' && --nivel === 0) {
72
+ try { return JSON.parse(s.slice(abre, k + 1)); } catch { return null; }
73
+ }
74
+ }
75
+ return null;
37
76
  }
38
77
 
39
78
  const catalogo = require('./catalogo');
@@ -229,7 +268,15 @@ function ultimaMensagem(messages) {
229
268
  function emitirDecisao(textoLido, onEvent) {
230
269
  const decisao = extrairDecisao(textoLido);
231
270
  if (!decisao) {
232
- onEvent({ assistant_message: { content: String(textoLido || '').trim() } });
271
+ // Resposta sem bloco legível. Se ela AINDA ASSIM traz a marca do
272
+ // protocolo, o motor falhou: mostrar isso cru — com "tool_call" e
273
+ // JSON na cara do usuário — é pior do que dizer que falhou.
274
+ let conteudo = String(textoLido || '').trim();
275
+ if (conteudo.includes(MARCA_INICIO) || conteudo.includes(MARCA_FIM)) {
276
+ conteudo = conteudo.split(MARCA_INICIO)[0].trim()
277
+ || 'O motor Claude Code devolveu uma decisão que não consegui ler. Tente de novo, ou volte ao motor padrão com /claude off.';
278
+ }
279
+ onEvent({ assistant_message: { content: conteudo } });
233
280
  return;
234
281
  }
235
282
  if (decisao.tool_call) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.2.0",
3
+ "version": "8.2.1",
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"
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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"