brito-cli 0.7.4 → 0.8.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/.brito/agents/reviewer.md +7 -7
- package/LICENSE +75 -75
- package/README.md +298 -259
- package/README.pt-BR.md +301 -262
- package/dist/agent/approval.js +19 -2
- package/dist/agent/claude-cli.js +1 -1
- package/dist/agent/loop.js +30 -7
- package/dist/agent/prompt.js +51 -25
- package/dist/agent/sessions.js +98 -0
- package/dist/cli.js +219 -25
- package/dist/desktop/serve.js +34 -11
- package/dist/engine.js +28 -4
- package/dist/graph/migrate.js +6 -6
- package/dist/local/digest.js +202 -0
- package/dist/local/slm.js +218 -0
- package/dist/mcp/server.js +4 -3
- package/dist/permissions.js +3 -3
- package/dist/provider/anthropic-params.js +32 -0
- package/dist/provider/anthropic.js +13 -9
- package/dist/provider/fallback.js +67 -8
- package/dist/provider/index.js +4 -0
- package/dist/provider/local.js +88 -0
- package/dist/provider/resolve.js +8 -0
- package/dist/repl.js +44 -5
- package/dist/rules.js +102 -0
- package/dist/skills/builtin.js +89 -0
- package/dist/skills/consolidate.js +7 -7
- package/dist/skills/describe.js +3 -2
- package/dist/skills/learn.js +23 -23
- package/dist/skills/store.js +64 -8
- package/dist/skills/trial.js +3 -3
- package/dist/tools/delegate.js +2 -1
- package/dist/tools/digest.js +29 -0
- package/dist/tools/fs.js +51 -8
- package/dist/tools/registry.js +10 -1
- package/dist/tools/skills.js +15 -13
- package/dist/tools/todo.js +22 -0
- package/dist/ui/ui.js +12 -0
- package/dist/util/todos.js +4 -0
- package/package.json +80 -77
- package/dist/setup.js +0 -110
package/dist/agent/approval.js
CHANGED
|
@@ -3,11 +3,22 @@ import net from "node:net";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { describeClaudeTool, perms } from "../permissions.js";
|
|
6
|
+
import { decide, loadRules } from "../rules.js";
|
|
6
7
|
/**
|
|
7
8
|
* Aprovações do claude CLI: o claude chama a tool MCP `approve` (--permission-prompt-tool), que roda no
|
|
8
9
|
* processo do servidor MCP e consulta ESTE processo por um socket unix, onde está o terminal (ou a janela do app).
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
/** Traduz uma tool do claude CLI para o nome e o input das tools do Brito e decide pelas regras de permissão. */
|
|
12
|
+
export function claudeDecision(root, tool, input) {
|
|
13
|
+
const file = typeof input?.file_path === "string" ? input.file_path : undefined;
|
|
14
|
+
const mapped = tool === "Bash" ? { name: "bash", input: { command: input?.command } } :
|
|
15
|
+
(tool === "Edit" || tool === "MultiEdit") && file ? { name: "edit_file", input: { path: file } } :
|
|
16
|
+
tool === "Write" && file ? { name: "write_file", input: { path: file } } :
|
|
17
|
+
{ name: tool, input };
|
|
18
|
+
return decide(loadRules(root), mapped.name, mapped.input, root);
|
|
19
|
+
}
|
|
20
|
+
/** `root` liga as regras de permissão do projeto; sem ele, toda chamada é perguntada como antes. */
|
|
21
|
+
export function startApprovalServer(io, root) {
|
|
11
22
|
// No Windows um socket unix em %TEMP% não é aceito por net.listen: o endereço tem que ser um named pipe.
|
|
12
23
|
const sock = process.platform === "win32" ? `\\\\.\\pipe\\brito-approve-${process.pid}` : path.join(os.tmpdir(), `brito-approve-${process.pid}.sock`);
|
|
13
24
|
try {
|
|
@@ -25,7 +36,13 @@ export function startApprovalServer(io) {
|
|
|
25
36
|
try {
|
|
26
37
|
const req = JSON.parse(buf.slice(0, nl));
|
|
27
38
|
const plan = req.tool_name === "ExitPlanMode";
|
|
28
|
-
|
|
39
|
+
// As regras do Brito valem também para as tools do claude: deny bloqueia, allow dispensa a pergunta.
|
|
40
|
+
const decision = root && !plan ? claudeDecision(root, req.tool_name, req.input) : "ask";
|
|
41
|
+
if (decision === "deny") {
|
|
42
|
+
conn.end(JSON.stringify({ allow: false, message: "Bloqueado por uma regra deny das permissões do Brito (.brito/settings.json). Não tente contornar." }) + "\n");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const ok = decision === "allow" || await io.ask(describeClaudeTool(req.tool_name, req.input), { force: plan });
|
|
29
46
|
if (ok && plan)
|
|
30
47
|
perms.mode = "manual";
|
|
31
48
|
reply = ok ? { allow: true } : { allow: false, message: plan ? "Plano não aprovado pelo usuário. Pergunte o que ajustar." : "O usuário negou a execução." };
|
package/dist/agent/claude-cli.js
CHANGED
|
@@ -212,7 +212,7 @@ export class ClaudeSession {
|
|
|
212
212
|
args(text, imageDirs = []) {
|
|
213
213
|
const { root, approvalSock, stats, mcpCommand } = this.o;
|
|
214
214
|
const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode, this.o.env);
|
|
215
|
-
const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load"];
|
|
215
|
+
const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load", "digest"];
|
|
216
216
|
const system = buildSystemPrompt(root, stats, skillsIndex(root)) +
|
|
217
217
|
`\n\n# Nesta sessão\nAs tools do Brito chegam com o prefixo \`${MCP_PREFIX}\` (ex.: ${MCP_PREFIX}graph_context). Use-as como descrito acima. ` +
|
|
218
218
|
`read_file/grep/glob/edit_file/bash correspondem a Read/Grep/Glob/Edit/Bash, e \`delegate\` corresponde à tool Agent/Task com os subagentes configurados (explore, plan, general e os do projeto).`;
|
package/dist/agent/loop.js
CHANGED
|
@@ -5,7 +5,7 @@ import { initialEffort, initialModel } from "../model.js";
|
|
|
5
5
|
import { providerFor } from "../provider/index.js";
|
|
6
6
|
import { compactAtFor, resolveModel } from "../provider/resolve.js";
|
|
7
7
|
import { fallbackStatus } from "../engine.js";
|
|
8
|
-
import { activateFallback, classifyAnthropicError, fallbackWarning, shortError, triggersFallback } from "../provider/fallback.js";
|
|
8
|
+
import { activateFallback, classifyAnthropicError, fallbackExpired, fallbackUntil, fallbackWarning, returnToPrimary, shortError, triggersFallback } from "../provider/fallback.js";
|
|
9
9
|
import { fmtBytes, findImages, imageBlock, splitOversize } from "../util/images.js";
|
|
10
10
|
export const MODEL = initialModel();
|
|
11
11
|
export class Agent {
|
|
@@ -80,6 +80,7 @@ export class Agent {
|
|
|
80
80
|
}
|
|
81
81
|
else
|
|
82
82
|
push({ role: "user", content: userText });
|
|
83
|
+
this.retryPrimaryIfExpired();
|
|
83
84
|
await this.compactIfNeeded();
|
|
84
85
|
let finalText = "";
|
|
85
86
|
for (let step = 0; step < this.maxSteps; step++) {
|
|
@@ -147,31 +148,53 @@ export class Agent {
|
|
|
147
148
|
try {
|
|
148
149
|
return await providerFor(this.model).send({
|
|
149
150
|
model: this.model,
|
|
150
|
-
|
|
151
|
+
// Na Anthropic a resposta vem por streaming, então o teto pode ser alto sem risco de timeout: um
|
|
152
|
+
// write_file grande não é mais cortado no meio.
|
|
153
|
+
maxTokens: target.kind === "openai" ? (thinking ? 16_384 : 8192) : 32_000,
|
|
151
154
|
system: typeof this.system === "function" ? this.system() : this.system,
|
|
152
155
|
tools: toApiTools(this.opts.tools),
|
|
153
156
|
messages: this.messages,
|
|
154
157
|
thinking,
|
|
158
|
+
effort: this.effort,
|
|
155
159
|
}, hooks);
|
|
156
160
|
}
|
|
157
161
|
catch (e) {
|
|
158
162
|
const fb = this.opts.fallback;
|
|
159
|
-
if (!fb ||
|
|
163
|
+
if (!fb || hooks.signal?.aborted)
|
|
160
164
|
throw e;
|
|
161
|
-
|
|
165
|
+
// Claude → qwen → SLM: com o fallback ativo só desce um degrau (qwen que falha cai no SLM), nunca o SLM.
|
|
166
|
+
const onQwen = fb.active && target.kind === "openai" && !target.local && target.name === "qwen";
|
|
167
|
+
if (fb.active ? !onQwen : target.kind !== "anthropic" || target.local)
|
|
168
|
+
throw e;
|
|
169
|
+
if (onQwen && target.kind !== "openai")
|
|
170
|
+
throw e;
|
|
171
|
+
const kind = onQwen ? "network" : classifyAnthropicError(e);
|
|
162
172
|
if (!triggersFallback(kind))
|
|
163
173
|
throw e;
|
|
164
|
-
const status = fallbackStatus(
|
|
174
|
+
const status = fallbackStatus(fb.primary, process.env, onQwen ? ["qwen"] : []);
|
|
165
175
|
if (!status.ready)
|
|
166
176
|
throw e;
|
|
167
177
|
const why = shortError(e);
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
const until = onQwen ? fb.until : fallbackUntil(kind, why);
|
|
179
|
+
this.ui.warn(fallbackWarning(kind, why, status.label, until));
|
|
180
|
+
activateFallback(fb, `${kind} (${why})`, Date.now(), until, status.name);
|
|
170
181
|
this.model = status.name;
|
|
171
182
|
await this.compactIfNeeded();
|
|
172
183
|
return this.callModel(hooks);
|
|
173
184
|
}
|
|
174
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Fallback temporário: passada a janela, o próximo turno tenta o Claude de novo (se falhar, `callModel` reativa o
|
|
188
|
+
* fallback com uma janela nova). Só com ANTHROPIC_API_KEY: sem ela o Claude não roda neste loop (volta via /model).
|
|
189
|
+
*/
|
|
190
|
+
retryPrimaryIfExpired() {
|
|
191
|
+
const fb = this.opts.fallback;
|
|
192
|
+
if (!fb || !fallbackExpired(fb) || !process.env.ANTHROPIC_API_KEY?.trim() || resolveModel(fb.primary).kind !== "anthropic")
|
|
193
|
+
return;
|
|
194
|
+
returnToPrimary(fb);
|
|
195
|
+
this.model = fb.primary;
|
|
196
|
+
this.ui.info(`voltando ao Claude (${fb.primary}) para testar se o limite acabou`);
|
|
197
|
+
}
|
|
175
198
|
async compactIfNeeded() {
|
|
176
199
|
if (estimateTokens(this.messages) < compactAtFor(this.model))
|
|
177
200
|
return;
|
package/dist/agent/prompt.js
CHANGED
|
@@ -23,33 +23,59 @@ export function buildSystemPrompt(root, graphStats, skills = "") {
|
|
|
23
23
|
break;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
return `Você é Brito, um agente de engenharia de software que trabalha em CLI. Responda em português do Brasil, de forma direta.
|
|
27
|
-
|
|
28
|
-
# Como trabalhar
|
|
29
|
-
- Você tem um GRAFO DE CONHECIMENTO do projeto (símbolos, chamadas, imports) e uma MEMÓRIA em grafo (notas). ${graphStats}
|
|
30
|
-
- Para entender código: comece por graph_search / graph_context / graph_callers / graph_callees / graph_impact. Só leia arquivos (read_file) nas linhas que o grafo apontou. Use grep apenas para texto que não é símbolo.
|
|
31
|
-
- Antes de alterar uma função ou arquivo, rode graph_callers / graph_impact e considere o que pode quebrar.
|
|
32
|
-
- Após editar muitos arquivos, rode graph_index para atualizar o grafo.
|
|
33
|
-
- Ao tomar uma decisão não óbvia, descobrir um bug ou uma convenção, grave com remember (com refs para arquivos/símbolos). Consulte recall no início de tarefas relevantes.
|
|
34
|
-
- Edições e comandos shell pedem aprovação do usuário. Prefira edit_file a write_file. Verifique seu trabalho (typecheck/testes) via bash quando existir.
|
|
35
|
-
- Para investigações amplas ou sub-tarefas grandes, use delegate: o subagente trabalha em contexto isolado e devolve só o resultado. Vários delegate somente-leitura (explore/plan) no mesmo turno rodam em paralelo. Passe \`focus\` com os símbolos relevantes. Não delegue o que resolve com 1–2 chamadas de graph_*.
|
|
36
|
-
- SKILLS: procedimentos reutilizáveis aprendidos neste projeto. Se alguma se aplica à tarefa, chame skill_load ANTES de começar e siga-a. Se ela estiver errada ou incompleta, corrija com skill_save. Ao terminar tarefas complexas, o sistema pode propor novas skills; não crie skill para fatos de uma tarefa única.
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
return `Você é Brito, um agente de engenharia de software que trabalha em CLI. Responda em português do Brasil, de forma direta.
|
|
27
|
+
|
|
28
|
+
# Como trabalhar
|
|
29
|
+
- Você tem um GRAFO DE CONHECIMENTO do projeto (símbolos, chamadas, imports) e uma MEMÓRIA em grafo (notas). ${graphStats}
|
|
30
|
+
- Para entender código: comece por graph_search / graph_context / graph_callers / graph_callees / graph_impact. Só leia arquivos (read_file) nas linhas que o grafo apontou. Use grep apenas para texto que não é símbolo.
|
|
31
|
+
- Antes de alterar uma função ou arquivo, rode graph_callers / graph_impact e considere o que pode quebrar.
|
|
32
|
+
- Após editar muitos arquivos, rode graph_index para atualizar o grafo.
|
|
33
|
+
- Ao tomar uma decisão não óbvia, descobrir um bug ou uma convenção, grave com remember (com refs para arquivos/símbolos). Consulte recall no início de tarefas relevantes.
|
|
34
|
+
- Edições e comandos shell pedem aprovação do usuário. Prefira edit_file a write_file. Verifique seu trabalho (typecheck/testes) via bash quando existir.
|
|
35
|
+
- Para investigações amplas ou sub-tarefas grandes, use delegate: o subagente trabalha em contexto isolado e devolve só o resultado. Vários delegate somente-leitura (explore/plan) no mesmo turno rodam em paralelo. Passe \`focus\` com os símbolos relevantes. Não delegue o que resolve com 1–2 chamadas de graph_*.
|
|
36
|
+
- SKILLS: procedimentos reutilizáveis aprendidos neste projeto. Se alguma se aplica à tarefa, chame skill_load ANTES de começar e siga-a. Se ela estiver errada ou incompleta, corrija com skill_save. Ao terminar tarefas complexas, o sistema pode propor novas skills; não crie skill para fatos de uma tarefa única.
|
|
37
|
+
- Em tarefas com 3+ passos, mantenha a lista com todo_write: um item in_progress por vez, marcado completed assim que terminar. Leia um arquivo (read_file) antes de editá-lo; se ele mudou desde a leitura, leia de novo.
|
|
38
|
+
- Seja conciso. Não repita conteúdo de arquivos na resposta.
|
|
39
|
+
|
|
40
|
+
# Ambiente
|
|
41
|
+
Diretório do projeto: ${root}
|
|
41
42
|
Data: ${new Date().toISOString().slice(0, 10)}${shellNote()}${skills ? `\n\n# Skills disponíveis (use skill_load)\n${skills}` : ""}${projectDoc}`;
|
|
42
43
|
}
|
|
43
|
-
import { skillsIndex } from "../skills/store.js";
|
|
44
|
+
import { listSkills, skillsIndex } from "../skills/store.js";
|
|
45
|
+
/**
|
|
46
|
+
* Repete o system prompt anterior enquanto `key()` não muda. O cache de prompt da API é um prefixo: qualquer byte
|
|
47
|
+
* diferente no system (a contagem de símbolos após um sync, a ordem das skills por uso) invalidaria o cache da
|
|
48
|
+
* conversa inteira a cada passo. A chave lista só o que muda o que o modelo deve fazer.
|
|
49
|
+
*/
|
|
50
|
+
export function memoSystem(key, build) {
|
|
51
|
+
let last;
|
|
52
|
+
let value = "";
|
|
53
|
+
return () => {
|
|
54
|
+
const k = key();
|
|
55
|
+
if (k !== last) {
|
|
56
|
+
last = k;
|
|
57
|
+
value = build();
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Chave do `memoSystem` da sessão principal: modo plano, grafo vazio ou não (o aviso de "grafo vazio" muda a
|
|
64
|
+
* orientação) e o conjunto de skills com o status. Contagens e ordem por uso ficam de fora de propósito.
|
|
65
|
+
*/
|
|
66
|
+
export function systemKey(root, planMode, graphEmpty) {
|
|
67
|
+
const skills = listSkills(root).map((s) => `${s.name}:${s.scope}:${s.status}`).sort().join(",");
|
|
68
|
+
return `${planMode}|${graphEmpty}|${skills}`;
|
|
69
|
+
}
|
|
44
70
|
export function buildSubagentPrompt(root, def) {
|
|
45
|
-
return `${def.prompt}
|
|
46
|
-
|
|
47
|
-
# Regras de subagente
|
|
48
|
-
- Você foi chamado por outro agente e NÃO vê a conversa dele: tudo que precisa está na tarefa.
|
|
49
|
-
- Use o grafo antes de ler arquivos: graph_search, graph_context, graph_callers, graph_callees, graph_impact. Leia só as linhas que ele apontar.
|
|
50
|
-
- Sua última mensagem é a única coisa que o chamador recebe. Seja objetivo e use caminhos \`arquivo:linha\`; sem preâmbulo.
|
|
51
|
-
|
|
52
|
-
# Ambiente
|
|
53
|
-
Diretório do projeto: ${root}
|
|
71
|
+
return `${def.prompt}
|
|
72
|
+
|
|
73
|
+
# Regras de subagente
|
|
74
|
+
- Você foi chamado por outro agente e NÃO vê a conversa dele: tudo que precisa está na tarefa.
|
|
75
|
+
- Use o grafo antes de ler arquivos: graph_search, graph_context, graph_callers, graph_callees, graph_impact. Leia só as linhas que ele apontar.
|
|
76
|
+
- Sua última mensagem é a única coisa que o chamador recebe. Seja objetivo e use caminhos \`arquivo:linha\`; sem preâmbulo.
|
|
77
|
+
|
|
78
|
+
# Ambiente
|
|
79
|
+
Diretório do projeto: ${root}
|
|
54
80
|
Data: ${new Date().toISOString().slice(0, 10)}${skillsIndex(root) ? `\n\n# Skills disponíveis (use skill_load)\n${skillsIndex(root)}` : ""}`;
|
|
55
81
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const MAX_SESSIONS = 50;
|
|
4
|
+
const ID = /^[\w-]{1,80}$/;
|
|
5
|
+
export const sessionsDir = (root) => path.join(root, ".brito", "sessions");
|
|
6
|
+
const fileOf = (root, id) => {
|
|
7
|
+
if (!ID.test(id))
|
|
8
|
+
throw new Error(`id de sessão inválido: ${id}`);
|
|
9
|
+
return path.join(sessionsDir(root), `${id}.json`);
|
|
10
|
+
};
|
|
11
|
+
const stripImages = (msgs) => msgs.map((m) => typeof m.content === "string" ? m : {
|
|
12
|
+
...m,
|
|
13
|
+
content: m.content.map((b) => (b.type === "image" || b.type === "document" ? { type: "text", text: "[anexo omitido ao salvar a sessão]" } : b)),
|
|
14
|
+
});
|
|
15
|
+
export function titleOf(msgs, fallback = "") {
|
|
16
|
+
const first = msgs?.find((m) => m.role === "user");
|
|
17
|
+
const text = typeof first?.content === "string" ? first.content : first?.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(" ") ?? fallback;
|
|
18
|
+
return (text || fallback).replace(/\s+/g, " ").trim().slice(0, 80);
|
|
19
|
+
}
|
|
20
|
+
/** Grava (ou atualiza) a sessão. Nunca derruba o turno: falha de disco vira aviso de quem chamou. */
|
|
21
|
+
export function saveSession(root, s, now = new Date()) {
|
|
22
|
+
const dir = sessionsDir(root);
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
// Conversas são pessoais e podem citar dados sensíveis: nunca vão para o git por acidente.
|
|
25
|
+
try {
|
|
26
|
+
fs.writeFileSync(path.join(dir, ".gitignore"), "*\n", { flag: "wx" });
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (e.code !== "EEXIST")
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
const prev = loadSession(root, s.id);
|
|
33
|
+
const out = {
|
|
34
|
+
v: 1, ...s,
|
|
35
|
+
...(s.messages ? { messages: stripImages(s.messages) } : {}),
|
|
36
|
+
createdAt: s.createdAt ?? prev?.createdAt ?? now.toISOString(),
|
|
37
|
+
updatedAt: now.toISOString(),
|
|
38
|
+
};
|
|
39
|
+
const file = fileOf(root, s.id);
|
|
40
|
+
const tmp = `${file}.tmp`;
|
|
41
|
+
fs.writeFileSync(tmp, JSON.stringify(out));
|
|
42
|
+
fs.renameSync(tmp, file); // atômico: um Ctrl+C no meio não deixa JSON cortado
|
|
43
|
+
prune(root);
|
|
44
|
+
}
|
|
45
|
+
export function loadSession(root, id) {
|
|
46
|
+
try {
|
|
47
|
+
const s = JSON.parse(fs.readFileSync(fileOf(root, id), "utf8"));
|
|
48
|
+
return s?.v === 1 && typeof s.id === "string" ? s : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Mais recentes primeiro. */
|
|
55
|
+
export function listSessions(root) {
|
|
56
|
+
let names;
|
|
57
|
+
try {
|
|
58
|
+
names = fs.readdirSync(sessionsDir(root)).filter((n) => n.endsWith(".json"));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return names
|
|
64
|
+
.flatMap((n) => { const s = loadSession(root, n.slice(0, -5)); return s ? [s] : []; })
|
|
65
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
66
|
+
}
|
|
67
|
+
export const latestSession = (root, engine) => listSessions(root).find((s) => !engine || s.engine === engine) ?? null;
|
|
68
|
+
/** Mantém as `MAX_SESSIONS` mais recentes, pela data do arquivo (sem abrir históricos que podem ter megabytes). */
|
|
69
|
+
function prune(root) {
|
|
70
|
+
const dir = sessionsDir(root);
|
|
71
|
+
const files = fs.readdirSync(dir).filter((n) => n.endsWith(".json")).map((n) => {
|
|
72
|
+
try {
|
|
73
|
+
return { n, t: fs.statSync(path.join(dir, n)).mtimeMs };
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { n, t: 0 };
|
|
77
|
+
}
|
|
78
|
+
}).sort((a, b) => b.t - a.t);
|
|
79
|
+
for (const f of files.slice(MAX_SESSIONS)) {
|
|
80
|
+
try {
|
|
81
|
+
fs.unlinkSync(path.join(dir, f.n));
|
|
82
|
+
}
|
|
83
|
+
catch { /* já removida */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Histórico salvo pronto para reenviar: se o processo caiu no meio de um turno, o último `tool_use` pode ter ficado sem
|
|
88
|
+
* `tool_result`, e a API recusaria a conversa. Fecha cada um com erro, como no Ctrl+C.
|
|
89
|
+
*/
|
|
90
|
+
export function repairHistory(msgs) {
|
|
91
|
+
const last = msgs[msgs.length - 1];
|
|
92
|
+
if (last?.role !== "assistant" || typeof last.content === "string")
|
|
93
|
+
return msgs;
|
|
94
|
+
const pending = last.content.filter((b) => b.type === "tool_use");
|
|
95
|
+
if (!pending.length)
|
|
96
|
+
return msgs;
|
|
97
|
+
return [...msgs, { role: "user", content: pending.map((u) => ({ type: "tool_result", tool_use_id: u.id, content: "Sessão interrompida antes desta tool terminar.", is_error: true })) }];
|
|
98
|
+
}
|