terminal-smart-cli 0.98.0 → 0.98.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/agent.js CHANGED
@@ -965,7 +965,7 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
965
965
  let inp = {}; try { inp = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
966
966
  if (onStep) onStep({ name: ' ↳ ' + nm, detail: argsShort(nm, inp) });
967
967
  const res = READONLY.has(nm) ? await tools.execute(nm, inp, { baseDir: cwd, token: k }) : { erro: 'ferramenta não permitida no sub-agente (só leitura).' };
968
- msgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(res).slice(0, TOOL_RESULT_CAP) });
968
+ msgs.push({ role: 'tool', tool_call_id: tc.id, content: intelligence.compactToolResult(res, TOOL_RESULT_CAP) });
969
969
  }
970
970
  }
971
971
  // Esgotou os passos ainda chamando ferramentas? Fecha com UM resumo (sem ferramentas) —
@@ -2368,7 +2368,7 @@ async function run(task, opts = {}) {
2368
2368
  const _cacheClass = core.classifyToolResult(result);
2369
2369
  if (_cacheClass.ok && READONLY.has(name)) {
2370
2370
  const _resultCap = name === 'ler_arquivos' ? 24000 : TOOL_RESULT_CAP;
2371
- _missionCache.remember(name, input, { toolCallId: tc.id, content: JSON.stringify(result).slice(0, _resultCap) });
2371
+ _missionCache.remember(name, input, { toolCallId: tc.id, content: intelligence.compactToolResult(result, _resultCap) });
2372
2372
  } else if (_cacheClass.ok && !READONLY.has(name)) {
2373
2373
  _missionCache.invalidate();
2374
2374
  }
@@ -2395,7 +2395,8 @@ async function run(task, opts = {}) {
2395
2395
  return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, cwd, context: lastCtx, needHuman: { motivo: result.motivo, o_que_fazer: result.o_que_fazer }, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace } };
2396
2396
  }
2397
2397
  const _resultCap = name === 'ler_arquivos' ? 24000 : TOOL_RESULT_CAP;
2398
- const _rawToolContent = _cachedContent || JSON.stringify(result).slice(0, _resultCap);
2398
+ // Era .slice() no JSON: cortava o FIM (onde está o erro) e entregava JSON quebrado.
2399
+ const _rawToolContent = _cachedContent || intelligence.compactToolResult(result, _resultCap);
2399
2400
  messages.push({ role: 'tool', tool_call_id: tc.id, content: isUntrustedToolOutput(name)
2400
2401
  ? untrustedToolEnvelope(name, _rawToolContent, lang)
2401
2402
  : _rawToolContent });
@@ -424,6 +424,107 @@ function cleanOperationalText(value, max) {
424
424
  .replace(/\s+/g, ' ').trim().slice(0, max || 300);
425
425
  }
426
426
 
427
+ // ── Compactação de saída de ferramenta ──────────────────────────────────────
428
+ // Saída de ferramenta é a maior fatia do contexto de um agente — bem maior que o que o
429
+ // usuário escreve ou o que o modelo responde. E boa parte dela é ruído para o modelo:
430
+ // código de cor, barra de progresso, a mesma linha de log repetida mil vezes.
431
+ //
432
+ // O jeito ingênuo de conter isso é cortar com slice(). Três coisas dão errado:
433
+ // 1. corta o FIM, que é justamente onde fica o erro — a parte que importa;
434
+ // 2. em JSON, rasga a estrutura no meio de uma string e entrega sintaxe inválida;
435
+ // 3. mantém o ANSI e as repetições, que continuam ocupando espaço pago.
436
+ // Head/tail resolve as três: preserva o começo (o que foi pedido) e o fim (como acabou).
437
+ const TOOL_OUTPUT_MAX = 6000; // ~1.500 tokens por resultado
438
+ const _TOOL_FIELD_FLOOR = 240; // nenhum campo encolhe abaixo disto: sobra virava enigma
439
+
440
+ function _stripAnsi(value) {
441
+ return String(value == null ? '' : value)
442
+ .replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
443
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, '');
444
+ }
445
+
446
+ // Linhas consecutivas idênticas viram uma com contador. O modelo não aprende nada na
447
+ // milésima repetição que não soubesse na segunda — mas paga por todas.
448
+ function _dedupeLines(value) {
449
+ const lines = String(value).split('\n');
450
+ const out = [];
451
+ for (let i = 0; i < lines.length; i++) {
452
+ let j = i;
453
+ while (j + 1 < lines.length && lines[j + 1] === lines[i] && lines[i].trim()) j++;
454
+ const reps = j - i + 1;
455
+ out.push(reps > 2 ? lines[i] + ' [repetida ' + reps + 'x]' : lines[i]);
456
+ if (reps === 2) out.push(lines[i]);
457
+ i = j;
458
+ }
459
+ return out.join('\n');
460
+ }
461
+
462
+ // 60% do orçamento para o começo, o resto para o fim. O começo diz o que a ferramenta
463
+ // estava fazendo; o fim diz como terminou. O miolo de uma saída grande é quase sempre
464
+ // a parte que se repete.
465
+ function _headTail(value, max) {
466
+ const s = String(value);
467
+ if (s.length <= max) return s;
468
+ const marca = (n) => '\n… [' + n + ' chars omitidos] …\n';
469
+ const head = Math.floor(max * 0.6);
470
+ const tail = Math.max(0, max - head - marca(s.length).length);
471
+ return s.slice(0, head) + marca(s.length - head - tail) + s.slice(s.length - tail);
472
+ }
473
+
474
+ function _limparStrings(node, depth, orcamento) {
475
+ if (depth > 6 || orcamento.n <= 0) return node;
476
+ if (typeof node === 'string') { orcamento.n--; return _dedupeLines(_stripAnsi(node)); }
477
+ if (Array.isArray(node)) return node.map(v => _limparStrings(v, depth + 1, orcamento));
478
+ if (node && typeof node === 'object') {
479
+ const out = {};
480
+ for (const k of Object.keys(node)) out[k] = _limparStrings(node[k], depth + 1, orcamento);
481
+ return out;
482
+ }
483
+ return node;
484
+ }
485
+
486
+ function _maiorCampo(node, depth, achado) {
487
+ if (depth > 6) return achado;
488
+ if (Array.isArray(node)) { node.forEach((v, i) => _maiorCampo(v, depth + 1, _alvo(node, i, v, achado))); return achado; }
489
+ if (node && typeof node === 'object') {
490
+ for (const k of Object.keys(node)) { _alvo(node, k, node[k], achado); _maiorCampo(node[k], depth + 1, achado); }
491
+ }
492
+ return achado;
493
+ }
494
+ function _alvo(dono, chave, valor, achado) {
495
+ if (typeof valor === 'string' && valor.length > achado.len && valor.length > _TOOL_FIELD_FLOOR) {
496
+ achado.dono = dono; achado.chave = chave; achado.len = valor.length;
497
+ }
498
+ return achado;
499
+ }
500
+
501
+ // Recebe o que a ferramenta devolveu (texto ou objeto) e entrega o que vai para o
502
+ // contexto. Objeto sai como JSON VÁLIDO: encolhe os campos de texto grandes, um de cada
503
+ // vez, começando pelo maior — em vez de cortar o JSON inteiro e entregar sintaxe quebrada.
504
+ function compactToolResult(value, max) {
505
+ const limite = Math.max(400, Number(max) || TOOL_OUTPUT_MAX);
506
+ if (value == null) return '';
507
+ if (typeof value !== 'object') return _headTail(_dedupeLines(_stripAnsi(value)), limite);
508
+
509
+ let obj = _limparStrings(value, 0, { n: 5000 });
510
+ let json = JSON.stringify(obj);
511
+ if (json == null) return '';
512
+ // Encolhe o maior campo de texto por vez. Cada volta tem que reduzir de verdade,
513
+ // senão isto viraria laço infinito num objeto que não dá mais para espremer.
514
+ for (let volta = 0; volta < 40 && json.length > limite; volta++) {
515
+ const alvo = _maiorCampo(obj, 0, { dono: null, chave: null, len: 0 });
516
+ if (!alvo.dono) break;
517
+ const sobra = json.length - limite;
518
+ const novo = Math.max(_TOOL_FIELD_FLOOR, alvo.len - sobra - 40);
519
+ if (novo >= alvo.len) break;
520
+ alvo.dono[alvo.chave] = _headTail(alvo.dono[alvo.chave], novo);
521
+ json = JSON.stringify(obj);
522
+ }
523
+ // Último recurso: objeto grande por QUANTIDADE de campos pequenos, não por tamanho de
524
+ // um campo. Aí não há o que espremer e o corte é assumido — mas ainda com o fim preservado.
525
+ return json.length > limite ? _headTail(json, limite) : json;
526
+ }
527
+
427
528
  function _safeToolName(value) {
428
529
  const name = String(value || 'unknown').replace(/[^a-z0-9_-]/gi, '_').slice(0, 80);
429
530
  return name || 'unknown';
@@ -614,6 +715,7 @@ module.exports = {
614
715
  RETIRED_MODELS, supportedModelId,
615
716
  installCatalogOverlay, resetCatalogOverlay, activeCatalog,
616
717
  normalizeModelId, modelInfo, legacyPriceMap, normalizeUsage, estimateCostUsd, catalogFreshness,
718
+ TOOL_OUTPUT_MAX, compactToolResult,
617
719
  createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff, cleanOperationalText, buildOperationalSummary,
618
720
  normalizeAgentPlan, agentRoleContract, routeModel,
619
721
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.98.0",
3
+ "version": "0.98.1",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"