primocode 8.22.0 → 8.22.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
@@ -349,6 +349,12 @@ async function runAct(userPrompt, opts) {
349
349
  // Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
350
350
  const jaChamado = repeticao.novo();
351
351
  let voltasEmFalso = 0;
352
+ // Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
353
+ // para de rodar e devolve ORDEM DE SEGUIR: sem isto o agente entrava em
354
+ // ciclo — buscava, não achava, lia "tente outra consulta", buscava de
355
+ // novo — até a cota do minuto acabar e o modelo responder vazio. Foi
356
+ // assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
357
+ let pesquisasSecas = 0;
352
358
  let rodadasParadas = 0;
353
359
 
354
360
  const messages = [];
@@ -515,9 +521,22 @@ async function runAct(userPrompt, opts) {
515
521
  // o modelo grátis engasgando. Cair direto no "parei sem concluir"
516
522
  // jogava a tarefa fora quando repetir a MESMA rodada quase sempre
517
523
  // resolve — visto em produção no primeiro pedido de vídeo.
518
- if (!finalText && respostasVazias < 2 && !PARAR) {
524
+ if (!finalText && respostasVazias < 3 && !PARAR) {
519
525
  respostasVazias++;
520
526
  if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo respondeu vazio — tentando de novo'));
527
+ // Repetir o MESMO contexto costuma produzir o mesmo vazio: se
528
+ // o turno anterior já engasgou, o que falta é um empurrão
529
+ // curto e concreto. A partir da segunda vez, entra um pedido
530
+ // explícito de fechamento — barato em tokens (é justamente
531
+ // quando a cota está no fim) e suficiente para o modelo
532
+ // escrever a resposta ou chamar finish.
533
+ if (respostasVazias >= 2) {
534
+ messages.push({
535
+ role: 'user',
536
+ content: 'Sua última resposta veio vazia. Não pesquise mais nem explique o processo: '
537
+ + 'faça agora a entrega pedida com o que você já tem e finalize.',
538
+ });
539
+ }
521
540
  iteration--;
522
541
  continue;
523
542
  }
@@ -616,6 +635,22 @@ async function runAct(userPrompt, opts) {
616
635
  temPasta = true;
617
636
  }
618
637
 
638
+ if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
639
+ voltasEmFalso++;
640
+ console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
641
+ console.log(ident(profundidade) + c.muted(' ⎿ ') + c.warn('3 pesquisas sem resultado — siga com o que já tem'));
642
+ messages.push({
643
+ role: 'tool',
644
+ tool_call_id: tc.id,
645
+ content: 'PARE DE PESQUISAR. Três buscas seguidas não trouxeram fonte nenhuma; '
646
+ + 'mais uma vai gastar a cota e devolver o mesmo nada. FAÇA AGORA a entrega '
647
+ + 'pedida com o que você já sabe. Se faltar um fato específico (número, data, '
648
+ + 'preço, nome de pessoa), NÃO invente: escreva a peça sem ele e avise o '
649
+ + 'usuário no final, em uma linha, o que não deu para confirmar.',
650
+ });
651
+ continue;
652
+ }
653
+
619
654
  algoRodou = true;
620
655
  console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
621
656
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
@@ -626,6 +661,11 @@ async function runAct(userPrompt, opts) {
626
661
  // não corrige um teste quebrado noutro lugar; antes isto zerava
627
662
  // a lista inteira e o agente conseguia dizer "concluído" com uma
628
663
  // falha real ainda pendente.
664
+ if (tc.name === 'pesquisar_referencia') {
665
+ if (result && result.ok === false) pesquisasSecas++;
666
+ else pesquisasSecas = 0; // achou fonte: o ciclo se rompeu
667
+ }
668
+
629
669
  if (result && result.ok === false) {
630
670
  falhasPendentes.push({
631
671
  tool: tc.name,
package/lib/referencia.js CHANGED
@@ -244,21 +244,116 @@ function relevante(cand, palavras) {
244
244
  * Foi o que salvou este caso — parisgroup.ai abre e tem o texto todo, enquanto
245
245
  * DuckDuckGo devolvia 202 e o Bing devolvia Paris, a cidade.
246
246
  *
247
- * Só dispara quando a consulta tem DUAS PALAVRAS COM INICIAL MAIÚSCULA
248
- * seguidas, que é como se escreve nome de empresa. "react hooks tutorial" não
249
- * dispara; "Paris Group" dispara.
247
+ * Reconhece nome de DUAS palavras com maiúscula ("Paris Group") e também de
248
+ * UMA palavra, mesmo em minúscula — que é como as pessoas digitam no meio de
249
+ * uma frase. Exigir duas maiúsculas custou caro: para "crie um vídeo para a
250
+ * tostudy, plataforma de aprendizagem com IA" a função devolvia lista vazia,
251
+ * o buscador não conhecia o nome, a pesquisa falhava — e o agente repetia
252
+ * buscas até a tarefa morrer, com tostudy.ai no ar o tempo todo.
250
253
  */
254
+
255
+ // Palavras que aparecem em qualquer pedido e NUNCA são o nome da marca. Sem
256
+ // esta peneira o chute viraria plataforma.com, aprendizagem.com, video.com.
257
+ const NAO_E_MARCA = new Set([
258
+ 'plataforma', 'plataformas', 'empresa', 'startup', 'site', 'oficial', 'aplicativo', 'app',
259
+ 'aprendizagem', 'aprendizado', 'ensino', 'educacao', 'educacional', 'curso', 'cursos', 'escola',
260
+ 'video', 'videos', 'reels', 'anuncio', 'campanha', 'marketing', 'institucional', 'apresentacao',
261
+ 'inteligencia', 'artificial', 'brasil', 'brasileira', 'sobre', 'para', 'como', 'uma', 'que',
262
+ 'crie', 'criar', 'faca', 'fazer', 'quero', 'preciso', 'estudo', 'estudos', 'tecnologia',
263
+ 'software', 'sistema', 'servico', 'servicos', 'produto', 'produtos', 'negocio', 'digital',
264
+ 'documentacao', 'tutorial', 'exemplo', 'exemplos', 'guia', 'referencia',
265
+ ]);
266
+
267
+ function _slug(txt) {
268
+ return String(txt || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
269
+ .toLowerCase().replace(/[^a-z0-9]/g, '');
270
+ }
271
+
251
272
  function sitesProvaveis(consulta) {
252
- const nome = /\b([A-ZÀ-Ý][\w\u00C0-\u024F]{1,})\s+([A-ZÀ-Ý][\w\u00C0-\u024F]{1,})\b/.exec(String(consulta || ''));
253
- if (!nome) return [];
254
- const slug = `${nome[1]}${nome[2]}`
255
- .normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
256
- if (slug.length < 5) return [];
257
- return ['com', 'ai', 'com.br', 'io', 'co'].map((tld) => ({
258
- titulo: `${nome[1]} ${nome[2]} — site oficial`,
259
- url: `https://www.${slug}.${tld}`,
260
- nota: 'site provável da empresa',
273
+ const texto = String(consulta || '');
274
+ const marcas = [];
275
+
276
+ // Entre aspas é a forma de a pessoa dizer "o nome é ESTE". Vem primeiro.
277
+ const citado = /["“'”]([^"“'”]{3,40})["“'”]/.exec(texto);
278
+ if (citado) marcas.push({ rotulo: citado[1].trim(), slug: _slug(citado[1]) });
279
+
280
+ const duplo = /\b([A-ZÀ-Ý][\wÀ-ɏ]{1,})\s+([A-ZÀ-Ý][\wÀ-ɏ]{1,})\b/.exec(texto);
281
+ if (duplo) marcas.push({ rotulo: `${duplo[1]} ${duplo[2]}`, slug: _slug(duplo[1] + duplo[2]) });
282
+
283
+ // Marca de UMA palavra só é chutada quando a consulta dá SINAL de que
284
+ // fala de um negócio ("plataforma", "empresa", "app", "site"...) ou põe o
285
+ // nome entre aspas. Sem essa exigência, "react hooks tutorial" viraria
286
+ // react.com e "como fazer um bolo" viraria fazer.com — dez buscas
287
+ // inúteis para cada pesquisa técnica.
288
+ const SINAL_DE_MARCA = /\b(plataforma|empresa|startup|site|marca|neg[óo]cio|produto|servi[çc]o|app|aplicativo|institucional|cliente|fabricante|loja|ag[êe]ncia|ecommerce|e-commerce|saas)\b/i;
289
+ const temSinal = SINAL_DE_MARCA.test(texto) || !!citado;
290
+ if (temSinal) {
291
+ const util = (bruto) => {
292
+ const s = _slug(bruto);
293
+ return (s.length >= 4 && s.length <= 24 && !NAO_E_MARCA.has(s) && !/^\d+$/.test(s)) ? s : null;
294
+ };
295
+ // O nome vem quase sempre depois de artigo ou preposição: "um mouse
296
+ // DA multilaser", "vídeo para A tostudy". Pegar simplesmente a
297
+ // primeira palavra não-genérica escolhia o verbo do pedido
298
+ // ("anunciar") e chutava anunciar.ai.
299
+ const depoisDeArtigo = [...texto.matchAll(/\b(?:d[aeo]s?|[ao]|n[ao]s?|à)\s+([A-Za-zÀ-ÿ][\wÀ-ÿ-]{3,23})\b/gi)];
300
+ for (const m of depoisDeArtigo) {
301
+ const s = util(m[1]);
302
+ if (s) { marcas.push({ rotulo: m[1], slug: s }); break; }
303
+ }
304
+ // Só quando o artigo não entregou nada: senão o verbo do pedido
305
+ // ("anunciar") entraria junto e gastaria dez sondas à toa.
306
+ if (!marcas.length) {
307
+ for (const bruto of texto.split(/[\s,.;:!?()]+/)) {
308
+ const s = util(bruto);
309
+ if (s) { marcas.push({ rotulo: bruto, slug: s }); break; }
310
+ }
311
+ }
312
+ }
313
+
314
+ const vistos = new Set();
315
+ const saida = [];
316
+ for (const m of marcas.slice(0, 2)) {
317
+ if (!m.slug || m.slug.length < 4 || vistos.has(m.slug)) continue;
318
+ vistos.add(m.slug);
319
+ for (const tld of ['ai', 'com', 'com.br', 'io', 'co']) {
320
+ // Com e sem "www": um resolve onde o outro não, e quem decide é a
321
+ // hospedagem. Como estes candidatos passam pela sonda antes de
322
+ // entrar na fila de leitura, tentar os dois não custa vaga.
323
+ saida.push({ titulo: `${m.rotulo} — site oficial`, url: `https://${m.slug}.${tld}`, nota: 'site provável da empresa' });
324
+ saida.push({ titulo: `${m.rotulo} — site oficial`, url: `https://www.${m.slug}.${tld}`, nota: 'site provável da empresa' });
325
+ }
326
+ }
327
+ return saida;
328
+ }
329
+
330
+ /**
331
+ * Sonda os domínios chutados e devolve só os que responderam, com o HTML já
332
+ * na mão. Sem isto, dez chutes entrariam na fila de leitura e expulsariam os
333
+ * resultados de verdade do buscador — e a maioria deles nem existe.
334
+ */
335
+ async function chutesVivos(chutes, limite = 2) {
336
+ if (!chutes.length) return { vivos: [], html: new Map() };
337
+ // 12 s, não 6: a sonda roda em paralelo e a home de uma empresa hoje pesa
338
+ // centenas de KB. Com 6 s o site CERTO chegava atrasado, era descartado
339
+ // como morto, e a pesquisa falhava com o site no ar — medido.
340
+ const respostas = await Promise.all(chutes.slice(0, 12).map(async (c) => {
341
+ const html = await pegar(c.url, { timeout: 12000 });
342
+ return html && html.length > 500 ? { cand: c, html } : null;
261
343
  }));
344
+ const vivos = [];
345
+ const html = new Map();
346
+ const dominios = new Set();
347
+ for (const r of respostas.filter(Boolean)) {
348
+ // www.x.ai e x.ai são o mesmo site: vale o primeiro que respondeu.
349
+ const raiz = r.cand.url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
350
+ if (dominios.has(raiz)) continue;
351
+ dominios.add(raiz);
352
+ vivos.push(r.cand);
353
+ html.set(r.cand.url, r.html);
354
+ if (vivos.length >= limite) break;
355
+ }
356
+ return { vivos, html };
262
357
  }
263
358
 
264
359
  // Links que não são referência de nada — resultado de busca sujo.
@@ -460,7 +555,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
460
555
  // O site provável da empresa vai NA FRENTE: quando a consulta nomeia um
461
556
  // negócio, o site dele é a melhor fonte que existe, e não depende de
462
557
  // buscador nenhum estar de bom humor.
463
- const chutes = sitesProvaveis(termo);
558
+ // Os chutes de domínio são SONDADOS antes de entrar na fila: só os que
559
+ // respondem ocupam vaga. São dez palpites (5 TLDs x com/sem www) e a
560
+ // maioria não existe — enfileirar todos expulsaria os resultados reais
561
+ // do buscador. A sonda roda em paralelo e o HTML já volta aproveitado.
562
+ const palpites = sitesProvaveis(termo);
563
+ const { vivos, html: htmlChutes } = await chutesVivos(palpites);
564
+ // A sonda ORDENA, não faz porteiro. Ela é uma corrida contra o relógio
565
+ // contra home pesada e rede lenta, e perdê-la não pode significar perder
566
+ // o site certo: quando nada responde a tempo, os primeiros palpites vão
567
+ // para a fila mesmo assim e o laço de leitura tenta de novo, sem pressa.
568
+ const chutes = vivos.length ? vivos : palpites.slice(0, 3);
464
569
  const palavras = palavrasDaConsulta(termo);
465
570
  const brutos = [...chutes, ...web, ...so, ...docsConhecidas(termo)];
466
571
 
@@ -481,7 +586,15 @@ async function pesquisar({ consulta, paginas = 3 }) {
481
586
  provavelCausa: descartados
482
587
  ? `Vieram ${descartados} link(s), mas nenhum era sobre "${termo}" — o buscador não achou o assunto e devolveu página aleatória. Fonte errada é pior que fonte nenhuma, então foram descartadas.`
483
588
  : 'Os buscadores costumam recusar chamadas automáticas (bloqueio ou limite por excesso de buscas seguidas), e não a sua conexão.',
484
- comoCorrigir: 'Tente OUTRA consulta, mais curta e específica. Se souber o site do assunto, vá direto nele com browser_open + browser_read — é o caminho mais confiável para fato de empresa.',
589
+ // "Tente outra consulta" era um convite ao ciclo: o agente
590
+ // reformulava a busca, falhava igual, e repetia até a cota acabar.
591
+ // A ordem agora é explícita — no máximo UMA nova tentativa, e por
592
+ // um caminho DIFERENTE (o site direto), senão segue sem a fonte.
593
+ comoCorrigir: 'NÃO repita esta busca com outras palavras — o buscador não conhece o assunto e '
594
+ + 'vai devolver o mesmo nada. Se souber (ou conseguir deduzir) o site, tente UMA vez '
595
+ + 'browser_open + browser_read nele. Se também não abrir, SIGA COM A TAREFA usando o que '
596
+ + 'você já sabe e avise ao usuário, no final, qual fato não deu para confirmar. Nunca invente '
597
+ + 'dado sobre empresa, número, data, preço ou pessoa.',
485
598
  };
486
599
  }
487
600
 
@@ -491,7 +604,9 @@ async function pesquisar({ consulta, paginas = 3 }) {
491
604
  const chutesLidos = [];
492
605
  for (const cand of candidatos) {
493
606
  if (lidas.length >= paginas) break;
494
- const html = await pegar(cand.url);
607
+ // A sonda já baixou o HTML dos chutes: reaproveita em vez de pedir
608
+ // a mesma página duas vezes.
609
+ const html = htmlChutes.get(cand.url) || await pegar(cand.url);
495
610
  if (!html) continue;
496
611
  const texto = textoDe(html);
497
612
  if (texto.length < 200) continue;
@@ -502,7 +617,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
502
617
  // da página, sim. Sem esta prova o agente escreveria sobre a errada.
503
618
  const ehChute = cand.nota === 'site provável da empresa';
504
619
  if (ehChute) {
505
- const nota = quantoBate({ titulo: texto.slice(0, 4000), url: cand.url }, palavras);
620
+ let nota = quantoBate({ titulo: texto.slice(0, 4000), url: cand.url }, palavras);
621
+ // O NOME DA MARCA no domínio, confirmado no texto da página, já é
622
+ // prova — e é a prova mais forte que existe. Exigir duas palavras
623
+ // genéricas da consulta ("plataforma", "aprendizagem") reprovava o
624
+ // site certo: tostudy.ai abria, dizia "ToStudy" na primeira linha,
625
+ // e mesmo assim era descartado por não repetir o vocabulário do
626
+ // pedido. O agente então repetia buscas até a tarefa morrer.
627
+ const marca = (cand.url.match(/^https?:\/\/(?:www\.)?([a-z0-9-]+)\./i) || [])[1] || '';
628
+ const textoLimpo = texto.slice(0, 8000).normalize('NFD')
629
+ .replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
630
+ if (marca.length >= 4 && textoLimpo.includes(marca.toLowerCase())) nota = Math.max(nota, 2);
506
631
  if (nota < Math.min(2, palavras.length)) continue;
507
632
  // Passar não basta: entre dois domínios do MESMO nome, fica o que
508
633
  // fala mais do assunto. parisgroup.ai e parisgroup.co os dois
@@ -532,8 +657,10 @@ async function pesquisar({ consulta, paginas = 3 }) {
532
657
  error: 'achei os links mas não consegui abrir nenhum',
533
658
  consulta: termo,
534
659
  links: candidatos.map((c) => c.url),
535
- comoCorrigir: 'Tente browser_open + browser_read num desses links, ou pesquise com outras palavras. '
536
- + 'Se nada abrir, diga ao usuário que não conseguiu confirmar os fatos — NUNCA invente dado sobre empresa, data, preço ou pessoa.',
660
+ comoCorrigir: 'Tente browser_open + browser_read num desses links — UMA vez. Não refaça a busca '
661
+ + 'com outras palavras: os links são estes mesmos. Se nada abrir, SIGA COM A TAREFA e diga ao '
662
+ + 'usuário, no final, que não conseguiu confirmar os fatos — NUNCA invente dado sobre empresa, '
663
+ + 'data, preço ou pessoa.',
537
664
  };
538
665
  }
539
666
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.22.0",
3
+ "version": "8.22.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": {