primocode 8.10.2 → 8.10.4

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
@@ -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}`;
@@ -367,6 +375,13 @@ async function runAct(userPrompt, opts) {
367
375
  // usuário — que não tem prompt nenhum para ajustar. É o modelo tropeçando
368
376
  // ao escrever o JSON da chamada, e quase sempre acerta na segunda.
369
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;
370
385
 
371
386
  let voltas = 0;
372
387
  for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
@@ -396,15 +411,30 @@ async function runAct(userPrompt, opts) {
396
411
  // Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
397
412
  // de devolver erro e jogar fora o trabalho. O usuário não precisa
398
413
  // saber que isso aconteceu — a tarefa é dele, o encanamento é nosso.
399
- if (ehTetoDeToken(e) && compactacoes < MAX_COMPACTACOES && !PARAR) {
400
- compactacoes++;
401
- const antes = messages.length;
402
- encolherParaContinuar(messages);
403
- if (!subagent) {
404
- 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;
405
437
  }
406
- iteration--; // esta volta não contou: nada foi decidido
407
- continue;
408
438
  }
409
439
  const amigavel = friendlyError(e);
410
440
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
@@ -428,15 +458,33 @@ async function runAct(userPrompt, opts) {
428
458
  continue;
429
459
  }
430
460
 
431
- if (streamError && ehTetoDeToken(streamError) && compactacoes < MAX_COMPACTACOES && !PARAR) {
432
- compactacoes++;
433
- const antes = messages.length;
434
- encolherParaContinuar(messages);
435
- if (!subagent) {
436
- console.log(ident(profundidade) + c.dim(` ⎿ conversa encolhida para continuar (${antes} → ${messages.length} mensagens)`));
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;
437
487
  }
438
- iteration--;
439
- continue;
440
488
  }
441
489
 
442
490
  if (streamError) {
@@ -648,5 +696,5 @@ async function runSubagent(args, ctx) {
648
696
  return resumo;
649
697
  }
650
698
 
651
- module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken,
699
+ module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, esperaDoErro,
652
700
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
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);
@@ -490,8 +503,20 @@ async function despachar(name, args, ctx) {
490
503
  return studio.manual();
491
504
  case 'studio_spec':
492
505
  return studio.spec(args.ferramenta || args.slug || '');
493
- case 'studio_criar':
494
- return studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
506
+ case 'studio_criar': {
507
+ const r = await studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
508
+ // Criou? ABRE NO NAVEGADOR, sozinho. O dono: "quando ele faz um
509
+ // vídeo, uma apresentação, abre automático — a pessoa não vai
510
+ // saber abrir". E cada "abre pra mim" digitado depois custava
511
+ // uma tarefa inteira de tokens para o modelo redescobrir o
512
+ // link. Abrir é determinístico: não se gasta modelo com isso.
513
+ if (r && r.ok !== false && r.ver) {
514
+ try { openInBrowser(r.ver); } catch {}
515
+ r.abertoNoNavegador = true;
516
+ r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
517
+ }
518
+ return r;
519
+ }
495
520
  case 'studio_ler':
496
521
  return studio.ler(args.id);
497
522
  case 'studio_atualizar':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.10.2",
3
+ "version": "8.10.4",
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": {
@@ -10,6 +10,11 @@ corte e o agente inventou a estrutura. Formato primeiro; detalhe depois. -->
10
10
 
11
11
  ## Exemplo completo
12
12
 
13
+ **ATENÇÃO — este exemplo ensina o FORMATO, não o conteúdo.** O assunto dele
14
+ (conta de luz, bandeira tarifária) é inventado para a demonstração. Copiar
15
+ qualquer frase dele para o SEU documento é ERRO: escreva cada cena com os
16
+ fatos da SUA tarefa. O servidor recusa documento que venha com estas frases.
17
+
13
18
  ```json
14
19
  {
15
20
  "ferramenta": "corte",
package/studio/server.py CHANGED
@@ -278,12 +278,52 @@ def criar(dados):
278
278
  })
279
279
 
280
280
 
281
+ # Frases que só existem nos EXEMPLOS das specs. Documento que chega com uma
282
+ # delas é o agente copiando o exemplo em vez de escrever o conteúdo do pedido
283
+ # — um usuário pediu um vídeo da empresa dele e recebeu cenas sobre "bandeira
284
+ # tarifária", o assunto fictício do exemplo do corte.
285
+ # Só frases INCONFUNDÍVEIS entram aqui: "energia solar no brasil" seria um
286
+ # pedido legítimo possível, e bloquear pedido legítimo é pior que deixar um
287
+ # plágio passar. Estas seis não existem fora do exemplo.
288
+ FRASES_DE_EXEMPLO = [
289
+ "bandeira tarifária em 12 meses", "bandeira tarifaria em 12 meses",
290
+ "não foi o seu chuveiro", "nao foi o seu chuveiro",
291
+ "saturadas no nordeste", "usina gerando sem escoar",
292
+ "segue para entender a próxima", "segue para entender a proxima",
293
+ ]
294
+
295
+
296
+ def _texto_do_doc(doc):
297
+ """Todo texto visível do documento, achatado, para inspeção."""
298
+ pedacos = []
299
+
300
+ def anda(x):
301
+ if isinstance(x, dict):
302
+ for v in x.values():
303
+ anda(v)
304
+ elif isinstance(x, list):
305
+ for v in x:
306
+ anda(v)
307
+ elif isinstance(x, str):
308
+ pedacos.append(x)
309
+
310
+ anda(doc)
311
+ return " ".join(pedacos).lower()
312
+
313
+
281
314
  def validar_doc(modelo, doc):
282
315
  """None quando o doc serve; senão, a frase que ENSINA o formato certo.
283
316
 
284
317
  A mensagem repete a estrutura mínima de propósito: ela volta para o agente
285
318
  como resultado da tool, e é a única chance de ele se corrigir sem o
286
319
  usuário ver um vídeo preto."""
320
+ texto = _texto_do_doc(doc)
321
+ for frase in FRASES_DE_EXEMPLO:
322
+ if frase in texto:
323
+ return ('o documento contém "%s" — isso é texto do EXEMPLO da spec, não do pedido '
324
+ 'do usuário. O exemplo ensina o FORMATO; o conteúdo é seu: reescreva cada '
325
+ 'cena com os fatos da tarefa (a pesquisa que você fez) e crie de novo.' % frase)
326
+
287
327
  if modelo == "cena":
288
328
  cenas = doc.get("cenas")
289
329
  if not isinstance(cenas, list) or not cenas:
@@ -228,7 +228,12 @@ var Narracao = (function () {
228
228
  return VozNeural.audioDe(texto, cfg);
229
229
  }
230
230
 
231
- function falando() { return !!sintese && sintese.speaking; }
231
+ function falando() {
232
+ /* A voz pode estar saindo por três lugares; o player precisa da verdade
233
+ de todos, senão corta a cena no meio da fala neural. */
234
+ if (typeof VozNeural !== "undefined" && VozNeural.falando && VozNeural.falando()) return true;
235
+ return !!sintese && sintese.speaking;
236
+ }
232
237
 
233
238
  function estimar(texto, velocidade) {
234
239
  var palavras = String(texto || "").trim().split(/\s+/).filter(Boolean).length;
@@ -185,6 +185,13 @@
185
185
  if (gravador && gravador.state === "recording") setTimeout(pararGravacao, 600);
186
186
  }
187
187
 
188
+ /* A narração da cena ainda está no ar? */
189
+ function narrando() {
190
+ if (vozGravada && !vozGravada.ended && !vozGravada.paused) return true;
191
+ if (typeof Narracao !== "undefined" && Narracao.falando && Narracao.falando()) return true;
192
+ return false;
193
+ }
194
+
188
195
  function laco() {
189
196
  if (!tocando) return;
190
197
  decorrido = (performance.now() - t0) / 1000;
@@ -194,6 +201,18 @@
194
201
  acc += duracaoDe(i);
195
202
  if (decorrido < acc) { alvo = i; break; }
196
203
  }
204
+ /* A CENA ESPERA A VOZ. O relógio andava só pela `duracao`, e uma narração
205
+ mais longa que a cena vazava por cima da seguinte — era a "narração
206
+ fora de sincronia". Se a fala da cena atual ainda está no ar, o relógio
207
+ congela na beirada da cena: quem dita o corte é a voz. */
208
+ var fimDaAtual = inicioDe(atual) + duracaoDe(atual);
209
+ if ((alvo !== atual || decorrido >= total()) && narrando()) {
210
+ t0 = performance.now() - (fimDaAtual - 0.05) * 1000;
211
+ decorrido = fimDaAtual - 0.05;
212
+ pintar();
213
+ quadro = requestAnimationFrame(laco);
214
+ return;
215
+ }
197
216
  if (decorrido >= total()) return fim();
198
217
  if (alvo !== atual) ir(alvo, true);
199
218
  pintar();
@@ -106,7 +106,7 @@ var VozNeural = (function () {
106
106
  var fechou = false;
107
107
  var relogio = setTimeout(function () {
108
108
  if (!fechou) { fechou = true; try { ws.close(); } catch (e) {} erro(new Error("voz neural nao respondeu")); }
109
- }, 20000);
109
+ }, 6000);
110
110
 
111
111
  function desistir(e) {
112
112
  if (fechou) return;
@@ -220,6 +220,11 @@ var VozNeural = (function () {
220
220
  if (tocando) { try { tocando.pause(); } catch (e) {} tocando = null; }
221
221
  }
222
222
 
223
+ /* O player pergunta isto para segurar a cena até a voz acabar. */
224
+ function falando() {
225
+ return !!(tocando && !tocando.paused && !tocando.ended);
226
+ }
227
+
223
228
  /* Serve para a interface avisar antes de o usuário montar o vídeo inteiro. */
224
229
  function testar(cfg) {
225
230
  return audioDe("Teste de voz do PrimoCode Studio.", cfg)
@@ -229,7 +234,7 @@ var VozNeural = (function () {
229
234
 
230
235
  return {
231
236
  vozes: vozes, vozPadrao: vozPadrao, graves: graves,
232
- audioDe: audioDe, falar: falar, parar: parar, testar: testar,
237
+ audioDe: audioDe, falar: falar, parar: parar, falando: falando, testar: testar,
233
238
  _selo: selo
234
239
  };
235
240
  })();