primocode 8.10.1 → 8.10.3

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
@@ -550,10 +550,19 @@ async function sendPrompt(userText) {
550
550
  let jaFeito = '';
551
551
  for (let i = 0; i < passos.length; i++) {
552
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, {
553
+ // Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
554
+ // pesquisa morreu e o texto "Parei sem concluir..." foi injetado
555
+ // como "use estes fatos" — e o passo seguinte obedeceu: criou o
556
+ // vídeo sem fato nenhum, proibido de pesquisar.
557
+ const contexto = !jaFeito ? ''
558
+ : `\n\nO QUE JÁ FOI FEITO ANTES (use estes fatos, não pesquise de novo):\n${jaFeito}`;
559
+ const semFatos = i > 0 && !jaFeito
560
+ ? '\n\nATENÇÃO: o passo anterior não trouxe fatos. Trabalhe só com o que está no pedido. '
561
+ + 'É PROIBIDO inventar dado de empresa, data, preço ou pessoa — na dúvida, não afirme.'
562
+ : '';
563
+ const pedido = passos[i].pedido + contexto + semFatos;
564
+
565
+ const rodarPasso = () => runAct(pedido, {
557
566
  server: state.config.server,
558
567
  model: state.config.model,
559
568
  effort: state.config.effort,
@@ -568,7 +577,19 @@ async function sendPrompt(userText) {
568
577
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
569
578
  motor: state.engine === 'claude-code' ? claudeEngine : api,
570
579
  });
571
- jaFeito = String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200);
580
+
581
+ r = await rodarPasso();
582
+ // Passo que desistiu (modelo mudo, voltas esgotadas) ganha UMA
583
+ // segunda chance — costuma ser tropeço transitório do modelo
584
+ // grátis. Falhou de novo: o plano segue com o aviso de "sem
585
+ // fatos", nunca com o texto da falha fantasiado de resultado.
586
+ if (!r.parado && r.concluiu === false && passos.length > 1) {
587
+ console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
588
+ r = await rodarPasso();
589
+ }
590
+ jaFeito = (r.concluiu !== false)
591
+ ? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
592
+ : '';
572
593
  if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
573
594
  }
574
595
  state.history.push({ role: 'user', content: userText });
package/lib/act.js CHANGED
@@ -293,6 +293,14 @@ function encolherParaContinuar(messages) {
293
293
  }
294
294
  }
295
295
 
296
+ /** Quanto esperar quando o minuto estourou. O provedor costuma dizer
297
+ * ("Please try again in 7.664s"); sem isso, meio minuto é seguro. */
298
+ function esperaDoErro(e) {
299
+ const m = /try again in ([0-9.]+)s/i.exec(String((e && e.message) || e || ''));
300
+ const s = m ? Math.ceil(parseFloat(m[1])) + 2 : 30;
301
+ return Math.min(70, Math.max(5, s)) * 1000;
302
+ }
303
+
296
304
  function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
297
305
  if (falhasPendentes.length) {
298
306
  return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
@@ -362,6 +370,19 @@ async function runAct(userPrompt, opts) {
362
370
  let compactacoes = 0;
363
371
  const MAX_COMPACTACOES = 2;
364
372
 
373
+ // O provedor recusa chamada de função malformada com um 400 que dizia
374
+ // "Failed to call a function. Please adjust your prompt." NA CARA do
375
+ // usuário — que não tem prompt nenhum para ajustar. É o modelo tropeçando
376
+ // ao escrever o JSON da chamada, e quase sempre acerta na segunda.
377
+ let funcoesQuebradas = 0;
378
+ // O teto de tokens é POR MINUTO, somando TODAS as chamadas — o plano, o
379
+ // resumo e cada rodada do loop dividem a mesma cota. Um pedido perfeito
380
+ // estoura se a rodada anterior já gastou o minuto. Encolher não resolve
381
+ // isso (num caso real "encolheu" de 2 para 2 mensagens); esperar a janela
382
+ // virar, sim.
383
+ let esperas = 0;
384
+ const MAX_ESPERAS = 3;
385
+
365
386
  let voltas = 0;
366
387
  for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
367
388
  voltas++;
@@ -390,15 +411,30 @@ async function runAct(userPrompt, opts) {
390
411
  // Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
391
412
  // de devolver erro e jogar fora o trabalho. O usuário não precisa
392
413
  // 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)`));
414
+ if (ehTetoDeToken(e) && !PARAR) {
415
+ if (messages.length > 8 && compactacoes < MAX_COMPACTACOES) {
416
+ compactacoes++;
417
+ const antes = messages.length;
418
+ encolherParaContinuar(messages);
419
+ if (!subagent) {
420
+ console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
421
+ }
422
+ iteration--; // esta volta não contou: nada foi decidido
423
+ continue;
424
+ }
425
+ if (esperas < MAX_ESPERAS) {
426
+ esperas++;
427
+ const ms = esperaDoErro(e);
428
+ if (!subagent) {
429
+ const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
430
+ await new Promise((r) => setTimeout(r, ms));
431
+ sp2.stop();
432
+ } else {
433
+ await new Promise((r) => setTimeout(r, ms));
434
+ }
435
+ iteration--;
436
+ continue;
399
437
  }
400
- iteration--; // esta volta não contou: nada foi decidido
401
- continue;
402
438
  }
403
439
  const amigavel = friendlyError(e);
404
440
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
@@ -409,22 +445,55 @@ async function runAct(userPrompt, opts) {
409
445
  emVoo = null;
410
446
  if (PARAR) break;
411
447
 
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
- }
448
+ // Chamada de função malformada: o modelo errou a SINTAXE da chamada,
449
+ // não a tarefa. Repetir a mesma rodada quase sempre resolve — e sem
450
+ // isto o 400 cru do provedor ("Failed to call a function. Please
451
+ // adjust your prompt.") caía na tela de quem não tem prompt nenhum
452
+ // para ajustar.
453
+ if (streamError && /failed to call a function|failed_generation|tool_use_failed/i.test(String(streamError))
454
+ && funcoesQuebradas < 2 && !PARAR) {
455
+ funcoesQuebradas++;
456
+ if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo tropeçou na chamada — tentando de novo'));
419
457
  iteration--;
420
458
  continue;
421
459
  }
422
460
 
461
+ if (streamError && ehTetoDeToken(streamError) && !PARAR) {
462
+ // Conversa longa: encolher resolve, porque o peso é o histórico.
463
+ if (messages.length > 8 && compactacoes < MAX_COMPACTACOES) {
464
+ compactacoes++;
465
+ const antes = messages.length;
466
+ encolherParaContinuar(messages);
467
+ if (!subagent) {
468
+ console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
469
+ }
470
+ iteration--;
471
+ continue;
472
+ }
473
+ // Conversa curta: o peso é o MINUTO, não o histórico — as chamadas
474
+ // anteriores gastaram a cota. Espera a janela virar e repete.
475
+ if (esperas < MAX_ESPERAS) {
476
+ esperas++;
477
+ const ms = esperaDoErro(streamError);
478
+ if (!subagent) {
479
+ const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
480
+ await new Promise((r) => setTimeout(r, ms));
481
+ sp2.stop();
482
+ } else {
483
+ await new Promise((r) => setTimeout(r, ms));
484
+ }
485
+ iteration--;
486
+ continue;
487
+ }
488
+ }
489
+
423
490
  if (streamError) {
424
491
  const amigavel = friendlyError(streamError);
425
- if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
492
+ if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` }, concluiu: false };
426
493
  console.log('\n' + c.err(amigavel));
427
- return { assistantMessage: { role: 'assistant', content: amigavel } };
494
+ // concluiu: false — o plano de passos não pode tratar um erro de
495
+ // provedor como resultado e passá-lo adiante como "fato".
496
+ return { assistantMessage: { role: 'assistant', content: amigavel }, concluiu: false };
428
497
  }
429
498
 
430
499
  // Sem tool_calls: o modelo respondeu e acabou.
@@ -561,6 +630,11 @@ async function runAct(userPrompt, opts) {
561
630
  }
562
631
  }
563
632
 
633
+ // O que o modelo entregou por vontade própria, ANTES dos textos de
634
+ // desistência. É esta a diferença que o plano de passos precisa: um passo
635
+ // que "parou sem concluir" não pode virar fato para o passo seguinte.
636
+ const concluiu = Boolean(finalText) && !PARAR;
637
+
564
638
  if (PARAR && !finalText) {
565
639
  finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
566
640
  if (!subagent) console.log('\n' + c.warn('⏹ ' + finalText));
@@ -594,6 +668,7 @@ async function runAct(userPrompt, opts) {
594
668
  // INTEIRO, não só o passo — senão o próximo passo começa logo depois
595
669
  // de o usuário ter mandado parar.
596
670
  parado: PARAR,
671
+ concluiu,
597
672
  };
598
673
  }
599
674
 
@@ -621,5 +696,5 @@ async function runSubagent(args, ctx) {
621
696
  return resumo;
622
697
  }
623
698
 
624
- module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken,
699
+ module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, esperaDoErro,
625
700
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
package/lib/studio.js CHANGED
@@ -256,7 +256,26 @@ const manual = () => comStudio(async () => ({ ok: true, ...(await pedir('GET', '
256
256
  const ferramentas = () => comStudio(async () => ({ ok: true, ferramentas: await pedir('GET', '/api/ferramentas') }));
257
257
  const spec = (slug) => comStudio(async () => ({ ok: true, spec: await pedir('GET', `/api/ferramentas/${encodeURIComponent(slug)}`) }));
258
258
  const listar = () => comStudio(async () => ({ ok: true, arquivos: await pedir('GET', '/api/arquivos') }));
259
- const ler = (id) => comStudio(async () => ({ ok: true, arquivo: await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}`) }));
259
+ // 404 de id é quase sempre id INVENTADO: num pedido real o agente criou o
260
+ // arquivo, recebeu "corte-founders-ea74b" de volta, e chamou studio_ler com
261
+ // "imersao-foulders-ai" — um id que ele imaginou. O erro seco ("não
262
+ // encontrado") não dizia isso, e ele insistia. O erro que ensina, diz.
263
+ async function comIdReal(id, fn) {
264
+ try {
265
+ return await fn();
266
+ } catch (e) {
267
+ if (!/HTTP 404/.test(String(e && e.message))) throw e;
268
+ let existem = [];
269
+ try { existem = (await pedir('GET', '/api/arquivos')).map((a) => a.id).slice(0, 8); } catch {}
270
+ return {
271
+ ok: false,
272
+ error: `O id "${id}" não existe. NÃO invente id: use EXATAMENTE o campo "id" que studio_criar devolveu.`
273
+ + (existem.length ? ` Os que existem agora: ${existem.join(', ')}.` : ''),
274
+ };
275
+ }
276
+ }
277
+
278
+ const ler = (id) => comStudio(() => comIdReal(id, async () => ({ ok: true, arquivo: await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}`) })));
260
279
 
261
280
  const criar = ({ ferramenta, titulo, doc }) => comStudio(async () => {
262
281
  if (!ferramenta) return { ok: false, error: 'Diga a ferramenta: prisma, tela, prosa, grade, corte ou traco.' };
@@ -265,14 +284,14 @@ const criar = ({ ferramenta, titulo, doc }) => comStudio(async () => {
265
284
  return { ok: true, ...r };
266
285
  });
267
286
 
268
- const atualizar = ({ id, titulo, doc }) => comStudio(async () => {
287
+ const atualizar = ({ id, titulo, doc }) => comStudio(() => comIdReal(id, async () => {
269
288
  if (!id) return { ok: false, error: 'Falta o id do arquivo.' };
270
289
  const corpo = {};
271
290
  if (titulo != null) corpo.titulo = titulo;
272
291
  if (doc != null) corpo.doc = doc;
273
292
  const r = await pedir('PUT', `/api/arquivos/${encodeURIComponent(id)}`, corpo);
274
293
  return { ok: true, ...r };
275
- });
294
+ }));
276
295
 
277
296
  // ── Mídia: a imagem de fundo e a música do usuário ───────────────────────
278
297
  // Sem isto o agente não tinha como pôr foto de fundo nem trilha própria: a
@@ -288,7 +307,15 @@ const midiaListar = () => comStudio(async () => ({ ok: true, midia: await pedir(
288
307
  const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
289
308
  if (!arquivo) return { ok: false, error: 'Diga o caminho do arquivo (arquivo: "foto.jpg").' };
290
309
  const caminho = path.resolve(arquivo);
291
- if (!fs.existsSync(caminho)) return { ok: false, error: `Arquivo não existe: ${arquivo}` };
310
+ if (!fs.existsSync(caminho)) {
311
+ return {
312
+ ok: false,
313
+ error: `Arquivo não existe: ${arquivo}. NÃO invente arquivo de mídia — num pedido real o agente `
314
+ + 'tentou fundo.jpg, trilha.mp3 e logo.png que nunca existiram. Sem arquivo do usuário, siga sem mídia: '
315
+ + 'fundo pode ser "aurora"/"raios"/"brilho" (gerados), e trilha pode ser gerada — '
316
+ + '"trilha":{"tipo":"gerada","estilo":"epico"}. O vídeo fica pronto do mesmo jeito.',
317
+ };
318
+ }
292
319
 
293
320
  const ext = path.extname(caminho).toLowerCase();
294
321
  if (!TIPOS_MIDIA[ext]) {
package/lib/tools.js CHANGED
@@ -379,6 +379,19 @@ async function despachar(name, args, ctx) {
379
379
  {
380
380
  switch (name) {
381
381
  case 'write_file': {
382
+ // Vídeo e áudio NÃO se escrevem com write_file. Num pedido
383
+ // real o agente criou um "video.mp4" de 43 bytes de texto e
384
+ // disse "vídeo criado com sucesso" — um arquivo falso, morto,
385
+ // entregue como resultado. O caminho certo existe e é dele:
386
+ // o Studio.
387
+ if (/\.(mp4|mov|avi|mkv|webm|mp3|wav|ogg|m4a|flac)$/i.test(String(args.path || ''))) {
388
+ return {
389
+ ok: false,
390
+ error: `"${args.path}" é arquivo de vídeo/áudio, e write_file só escreve TEXTO — isso criaria um arquivo falso que não abre. `
391
+ + 'Vídeo se faz no Studio: studio_criar com ferramenta "corte" (leia studio_spec antes). '
392
+ + 'O Studio devolve o link "ver" — é ESSE o vídeo. Não use ffmpeg: não é preciso.',
393
+ };
394
+ }
382
395
  const filePath = resolveInProject(args.path, projectDir);
383
396
  const content = args.content || '';
384
397
  const created = !fs.existsSync(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.10.1",
3
+ "version": "8.10.3",
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": {