brito-cli 0.7.5 → 0.9.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 +54 -4
- package/README.pt-BR.md +54 -4
- package/dist/agent/approval.js +19 -2
- package/dist/agent/claude-cli.js +10 -4
- package/dist/agent/loop.js +38 -9
- package/dist/agent/prompt.js +27 -1
- package/dist/agent/sessions.js +98 -0
- package/dist/cli.js +296 -26
- package/dist/desktop/panels.js +93 -0
- package/dist/desktop/protocol.js +5 -0
- package/dist/desktop/serve.js +191 -23
- package/dist/engine.js +28 -4
- package/dist/local/digest.js +202 -0
- package/dist/local/slm.js +218 -0
- package/dist/mcp/server.js +36 -3
- package/dist/model.js +21 -0
- package/dist/orchestrator.js +131 -0
- package/dist/permissions.js +4 -0
- 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 +49 -6
- package/dist/rules.js +102 -0
- package/dist/skills/builtin.js +89 -0
- package/dist/skills/describe.js +3 -2
- package/dist/skills/learn.js +2 -2
- package/dist/skills/store.js +64 -8
- package/dist/skills/trial.js +3 -3
- package/dist/tools/delegate.js +31 -22
- package/dist/tools/digest.js +29 -0
- package/dist/tools/fs.js +51 -8
- package/dist/tools/implement.js +50 -0
- package/dist/tools/registry.js +10 -1
- package/dist/tools/skills.js +10 -6
- package/dist/tools/spec.js +47 -0
- package/dist/tools/todo.js +22 -0
- package/dist/ui/ui.js +12 -0
- package/dist/util/todos.js +4 -0
- package/package.json +10 -6
- package/dist/setup.js +0 -110
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalHome } from "../skills/store.js";
|
|
5
|
+
/**
|
|
6
|
+
* SLM embutido (llama.cpp via `node-llama-cpp`, dependência OPCIONAL): roda dentro do processo, sem servidor.
|
|
7
|
+
* Hoje serve às tarefas auxiliares (reflexão que propõe skills). Só entra em ação depois de `brito slm pull`
|
|
8
|
+
* (o download é sempre uma decisão explícita) e pode ser desligado com `BRITO_SLM=off`.
|
|
9
|
+
*/
|
|
10
|
+
export const SLM_URI = "hf:unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M";
|
|
11
|
+
export const slmUri = () => process.env.BRITO_SLM_MODEL?.trim() || SLM_URI;
|
|
12
|
+
export const slmDir = () => path.join(globalHome(), "models");
|
|
13
|
+
export const slmDisabled = () => process.env.BRITO_SLM === "off";
|
|
14
|
+
/** O nome do pacote vai numa variável: o tsc e o bundler não tentam resolver uma dependência opcional. */
|
|
15
|
+
const LIB = "node-llama-cpp";
|
|
16
|
+
let libPromise = null;
|
|
17
|
+
export function loadLib() {
|
|
18
|
+
return (libPromise ??= import(LIB).catch(() => null));
|
|
19
|
+
}
|
|
20
|
+
/** Arquivo do modelo já baixado (null = ainda não baixou). Um caminho local no BRITO_SLM_MODEL conta direto. */
|
|
21
|
+
export async function modelFile() {
|
|
22
|
+
const uri = slmUri();
|
|
23
|
+
if (!uri.startsWith("hf:") && !/^https?:/.test(uri))
|
|
24
|
+
return fs.existsSync(uri) ? uri : null;
|
|
25
|
+
const lib = await loadLib();
|
|
26
|
+
if (!lib)
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
const p = await lib.resolveModelFile(uri, { directory: slmDir(), download: false, cli: false });
|
|
30
|
+
return p && fs.existsSync(p) ? p : null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function slmStatus() {
|
|
37
|
+
const lib = await loadLib();
|
|
38
|
+
const file = lib ? await modelFile() : null;
|
|
39
|
+
const disabled = slmDisabled();
|
|
40
|
+
return { lib: !!lib, file, uri: slmUri(), disabled, ready: !!lib && !!file && !disabled };
|
|
41
|
+
}
|
|
42
|
+
/** Baixa o modelo (retomável). `onProgress` recebe bytes baixados e total. */
|
|
43
|
+
export async function pullModel(onProgress, signal) {
|
|
44
|
+
const lib = await loadLib();
|
|
45
|
+
if (!lib)
|
|
46
|
+
throw new Error("node-llama-cpp não está instalado (é uma dependência opcional). Rode: npm install node-llama-cpp");
|
|
47
|
+
return lib.resolveModelFile(slmUri(), {
|
|
48
|
+
directory: slmDir(), download: "auto", cli: false, signal,
|
|
49
|
+
onProgress: (s) => onProgress?.(s.downloadedSize, s.totalSize),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Apaga o modelo baixado. Devolve o que foi removido. */
|
|
53
|
+
export async function removeModel() {
|
|
54
|
+
const f = await modelFile();
|
|
55
|
+
if (!f || !f.startsWith(slmDir()))
|
|
56
|
+
return null;
|
|
57
|
+
fs.rmSync(f, { force: true });
|
|
58
|
+
return f;
|
|
59
|
+
}
|
|
60
|
+
let loaded = null;
|
|
61
|
+
let idleTimer = null;
|
|
62
|
+
const IDLE_MS = 120_000;
|
|
63
|
+
async function ensureLoaded() {
|
|
64
|
+
const lib = await loadLib();
|
|
65
|
+
const file = await modelFile();
|
|
66
|
+
if (!lib || !file)
|
|
67
|
+
throw new Error("SLM indisponível: rode `brito slm pull`");
|
|
68
|
+
loaded ??= (async () => {
|
|
69
|
+
const llama = await lib.getLlama({ progressLogs: false, logLevel: "error" });
|
|
70
|
+
const model = await llama.loadModel({ modelPath: file });
|
|
71
|
+
return { llama, model };
|
|
72
|
+
})().catch((e) => { loaded = null; throw e; });
|
|
73
|
+
return loaded;
|
|
74
|
+
}
|
|
75
|
+
/** Libera a memória do modelo depois de um tempo parado (o processo não fica preso por causa disso). */
|
|
76
|
+
function scheduleUnload() {
|
|
77
|
+
if (idleTimer)
|
|
78
|
+
clearTimeout(idleTimer);
|
|
79
|
+
idleTimer = setTimeout(() => void unloadSlm(), IDLE_MS);
|
|
80
|
+
idleTimer.unref?.();
|
|
81
|
+
}
|
|
82
|
+
export async function unloadSlm() {
|
|
83
|
+
if (idleTimer) {
|
|
84
|
+
clearTimeout(idleTimer);
|
|
85
|
+
idleTimer = null;
|
|
86
|
+
}
|
|
87
|
+
const l = loaded;
|
|
88
|
+
loaded = null;
|
|
89
|
+
if (!l)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
const { model, llama } = await l;
|
|
93
|
+
await model.dispose();
|
|
94
|
+
await llama.dispose();
|
|
95
|
+
}
|
|
96
|
+
catch { /* já liberado */ }
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Uma geração isolada (sessão nova a cada chamada, sem histórico). Com `schema`, a saída é forçada a ser JSON
|
|
100
|
+
* válido por gramática: é o que torna modelos pequenos confiáveis em tarefas estruturadas.
|
|
101
|
+
*/
|
|
102
|
+
export async function slmGenerate(o) {
|
|
103
|
+
const lib = await loadLib();
|
|
104
|
+
const { model, llama } = await ensureLoaded();
|
|
105
|
+
const context = await model.createContext({ contextSize: { max: 8192 } });
|
|
106
|
+
try {
|
|
107
|
+
const session = new lib.LlamaChatSession({ contextSequence: context.getSequence(), systemPrompt: o.system });
|
|
108
|
+
const grammar = o.schema ? await llama.createGrammarForJsonSchema(o.schema) : undefined;
|
|
109
|
+
return await session.prompt(o.user, { grammar, maxTokens: o.maxTokens ?? 1500, temperature: o.temperature ?? 0.2, signal: o.signal });
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await context.dispose();
|
|
113
|
+
scheduleUnload();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ------------------------------------------------------------------ chat com tools (fallback do Claude)
|
|
117
|
+
/** Contexto do chat do SLM (BRITO_SLM_CTX; o padrão cabe numa GPU/RAM modesta). */
|
|
118
|
+
export const slmChatCtx = () => { const n = Number(process.env.BRITO_SLM_CTX); return n >= 2048 ? Math.floor(n) : 16_384; };
|
|
119
|
+
/** Um passo de chat: devolve o texto e as chamadas de tool pedidas (sem executá-las). */
|
|
120
|
+
export async function slmChat(o) {
|
|
121
|
+
const lib = await loadLib();
|
|
122
|
+
const { model } = await ensureLoaded();
|
|
123
|
+
const context = await model.createContext({ contextSize: { max: slmChatCtx() } });
|
|
124
|
+
try {
|
|
125
|
+
const chat = new lib.LlamaChat({ contextSequence: context.getSequence() });
|
|
126
|
+
const functions = o.functions && Object.keys(o.functions).length
|
|
127
|
+
? Object.fromEntries(Object.entries(o.functions).map(([name, f]) => [name, lib.defineChatSessionFunction({ description: f.description, params: f.params, handler: () => "" })]))
|
|
128
|
+
: undefined;
|
|
129
|
+
const res = await chat.generateResponse(o.history, {
|
|
130
|
+
functions, maxTokens: o.maxTokens ?? 2048, temperature: o.temperature ?? 0.3, signal: o.signal,
|
|
131
|
+
onTextChunk: (t) => { if (t)
|
|
132
|
+
o.onText?.(t); },
|
|
133
|
+
});
|
|
134
|
+
const calls = (res.functionCalls ?? []).map((c) => ({ name: String(c.functionName), input: c.params ?? {} }));
|
|
135
|
+
return { text: String(res.response ?? ""), calls, truncated: res.metadata?.stopReason === "maxTokens" };
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await context.dispose();
|
|
139
|
+
scheduleUnload();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Versão síncrona de `slmStatus().ready` (o `fallbackStatus` é síncrono): biblioteca resolvível e um .gguf baixado. */
|
|
143
|
+
export function slmReadySync() {
|
|
144
|
+
if (slmDisabled())
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
createRequire(import.meta.url).resolve(LIB);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const uri = slmUri();
|
|
153
|
+
if (!uri.startsWith("hf:") && !/^https?:/.test(uri))
|
|
154
|
+
return fs.existsSync(uri);
|
|
155
|
+
try {
|
|
156
|
+
return fs.readdirSync(slmDir()).some((f) => f.endsWith(".gguf"));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ------------------------------------------------------------------ reflexão
|
|
163
|
+
const SKILLS_SCHEMA = {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
skills: {
|
|
167
|
+
type: "array", maxItems: 2,
|
|
168
|
+
items: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
name: { type: "string" }, description: { type: "string" }, body: { type: "string" },
|
|
172
|
+
refs: { type: "array", items: { type: "string" } }, reason: { type: "string" },
|
|
173
|
+
},
|
|
174
|
+
required: ["name", "description", "body", "refs", "reason"],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ["skills"],
|
|
179
|
+
};
|
|
180
|
+
const JSON_NOTE = `
|
|
181
|
+
|
|
182
|
+
# Formato da resposta
|
|
183
|
+
Responda SOMENTE com JSON no formato {"skills":[{"name","description","body","refs","reason"}]}. Cada item equivale a uma chamada de skill_save. Se nada vale virar skill, responda {"skills":[]}. O name é kebab-case; o body é markdown com ## Quando usar, ## Passos, ## Armadilhas e ## Verificação.
|
|
184
|
+
Regras do conteúdo: (1) o name descreve o TIPO de tarefa (ex.: "ficha-de-leitura"), nunca um autor, livro, arquivo ou pessoa específicos; troque esses por parâmetros ("o autor", "o capítulo"). (2) Use só caminhos RELATIVOS à raiz do projeto, nunca caminhos absolutos nem nomes de usuário (escreva "a partir da raiz do projeto"). (3) Se a skill já existe na lista de skills existentes, reutilize o MESMO name em vez de criar outra. (4) Cada passo é uma ação concreta; a Verificação diz como saber que terminou.`;
|
|
185
|
+
/** Converte a saída JSON do SLM nos blocos `tool_use` que a reflexão já sabe processar. Tolera lixo: devolve [] se inválido. */
|
|
186
|
+
export function skillsToBlocks(json) {
|
|
187
|
+
let data;
|
|
188
|
+
try {
|
|
189
|
+
data = JSON.parse(json);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const list = Array.isArray(data?.skills) ? data.skills : [];
|
|
195
|
+
return list.flatMap((s, i) => s && typeof s.name === "string" && typeof s.description === "string" && typeof s.body === "string"
|
|
196
|
+
? [{ type: "tool_use", id: `slm_${i}`, name: "skill_save", input: { name: s.name, description: s.description, body: s.body, refs: Array.isArray(s.refs) ? s.refs.filter((r) => typeof r === "string") : [], reason: typeof s.reason === "string" ? s.reason : "" } }]
|
|
197
|
+
: []);
|
|
198
|
+
}
|
|
199
|
+
/** `Complete` da reflexão rodando no SLM local. */
|
|
200
|
+
export const slmComplete = async ({ system, user, maxTokens }) => {
|
|
201
|
+
const text = await slmGenerate({ system: system + JSON_NOTE, user, schema: SKILLS_SCHEMA, maxTokens: Math.min(maxTokens, 2500) });
|
|
202
|
+
return skillsToBlocks(text);
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Escolhe o `Complete` da reflexão: o SLM local quando está baixado e habilitado; senão o `fallback` (o modelo de
|
|
206
|
+
* sempre). Se o SLM falhar no meio (memória, binário), cai no `fallback` em vez de perder a reflexão.
|
|
207
|
+
*/
|
|
208
|
+
export function withSlm(fallback) {
|
|
209
|
+
return async (req) => {
|
|
210
|
+
if (!slmDisabled() && (await slmStatus()).ready) {
|
|
211
|
+
try {
|
|
212
|
+
return await slmComplete(req);
|
|
213
|
+
}
|
|
214
|
+
catch { /* cai no modelo de sempre */ }
|
|
215
|
+
}
|
|
216
|
+
return fallback(req);
|
|
217
|
+
};
|
|
218
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -5,6 +5,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import "../tools/graph.js";
|
|
7
7
|
import "../tools/skills.js";
|
|
8
|
+
// O implementador do modo orquestrador roda aqui e edita/roda comandos; fs e bash só existem para ele (não são expostos).
|
|
9
|
+
import "../tools/fs.js";
|
|
10
|
+
import "../tools/bash.js";
|
|
11
|
+
import { implementIo } from "../tools/implement.js";
|
|
12
|
+
import "../tools/spec.js";
|
|
13
|
+
import { ORCH_ONLY, orch } from "../orchestrator.js";
|
|
14
|
+
import "../tools/digest.js";
|
|
8
15
|
import { allTools, execTool } from "../tools/registry.js";
|
|
9
16
|
import { GraphStore } from "../graph/store.js";
|
|
10
17
|
import { INDEXED_EXTENSIONS, indexProject } from "../graph/indexer.js";
|
|
@@ -27,13 +34,16 @@ export const MCP_INSTRUCTIONS = [
|
|
|
27
34
|
* tools de consulta, que são baratas e de uso frequente. As de escrita e a de aprovação (interna) continuam adiadas.
|
|
28
35
|
*/
|
|
29
36
|
export const ALWAYS_LOAD_META = "anthropic/alwaysLoad";
|
|
30
|
-
export const alwaysLoaded = (tool) => /^(graph_|recall$|remember$|skill_load$)/.test(tool);
|
|
37
|
+
export const alwaysLoaded = (tool) => /^(graph_|recall$|remember$|skill_load$|digest$|implement$)/.test(tool);
|
|
31
38
|
/** Prefixo com que o claude CLI enxerga as tools deste servidor. */
|
|
32
39
|
export const MCP_PREFIX = `mcp__${MCP_NAME}__`;
|
|
33
40
|
/** Tool usada como --permission-prompt-tool: encaminha o pedido ao REPL do Brito. */
|
|
34
41
|
export const APPROVE_TOOL = "approve";
|
|
35
42
|
// fs/bash/delegate o próprio claude já tem (Read/Edit/Bash/Task); aqui vai só o que é do Brito.
|
|
36
|
-
|
|
43
|
+
/** Tools que o claude enxerga; `implement` e `spec_save` só com o modo orquestrador ligado (BRITO_ORCH=1). */
|
|
44
|
+
export const exposed = () => allTools().filter((t) => /^(graph_|skill_)/.test(t.name) || t.name === "remember" || t.name === "recall" || t.name === "digest" || (ORCH_ONLY.includes(t.name) && orch.on));
|
|
45
|
+
/** Nome do pedido de aprovação de uma ação do implementador (o terminal mostra só a pergunta). */
|
|
46
|
+
export const IMPL_ASK = "BritoImplementer";
|
|
37
47
|
/** Pergunta ao REPL (socket unix) se o claude pode usar a tool; sem socket, nega. */
|
|
38
48
|
function askRepl(toolName, input) {
|
|
39
49
|
const sock = process.env.BRITO_APPROVE_SOCK;
|
|
@@ -77,7 +87,28 @@ export async function serveMcp(root) {
|
|
|
77
87
|
...(alwaysLoaded(t.name) ? { _meta: { [ALWAYS_LOAD_META]: true } } : {}),
|
|
78
88
|
}))],
|
|
79
89
|
}));
|
|
80
|
-
|
|
90
|
+
/**
|
|
91
|
+
* O claude aprovou a chamada de `implement`, não cada edição dentro dela: as do implementador pedem aprovação ao
|
|
92
|
+
* terminal pelo mesmo socket, conforme o modo. O progresso vai como notificação MCP (o stdout daqui é o protocolo).
|
|
93
|
+
*/
|
|
94
|
+
const runImplement = async (args, extra, token) => {
|
|
95
|
+
let step = 0;
|
|
96
|
+
implementIo.log = (msg) => {
|
|
97
|
+
if (token === undefined) {
|
|
98
|
+
process.stderr.write(msg + "\n");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress: ++step, message: msg } }).catch(() => { });
|
|
102
|
+
};
|
|
103
|
+
const warn = (msg) => implementIo.log(msg);
|
|
104
|
+
implementIo.sink = { turnStart() { }, turnEnd() { }, text() { }, textEnd() { }, thinking() { }, toolStart() { }, toolEnd() { }, info: warn, warn, error: warn };
|
|
105
|
+
// Quem chama este servidor é sempre o claude CLI: o principal é um Claude, não o modelo salvo no config.
|
|
106
|
+
const implCtx = { ...ctx, ask: async (q) => (await askRepl(IMPL_ASK, { question: q })).allow, model: () => "claude" };
|
|
107
|
+
const r = await execTool("implement", args, implCtx);
|
|
108
|
+
ctx.dirty = true; // o implementador editou arquivos: a próxima consulta ao grafo reindexa
|
|
109
|
+
return r.isError ? `[ERRO] ${r.text}` : r.text;
|
|
110
|
+
};
|
|
111
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
81
112
|
if (req.params.name === APPROVE_TOOL) {
|
|
82
113
|
const a = (req.params.arguments ?? {});
|
|
83
114
|
const r = await askRepl(String(a.tool_name ?? "?"), a.input ?? {});
|
|
@@ -87,6 +118,8 @@ export async function serveMcp(root) {
|
|
|
87
118
|
if (!exposed().some((t) => t.name === req.params.name)) {
|
|
88
119
|
return { content: [{ type: "text", text: `Tool desconhecida: ${req.params.name}` }], isError: true };
|
|
89
120
|
}
|
|
121
|
+
if (req.params.name === "implement")
|
|
122
|
+
return { content: [{ type: "text", text: await runImplement(req.params.arguments ?? {}, extra, req.params._meta?.progressToken) }] };
|
|
90
123
|
const r = await execTool(req.params.name, req.params.arguments ?? {}, ctx);
|
|
91
124
|
return { content: [{ type: "text", text: r.text }], isError: r.isError };
|
|
92
125
|
});
|
package/dist/model.js
CHANGED
|
@@ -30,6 +30,27 @@ function saveConfig(patch) {
|
|
|
30
30
|
writeFileSync(configPath(), JSON.stringify({ ...cfg, ...patch }, null, 2) + "\n");
|
|
31
31
|
}
|
|
32
32
|
export const saveModel = (model) => saveConfig({ model });
|
|
33
|
+
/** Perfil que implementa as tasks no modo orquestrador (`implModel` no config). */
|
|
34
|
+
export function loadSavedImplModel() {
|
|
35
|
+
try {
|
|
36
|
+
const m = JSON.parse(readFileSync(configPath(), "utf8"))?.implModel;
|
|
37
|
+
return typeof m === "string" && m.trim() ? m.trim() : undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const saveImplModel = (implModel) => saveConfig({ implModel });
|
|
44
|
+
/** Modo orquestrador ligado ao abrir o Brito (`orchestrator: true` no config, gravado por `/orquestrar on --global`). */
|
|
45
|
+
export function loadSavedOrch() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(configPath(), "utf8"))?.orchestrator === true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const saveOrch = (orchestrator) => saveConfig({ orchestrator });
|
|
33
54
|
/**
|
|
34
55
|
* Escada de esforço do Hermes (EFFORT_LADDER, do mais fraco ao mais forte). `none` desliga o raciocínio;
|
|
35
56
|
* `minimal` e `ultra` são internos: o claude CLI não os aceita, então `clampEffort` os mapeia.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { allTools } from "./tools/registry.js";
|
|
2
|
+
import { loadProfiles, loadSavedImplModel, loadSavedOrch } from "./model.js";
|
|
3
|
+
import { missingKeyMessage } from "./provider/openai-compat.js";
|
|
4
|
+
import { resolveModel } from "./provider/resolve.js";
|
|
5
|
+
// Modo orquestrador: o modelo principal (Claude) planeja e revisa; um perfil de endpoint (o qwen) implementa cada task
|
|
6
|
+
// pela tool `implement`. Estado global da sessão, como o `perms`.
|
|
7
|
+
/** Prioridade: BRITO_IMPL_MODEL > config global (`implModel`) > qwen. */
|
|
8
|
+
export const initialImplModel = () => process.env.BRITO_IMPL_MODEL?.trim() || loadSavedImplModel() || "qwen";
|
|
9
|
+
/** No servidor MCP (outro processo) o modo vem ligado pelo env; no REPL e no app começa desligado. */
|
|
10
|
+
export const orch = { on: process.env.BRITO_ORCH === "1", model: initialImplModel() };
|
|
11
|
+
/** Edição de arquivo fica só com o implementador; o principal lê, delega e verifica (bash continua para os testes). */
|
|
12
|
+
export const ORCH_BLOCKED = ["write_file", "edit_file"];
|
|
13
|
+
/** Tools que só existem com o modo ligado: a delegação e a gravação da spec/tasks em spec/. */
|
|
14
|
+
export const ORCH_ONLY = ["implement", "spec_save"];
|
|
15
|
+
/**
|
|
16
|
+
* Tools do agente principal. Desligado: todas menos as do modo (`implement`, `spec_save`). Ligado: sem as de edição. Função: o modo muda no meio
|
|
17
|
+
* da sessão e o loop relê a lista a cada passo.
|
|
18
|
+
*/
|
|
19
|
+
export function mainTools() {
|
|
20
|
+
const hidden = orch.on ? ORCH_BLOCKED : ORCH_ONLY;
|
|
21
|
+
return new Set(allTools().map((t) => t.name).filter((n) => !hidden.includes(n)));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* O implementador está pronto? Só um perfil de endpoint válido, com a chave no ambiente, e diferente do principal
|
|
25
|
+
* (o qwen orquestrando o qwen não ganha nada). Relê o config a cada chamada, como os perfis.
|
|
26
|
+
*/
|
|
27
|
+
export function implStatus(main, impl, env) {
|
|
28
|
+
const name = impl.trim().toLowerCase();
|
|
29
|
+
if (main.trim().toLowerCase() === name)
|
|
30
|
+
return { ready: false, reason: `o modelo principal já é o ${impl}` };
|
|
31
|
+
const { profiles, errors, rejected } = loadProfiles();
|
|
32
|
+
if (rejected.includes(name)) {
|
|
33
|
+
const why = errors.find((e) => e.toLowerCase().includes(`"${name}"`)) ?? "";
|
|
34
|
+
return { ready: false, reason: `o perfil "${name}" em ~/.brito/config.json está inválido (${why})` };
|
|
35
|
+
}
|
|
36
|
+
const p = profiles[name];
|
|
37
|
+
if (!p || resolveModel(name).kind !== "openai")
|
|
38
|
+
return { ready: false, reason: `sem perfil "${name}" em providers no ~/.brito/config.json` };
|
|
39
|
+
if (!env[p.apiKeyEnv]?.trim())
|
|
40
|
+
return { ready: false, reason: missingKeyMessage(p) };
|
|
41
|
+
return { ready: true, label: p.label };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Desliga o modo quando o implementador deixa de estar pronto para o principal `main` (fallback ou `/model` para o
|
|
45
|
+
* próprio qwen, chave removida…). Nunca em silêncio: o motivo vai para `warn`.
|
|
46
|
+
*/
|
|
47
|
+
export function orchGuard(main, warn) {
|
|
48
|
+
if (!orch.on)
|
|
49
|
+
return;
|
|
50
|
+
const s = implStatus(main, orch.model, process.env);
|
|
51
|
+
if (s.ready)
|
|
52
|
+
return;
|
|
53
|
+
orch.on = false;
|
|
54
|
+
warn(`modo orquestrador desligado: ${s.reason}`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ao abrir o REPL ou o app: liga o modo se ele foi salvo com `/orquestrar on --global`. Se o implementador não estiver
|
|
58
|
+
* pronto para o principal `main`, fica desligado e o motivo vai para `warn`. O servidor MCP não usa: lá o modo vem do env.
|
|
59
|
+
*/
|
|
60
|
+
export function restoreOrch(main, warn, info) {
|
|
61
|
+
if (orch.on || !loadSavedOrch())
|
|
62
|
+
return;
|
|
63
|
+
const s = implStatus(main, orch.model, process.env);
|
|
64
|
+
if (!s.ready) {
|
|
65
|
+
warn(`modo orquestrador salvo como ligado, mas não ligou: ${s.reason}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
orch.on = true;
|
|
69
|
+
info(`modo orquestrador ligado (salvo): ${main} planeja e revisa, ${s.label} implementa`);
|
|
70
|
+
}
|
|
71
|
+
/** O servidor MCP roda em outro processo (subido pelo claude CLI a cada turno): o modo e o perfil chegam pelo env. */
|
|
72
|
+
export const ORCH_ENV = "BRITO_ORCH";
|
|
73
|
+
/** Env do servidor MCP: liga a tool `implement` lá e diz qual perfil implementa. */
|
|
74
|
+
export const orchEnv = () => (orch.on ? { [ORCH_ENV]: "1", BRITO_IMPL_MODEL: orch.model } : {});
|
|
75
|
+
/** Uma task do implementador leva minutos: o timeout padrão do claude CLI para tools MCP cortaria no meio (30 min). */
|
|
76
|
+
export const ORCH_MCP_TIMEOUT = "1800000";
|
|
77
|
+
/** Tools nativas do claude CLI que editam arquivos: bloqueadas com o modo ligado. */
|
|
78
|
+
export const CLAUDE_EDIT_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
79
|
+
/** Acrescentado ao system prompt do agente principal enquanto o modo está ligado. */
|
|
80
|
+
export const ORCH_NOTE = `
|
|
81
|
+
|
|
82
|
+
# MODO ORQUESTRADOR ATIVO
|
|
83
|
+
Você planeja e revisa; quem implementa é outro modelo, pela tool implement. Você não tem write_file nem edit_file: sua única escrita é spec_save, que grava em spec/<feature>/.
|
|
84
|
+
- Divida o pedido em tasks pequenas e independentes e mande UMA por chamada de implement, em ordem.
|
|
85
|
+
- Antes da primeira chamada de implement, grave com spec_save (feature = slug curto do pedido):
|
|
86
|
+
- spec.md: objetivo, contexto, requisitos, contratos que não podem mudar, fora de escopo e riscos;
|
|
87
|
+
- tasks.md: lista numerada de tasks como checklist (\`- [ ] 1. título\`), cada uma com objetivo, arquivos, critério de pronto e comando de verificação.
|
|
88
|
+
Depois mostre ao usuário o caminho dos dois arquivos e um resumo da lista, e encerre a resposta pedindo aprovação. Só delegue depois que ele aprovar, seguindo o tasks.md aprovado.
|
|
89
|
+
- Se o usuário pedir mudanças, ou se for preciso mudar a lista no meio do caminho (task nova, ordem diferente), atualize spec.md/tasks.md com spec_save e peça aprovação de novo antes de seguir. Não precisa pedir de novo se o usuário acabou de aprovar um plano com essas mesmas tasks (modo plano).
|
|
90
|
+
- Ao mandar uma task ao implement, cite o caminho da spec (spec/<feature>/spec.md) para ele consultar, mas mantenha a task autocontida.
|
|
91
|
+
- Quando uma task passar na sua revisão, marque-a como feita no tasks.md (\`- [x]\`) com spec_save; se ficar pendente, anote o motivo embaixo dela.
|
|
92
|
+
- O implementador não vê esta conversa: cada task precisa trazer objetivo, arquivos e símbolos envolvidos, contratos que não podem mudar, critério de pronto e o comando de verificação (\`verify\`). Passe \`focus\` com os símbolos centrais.
|
|
93
|
+
- O resultado traz o relato do implementador, o git status/diff e a saída da verificação, rodada pela própria tool. Julgue pelo diff e pela verificação, não pelo relato: leia os trechos alterados quando precisar.
|
|
94
|
+
- Se a task ficou errada ou incompleta, chame implement de novo com o que está errado e o que fazer. No máximo 2 reenvios por task; depois disso, explique ao usuário o que falta.
|
|
95
|
+
- Ao final, resuma o que foi feito, o que a verificação provou e o que ficou pendente, e aponte o tasks.md atualizado.`;
|
|
96
|
+
/** Complemento do ORCH_NOTE no claude CLI, onde os nomes das tools são outros. */
|
|
97
|
+
export const ORCH_NOTE_CLAUDE = `
|
|
98
|
+
- Nesta sessão as tools chegam como mcp__brito__implement e mcp__brito__spec_save; Edit, Write, MultiEdit e NotebookEdit estão bloqueadas. Não use Bash para escrever arquivos: isso também é trabalho do implementador.`;
|
|
99
|
+
/** Prompt do subagente que implementa: escopo fechado, verificação obrigatória e uma resposta que dá para conferir. */
|
|
100
|
+
const IMPLEMENTER_PROMPT = `Você implementa UMA task, descrita no pedido. Você não vê a conversa do orquestrador: tudo que precisa está no pedido.
|
|
101
|
+
|
|
102
|
+
# Escopo
|
|
103
|
+
- Faça só o que a task pede. Não refatore, não renomeie e não "melhore" código fora dela.
|
|
104
|
+
- Não invente arquivo, classe, API ou requisito. Se algo do pedido não existir no projeto, pare e diga isso em PENDENTE.
|
|
105
|
+
- Não mude contratos existentes (assinaturas, rotas, formato de resposta) sem que o pedido mande.
|
|
106
|
+
- Não apague nem enfraqueça testes para a verificação passar.
|
|
107
|
+
|
|
108
|
+
# Como trabalhar
|
|
109
|
+
- Comece pelo grafo (graph_context / graph_callers) dos símbolos citados; leia só as linhas apontadas.
|
|
110
|
+
- Antes de criar um arquivo novo, abra um arquivo existente do mesmo tipo e siga a estrutura dele.
|
|
111
|
+
- Comportamento novo ganha teste, no estilo dos testes que já existem.
|
|
112
|
+
|
|
113
|
+
# Verificação
|
|
114
|
+
- Rode o comando de verificação do pedido (ou o de testes do projeto). Pronto é o que ele prova.
|
|
115
|
+
- Se falhar por causa do código: leia só a parte relevante do erro, corrija e rode de novo. No máximo 3 tentativas.
|
|
116
|
+
- Se falhar por ambiente (serviço fora do ar, porta ocupada), não tente consertar: registre e siga.
|
|
117
|
+
|
|
118
|
+
# Resposta final
|
|
119
|
+
Responda só com:
|
|
120
|
+
FEITO: arquivos criados ou alterados, um por linha
|
|
121
|
+
VERIFICAÇÃO: comando rodado e resultado (passou / falhou / não rodou e por quê)
|
|
122
|
+
PENDENTE: o que faltou, ou "nada"`;
|
|
123
|
+
/** Fora do registro de agentes: não aparece no `delegate` nem vira subagente do claude CLI; só a tool `implement` o usa. */
|
|
124
|
+
export const IMPLEMENTER = {
|
|
125
|
+
name: "implementer",
|
|
126
|
+
description: "Implementa uma task bem especificada (edita arquivos e roda a verificação).",
|
|
127
|
+
tools: "all",
|
|
128
|
+
get model() { return orch.model; },
|
|
129
|
+
prompt: IMPLEMENTER_PROMPT,
|
|
130
|
+
source: "builtin",
|
|
131
|
+
};
|
package/dist/permissions.js
CHANGED
|
@@ -34,6 +34,10 @@ export function describeClaudeTool(name, input) {
|
|
|
34
34
|
case "MultiEdit": return `editar ${i.file_path} (${(i.edits ?? []).length} trocas)`;
|
|
35
35
|
case "NotebookEdit": return `editar notebook ${i.notebook_path}`;
|
|
36
36
|
case "ExitPlanMode": return `plano proposto — aprovar e sair do modo plano?\n${String(i.plan ?? "")}`;
|
|
37
|
+
// Ação do implementador do modo orquestrador (servidor MCP): a pergunta já vem montada pelo Brito.
|
|
38
|
+
case "BritoImplementer": return `implementador · ${String(i.question ?? "")}`;
|
|
39
|
+
case "mcp__brito__spec_save": return `gravar spec/${String(i.feature ?? "?")}/${String(i.file ?? "?")} (${String(i.content ?? "").length} chars)`;
|
|
40
|
+
case "mcp__brito__implement": return `delegar ao implementador: ${one(i.task, 300)}${i.verify ? `\n verificação: ${one(i.verify)}` : ""}`;
|
|
37
41
|
default: return `${name}: ${one(JSON.stringify(i), 200)}`;
|
|
38
42
|
}
|
|
39
43
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { clampEffort, isEffort } from "../model.js";
|
|
2
|
+
/** Modelos com raciocínio adaptativo e `output_config.effort` (4.6 em diante). Os mais antigos seguem sem raciocínio. */
|
|
3
|
+
const ADAPTIVE = /claude-(fable|mythos)-|claude-(opus|sonnet|haiku)-5|claude-opus-4-[678]|claude-sonnet-4-6/;
|
|
4
|
+
/** Não aceitam desligar o raciocínio: o mínimo é esforço `low`. */
|
|
5
|
+
const ALWAYS_THINKS = /claude-(fable|mythos)-|claude-opus-5-5/;
|
|
6
|
+
/** Desligar é `between_tools` (`disabled` dá 400). */
|
|
7
|
+
const BETWEEN_TOOLS = /claude-sonnet-5-5/;
|
|
8
|
+
/** Os 4.6 não têm `xhigh`. */
|
|
9
|
+
const NO_XHIGH = /claude-(opus|sonnet)-4-6/;
|
|
10
|
+
/**
|
|
11
|
+
* Traduz o esforço do Brito (`/reasoning`) nos parâmetros da API da Anthropic para o modelo dado.
|
|
12
|
+
* `thinking: false` (ou esforço `none`) desliga o raciocínio onde o modelo permite; vazio = padrão do modelo.
|
|
13
|
+
*/
|
|
14
|
+
export function thinkingParams(wireModel, effort, thinking = true) {
|
|
15
|
+
if (!ADAPTIVE.test(wireModel))
|
|
16
|
+
return {};
|
|
17
|
+
const off = !thinking || effort === "none";
|
|
18
|
+
if (off) {
|
|
19
|
+
if (ALWAYS_THINKS.test(wireModel))
|
|
20
|
+
return { thinking: { type: "adaptive" }, output_config: { effort: "low" } };
|
|
21
|
+
if (BETWEEN_TOOLS.test(wireModel))
|
|
22
|
+
return { thinking: { type: "between_tools" } };
|
|
23
|
+
return { thinking: { type: "disabled" } };
|
|
24
|
+
}
|
|
25
|
+
if (!isEffort(effort))
|
|
26
|
+
return { thinking: { type: "adaptive" } };
|
|
27
|
+
const supported = NO_XHIGH.test(wireModel) ? ["low", "medium", "high", "max"] : ["low", "medium", "high", "xhigh", "max"];
|
|
28
|
+
return { thinking: { type: "adaptive" }, output_config: { effort: clampEffort(effort, supported) } };
|
|
29
|
+
}
|
|
30
|
+
/** Raciocinar e responder dividem o `max_tokens`: com raciocínio ligado, um teto pequeno cortaria a resposta. */
|
|
31
|
+
export const THINKING_MIN_TOKENS = 16_000;
|
|
32
|
+
export const maxTokensFor = (p, requested) => p.thinking?.type === "adaptive" ? Math.max(requested, THINKING_MIN_TOKENS) : requested;
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import { resolveModel } from "./resolve.js";
|
|
3
|
+
import { maxTokensFor, thinkingParams } from "./anthropic-params.js";
|
|
3
4
|
/** API da Anthropic, exatamente como o Brito sempre chamou (system em cache, streaming quando há quem leia o texto). */
|
|
4
5
|
export class AnthropicProvider {
|
|
5
6
|
kind = "anthropic";
|
|
6
7
|
client = new Anthropic();
|
|
7
8
|
async send(req, hooks = {}) {
|
|
9
|
+
const wireModel = resolveModel(req.model).wireModel; // aliases (sonnet, opus…) viram o id real
|
|
10
|
+
const thinking = thinkingParams(wireModel, req.effort, req.thinking ?? true);
|
|
8
11
|
const params = {
|
|
9
|
-
model:
|
|
10
|
-
max_tokens: req.maxTokens,
|
|
12
|
+
model: wireModel,
|
|
13
|
+
max_tokens: maxTokensFor(thinking, req.maxTokens),
|
|
14
|
+
// Dois pontos de cache: o system (estável na sessão) e, pelo `cache_control` do topo, o fim da conversa.
|
|
15
|
+
// Sem o segundo, cada passo do loop pagaria o histórico inteiro de novo.
|
|
16
|
+
cache_control: { type: "ephemeral" },
|
|
11
17
|
system: [{ type: "text", text: req.system, cache_control: { type: "ephemeral" } }],
|
|
12
18
|
...(req.tools?.length ? { tools: req.tools } : {}),
|
|
19
|
+
...thinking,
|
|
13
20
|
messages: req.messages,
|
|
14
21
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
// Sempre em streaming: com `max_tokens` alto, o SDK recusa (ou deixa estourar o timeout de) uma chamada simples.
|
|
23
|
+
const stream = this.client.messages.stream(params, { signal: hooks.signal });
|
|
24
|
+
if (hooks.onText)
|
|
18
25
|
stream.on("text", (t) => hooks.onText(t));
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
else
|
|
22
|
-
msg = await this.client.messages.create(params, { signal: hooks.signal });
|
|
26
|
+
const msg = await stream.finalMessage();
|
|
23
27
|
const u = msg.usage;
|
|
24
28
|
const content = msg.content.flatMap((b) => b.type === "text" ? [{ type: "text", text: b.text }] : b.type === "tool_use" ? [{ type: "tool_use", id: b.id, name: b.name, input: b.input }] : []);
|
|
25
29
|
const stopReason = msg.stop_reason === "end_turn" || msg.stop_reason === "tool_use" || msg.stop_reason === "max_tokens" ? msg.stop_reason : "other";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import { FALLBACK_CHAIN } from "../engine.js";
|
|
3
|
+
import { SLM_MODEL, resolveModel } from "./resolve.js";
|
|
3
4
|
export const triggersFallback = (k) => k !== "other";
|
|
4
5
|
/**
|
|
5
6
|
* Classifica um erro da API da Anthropic (já depois dos retries do SDK). Na dúvida, `other`: melhor não trocar do que
|
|
@@ -100,15 +101,57 @@ export function chooseModel(s, model) {
|
|
|
100
101
|
s.active = false;
|
|
101
102
|
s.reason = undefined;
|
|
102
103
|
s.at = undefined;
|
|
104
|
+
s.until = undefined;
|
|
105
|
+
s.using = undefined;
|
|
103
106
|
}
|
|
104
107
|
/** A troca automática aconteceu. */
|
|
105
|
-
export function activateFallback(s, reason, at = Date.now()) {
|
|
108
|
+
export function activateFallback(s, reason, at = Date.now(), until, using) {
|
|
106
109
|
s.active = true;
|
|
107
110
|
s.reason = reason;
|
|
108
111
|
s.at = at;
|
|
112
|
+
if (until !== undefined)
|
|
113
|
+
s.until = until;
|
|
114
|
+
if (using !== undefined)
|
|
115
|
+
s.using = using;
|
|
116
|
+
}
|
|
117
|
+
/** Minutos de fallback por tipo de falha (BRITO_FALLBACK_MINUTES sobrepõe): cota/auth demoram mais a voltar que sobrecarga. */
|
|
118
|
+
const WINDOW_MIN = { quota: 60, auth: 60, rate_limit: 10, overloaded: 10, network: 10, other: 10 };
|
|
119
|
+
/**
|
|
120
|
+
* Até quando o fallback vale. Usa o horário de reset que o erro informa ("resets 3pm", "resets at 15:30") quando existe
|
|
121
|
+
* (no futuro e em até 12h); senão a janela por tipo de falha.
|
|
122
|
+
*/
|
|
123
|
+
export function fallbackUntil(kind, why, env = process.env, now = Date.now()) {
|
|
124
|
+
const m = /resets?(?: at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i.exec(why);
|
|
125
|
+
if (m) {
|
|
126
|
+
let h = Number(m[1]);
|
|
127
|
+
const min = Number(m[2] ?? 0);
|
|
128
|
+
const ap = m[3]?.toLowerCase();
|
|
129
|
+
if (ap === "pm" && h < 12)
|
|
130
|
+
h += 12;
|
|
131
|
+
if (ap === "am" && h === 12)
|
|
132
|
+
h = 0;
|
|
133
|
+
if (h <= 23 && min <= 59) {
|
|
134
|
+
const d = new Date(now);
|
|
135
|
+
d.setHours(h, min, 0, 0);
|
|
136
|
+
if (d.getTime() <= now)
|
|
137
|
+
d.setDate(d.getDate() + 1);
|
|
138
|
+
if (d.getTime() - now <= 12 * 3_600_000)
|
|
139
|
+
return d.getTime() + 60_000;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const custom = Number(env.BRITO_FALLBACK_MINUTES);
|
|
143
|
+
return now + (custom > 0 ? custom : WINDOW_MIN[kind]) * 60_000;
|
|
144
|
+
}
|
|
145
|
+
/** A janela do fallback acabou: o próximo turno deve tentar o Claude de novo. */
|
|
146
|
+
export const fallbackExpired = (s, now = Date.now()) => s.active && s.until !== undefined && now >= s.until;
|
|
147
|
+
/** Volta ao modelo principal (sem apagar o motivo da última troca: o aviso o usa). */
|
|
148
|
+
export function returnToPrimary(s) {
|
|
149
|
+
s.active = false;
|
|
150
|
+
s.until = undefined;
|
|
151
|
+
s.using = undefined;
|
|
109
152
|
}
|
|
110
153
|
/** Aviso impresso na troca (modo API e claude CLI): a troca nunca é silenciosa. */
|
|
111
|
-
export const fallbackWarning = (kind, why, label) => `⚠ Claude falhou (${kind}: ${why}) — seguindo com ${label}. /fallback para detalhes, /model <claude> para voltar.`;
|
|
154
|
+
export const fallbackWarning = (kind, why, label, until) => `⚠ Claude falhou (${kind}: ${why}) — seguindo com ${label}${until ? `; tento o Claude de novo às ${hhmm(until)}` : ""}. /fallback para detalhes, /model <claude> para voltar.`;
|
|
112
155
|
/**
|
|
113
156
|
* O turno do claude CLI que falhou deve ser refeito no qwen? Só com falha de conta/capacidade, fallback pronto e
|
|
114
157
|
* ainda não usado na sessão. Interrupção não deixa `failure` (a sessão filtra), mas o `aborted` fica de garantia.
|
|
@@ -127,6 +170,19 @@ export const retryPrompt = (text, resumed = false) => `${RETRY_NOTE}${resumed ?
|
|
|
127
170
|
*/
|
|
128
171
|
export const onceRestore = (restore, fallbackActive) => (fallbackActive ? null : restore);
|
|
129
172
|
const hhmm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; };
|
|
173
|
+
/**
|
|
174
|
+
* Selo do motor para a barra de status: de onde vêm as respostas agora (nuvem, endpoint ou local) e se é fallback.
|
|
175
|
+
* `fallback` = true quando a troca automática está ativa (o selo vira aviso e diz quando o Claude é tentado de novo).
|
|
176
|
+
*/
|
|
177
|
+
export function engineChip(model, fb) {
|
|
178
|
+
const r = resolveModel(model);
|
|
179
|
+
const where = r.kind === "anthropic" ? "☁ nuvem" : r.local ? "💻 local" : "🌐 endpoint";
|
|
180
|
+
const text = `${where} · ${r.label}`;
|
|
181
|
+
if (!fb?.active)
|
|
182
|
+
return { text, fallback: false };
|
|
183
|
+
const back = fb.until ? `; Claude volta ${hhmm(fb.until)}` : "; /model para voltar";
|
|
184
|
+
return { text: `⚠ FALLBACK ${text}${back}`, fallback: true };
|
|
185
|
+
}
|
|
130
186
|
/**
|
|
131
187
|
* Linhas do `/fallback`. Principal = o modelo vivo, salvo com o fallback ativo (aí é o principal original e o qwen
|
|
132
188
|
* aparece em uso). `status` vem de `fallbackStatus(state.primary, env)`; `engineLabel` é o motor do principal.
|
|
@@ -134,12 +190,15 @@ const hhmm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).pa
|
|
|
134
190
|
export function renderFallback(state, liveModel, engineLabel, status) {
|
|
135
191
|
const primary = state.active ? state.primary : liveModel;
|
|
136
192
|
const name = status.label ?? "—";
|
|
137
|
-
const self = !state.active && primary.trim().toLowerCase()
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
193
|
+
const self = !state.active && !status.ready && FALLBACK_CHAIN.includes(primary.trim().toLowerCase());
|
|
194
|
+
const using = state.using === SLM_MODEL ? "SLM local" : "qwen";
|
|
195
|
+
const src = (state.active ? state.using : status.ready ? ("name" in status ? status.name : "") : "") === SLM_MODEL ? "local · brito slm" : "qwen · ~/.brito/config.json";
|
|
196
|
+
const fb = state.active ? `${name} ${src} ● em uso`
|
|
197
|
+
: status.ready ? `${name} ${src} ✓ pronto`
|
|
198
|
+
: self ? `${name} — o modelo principal já é o ${primary.trim().toLowerCase() === SLM_MODEL ? "SLM" : "qwen"}`
|
|
141
199
|
: `${name} ✗ ${status.reason}`;
|
|
142
|
-
const
|
|
200
|
+
const volta = state.until ? ` — o Claude é tentado de novo às ${hhmm(state.until)}` : "";
|
|
201
|
+
const estado = state.active ? `ATIVO desde ${hhmm(state.at ?? Date.now())}${state.using ? ` (${using})` : ""} — Claude falhou: ${state.reason ?? "?"}${volta}. /model <claude> para voltar${state.until ? " agora" : ""}`
|
|
143
202
|
: status.ready ? "inativo — troca automática se o Claude falhar por limite, crédito, auth ou sobrecarga"
|
|
144
203
|
: self ? "não se aplica"
|
|
145
204
|
: "indisponível — sem troca automática nesta sessão";
|