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 +26 -5
- package/lib/act.js +93 -18
- package/lib/studio.js +31 -4
- package/lib/tools.js +13 -0
- package/package.json +1 -1
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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) &&
|
|
394
|
-
compactacoes
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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.
|
|
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": {
|