brito-cli 0.6.3 → 0.7.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 +42 -0
- package/README.pt-BR.md +49 -0
- package/dist/agent/agents.js +1 -1
- package/dist/agent/claude-cli.js +129 -11
- package/dist/agent/context.js +8 -5
- package/dist/agent/loop.js +71 -25
- package/dist/cli.js +312 -111
- package/dist/desktop/serve.js +133 -32
- package/dist/engine.js +79 -0
- package/dist/graph/store.js +15 -0
- package/dist/model.js +48 -2
- package/dist/provider/anthropic.js +31 -0
- package/dist/provider/fallback.js +147 -0
- package/dist/provider/index.js +27 -0
- package/dist/provider/openai-compat.js +151 -0
- package/dist/provider/openai-wire.js +89 -0
- package/dist/provider/resolve.js +27 -0
- package/dist/provider/sse.js +33 -0
- package/dist/provider/types.js +1 -0
- package/dist/repl.js +57 -6
- package/dist/skills/learn.js +3 -2
- package/dist/tools/delegate.js +3 -1
- package/dist/ui/dock.js +2 -2
- package/dist/util/images.js +10 -5
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { buildChatBody, mapFinish, mapUsage, toBlocks } from "./openai-wire.js";
|
|
2
|
+
import { parseSSE } from "./sse.js";
|
|
3
|
+
const RETRIES = 2;
|
|
4
|
+
export const missingKeyMessage = (p) => `defina a variável de ambiente ${p.apiKeyEnv} com a chave do endpoint "${p.name}"`;
|
|
5
|
+
/**
|
|
6
|
+
* Junta os pedaços do streaming. O raciocínio é guardado e sai de uma vez (a UI desenha uma caixa por chamada), no
|
|
7
|
+
* primeiro pedaço de resposta ou de tool; o texto sai sem os espaços que o vLLM põe antes da resposta.
|
|
8
|
+
*/
|
|
9
|
+
export class ChatStreamAccumulator {
|
|
10
|
+
reasoning = "";
|
|
11
|
+
reasoningOut = false;
|
|
12
|
+
text = "";
|
|
13
|
+
emitted = 0;
|
|
14
|
+
calls = [];
|
|
15
|
+
finish = null;
|
|
16
|
+
usage;
|
|
17
|
+
push(chunk) {
|
|
18
|
+
const out = {};
|
|
19
|
+
// O vLLM manda falhas depois que o stream começou como `data: {"error": …}`.
|
|
20
|
+
if (chunk?.error)
|
|
21
|
+
throw new Error(`o endpoint falhou no meio da resposta: ${chunk.error.message ?? JSON.stringify(chunk.error)}`);
|
|
22
|
+
if (chunk?.usage)
|
|
23
|
+
this.usage = chunk.usage;
|
|
24
|
+
const choice = chunk?.choices?.[0];
|
|
25
|
+
if (!choice)
|
|
26
|
+
return out;
|
|
27
|
+
const d = choice.delta ?? {};
|
|
28
|
+
if (typeof d.reasoning_content === "string")
|
|
29
|
+
this.reasoning += d.reasoning_content;
|
|
30
|
+
if (typeof d.reasoning === "string")
|
|
31
|
+
this.reasoning += d.reasoning;
|
|
32
|
+
for (const tc of d.tool_calls ?? []) {
|
|
33
|
+
const i = Number(tc.index ?? 0);
|
|
34
|
+
const call = (this.calls[i] ??= { id: "", name: "", arguments: "" });
|
|
35
|
+
if (tc.id)
|
|
36
|
+
call.id = String(tc.id);
|
|
37
|
+
if (tc.function?.name)
|
|
38
|
+
call.name = String(tc.function.name);
|
|
39
|
+
if (tc.function?.arguments)
|
|
40
|
+
call.arguments += String(tc.function.arguments);
|
|
41
|
+
}
|
|
42
|
+
if (typeof d.content === "string")
|
|
43
|
+
this.text += d.content;
|
|
44
|
+
if (choice.finish_reason)
|
|
45
|
+
this.finish = choice.finish_reason;
|
|
46
|
+
const visible = this.text.trimStart();
|
|
47
|
+
const answering = visible.length > this.emitted || this.calls.length > 0;
|
|
48
|
+
if (answering)
|
|
49
|
+
Object.assign(out, this.flushThinking());
|
|
50
|
+
if (visible.length > this.emitted) {
|
|
51
|
+
out.text = visible.slice(this.emitted);
|
|
52
|
+
this.emitted = visible.length;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** O servidor disse que terminou? Sem isso, a conexão caiu no meio. */
|
|
57
|
+
get finished() { return this.finish !== null; }
|
|
58
|
+
/** Raciocínio ainda não entregue (uma vez só). */
|
|
59
|
+
flushThinking() {
|
|
60
|
+
if (this.reasoningOut || !this.reasoning.trim())
|
|
61
|
+
return {};
|
|
62
|
+
this.reasoningOut = true;
|
|
63
|
+
return { thinking: this.reasoning.trim() };
|
|
64
|
+
}
|
|
65
|
+
finishMessage() {
|
|
66
|
+
const calls = this.calls.filter(Boolean).map((c, i) => ({ ...c, id: c.id || `call_${i}` }));
|
|
67
|
+
return { content: toBlocks(this.text, calls), stopReason: mapFinish(this.finish), usage: mapUsage(this.usage), reasoning: this.reasoning };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Motivo legível de um erro do endpoint: o OCI embrulha o erro do vLLM em JSON dentro de JSON. */
|
|
71
|
+
function errorReason(body) {
|
|
72
|
+
let v = body.trim();
|
|
73
|
+
for (let i = 0; i < 3 && typeof v === "string"; i++) {
|
|
74
|
+
try {
|
|
75
|
+
v = JSON.parse(v);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
v = v?.error?.message ?? v?.message ?? v?.error ?? v;
|
|
81
|
+
}
|
|
82
|
+
const s = (typeof v === "string" ? v : JSON.stringify(v ?? "")).trim();
|
|
83
|
+
return s.length > 300 ? s.slice(0, 300) + "…" : s;
|
|
84
|
+
}
|
|
85
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
86
|
+
const retryable = (status) => status === 429 || status >= 500;
|
|
87
|
+
/** Endpoint compatível com a API Chat Completions da OpenAI (ex.: Qwen num cluster dedicado do OCI). Sempre em streaming. */
|
|
88
|
+
export class OpenAICompatProvider {
|
|
89
|
+
profile;
|
|
90
|
+
fetchImpl;
|
|
91
|
+
env;
|
|
92
|
+
retryDelayMs;
|
|
93
|
+
kind = "openai";
|
|
94
|
+
constructor(profile, fetchImpl = fetch, env = process.env, retryDelayMs = 500) {
|
|
95
|
+
this.profile = profile;
|
|
96
|
+
this.fetchImpl = fetchImpl;
|
|
97
|
+
this.env = env;
|
|
98
|
+
this.retryDelayMs = retryDelayMs;
|
|
99
|
+
}
|
|
100
|
+
async send(req, hooks = {}) {
|
|
101
|
+
const p = this.profile;
|
|
102
|
+
const key = this.env[p.apiKeyEnv]?.trim();
|
|
103
|
+
if (!key)
|
|
104
|
+
throw new Error(missingKeyMessage(p));
|
|
105
|
+
const url = `${p.baseURL.replace(/\/+$/, "")}/chat/completions`;
|
|
106
|
+
const init = {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "content-type": "application/json", accept: "text/event-stream", authorization: `Bearer ${key}` },
|
|
109
|
+
body: JSON.stringify(buildChatBody(req, p, true)),
|
|
110
|
+
signal: hooks.signal,
|
|
111
|
+
};
|
|
112
|
+
let res;
|
|
113
|
+
for (let attempt = 0;; attempt++) {
|
|
114
|
+
try {
|
|
115
|
+
res = await this.fetchImpl(url, init);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (hooks.signal?.aborted || attempt >= RETRIES)
|
|
119
|
+
throw new Error(`não consegui falar com o endpoint ${p.label}: ${e?.message ?? e}`);
|
|
120
|
+
await sleep(this.retryDelayMs * 2 ** attempt);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (res.ok || !retryable(res.status) || attempt >= RETRIES || hooks.signal?.aborted)
|
|
124
|
+
break;
|
|
125
|
+
await res.body?.cancel().catch(() => { });
|
|
126
|
+
await sleep(this.retryDelayMs * 2 ** attempt);
|
|
127
|
+
}
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const reason = errorReason(await res.text().catch(() => ""));
|
|
130
|
+
const hint = res.status === 401 || res.status === 403 ? ` — confira a chave em ${p.apiKeyEnv}` : "";
|
|
131
|
+
throw new Error(`endpoint ${p.label} respondeu HTTP ${res.status}${reason ? `: ${reason}` : ""} (opc-request-id: ${res.headers.get("opc-request-id") ?? "?"})${hint}`);
|
|
132
|
+
}
|
|
133
|
+
if (!res.body)
|
|
134
|
+
throw new Error(`endpoint ${p.label} respondeu sem corpo`);
|
|
135
|
+
const acc = new ChatStreamAccumulator();
|
|
136
|
+
for await (const chunk of parseSSE(res.body)) {
|
|
137
|
+
const out = acc.push(chunk);
|
|
138
|
+
if (out.thinking)
|
|
139
|
+
hooks.onThinking?.(out.thinking);
|
|
140
|
+
if (out.text)
|
|
141
|
+
hooks.onText?.(out.text);
|
|
142
|
+
}
|
|
143
|
+
if (!acc.finished)
|
|
144
|
+
throw new Error(`a conexão com o endpoint ${p.label} caiu antes do fim da resposta`);
|
|
145
|
+
const last = acc.flushThinking();
|
|
146
|
+
if (last.thinking)
|
|
147
|
+
hooks.onThinking?.(last.thinking);
|
|
148
|
+
const { reasoning: _r, ...msg } = acc.finishMessage();
|
|
149
|
+
return msg;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Texto de um tool_result (string ou lista de blocos; imagens dentro do resultado não têm equivalente e caem). */
|
|
2
|
+
const resultText = (c) => typeof c === "string" ? c : (c ?? []).map((b) => (b.type === "text" ? b.text : "")).join("");
|
|
3
|
+
export function toChatMessages(system, msgs, vision) {
|
|
4
|
+
const out = [{ role: "system", content: system }];
|
|
5
|
+
let droppedImages = 0;
|
|
6
|
+
for (const m of msgs) {
|
|
7
|
+
if (typeof m.content === "string") {
|
|
8
|
+
out.push(m.role === "user" ? { role: "user", content: m.content } : { role: "assistant", content: m.content });
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (m.role === "assistant") {
|
|
12
|
+
const text = m.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
|
|
13
|
+
const calls = m.content.flatMap((b) => b.type === "tool_use" ? [{ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } }] : []);
|
|
14
|
+
out.push(calls.length ? { role: "assistant", content: text || null, tool_calls: calls } : { role: "assistant", content: text });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
// Usuário: resultados de tool viram mensagens `tool` (logo após o assistant que pediu); o resto vira uma mensagem `user`.
|
|
18
|
+
const parts = [];
|
|
19
|
+
for (const b of m.content) {
|
|
20
|
+
if (b.type === "tool_result")
|
|
21
|
+
out.push({ role: "tool", tool_call_id: b.tool_use_id, content: (b.is_error ? "ERRO: " : "") + resultText(b.content) });
|
|
22
|
+
else if (b.type === "text")
|
|
23
|
+
parts.push({ type: "text", text: b.text });
|
|
24
|
+
else if (b.type === "image" && b.source.type === "base64") {
|
|
25
|
+
if (vision)
|
|
26
|
+
parts.push({ type: "image_url", image_url: { url: `data:${b.source.media_type};base64,${b.source.data}` } });
|
|
27
|
+
// Sem visão a imagem vira um marcador: o modelo sabe que havia uma (ex.: histórico herdado do Claude no fallback).
|
|
28
|
+
else {
|
|
29
|
+
droppedImages++;
|
|
30
|
+
parts.push({ type: "text", text: "[imagem omitida]" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!parts.length)
|
|
35
|
+
continue;
|
|
36
|
+
const onlyText = parts.every((p) => p.type === "text");
|
|
37
|
+
out.push({ role: "user", content: onlyText ? parts.map((p) => p.text).join("\n") : parts });
|
|
38
|
+
}
|
|
39
|
+
return { messages: out, droppedImages };
|
|
40
|
+
}
|
|
41
|
+
export const toChatTools = (tools) => tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema } }));
|
|
42
|
+
export function buildChatBody(req, p, stream) {
|
|
43
|
+
const body = {
|
|
44
|
+
model: p.model,
|
|
45
|
+
messages: toChatMessages(req.system, req.messages, p.vision).messages,
|
|
46
|
+
max_tokens: req.maxTokens,
|
|
47
|
+
stream,
|
|
48
|
+
};
|
|
49
|
+
// Qwen/vLLM: liga/desliga o raciocínio pelo template do chat. Só com `thinkingToggle`: a OpenAI e outros servidores
|
|
50
|
+
// recusam parâmetro desconhecido.
|
|
51
|
+
if (p.thinkingToggle)
|
|
52
|
+
body.chat_template_kwargs = { enable_thinking: req.thinking !== false };
|
|
53
|
+
if (stream)
|
|
54
|
+
body.stream_options = { include_usage: true };
|
|
55
|
+
if (req.tools?.length)
|
|
56
|
+
body.tools = toChatTools(req.tools);
|
|
57
|
+
return body;
|
|
58
|
+
}
|
|
59
|
+
export function mapFinish(reason) {
|
|
60
|
+
return reason === "tool_calls" ? "tool_use" : reason === "stop" ? "end_turn" : reason === "length" ? "max_tokens" : "other";
|
|
61
|
+
}
|
|
62
|
+
/** JSON inválido vira um input que o schema da tool rejeita: o modelo recebe "Input inválido" e tenta de novo. */
|
|
63
|
+
export function parseToolArgs(raw) {
|
|
64
|
+
if (!raw.trim())
|
|
65
|
+
return {};
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return { _argumentosInvalidos: raw };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const mapUsage = (u) => ({ input: u?.prompt_tokens ?? 0, output: u?.completion_tokens ?? 0, cacheRead: u?.prompt_tokens_details?.cached_tokens ?? 0 });
|
|
74
|
+
/** Junta texto e chamadas de tool na ordem do formato interno (texto primeiro). O vLLM põe "\n\n" antes da resposta. */
|
|
75
|
+
export function toBlocks(text, calls) {
|
|
76
|
+
const blocks = [];
|
|
77
|
+
const t = text.trimStart();
|
|
78
|
+
if (t)
|
|
79
|
+
blocks.push({ type: "text", text: t });
|
|
80
|
+
for (const c of calls)
|
|
81
|
+
blocks.push({ type: "tool_use", id: c.id, name: c.name, input: parseToolArgs(c.arguments) });
|
|
82
|
+
return blocks;
|
|
83
|
+
}
|
|
84
|
+
export function fromChatCompletion(json) {
|
|
85
|
+
const choice = json?.choices?.[0];
|
|
86
|
+
const msg = choice?.message ?? {};
|
|
87
|
+
const calls = (msg.tool_calls ?? []).map((c) => ({ id: String(c.id), name: String(c.function?.name ?? ""), arguments: String(c.function?.arguments ?? "") }));
|
|
88
|
+
return { content: toBlocks(msg.content ?? "", calls), stopReason: mapFinish(choice?.finish_reason), usage: mapUsage(json?.usage), reasoning: msg.reasoning_content ?? "" };
|
|
89
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MODEL_ALIASES, loadProfiles } from "../model.js";
|
|
2
|
+
/** Janela dos modelos Claude quando nada informa outra. */
|
|
3
|
+
const CLAUDE_WINDOW = 200_000;
|
|
4
|
+
const COMPACT_DEFAULT = 120_000;
|
|
5
|
+
/** Nome → provedor: perfil configurado (sem diferenciar maiúsculas) > alias do Claude > id cru do Claude. */
|
|
6
|
+
export function resolveModel(name) {
|
|
7
|
+
const key = name.trim().toLowerCase();
|
|
8
|
+
const profile = loadProfiles().profiles[key];
|
|
9
|
+
if (profile)
|
|
10
|
+
return { kind: "openai", name: key, wireModel: profile.model, label: profile.label, contextWindow: profile.contextWindow, profile };
|
|
11
|
+
const wire = MODEL_ALIASES[key] ?? name.trim();
|
|
12
|
+
return { kind: "anthropic", name: wire, wireModel: wire, label: wire, contextWindow: CLAUDE_WINDOW };
|
|
13
|
+
}
|
|
14
|
+
/** Janela mostrada na barra/`/context`: BRITO_CONTEXT_WINDOW > perfil > padrão do Claude. */
|
|
15
|
+
export function contextWindowFor(name) {
|
|
16
|
+
const env = Number(process.env.BRITO_CONTEXT_WINDOW);
|
|
17
|
+
return env > 0 ? env : resolveModel(name).contextWindow;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Quando compactar: BRITO_COMPACT_TOKENS (ou 120k). Em perfis de endpoint, nunca acima de 60% da janela (janelas
|
|
21
|
+
* pequenas estourariam antes); no Claude fica como sempre foi.
|
|
22
|
+
*/
|
|
23
|
+
export function compactAtFor(name) {
|
|
24
|
+
const env = Number(process.env.BRITO_COMPACT_TOKENS);
|
|
25
|
+
const base = env > 0 ? env : COMPACT_DEFAULT;
|
|
26
|
+
return resolveModel(name).kind === "openai" ? Math.min(base, Math.floor(0.6 * contextWindowFor(name))) : base;
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lê um corpo `text/event-stream` e devolve o JSON de cada linha `data:`. Linhas podem chegar partidas entre pedaços
|
|
3
|
+
* da rede e com `\r\n`; `data: [DONE]` encerra. Linha que não é JSON é ignorada (comentários/keep-alive).
|
|
4
|
+
*/
|
|
5
|
+
export async function* parseSSE(body) {
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
let buf = "";
|
|
8
|
+
const reader = body.getReader();
|
|
9
|
+
try {
|
|
10
|
+
for (;;) {
|
|
11
|
+
const { done, value } = await reader.read();
|
|
12
|
+
buf += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
13
|
+
const lines = buf.split(/\r?\n/);
|
|
14
|
+
buf = done ? "" : lines.pop();
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
if (!line.startsWith("data:"))
|
|
17
|
+
continue;
|
|
18
|
+
const data = line.slice(5).trim();
|
|
19
|
+
if (data === "[DONE]")
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
yield JSON.parse(data);
|
|
23
|
+
}
|
|
24
|
+
catch { /* não é JSON */ }
|
|
25
|
+
}
|
|
26
|
+
if (done)
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
reader.releaseLock();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/repl.js
CHANGED
|
@@ -19,6 +19,56 @@ const userName = () => {
|
|
|
19
19
|
import { listPending, listSkills, deleteSkill, promoteSkill, globalHome } from "./skills/store.js";
|
|
20
20
|
import { listAgents } from "./agent/agents.js";
|
|
21
21
|
import { perms, cycleMode, MODE_LABEL } from "./permissions.js";
|
|
22
|
+
/**
|
|
23
|
+
* Backend com o motor substituível no meio da sessão (claude CLI → qwen): o REPL monta o menu uma vez, então os
|
|
24
|
+
* comandos ficam estáveis e o que o motor atual não tem responde "indisponível". `usage`/`cost` somam todos os
|
|
25
|
+
* motores já usados (cada um uma vez, mesmo que volte a ser o atual).
|
|
26
|
+
*/
|
|
27
|
+
export function switchableBackend(initial) {
|
|
28
|
+
let cur = initial;
|
|
29
|
+
const seen = new Set([initial]);
|
|
30
|
+
const commands = {};
|
|
31
|
+
const delegate = (name, desc) => {
|
|
32
|
+
commands[name] = {
|
|
33
|
+
desc,
|
|
34
|
+
run: async (args) => {
|
|
35
|
+
const cmd = cur.commands[name];
|
|
36
|
+
if (cmd)
|
|
37
|
+
await cmd.run(args);
|
|
38
|
+
else
|
|
39
|
+
ui.warn(`${name} não está disponível no ${cur.model()}`);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const adopt = (b) => { for (const [name, v] of Object.entries(b.commands))
|
|
44
|
+
if (!commands[name])
|
|
45
|
+
delegate(name, v.desc); };
|
|
46
|
+
adopt(initial);
|
|
47
|
+
return {
|
|
48
|
+
model: () => cur.model(),
|
|
49
|
+
contextTokens: () => cur.contextTokens(),
|
|
50
|
+
usage: () => {
|
|
51
|
+
const all = [...seen].map((b) => b.usage());
|
|
52
|
+
return { input: all.reduce((s, u) => s + u.input, 0), output: all.reduce((s, u) => s + u.output, 0), cacheRead: all.reduce((s, u) => s + (u.cacheRead ?? 0), 0) };
|
|
53
|
+
},
|
|
54
|
+
get ctxWindow() { return cur.ctxWindow; },
|
|
55
|
+
cost: () => {
|
|
56
|
+
const all = [...seen].map((b) => b.cost?.()).filter((x) => x !== undefined);
|
|
57
|
+
return all.length ? all.reduce((s, x) => s + x, 0) : undefined;
|
|
58
|
+
},
|
|
59
|
+
turn: (text, signal) => cur.turn(text, signal),
|
|
60
|
+
abort: () => cur.abort?.(),
|
|
61
|
+
// Todos os motores: o claude CLI fora de uso guarda a sessão (--resume) e os turnos que o fallback herdaria.
|
|
62
|
+
reset: () => { for (const b of seen)
|
|
63
|
+
b.reset(); },
|
|
64
|
+
afterTurn: async () => { await cur.afterTurn?.(); },
|
|
65
|
+
onShell: () => cur.onShell?.(),
|
|
66
|
+
commands,
|
|
67
|
+
close: async () => { await cur.close(); },
|
|
68
|
+
swap: (next) => { cur = next; seen.add(next); adopt(next); },
|
|
69
|
+
current: () => cur,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
22
72
|
const COMMON = {
|
|
23
73
|
"/help": "esta ajuda",
|
|
24
74
|
"/agents": "subagentes disponíveis",
|
|
@@ -188,15 +238,16 @@ export async function runRepl(b, o) {
|
|
|
188
238
|
await shutdown();
|
|
189
239
|
return;
|
|
190
240
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!run) {
|
|
194
|
-
ui.warn(`comando desconhecido: ${cmd}. Digite /help.`);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
241
|
+
const run = line.startsWith("/") ? common[key] ?? b.commands[key]?.run : undefined;
|
|
242
|
+
if (run) {
|
|
197
243
|
await run(args);
|
|
198
244
|
return;
|
|
199
245
|
}
|
|
246
|
+
// "/Users/x/arquivo.pdf" é caminho, não comando: segue como mensagem ao agente
|
|
247
|
+
if (line.startsWith("/") && !cmd.slice(1).includes("/")) {
|
|
248
|
+
ui.warn(`comando desconhecido: ${cmd}. Digite /help.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
200
251
|
const ac = (running = new AbortController());
|
|
201
252
|
dock?.setBusy(true);
|
|
202
253
|
try {
|
package/dist/skills/learn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { providerFor } from "../provider/index.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { execTool, toApiTools } from "../tools/registry.js";
|
|
4
4
|
import { perms } from "../permissions.js";
|
|
@@ -99,7 +99,8 @@ export function recordOutcomes(turn, ctx) {
|
|
|
99
99
|
return [...(ctx.loadedSkills ?? [])].flatMap((n) => recordOutcome(ctx.root, n, verdict === "win") ?? []);
|
|
100
100
|
}
|
|
101
101
|
export const defaultComplete = async ({ model, system, user, tools, maxTokens }) => {
|
|
102
|
-
|
|
102
|
+
// Raciocínio desligado: o julgamento cabe no orçamento e a resposta útil é a chamada de skill_save.
|
|
103
|
+
const res = await providerFor(model).send({ model, maxTokens, system, tools, messages: [{ role: "user", content: user }], thinking: false });
|
|
103
104
|
return res.content;
|
|
104
105
|
};
|
|
105
106
|
/**
|
package/dist/tools/delegate.js
CHANGED
|
@@ -3,6 +3,8 @@ import { defineTool, allTools, execTool } from "./registry.js";
|
|
|
3
3
|
import { Agent } from "../agent/loop.js";
|
|
4
4
|
import { getAgent, listAgents } from "../agent/agents.js";
|
|
5
5
|
import { buildSubagentPrompt } from "../agent/prompt.js";
|
|
6
|
+
import { subagentModel } from "../engine.js";
|
|
7
|
+
import { initialModel } from "../model.js";
|
|
6
8
|
import { ui } from "../ui/ui.js";
|
|
7
9
|
let seq = 0;
|
|
8
10
|
/** Conjunto de tools de um agente; `delegate` nunca é repassada (sem recursão). */
|
|
@@ -47,7 +49,7 @@ defineTool({
|
|
|
47
49
|
}
|
|
48
50
|
log(`iniciado: ${task.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
49
51
|
const sub = new Agent(buildSubagentPrompt(ctx.root, def), ctx, {
|
|
50
|
-
model: def.model,
|
|
52
|
+
model: subagentModel(def, ctx.model?.() ?? initialModel(), process.env),
|
|
51
53
|
maxSteps: 25,
|
|
52
54
|
quiet: true,
|
|
53
55
|
tools: toolSet(def),
|
package/dist/ui/dock.js
CHANGED
|
@@ -8,7 +8,7 @@ const graphemes = (s) => Array.from(seg.segment(s), (x) => x.segment);
|
|
|
8
8
|
const ANSI = /\x1b\[[0-9;]*[A-Za-z]/y;
|
|
9
9
|
const MAX_INPUT_ROWS = 8;
|
|
10
10
|
const MAX_MENU = 7;
|
|
11
|
-
const PASTE_RE = /\[(?:colado|imagem) #(\d+): [^\]]*\]/g;
|
|
11
|
+
const PASTE_RE = /\[(?:colado|imagem|anexo) #(\d+): [^\]]*\]/g;
|
|
12
12
|
export class Dock {
|
|
13
13
|
opts;
|
|
14
14
|
onSubmit = () => { };
|
|
@@ -415,7 +415,7 @@ export class Dock {
|
|
|
415
415
|
if (imgs.length) {
|
|
416
416
|
const n = ++this.pasteN;
|
|
417
417
|
this.pastes.set(n, imgs.map((i) => (/\s/.test(i.path) ? `"${i.path}"` : i.path)).join(" "));
|
|
418
|
-
this.insert(`[
|
|
418
|
+
this.insert(`[anexo #${n}: ${imgs.length === 1 ? imgs[0].name : `${imgs.length} arquivos`}] `);
|
|
419
419
|
}
|
|
420
420
|
else if (lines >= 2 || text.length > 300) {
|
|
421
421
|
const n = ++this.pasteN;
|
package/dist/util/images.js
CHANGED
|
@@ -2,10 +2,13 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
const TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
6
|
-
const IMG_EXT = /\.(png|jpe?g|gif|webp)$/i;
|
|
7
|
-
/** Limite da API por imagem (5 MB) e teto de anexos por mensagem. */
|
|
5
|
+
const TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", pdf: "application/pdf" };
|
|
6
|
+
const IMG_EXT = /\.(png|jpe?g|gif|webp|pdf)$/i;
|
|
7
|
+
/** Limite da API por imagem (5 MB), por PDF (32 MB) e teto de anexos por mensagem. */
|
|
8
8
|
export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
export const MAX_PDF_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
export const limitOf = (i) => (i.mediaType === "application/pdf" ? MAX_PDF_BYTES : MAX_IMAGE_BYTES);
|
|
11
|
+
export const isPdf = (i) => i.mediaType === "application/pdf";
|
|
9
12
|
export const MAX_IMAGES = 5;
|
|
10
13
|
/** Divide o texto em tokens, respeitando aspas e (fora do Windows) `\ ` como espaço do nome do arquivo. */
|
|
11
14
|
function tokens(text) {
|
|
@@ -67,9 +70,11 @@ export function droppedImages(text, root = process.cwd()) {
|
|
|
67
70
|
export const fmtBytes = (n) => (n >= 1_048_576 ? `${(n / 1_048_576).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} kB`);
|
|
68
71
|
/** Separa o que dá para enviar do que passa do limite da API. */
|
|
69
72
|
export function splitOversize(imgs) {
|
|
70
|
-
return { ok: imgs.filter((i) => i.bytes <=
|
|
73
|
+
return { ok: imgs.filter((i) => i.bytes <= limitOf(i)), tooBig: imgs.filter((i) => i.bytes > limitOf(i)) };
|
|
71
74
|
}
|
|
72
|
-
/** Bloco `image` da API da Anthropic, com o arquivo em base64. */
|
|
75
|
+
/** Bloco `image` (ou `document`, para PDF) da API da Anthropic, com o arquivo em base64. */
|
|
73
76
|
export function imageBlock(img) {
|
|
77
|
+
if (isPdf(img))
|
|
78
|
+
return { type: "document", source: { type: "base64", media_type: "application/pdf", data: fs.readFileSync(img.path).toString("base64") } };
|
|
74
79
|
return { type: "image", source: { type: "base64", media_type: img.mediaType, data: fs.readFileSync(img.path).toString("base64") } };
|
|
75
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Coding-agent CLI with a code knowledge graph, memory and reviewable skills / CLI de agente de código com grafo de conhecimento, memória e skills revisáveis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|