terminal-smart-cli 0.98.0 → 0.99.0
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/README.md +7 -5
- package/bin/ts.js +344 -196
- package/hub/cli.js +255 -0
- package/hub/controle.js +262 -0
- package/hub/dpapi.js +55 -0
- package/hub/estado.js +274 -0
- package/hub/exportar-telas.js +149 -0
- package/hub/hub.js +717 -0
- package/hub/mascara.js +67 -0
- package/hub/parar.js +151 -0
- package/hub/principal.js +5 -0
- package/hub/proto/PROTOCOLO.md +789 -0
- package/hub/proto/ts-proto.js +1643 -0
- package/hub/relay-conexao.js +200 -0
- package/hub/telas-arquivo.js +130 -0
- package/hub/tmux.js +189 -0
- package/hub/vps/README.md +109 -0
- package/hub/vps/tmux-agentes.conf +5 -0
- package/hub/vps/ts-hub-exportar-telas.service +30 -0
- package/hub/vps/ts-hub-parar-consumir +67 -0
- package/hub/vps/ts-hub-parar.path +16 -0
- package/hub/vps/ts-hub-parar.service +23 -0
- package/hub/vps/ts-hub-parar.tmpfiles.conf +10 -0
- package/hub/vps/ts-hub.service +39 -0
- package/hub/vps/ts-tmux.service +19 -0
- package/lib/acp.js +8 -2
- package/lib/agent.js +186 -87
- package/lib/chat-local.js +139 -0
- package/lib/core.js +336 -23
- package/lib/doctor.js +13 -5
- package/lib/i18n.js +69 -31
- package/lib/ia-local.js +388 -0
- package/lib/intelligence-core.js +108 -1
- package/lib/keyring.js +0 -1
- package/lib/meta.js +8 -8
- package/lib/personal-agent-llm.js +99 -26
- package/lib/sentinela.js +2 -2
- package/lib/tools.js +638 -8
- package/package.json +4 -2
package/lib/agent.js
CHANGED
|
@@ -756,6 +756,10 @@ function browserApprovalRequest(input = {}) {
|
|
|
756
756
|
async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null, creditBudget = null, maxCompletionTokens = null, tries = 4 }) {
|
|
757
757
|
// RESILIÊNCIA: o gateway CDC pode reiniciar/oscilar no meio de uma missão longa.
|
|
758
758
|
// withRetry cobre conn/timeout/5xx (backoff+jitter); NUNCA re-tenta no_credits/auth.
|
|
759
|
+
// _tsPinned/_tsTask (marcas do resumo e da tarefa fixos) são internas: provedores estritos
|
|
760
|
+
// recusam campo extra na mensagem.
|
|
761
|
+
const wireMessages = (messages || []).map(m => (m && typeof m === 'object' && ('_tsPinned' in m || '_tsTask' in m))
|
|
762
|
+
? (({ _tsPinned, _tsTask, ...rest }) => rest)(m) : m);
|
|
759
763
|
return withRetry(async () => {
|
|
760
764
|
const ctrl = new AbortController();
|
|
761
765
|
let timer;
|
|
@@ -770,7 +774,7 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
|
|
|
770
774
|
? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) }
|
|
771
775
|
: {}),
|
|
772
776
|
},
|
|
773
|
-
body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages,
|
|
777
|
+
body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages: wireMessages,
|
|
774
778
|
...(Number.isFinite(Number(maxCompletionTokens)) && Number(maxCompletionTokens) > 0
|
|
775
779
|
? { max_completion_tokens: Math.max(32, Math.min(Math.floor(Number(maxCompletionTokens)), 16000)) }
|
|
776
780
|
: {}),
|
|
@@ -965,7 +969,7 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
|
|
|
965
969
|
let inp = {}; try { inp = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
|
|
966
970
|
if (onStep) onStep({ name: ' ↳ ' + nm, detail: argsShort(nm, inp) });
|
|
967
971
|
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:
|
|
972
|
+
msgs.push({ role: 'tool', tool_call_id: tc.id, content: intelligence.compactToolResult(res, TOOL_RESULT_CAP) });
|
|
969
973
|
}
|
|
970
974
|
}
|
|
971
975
|
// Esgotou os passos ainda chamando ferramentas? Fecha com UM resumo (sem ferramentas) —
|
|
@@ -981,25 +985,19 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
|
|
|
981
985
|
return { resumo: out || (lang !== 'en' ? '(o sub-agente não produziu um resumo)' : '(sub-agent produced no summary)'), _tin: tin, _tout: tout, _tcach: tcach, _billed: billed };
|
|
982
986
|
}
|
|
983
987
|
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
onRemote({ id: st.id, ttl: st.ttl || 120 });
|
|
992
|
-
const t0 = Date.now();
|
|
993
|
-
while (Date.now() - t0 < 150000) {
|
|
994
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
995
|
-
let p = null;
|
|
996
|
-
try { p = await api('/api/cli/approval/poll', { method: 'POST', token, body: { id: st.id }, timeoutMs: 15000 }); } catch (_) { continue; }
|
|
997
|
-
if (p && p.status === 'approved') return { approved: true, remoteTried: true };
|
|
998
|
-
if (p && ['refused', 'expired', 'not_found'].includes(p.status)) return { approved: false, remoteTried: true };
|
|
999
|
-
}
|
|
1000
|
-
return { approved: false, remoteTried: true };
|
|
988
|
+
// Motivo curto e SEM segredo de uma falha do resumo automático (vai pra tela, pro
|
|
989
|
+
// stream-json e pra ACP): código + mensagem, mascarados e cortados.
|
|
990
|
+
function compactFailReason(e) {
|
|
991
|
+
const code = e && e.code ? String(e.code) : '';
|
|
992
|
+
const msg = String((e && e.message) || e || '').replace(/\s+/g, ' ').trim();
|
|
993
|
+
const txt = code && !msg.includes(code) ? `${code}: ${msg}` : (msg || code);
|
|
994
|
+
return core.redactSecrets(txt || 'erro desconhecido').slice(0, 160);
|
|
1001
995
|
}
|
|
1002
996
|
|
|
997
|
+
// Em --yes não há ninguém no terminal para aprovar: o que exige aprovação é RECUSADO na hora
|
|
998
|
+
// (fail-safe). A aprovação é sempre LOCAL — no terminal (askApprove) ou no editor (ACP).
|
|
999
|
+
// O aviso/aprovação pelo celular virá pelo hub; enquanto isso, nada sai desta máquina.
|
|
1000
|
+
|
|
1003
1001
|
/**
|
|
1004
1002
|
* Roda o agente. opts:
|
|
1005
1003
|
* token (sessão ts) · lang · yes (auto-aprova NÃO-destrutivos; destrutivo recusa)
|
|
@@ -1008,9 +1006,14 @@ async function _remoteApprove(comando, token, onRemote) {
|
|
|
1008
1006
|
* onStep({name, detail, auto}) — passo iniciado (pra UI; auto=destrutivo aprovado em full-auto)
|
|
1009
1007
|
* onStepDone({name, ok, status, evidence}) — passo concluído (✓/✗ por etapa)
|
|
1010
1008
|
* askApprove(cmd|{kind,cmd}) → Promise<boolean|'all'> ('all' liga o full-auto na sessão)
|
|
1009
|
+
* compactFirst — compacta o histórico retomado antes do primeiro passo (sessão perto do limite)
|
|
1010
|
+
* noProactiveCompact — a pessoa escolheu "Continuar assim" na folha de 95%: sem compactação
|
|
1011
|
+
* pelo limiar (só no limite útil ou num 400 de contexto)
|
|
1012
|
+
* Retorno: context = {used, window, usable, measured} da ÚLTIMA chamada; compaction =
|
|
1013
|
+
* {count, ok, reason} (ok=false quando algum resumo automático falhou e a parte antiga se perdeu).
|
|
1011
1014
|
*/
|
|
1012
1015
|
async function run(task, opts = {}) {
|
|
1013
|
-
const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}
|
|
1016
|
+
const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {} } = opts;
|
|
1014
1017
|
// Costura exclusiva da API programática/testes: permite simular o gateway sem
|
|
1015
1018
|
// expor uma flag no CLI nem alterar o caminho normal de produção.
|
|
1016
1019
|
const llmCall = typeof opts.llmFn === 'function' ? opts.llmFn : llm;
|
|
@@ -1108,11 +1111,18 @@ async function run(task, opts = {}) {
|
|
|
1108
1111
|
// o gate por plano no backend (402 plan_limit → mensagem de upgrade).
|
|
1109
1112
|
const k = opts.keyOverride || await keyring.resolve(token, { feature: 'cli_agent' });
|
|
1110
1113
|
if (!k || !k.key) throw new ApiError('ai_key', {});
|
|
1114
|
+
// Segredos desta execução que não estão em ~/.ts (a chave de IA em uso, inclusive a da nuvem):
|
|
1115
|
+
// somam-se aos do ts na máscara das saídas de ferramenta e da aprovação remota.
|
|
1116
|
+
const _runSecrets = [k.key];
|
|
1117
|
+
const _maskOut = (x) => tools.maskKnownSecrets(x, [token, ..._runSecrets]);
|
|
1111
1118
|
// A chave BYOK ainda é validada antes da primeira chamada. No modo Automático,
|
|
1112
1119
|
// ela escolhe o provedor, mas os papéis continuam obedecendo ao plano. Para
|
|
1113
1120
|
// escolher um modelo BYOK específico fora dessa regra, o usuário usa --modelo.
|
|
1114
1121
|
const erroModeloByok = validarModeloByok(k);
|
|
1115
1122
|
if (erroModeloByok) throw erroModeloByok;
|
|
1123
|
+
// X-TS-Credit-Budget só faz sentido no gateway do TS (créditos legados). Com chave
|
|
1124
|
+
// própria ou assinatura pessoal, o cabeçalho iria para um provedor de terceiros.
|
|
1125
|
+
const _creditBudget = (n) => (k.source === 'cloud' ? n : null);
|
|
1116
1126
|
let selectedModel = model;
|
|
1117
1127
|
// BYOK: o catálogo do roteador é do GATEWAY (ids tipo "deepseek-v4-flash"); o provedor
|
|
1118
1128
|
// do usuário tem os seus ("deepseek-ai/deepseek-v4-flash") e devolve 404 pro id errado.
|
|
@@ -1366,13 +1376,20 @@ async function run(task, opts = {}) {
|
|
|
1366
1376
|
{ role: 'system', content: systemPrompt(lang, cwd) + _memBlock + _busBlock + _interopBlock + skillsBlock + sugestaoBlock + evolveBlock + _erroBlock + planBlock + strictScopeBlock + _auditBlock + _mcpBlock + _hookCtx + _sharedChannelBlock },
|
|
1367
1377
|
...(_projectBrief ? [{ role: 'user', content: _projectBrief }] : []),
|
|
1368
1378
|
...(_memoryRecallMessage ? [_memoryRecallMessage] : []),
|
|
1369
|
-
|
|
1379
|
+
// _tsTask: a tarefa desta execução é FIXA — sobrevive à compactação e aos cortes da sessão
|
|
1380
|
+
{ role: 'user', content: taskText, _tsTask: true },
|
|
1370
1381
|
];
|
|
1371
1382
|
// CONTINUAR sessão anterior (ts agente --continuar): reaproveita o histórico, MAS com o system
|
|
1372
|
-
// prompt FRESCO (memória/skills atualizadas) + a nova tarefa no fim.
|
|
1383
|
+
// prompt FRESCO (memória/skills atualizadas) + a nova tarefa no fim. O corte preserva o
|
|
1384
|
+
// resumo fixo da compactação anterior e nunca começa numa resposta 'tool' órfã.
|
|
1373
1385
|
if (Array.isArray(opts.priorMessages) && opts.priorMessages.length) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1386
|
+
// A tarefa da sessão anterior vira histórico comum (sem a marca): a fixa agora é a nova.
|
|
1387
|
+
// Saídas de ferramenta da sessão salva passam pela máscara dos segredos conhecidos (sessões
|
|
1388
|
+
// gravadas antes dela existir, ou um segredo novo desde então).
|
|
1389
|
+
const convo = core.safeTail(opts.priorMessages.filter(m => m && m.role && m.role !== 'system'), 40)
|
|
1390
|
+
.map(m => (core.isPinnedTask(m) ? (({ _tsTask, ...rest }) => rest)(m) : m))
|
|
1391
|
+
.map(m => (m.role === 'tool' && typeof m.content === 'string' ? Object.assign({}, m, { content: _maskOut(m.content) }) : m));
|
|
1392
|
+
messages = [messages[0], ...(_projectBrief ? [{ role: 'user', content: _projectBrief }] : []), ...(_memoryRecallMessage ? [_memoryRecallMessage] : []), ...convo, { role: 'user', content: taskText, _tsTask: true }];
|
|
1376
1393
|
}
|
|
1377
1394
|
const acc = { inTok: 0, outTok: 0, cachedTok: 0 };
|
|
1378
1395
|
const _roleTrace = [];
|
|
@@ -1478,7 +1495,7 @@ async function run(task, opts = {}) {
|
|
|
1478
1495
|
try {
|
|
1479
1496
|
const reply = await llmCall({
|
|
1480
1497
|
baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel,
|
|
1481
|
-
toolsOverride: mainTools, creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets.execution.credits - _phaseUsage.execution.credits)),
|
|
1498
|
+
toolsOverride: mainTools, creditBudget: _creditBudget(Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets.execution.credits - _phaseUsage.execution.credits))),
|
|
1482
1499
|
...extra, maxCompletionTokens: executorCompletionCap(extra.maxCompletionTokens), signalMs, tries: 1,
|
|
1483
1500
|
});
|
|
1484
1501
|
if (model && forcedModelMismatch(model, reply && reply.model)) {
|
|
@@ -1532,7 +1549,7 @@ async function run(task, opts = {}) {
|
|
|
1532
1549
|
onStep({ name: 'agente_' + role, detail: roleModel, phase, model: roleModel });
|
|
1533
1550
|
const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: roleModel, noTools: true, signalMs,
|
|
1534
1551
|
maxCompletionTokens: role === 'planner' ? 2200 : 1400,
|
|
1535
|
-
creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets[phase].credits - _phaseUsage[phase].credits)), tries: 1,
|
|
1552
|
+
creditBudget: _creditBudget(Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets[phase].credits - _phaseUsage[phase].credits))), tries: 1,
|
|
1536
1553
|
messages: [{ role: 'system', content: contract.prompt + '\n' + system }, { role: 'user', content: user }] });
|
|
1537
1554
|
if (roleForcedModel && forcedModelMismatch(roleForcedModel, r && r.model)) {
|
|
1538
1555
|
const err = new ApiError(`Modelo forçado não foi respeitado: solicitado "${roleForcedModel}", recebido "${r.model}". A missão foi interrompida sem fallback.`, { status: 409, code: 'model_substituted' });
|
|
@@ -1601,16 +1618,45 @@ async function run(task, opts = {}) {
|
|
|
1601
1618
|
});
|
|
1602
1619
|
} catch (_) {}
|
|
1603
1620
|
};
|
|
1604
|
-
|
|
1621
|
+
// Ocupação da janela na ÚLTIMA chamada (não a soma do turno). measured=true só quando o
|
|
1622
|
+
// provedor devolveu prompt_tokens; sem isso é a estimativa chars/4 do histórico.
|
|
1623
|
+
// Modo assinatura (personal): o prompt enviado é recortado no cliente (personal-agent-llm,
|
|
1624
|
+
// ~23.800 caracteres) e o usage vem somado do backend — nem o histórico inteiro nem a janela do
|
|
1625
|
+
// modelo do gateway medem o que vai de fato. O medidor fica DESLIGADO (used:0, personal:true):
|
|
1626
|
+
// sem rodapé, sem aviso de 80% e sem a folha de 95%. A compactação segue pela estimativa.
|
|
1627
|
+
const _toolsTok = k.source === 'personal' ? 0 : Math.ceil(JSON.stringify(mainTools || []).length / 4);
|
|
1628
|
+
const _ctxEstimate = () => estMsgsTok(messages) + _toolsTok;
|
|
1629
|
+
const _ctxSnapshot = (used, measured) => (k.source === 'personal'
|
|
1630
|
+
? { used: 0, window: ctxWindow, usable: core.usableWindow(ctxWindow), measured: false, personal: true }
|
|
1631
|
+
: { used, window: ctxWindow, usable: core.usableWindow(ctxWindow), measured: !!measured });
|
|
1632
|
+
let lastCtx = _ctxSnapshot(0, false);
|
|
1633
|
+
const _progress = (extra = {}) => Object.assign({ inTok: acc.inTok, outTok: acc.outTok, cachedTok: acc.cachedTok, steps, model: usedModel, credits: charged + _visionCredits, elapsedMs: Date.now() - missionStartedAt, context: lastCtx }, extra);
|
|
1605
1634
|
|
|
1606
1635
|
// Compacta o histórico quando cruza o limiar (ou quando force=true após um 400 de
|
|
1607
|
-
// contexto): mensagens antigas viram UM resumo denso
|
|
1608
|
-
//
|
|
1636
|
+
// contexto ou a pedido do chamador): mensagens antigas viram UM resumo denso, FIXO no
|
|
1637
|
+
// índice 1; as últimas KEEP_TAIL ficam íntegras. A chamada usa o MESMO llmCall/k da
|
|
1638
|
+
// execução. Falhou o resumo? A parte antiga é descartada, mas a falha fica VISÍVEL:
|
|
1639
|
+
// passo bloqueado com o motivo + out.compaction.ok=false. Nunca lança.
|
|
1609
1640
|
let _lastCompactLen = 0; // guarda anti-thrash: só recompacta depois do histórico CRESCER
|
|
1641
|
+
let pinnedSummary = null;
|
|
1642
|
+
const _compaction = { count: 0, ok: true, reason: null };
|
|
1643
|
+
const TASK_PIN_MAX = 8000;
|
|
1644
|
+
const _pinnedTaskCopy = (m) => {
|
|
1645
|
+
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
1646
|
+
if (c.length <= TASK_PIN_MAX) return m;
|
|
1647
|
+
return { role: 'user', _tsTask: true, content: c.slice(0, TASK_PIN_MAX) + (lang !== 'en'
|
|
1648
|
+
? '\n[… pedido cortado aqui para caber no contexto]' : '\n[… request cut here to fit the context]') };
|
|
1649
|
+
};
|
|
1610
1650
|
async function _compactIfNeeded(force) {
|
|
1611
1651
|
try {
|
|
1612
|
-
|
|
1613
|
-
|
|
1652
|
+
// gatilho: o maior entre a estimativa (mensagens + esquemas de ferramentas) e o
|
|
1653
|
+
// prompt medido na última chamada — o medido inclui o que a estimativa não vê.
|
|
1654
|
+
const used = Math.max(_ctxEstimate(), Number(lastCtx.used) || 0);
|
|
1655
|
+
// "Continuar assim" na folha de 95% (opts.noProactiveCompact): o limiar proativo não vale
|
|
1656
|
+
// nesta execução — compacta só quando o histórico já não cabe no limite útil (a próxima
|
|
1657
|
+
// chamada estouraria) ou depois de um 400 de contexto (force).
|
|
1658
|
+
const threshold = opts.noProactiveCompact ? core.usableWindow(ctxWindow) : ctxWindow * COMPACT_AT;
|
|
1659
|
+
if (!force && used < threshold) return;
|
|
1614
1660
|
if (messages.length < KEEP_TAIL + 4) return; // pouco histórico — nada útil a resumir
|
|
1615
1661
|
// recém-compactado e pouca coisa nova? não compacta de novo (compactar toda iteração
|
|
1616
1662
|
// destrói a memória de trabalho do agente — ele começa a reler arquivos em loop)
|
|
@@ -1619,33 +1665,70 @@ async function run(task, opts = {}) {
|
|
|
1619
1665
|
let cut = Math.max(1, messages.length - KEEP_TAIL);
|
|
1620
1666
|
while (cut < messages.length && messages[cut].role === 'tool') cut++;
|
|
1621
1667
|
if (cut >= messages.length - 1) return;
|
|
1622
|
-
|
|
1623
|
-
|
|
1668
|
+
// o resumo anterior entra INTEIRO (sem o corte de 1500 das mensagens comuns) e é
|
|
1669
|
+
// substituído pelo novo; nunca fica duplicado nem é tratado como conversa.
|
|
1670
|
+
const previous = messages.slice(1).filter(m => core.isPinnedSummary(m)).map(m => core.pinnedSummaryText(m)).filter(Boolean).join('\n\n');
|
|
1671
|
+
const old = messages.slice(1, cut).filter(m => !core.isPinnedSummary(m));
|
|
1672
|
+
const tail = messages.slice(cut).filter(m => !core.isPinnedSummary(m));
|
|
1673
|
+
// a tarefa desta execução entra no bloco (dá o objetivo ao resumo) e, se caiu na parte
|
|
1674
|
+
// antiga, volta logo depois do resumo, FIXA — sem ela o rabo costuma ter só assistant/
|
|
1675
|
+
// tool e o modelo perde a missão. Pedido enorme (pipe, @arquivo) volta cortado.
|
|
1676
|
+
const taskOld = old.find(m => core.isPinnedTask(m)) || null;
|
|
1624
1677
|
const lines = old.map(m => {
|
|
1625
1678
|
let c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
1626
1679
|
if (m.role === 'tool') c = c.slice(0, 300);
|
|
1627
1680
|
if (m.tool_calls) c += ' ' + m.tool_calls.map(t => `[chamou ${(t.function && t.function.name) || '?'}(${String((t.function && t.function.arguments) || '').slice(0, 120)})]`).join(' ');
|
|
1628
1681
|
return `${m.role}: ${c.slice(0, 1500)}`;
|
|
1629
|
-
}).join('\n')
|
|
1630
|
-
|
|
1682
|
+
}).join('\n');
|
|
1683
|
+
const prevBlock = previous ? (lang !== 'en'
|
|
1684
|
+
? `RESUMO ANTERIOR (mantenha o que continua válido e atualize com a conversa que veio depois):\n${previous}\n\nCONVERSA DESDE O RESUMO ANTERIOR:\n`
|
|
1685
|
+
: `PREVIOUS SUMMARY (keep what is still valid and update it with the conversation that followed):\n${previous}\n\nCONVERSATION SINCE THE PREVIOUS SUMMARY:\n`) : '';
|
|
1686
|
+
const block = prevBlock + lines.slice(0, Math.max(0, 60000 - prevBlock.length));
|
|
1687
|
+
onThinking(_progress({ compacting: true }));
|
|
1688
|
+
let resumo = '', failReason = null;
|
|
1631
1689
|
try {
|
|
1632
1690
|
const sys = lang !== 'en'
|
|
1633
1691
|
? 'Resuma a conversa de agente abaixo em UM bloco curto e denso (máx ~300 palavras): objetivo, o que já foi feito (arquivos/comandos e resultados), decisões tomadas e o que falta. Preserve caminhos de arquivos e fatos técnicos EXATOS. Sem preâmbulo.'
|
|
1634
1692
|
: 'Summarize the agent conversation below into ONE short dense block (max ~300 words): goal, what was done (files/commands and results), decisions, and what remains. Preserve EXACT file paths and technical facts. No preamble.';
|
|
1635
|
-
|
|
1693
|
+
// purpose:'compact' — o modo pessoal manda o bloco inteiro (não o recorte de 8 mensagens).
|
|
1694
|
+
const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, purpose: 'compact',
|
|
1695
|
+
signalMs: opts.personalSubscription ? 120000 : 60000, maxCompletionTokens: 900,
|
|
1696
|
+
creditBudget: _creditBudget(Math.max(1, maxCredits - charged - _visionCredits)),
|
|
1697
|
+
messages: [{ role: 'system', content: sys }, { role: 'user', content: block }] });
|
|
1636
1698
|
const u = r.usage || {};
|
|
1637
1699
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
1638
1700
|
charged += (r.billing && r.billing.charged) || 0;
|
|
1639
|
-
resumo = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim().slice(0, 6000);
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1701
|
+
resumo = String((r.msg && r.msg.content) || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim().slice(0, 6000);
|
|
1702
|
+
if (!resumo) failReason = lang !== 'en' ? 'o modelo devolveu um resumo vazio' : 'the model returned an empty summary';
|
|
1703
|
+
} catch (e) { failReason = compactFailReason(e); }
|
|
1704
|
+
_compaction.count += 1;
|
|
1705
|
+
const pctTxt = `~${Math.round(used / 1000)}k tok (${Math.round(100 * used / ctxWindow)}% ${lang !== 'en' ? 'da janela' : 'of the window'})`;
|
|
1706
|
+
if (!resumo) {
|
|
1707
|
+
_compaction.ok = false;
|
|
1708
|
+
_compaction.reason = failReason;
|
|
1709
|
+
// sem resumo novo, o anterior (se havia) continua valendo — só a parte nova se perde
|
|
1710
|
+
const lostNote = lang !== 'en'
|
|
1711
|
+
? '(o resumo automático da parte seguinte falhou; essa parte do histórico foi descartada por limite de contexto)'
|
|
1712
|
+
: '(the auto-summary of the following part failed; that part of the history was dropped due to the context limit)';
|
|
1713
|
+
resumo = previous
|
|
1714
|
+
? (previous.includes(lostNote) ? previous : previous + '\n\n' + lostNote)
|
|
1715
|
+
: (lang !== 'en'
|
|
1716
|
+
? '(resumo automático indisponível — a parte antiga do histórico foi descartada por limite de contexto)'
|
|
1717
|
+
: '(auto-summary unavailable — older history was dropped due to the context limit)');
|
|
1718
|
+
// UM evento só, já como bloqueado: a ACP o abre como 'failed' e o stream-json como
|
|
1719
|
+
// 'blocked' — nada de 'completed' seguido de falha para o mesmo passo.
|
|
1720
|
+
onStep({ name: 'compactar_contexto', blocked: true, detail: lang !== 'en'
|
|
1721
|
+
? `resumo falhou (${failReason}); parte antiga descartada · ${pctTxt}`
|
|
1722
|
+
: `summary failed (${failReason}); older part dropped · ${pctTxt}` });
|
|
1723
|
+
} else {
|
|
1724
|
+
onStep({ name: 'compactar_contexto', detail: `${pctTxt} → ${lang !== 'en' ? 'resumo' : 'summary'}` });
|
|
1725
|
+
}
|
|
1726
|
+
pinnedSummary = { role: 'user', content: (lang !== 'en' ? core.SUMMARY_LABEL.pt : core.SUMMARY_LABEL.en) + resumo, _tsPinned: true };
|
|
1727
|
+
messages = [messages[0], pinnedSummary, ...(taskOld ? [_pinnedTaskCopy(taskOld)] : []), ...tail];
|
|
1648
1728
|
_lastCompactLen = messages.length;
|
|
1729
|
+
// o medido da última chamada ficou velho: até a próxima chamada vale a estimativa
|
|
1730
|
+
lastCtx = _ctxSnapshot(_ctxEstimate(), false);
|
|
1731
|
+
onThinking(_progress({ compacting: false }));
|
|
1649
1732
|
} catch (_) { /* compactação é best-effort — jamais derruba a run */ }
|
|
1650
1733
|
}
|
|
1651
1734
|
// cobrança Smart Credits (best-effort — o teto da sk-hub já protege no gateway)
|
|
@@ -1672,7 +1755,7 @@ async function run(task, opts = {}) {
|
|
|
1672
1755
|
_planDecision = preflight;
|
|
1673
1756
|
finalText = `Preciso de uma decisão sua antes de continuar: ${_planDecision.question}`;
|
|
1674
1757
|
_logEp('human');
|
|
1675
|
-
return { text: finalText, steps, credits: 0, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx,
|
|
1758
|
+
return { text: finalText, steps, credits: 0, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction),
|
|
1676
1759
|
needHuman: { motivo: _planDecision.reason, o_que_fazer: _planDecision.question },
|
|
1677
1760
|
orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace }, completion: { ok: false, reason: 'human_decision' }, missionCache: _missionCache.stats() };
|
|
1678
1761
|
}
|
|
@@ -1702,7 +1785,7 @@ async function run(task, opts = {}) {
|
|
|
1702
1785
|
finalText = `Preciso de uma decisão sua antes de continuar: ${_planDecision.question}`;
|
|
1703
1786
|
await _bill();
|
|
1704
1787
|
_logEp('human');
|
|
1705
|
-
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx,
|
|
1788
|
+
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction),
|
|
1706
1789
|
needHuman: { motivo: _planDecision.reason || 'Falta uma escolha material para executar com segurança.', o_que_fazer: _planDecision.question },
|
|
1707
1790
|
orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace }, completion: { ok: false, reason: 'human_decision' }, missionCache: _missionCache.stats() };
|
|
1708
1791
|
}
|
|
@@ -1725,8 +1808,10 @@ async function run(task, opts = {}) {
|
|
|
1725
1808
|
guardStopped = _guard();
|
|
1726
1809
|
if (guardStopped) break;
|
|
1727
1810
|
// passa o snapshot de progresso pra a status line ao vivo (tempo é contado no cliente)
|
|
1728
|
-
onThinking({ iter
|
|
1729
|
-
|
|
1811
|
+
onThinking(_progress({ iter }));
|
|
1812
|
+
// proativo: compacta ao cruzar ~65% da janela. compactFirst = o chamador decidiu compactar
|
|
1813
|
+
// antes do primeiro passo (sessão retomada já perto do limite útil, item 22).
|
|
1814
|
+
await _compactIfNeeded(iter === 0 && !!opts.compactFirst);
|
|
1730
1815
|
let r;
|
|
1731
1816
|
// gateway oscilou → mostra "reconectando" em vez de morrer calado (confiabilidade visível)
|
|
1732
1817
|
const _onGwRetry = (e) => onStep({ name: 'gateway', detail: (lang !== 'en' ? 'reconectando ' : 'reconnecting ') + e.attempt + '/' + e.tries + ' (' + e.reason + ')', retry: true });
|
|
@@ -1758,7 +1843,10 @@ async function run(task, opts = {}) {
|
|
|
1758
1843
|
_phaseUsage.execution.tokens += (u.prompt_tokens || 0) + (u.completion_tokens || 0);
|
|
1759
1844
|
_phaseUsage.execution.attempts += 1;
|
|
1760
1845
|
acc.cachedTok += (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cached_tokens || 0;
|
|
1761
|
-
|
|
1846
|
+
// modo pessoal: o usage do backend vem somado no turno (Codex) ou sem cache (Claude) e o
|
|
1847
|
+
// prompt é recortado no cliente — não serve como medida; fica a estimativa do histórico.
|
|
1848
|
+
const _measured = k.source !== 'personal' && Number(u.prompt_tokens) > 0;
|
|
1849
|
+
lastCtx = _ctxSnapshot(_measured ? Number(u.prompt_tokens) : _ctxEstimate(), _measured);
|
|
1762
1850
|
if (r.model) usedModel = r.model;
|
|
1763
1851
|
|
|
1764
1852
|
const tcs = r.msg.tool_calls || [];
|
|
@@ -1919,8 +2007,8 @@ async function run(task, opts = {}) {
|
|
|
1919
2007
|
|
|
1920
2008
|
// ENFORCEMENT do modo só-leitura PRIMEIRO: bloqueia ferramentas que alteram/rodam ANTES de
|
|
1921
2009
|
// qualquer efeito colateral. Não basta OMITIR a ferramenta (o modelo pode chamá-la mesmo
|
|
1922
|
-
// assim); e se o gate destrutivo abaixo rodasse primeiro, chegaria a pedir aprovação
|
|
1923
|
-
//
|
|
2010
|
+
// assim); e se o gate destrutivo abaixo rodasse primeiro, chegaria a pedir aprovação
|
|
2011
|
+
// por um comando que jamais executaria.
|
|
1924
2012
|
// (tools MCP read-only — needsApproval=false — são permitidas em roMode; as mutantes não)
|
|
1925
2013
|
if (roMode && !READONLY.has(name) && name !== 'explorar' && !(_mcp.route[name] && !_mcp.route[name].needsApproval)) {
|
|
1926
2014
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
@@ -1930,8 +2018,8 @@ async function run(task, opts = {}) {
|
|
|
1930
2018
|
}
|
|
1931
2019
|
// RECUSA INSTANTÂNEA de AUTO-DESTRUTIVOS (vem ANTES do gate de aprovação): comandos que
|
|
1932
2020
|
// matam a própria missão (o processo do agente / o dev server que ela usa) ou a máquina
|
|
1933
|
-
// são recusados NA HORA — sem askApprove,
|
|
1934
|
-
//
|
|
2021
|
+
// são recusados NA HORA — sem askApprove (antes, a espera da aprovação desperdiçava ~150s POR
|
|
2022
|
+
// tentativa no incidente do jogo, em que o modelo insistia em "taskkill /f /im node").
|
|
1935
2023
|
// Só o comando LOCAL (executar_comando roda NESTE processo/cwd); o remoto tem outro pid/cwd
|
|
1936
2024
|
// e segue pelo gate de aprovação comum.
|
|
1937
2025
|
if (result === undefined && name === 'executar_comando') {
|
|
@@ -1948,6 +2036,12 @@ async function run(task, opts = {}) {
|
|
|
1948
2036
|
const sd = tools.selfDestructiveReason(String(input.comando || ''), { cwd });
|
|
1949
2037
|
if (sd) { onStep({ name, detail: argsShort(name, input), blocked: true }); result = { erro: sd }; }
|
|
1950
2038
|
}
|
|
2039
|
+
// Credenciais do ts (~/.ts/config.json, providers.json, mcp*, sessions, agente) lidas por
|
|
2040
|
+
// shell: recusa como o ler_arquivo — nem aprovação nem --yolo liberam, o conteúdo iria pro modelo.
|
|
2041
|
+
if (result === undefined && name === 'executar_comando') {
|
|
2042
|
+
const tr = tools.tsSecretReadReason(String(input.comando || ''), { cwd });
|
|
2043
|
+
if (tr) { onStep({ name, detail: argsShort(name, input), blocked: true }); result = { erro: tr }; }
|
|
2044
|
+
}
|
|
1951
2045
|
// Defesa no dispatcher também cobre executar_remoto; o executor local tem
|
|
1952
2046
|
// a mesma checagem para que nenhum chamador consiga contorná-la.
|
|
1953
2047
|
if (result === undefined && ['executar_comando', 'executar_remoto'].includes(name)) {
|
|
@@ -1989,22 +2083,22 @@ async function run(task, opts = {}) {
|
|
|
1989
2083
|
// explícita: full-auto não publica em produção silenciosamente.
|
|
1990
2084
|
if (result === undefined && name === 'executar_remoto' && isRemoteDeployCommand(input.comando)) {
|
|
1991
2085
|
const label = `DEPLOY REMOTO → ${String(input.comando || '').slice(0, 1200)}`;
|
|
1992
|
-
let approved = false
|
|
2086
|
+
let approved = false;
|
|
1993
2087
|
if (!yes) {
|
|
1994
2088
|
const dec = await askApprove({ kind: 'deploy', cmd: label, warning: 'Este comando pode publicar, reiniciar ou alterar o ambiente remoto.' });
|
|
1995
2089
|
approved = decideApproval({ autoAll: false, decision: dec }).approved;
|
|
1996
|
-
}
|
|
2090
|
+
}
|
|
1997
2091
|
if (!approved) {
|
|
1998
2092
|
result = { erro: yes
|
|
1999
|
-
?
|
|
2093
|
+
? 'RECUSADO automaticamente: deploy remoto exige a aprovação de quem está no terminal, e em --yes não há quem aprove. Explique ao usuário e sugira rodar interativamente.'
|
|
2000
2094
|
: 'O usuário recusou o deploy remoto. Não publique nem reinicie o ambiente.' };
|
|
2001
2095
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
2002
2096
|
}
|
|
2003
2097
|
}
|
|
2004
2098
|
// Gate de segurança: destrutivo → humano decide (só se ainda não foi bloqueado acima).
|
|
2005
|
-
// Interativo pergunta no terminal; em --yes (cron)
|
|
2099
|
+
// Interativo pergunta no terminal; em --yes (cron) não há quem aprove: recusa.
|
|
2006
2100
|
if (result === undefined && !_polAllow && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
|
|
2007
|
-
let approved = false
|
|
2101
|
+
let approved = false;
|
|
2008
2102
|
if (autoApproveDestructive) {
|
|
2009
2103
|
// FULL-AUTO não abre mão do anti-catástrofe: os auto-destrutivos de máquina/processo/
|
|
2010
2104
|
// pasta já caíram no gate acima (selfDestructiveReason). O que sobra é ~/.ts por shell
|
|
@@ -2026,13 +2120,11 @@ async function run(task, opts = {}) {
|
|
|
2026
2120
|
const dec = await askApprove({ kind: 'destructive', cmd: _cmd, warning: core.globalStateWarning(_cmd) });
|
|
2027
2121
|
const d = decideApproval({ autoAll: false, decision: dec });
|
|
2028
2122
|
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
2029
|
-
}
|
|
2123
|
+
}
|
|
2030
2124
|
// result === undefined evita sobrescrever a mensagem específica do bloqueio ~/.ts do full-auto
|
|
2031
2125
|
if (!approved && result === undefined) {
|
|
2032
2126
|
result = { erro: yes
|
|
2033
|
-
? (
|
|
2034
|
-
? 'RECUSADO: o dono NEGOU (ou não respondeu em 2 min) a aprovação remota deste comando destrutivo. Não repita; siga sem ele e relate no resumo.'
|
|
2035
|
-
: 'RECUSADO automaticamente: comando destrutivo não roda em modo --yes (sem Telegram vinculado pra aprovação remota). Explique ao usuário e sugira rodar interativamente ou vincular o Telegram.')
|
|
2127
|
+
? 'RECUSADO automaticamente: comando destrutivo não roda em modo --yes (não há quem aprove no terminal). Não repita; siga sem ele, relate no resumo e sugira rodar interativamente (ou com --yolo, se o dono quiser aprovar tudo).'
|
|
2036
2128
|
: 'O usuário RECUSOU este comando destrutivo. Não repita; proponha uma alternativa segura ou explique o que seria necessário.' };
|
|
2037
2129
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
2038
2130
|
}
|
|
@@ -2046,14 +2138,14 @@ async function run(task, opts = {}) {
|
|
|
2046
2138
|
const payload = JSON.stringify(input || {});
|
|
2047
2139
|
const provider = name.includes('outlook') || name.includes('microsoft365') ? 'MICROSOFT 365' : 'GOOGLE WORKSPACE';
|
|
2048
2140
|
const label = `${provider} → ${name}: ${payload.length > 1500 ? payload.slice(0, 1500) + ` …(+${payload.length - 1500} chars)` : payload}`;
|
|
2049
|
-
let approved = false
|
|
2141
|
+
let approved = false;
|
|
2050
2142
|
if (!yes) {
|
|
2051
2143
|
const dec = await askApprove({ kind: 'cloud_mutation', cmd: label, warning: name.includes('enviar_') ? 'O e-mail será enviado ao destinatário.' : `Dados da conta ${provider} serão alterados.` });
|
|
2052
2144
|
approved = decideApproval({ autoAll: false, decision: dec }).approved;
|
|
2053
|
-
}
|
|
2145
|
+
}
|
|
2054
2146
|
if (!approved) {
|
|
2055
2147
|
result = { erro: yes
|
|
2056
|
-
?
|
|
2148
|
+
? `RECUSADO automaticamente: alteração ${provider} exige a aprovação de quem está no terminal, e em --yes não há quem aprove.`
|
|
2057
2149
|
: `O usuário recusou a alteração na conta ${provider}. Não repita nesta sessão.` };
|
|
2058
2150
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
2059
2151
|
}
|
|
@@ -2067,7 +2159,7 @@ async function run(task, opts = {}) {
|
|
|
2067
2159
|
? String(input.caminho || '')
|
|
2068
2160
|
: JSON.stringify(input || {}).slice(0, 600);
|
|
2069
2161
|
const label = `DISPOSITIVO Android → ${name}: ${safe}`;
|
|
2070
|
-
let approved = false
|
|
2162
|
+
let approved = false;
|
|
2071
2163
|
if (autoApproveDestructive) {
|
|
2072
2164
|
approved = true;
|
|
2073
2165
|
onStep({ name, detail: argsShort(name, input), auto: true });
|
|
@@ -2076,14 +2168,10 @@ async function run(task, opts = {}) {
|
|
|
2076
2168
|
const d = decideApproval({ autoAll: false, decision: dec });
|
|
2077
2169
|
approved = d.approved;
|
|
2078
2170
|
if (d.enableAll) autoApproveDestructive = true;
|
|
2079
|
-
} else {
|
|
2080
|
-
({ approved, remoteTried } = await _remoteApprove(label, token, onRemote));
|
|
2081
2171
|
}
|
|
2082
2172
|
if (!approved) {
|
|
2083
2173
|
result = { erro: yes
|
|
2084
|
-
?
|
|
2085
|
-
? 'RECUSADO: o dono negou ou não respondeu à ação no dispositivo.'
|
|
2086
|
-
: 'RECUSADO automaticamente: ação em dispositivo físico exige aprovação remota.')
|
|
2174
|
+
? 'RECUSADO automaticamente: ação em dispositivo físico exige a aprovação de quem está no terminal, e em --yes não há quem aprove.'
|
|
2087
2175
|
: 'O usuário recusou a ação no dispositivo. Não repita nesta sessão.' };
|
|
2088
2176
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
2089
2177
|
}
|
|
@@ -2095,14 +2183,14 @@ async function run(task, opts = {}) {
|
|
|
2095
2183
|
if (result === undefined && name === 'navegador') {
|
|
2096
2184
|
const browserRequest = browserApprovalRequest(input);
|
|
2097
2185
|
if (browserRequest) {
|
|
2098
|
-
let approved = false
|
|
2186
|
+
let approved = false;
|
|
2099
2187
|
if (!yes) {
|
|
2100
2188
|
const dec = await askApprove(browserRequest);
|
|
2101
2189
|
approved = decideApproval({ autoAll: false, decision: dec }).approved;
|
|
2102
|
-
}
|
|
2190
|
+
}
|
|
2103
2191
|
if (!approved) {
|
|
2104
2192
|
result = { erro: yes
|
|
2105
|
-
?
|
|
2193
|
+
? 'RECUSADO automaticamente: ação no navegador exige a aprovação de quem está no terminal, e em --yes não há quem aprove.'
|
|
2106
2194
|
: 'O usuário recusou a ação no navegador. A página continua apenas em modo de leitura.' };
|
|
2107
2195
|
onStep({ name: 'navegador', detail: browserRequest.cmd, blocked: true });
|
|
2108
2196
|
}
|
|
@@ -2140,22 +2228,22 @@ async function run(task, opts = {}) {
|
|
|
2140
2228
|
}
|
|
2141
2229
|
}
|
|
2142
2230
|
// Gate de aprovação MCP: tool mutante (annotations/verbo de escrita) → humano decide,
|
|
2143
|
-
// mesmo fluxo do destrutivo (interativo pergunta; --yes
|
|
2231
|
+
// mesmo fluxo do destrutivo (interativo pergunta; --yes recusa).
|
|
2144
2232
|
if (result === undefined && _mcp.route[name] && _mcp.route[name].needsApproval) {
|
|
2145
2233
|
// input COMPLETO no label (nunca esconde payload atrás de truncamento — o gate nativo
|
|
2146
2234
|
// mostra o comando inteiro; o MCP faz igual, só marca quantos chars quando é enorme).
|
|
2147
2235
|
const _js = JSON.stringify(input);
|
|
2148
2236
|
const _lbl = 'MCP ' + _mcp.route[name].server + ' → ' + _mcp.route[name].realName + ' ' + (_js.length > 1500 ? _js.slice(0, 1500) + ' …(+' + (_js.length - 1500) + ' chars)' : _js);
|
|
2149
|
-
let approved = false
|
|
2237
|
+
let approved = false;
|
|
2150
2238
|
if (autoApproveDestructive) { approved = true; onStep({ name, detail: argsShort(name, input), auto: true }); }
|
|
2151
2239
|
else if (!yes) {
|
|
2152
2240
|
const dec = await askApprove(_lbl);
|
|
2153
2241
|
const d = decideApproval({ autoAll: false, decision: dec });
|
|
2154
2242
|
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
2155
|
-
}
|
|
2243
|
+
}
|
|
2156
2244
|
if (!approved) {
|
|
2157
2245
|
result = { erro: yes
|
|
2158
|
-
?
|
|
2246
|
+
? 'RECUSADO automaticamente: ferramenta MCP mutante não roda em --yes (não há quem aprove no terminal). Siga sem ela.'
|
|
2159
2247
|
: 'O usuário RECUSOU esta ferramenta MCP. Não repita; proponha uma alternativa.' };
|
|
2160
2248
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
2161
2249
|
}
|
|
@@ -2302,6 +2390,8 @@ async function run(task, opts = {}) {
|
|
|
2302
2390
|
if (result === undefined) {
|
|
2303
2391
|
onStep({ name, detail: argsShort(name, input) });
|
|
2304
2392
|
result = await tools.execute(name, input, { confineDir, baseDir: cwd, token, allowedTools: _allowedToolNames, trustedReadPaths: _trustedSkillPaths });
|
|
2393
|
+
// o executor já mascara os segredos do ts; aqui entra também a chave de IA da execução
|
|
2394
|
+
result = _maskOut(result);
|
|
2305
2395
|
steps++; _ran = true;
|
|
2306
2396
|
// TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
|
|
2307
2397
|
// sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
|
|
@@ -2368,7 +2458,7 @@ async function run(task, opts = {}) {
|
|
|
2368
2458
|
const _cacheClass = core.classifyToolResult(result);
|
|
2369
2459
|
if (_cacheClass.ok && READONLY.has(name)) {
|
|
2370
2460
|
const _resultCap = name === 'ler_arquivos' ? 24000 : TOOL_RESULT_CAP;
|
|
2371
|
-
_missionCache.remember(name, input, { toolCallId: tc.id, content:
|
|
2461
|
+
_missionCache.remember(name, input, { toolCallId: tc.id, content: intelligence.compactToolResult(result, _resultCap) });
|
|
2372
2462
|
} else if (_cacheClass.ok && !READONLY.has(name)) {
|
|
2373
2463
|
_missionCache.invalidate();
|
|
2374
2464
|
}
|
|
@@ -2383,6 +2473,9 @@ async function run(task, opts = {}) {
|
|
|
2383
2473
|
&& _researchCalls >= Math.ceil(RESEARCH_MAX * 2 / 3) && _researchCalls <= RESEARCH_MAX) {
|
|
2384
2474
|
result = Object.assign({}, result, { _aviso: 'Você já pesquisou ' + _researchCalls + 'x (limite ' + RESEARCH_MAX + '). Se já tem o suficiente, PARE de pesquisar e ENTREGUE o resultado agora.' });
|
|
2385
2475
|
}
|
|
2476
|
+
// REDE DE SEGURANÇA por valor (tools.maskKnownSecrets): o que sai de MCP, navegador, sub-agente
|
|
2477
|
+
// e das mensagens de bloqueio passa pela mesma máscara antes do status, do log e do modelo.
|
|
2478
|
+
result = _maskOut(result);
|
|
2386
2479
|
// STATUS POR ETAPA (UX): ✓ verde / ✗ vermelho por passo, a partir do ToolResult TIPADO
|
|
2387
2480
|
// (core.classifyToolResult) — o usuário vê na hora o que deu certo/errado (antes as ⚙
|
|
2388
2481
|
// não indicavam resultado nenhum). Só para passos que REALMENTE rodaram (não gate/bloqueio).
|
|
@@ -2390,12 +2483,18 @@ async function run(task, opts = {}) {
|
|
|
2390
2483
|
// BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz →
|
|
2391
2484
|
// encerra o turno devolvendo o pedido; a missão pausa e chama o usuário.
|
|
2392
2485
|
if (result && result._needHuman) {
|
|
2486
|
+
// Sessão retomável (--continuar "feito, continua"): devolve o histórico, com resposta a
|
|
2487
|
+
// ESTA chamada (o pedido ao humano) e às que sobraram no lote (não executadas) — um
|
|
2488
|
+
// tool_call sem resposta faria o provedor recusar a retomada com 400.
|
|
2489
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ aguardando_humano: true, motivo: result.motivo, o_que_fazer: result.o_que_fazer }) });
|
|
2490
|
+
for (const rest of tcs.slice(tcs.indexOf(tc) + 1)) messages.push({ role: 'tool', tool_call_id: rest.id, content: JSON.stringify({ erro: 'Não executado: o agente parou para pedir uma decisão humana.' }) });
|
|
2393
2491
|
await _bill();
|
|
2394
2492
|
_logEp('human');
|
|
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 } };
|
|
2493
|
+
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction), needHuman: { motivo: result.motivo, o_que_fazer: result.o_que_fazer }, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace } };
|
|
2396
2494
|
}
|
|
2397
2495
|
const _resultCap = name === 'ler_arquivos' ? 24000 : TOOL_RESULT_CAP;
|
|
2398
|
-
|
|
2496
|
+
// Era .slice() no JSON: cortava o FIM (onde está o erro) e entregava JSON quebrado.
|
|
2497
|
+
const _rawToolContent = _maskOut(_cachedContent || intelligence.compactToolResult(result, _resultCap));
|
|
2399
2498
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: isUntrustedToolOutput(name)
|
|
2400
2499
|
? untrustedToolEnvelope(name, _rawToolContent, lang)
|
|
2401
2500
|
: _rawToolContent });
|
|
@@ -2427,8 +2526,8 @@ async function run(task, opts = {}) {
|
|
|
2427
2526
|
? `Interrompi esta missão porque ela atingiu ${labels[guardStopped.kind] || 'o limite de segurança'}. Foram executados ${steps} passo(s) e consumidos ${charged + _visionCredits} crédito(s). O que já foi concluído permanece válido; a etapa que falhou não foi declarada como pronta. Revise o último erro antes de continuar ou aumente o orçamento explicitamente.`
|
|
2428
2527
|
: `I stopped this mission because it reached ${labels[guardStopped.kind] || 'the safety limit'}. ${steps} step(s) ran and ${charged + _visionCredits} credit(s) were consumed. Completed work remains valid; the failed step was not reported as done. Review the last error before continuing or explicitly raise the budget.`;
|
|
2429
2528
|
if (guardStopped.kind === 'credits' && guardStopped.required) finalText += lang !== 'en'
|
|
2430
|
-
? ` A próxima chamada precisava de ${guardStopped.required} crédito(s), mas havia ${guardStopped.available} disponível(is). Retome com --
|
|
2431
|
-
: ` The next call required ${guardStopped.required} credit(s), but ${guardStopped.available} were available. Resume with --
|
|
2529
|
+
? ` A próxima chamada precisava de ${guardStopped.required} crédito(s), mas havia ${guardStopped.available} disponível(is). Retome com --orcamento ${suggestedBudget} ou mais (créditos legados da IA do Terminal Smart).`
|
|
2530
|
+
: ` The next call required ${guardStopped.required} credit(s), but ${guardStopped.available} were available. Resume with --orcamento ${suggestedBudget} or more (Terminal Smart AI legacy credits).`;
|
|
2432
2531
|
}
|
|
2433
2532
|
// Se as ferramentas e os critérios já provaram a entrega, não faça mais uma
|
|
2434
2533
|
// chamada ao provedor apenas para reescrever o resumo. Além de custar, essa
|
|
@@ -2450,7 +2549,7 @@ async function run(task, opts = {}) {
|
|
|
2450
2549
|
try {
|
|
2451
2550
|
const signalMs = modelCallWindow({ maxDurationMs, elapsedMs: Date.now() - missionStartedAt, capMs: modelCallCap(opts.personalSubscription, _complexTask) });
|
|
2452
2551
|
if (!signalMs) throw new ApiError('model_timeout', { code: 'timeout' });
|
|
2453
|
-
const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs, maxCompletionTokens: 900, tries: 1, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
|
|
2552
|
+
const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs, maxCompletionTokens: 900, tries: 1, creditBudget: _creditBudget(Math.max(1, maxCredits - charged - _visionCredits)) });
|
|
2454
2553
|
const u = r.usage || {};
|
|
2455
2554
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
2456
2555
|
charged += (r.billing && r.billing.charged) || 0;
|
|
@@ -2573,7 +2672,7 @@ async function run(task, opts = {}) {
|
|
|
2573
2672
|
}));
|
|
2574
2673
|
await Promise.all(_syncTasks);
|
|
2575
2674
|
} catch (_) {}
|
|
2576
|
-
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
|
|
2675
|
+
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction), toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
|
|
2577
2676
|
}
|
|
2578
2677
|
|
|
2579
|
-
module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, powershellFileChainIssue, reconcileBillingClaims, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
|
|
2678
|
+
module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, powershellFileChainIssue, reconcileBillingClaims, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest, compactFailReason } };
|