kafox-cli 0.3.0 → 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 +130 -11
- package/bin/kafox.js +73 -2
- package/package.json +4 -3
- package/src/agent.js +385 -0
- package/src/auth.js +77 -30
- package/src/cli.js +83 -10
- package/src/diff.js +193 -0
- package/src/gateway.js +235 -48
- package/src/login-screen.js +203 -0
- package/src/logo.js +108 -0
- package/src/mascot.js +3 -31
- package/src/mcp.js +557 -0
- package/src/models.js +1 -4
- package/src/runner.js +98 -0
- package/src/sessions.js +138 -0
- package/src/storage.js +2 -1
- package/src/subagents.js +156 -0
- package/src/theme.js +327 -3
- package/src/tools.js +371 -0
- package/src/tui.js +1707 -213
package/README.md
CHANGED
|
@@ -23,44 +23,163 @@ Requisito: Node.js 22.5+.
|
|
|
23
23
|
kafox login
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`~/.kafox/token.json` (permissao 600).
|
|
26
|
+
O terminal vira uma tela de login (arte grande em cima, link no centro): abra a
|
|
27
|
+
URL `https://kafox.pypag.com/cli?code=...`, entre na sua conta e clique em
|
|
28
|
+
**Autorizar**. A sessao fica em `~/.kafox/token.json` (permissao 600). No primeiro
|
|
29
|
+
`kafox` sem sessao ele pergunta se quer entrar e abre a mesma tela.
|
|
29
30
|
|
|
30
31
|
## Comandos
|
|
31
32
|
|
|
32
33
|
| Comando | O que faz |
|
|
33
34
|
| --- | --- |
|
|
34
35
|
| `kafox` | abre a interface de tela cheia |
|
|
36
|
+
| `kafox --allow-all` | abre ja aprovando tudo (mesmo que `KAFOX_ALLOW_ALL=1`) |
|
|
35
37
|
| `kafox login` / `logout` / `whoami` | sessao da conta |
|
|
36
38
|
| `kafox models` | lista modelos e precos por 1M de tokens |
|
|
37
39
|
| `kafox pricing` | pacotes de recarga |
|
|
38
40
|
| `kafox balance` | saldo atual |
|
|
39
41
|
| `kafox key` | cria uma API key |
|
|
40
42
|
| `kafox ask "pergunta" [-m modelo]` | pergunta ao gateway |
|
|
43
|
+
| `kafox agent "objetivo" [-y] [--max-steps 12] [--json]` | roda o agente sem TUI (script/CI) |
|
|
44
|
+
| `kafox history` | lista as conversas salvas (titulo + como retomar) |
|
|
45
|
+
| `kafox resume [codigo]` | retoma uma conversa; sem codigo abre o seletor na tela |
|
|
46
|
+
| `kafox continue` / `kafox -c` | volta para a conversa mais recente |
|
|
41
47
|
| `kafox status` | conta, saldo e gateway |
|
|
42
48
|
| `kafox theme` | seletor de tema |
|
|
43
49
|
| `kafox background [url]` | imagem de fundo do terminal |
|
|
44
50
|
| `kafox compact` / `clear` | compacta / limpa o contexto do REPL |
|
|
45
51
|
|
|
52
|
+
## Conversas salvas
|
|
53
|
+
|
|
54
|
+
Toda conversa fica em `~/.kafox/chats/<codigo>.json`. Ao sair com **Ctrl+C** a
|
|
55
|
+
CLI mostra o codigo:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Retome esta conversa: kafox --resume 3f1c9a0b
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`kafox history` lista tudo:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
- hello world em python solicitado, usa arquivo e roda teste
|
|
65
|
+
kafox resume 3f1c9a0b
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`kafox resume` (sem codigo) abre o seletor com os titulos das conversas — setas
|
|
69
|
+
ou mouse escolhem, Enter retoma. `kafox continue` (ou `-c`) vai direto para a
|
|
70
|
+
ultima.
|
|
71
|
+
|
|
72
|
+
## Agentes e subagentes
|
|
73
|
+
|
|
74
|
+
O agente tem a ferramenta `spawn_subagent`: ele lança subagentes com objetivo
|
|
75
|
+
próprio, cada um num contexto separado, e recebe só o relatório final. Vão até
|
|
76
|
+
3 tarefas por chamada e as independentes rodam em paralelo. Por padrão o
|
|
77
|
+
subagente só lê (`read_file`, `list_dir`); escrever, editar ou executar exige
|
|
78
|
+
declarar a ferramenta na tarefa — e continua passando pela aprovação normal
|
|
79
|
+
(ou pelo `/allow`).
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
kafox agent "revise src/agent.js e aponte bugs" --max-steps 8
|
|
83
|
+
kafox agent "rode os testes do backend e resuma as falhas" -y --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Para ver o que os subagentes da sessão fizeram (objetivo, ferramentas, tempo e
|
|
87
|
+
se terminaram), use `/agents` — o mesmo comando funciona no REPL e na TUI.
|
|
88
|
+
|
|
46
89
|
Na interface: digite `/` para o menu (↑/↓ navega, Tab completa, Enter executa),
|
|
47
|
-
`/model` abre o seletor de modelo
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
90
|
+
`/model` abre o seletor de modelo e `/theme` o de tema (↑/↓, clique do mouse,
|
|
91
|
+
Enter aplica, Esc cancela). A resposta chega em streaming, com bloco `Thinking`
|
|
92
|
+
quando o modelo manda o raciocinio. PgUp/PgDn ou a roda do mouse rolam a conversa;
|
|
93
|
+
Ctrl+Y copia a ultima resposta e F2 solta o mouse para selecionar texto.
|
|
94
|
+
|
|
95
|
+
Enquanto o modelo trabalha a caixa de texto mostra `trabalhando...` com os
|
|
96
|
+
pontinhos correndo, e o nome do modelo aparece fora dela, na linha de baixo.
|
|
97
|
+
Se a rede cair (`fetch failed`), a CLI tenta de novo sozinha ate 20 vezes
|
|
98
|
+
dobrando a espera (3s, 6s, 12s, 24s... ate 60s) e mostra a contagem na linha do
|
|
99
|
+
modelo; **Esc** cancela a espera na hora. O agente nao tem limite de passos: ele
|
|
100
|
+
continua ate terminar.
|
|
101
|
+
|
|
102
|
+
Nao precisa esperar a resposta para escrever de novo: **Enter enfileira** a
|
|
103
|
+
mensagem (a fila aparece acima do input e roda em ordem assim que a atual
|
|
104
|
+
termina). Para parar a IA na hora, **Esc duas vezes** — o primeiro Esc so avisa,
|
|
105
|
+
o segundo interrompe o passo em andamento e guarda na conversa o que ja tinha
|
|
106
|
+
chegado.
|
|
107
|
+
|
|
108
|
+
Demais comandos: `/help`, `/models`, `/balance`, `/pricing`, `/key`, `/login`,
|
|
109
|
+
`/logout`, `/whoami`, `/mcp`, `/allow`, `/theme`, `/background`, `/clear`,
|
|
110
|
+
`/compact`, `/status`, `/exit`.
|
|
111
|
+
|
|
112
|
+
O `/theme` traz 29 temas (default, dark, light, brutalist, neon, paper,
|
|
113
|
+
dracula, nord, gruvbox, monokai, tokyo-night, catppuccin, solarized, midnight,
|
|
114
|
+
ocean, forest, sunset, plum, synthwave, cyberpunk, amoled, matrix, terminal,
|
|
115
|
+
solarized-light, rose, mint, sepia, ice e high-contrast).
|
|
116
|
+
|
|
117
|
+
## Ferramentas do agente
|
|
118
|
+
|
|
119
|
+
Com o saldo em dia a TUI deixa o modelo ler e mexer no projeto. Cada chamada
|
|
120
|
+
aparece como um card na conversa, com o diff do que foi editado.
|
|
121
|
+
|
|
122
|
+
| Ferramenta | O que faz |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `read_file` | le um arquivo (com `offset`/`limit`) |
|
|
125
|
+
| `list_dir` | lista uma pasta |
|
|
126
|
+
| `write_file` | cria ou reescreve um arquivo |
|
|
127
|
+
| `edit_file` | troca um trecho exato do arquivo |
|
|
128
|
+
| `run_command` | roda um comando no diretorio de trabalho |
|
|
129
|
+
|
|
130
|
+
`run_command` sai como uma janela de terminal: o comando em `$ ...`, a saida
|
|
131
|
+
dentro da janela (erro em vermelho) e o rodape com `exit` e o tempo; enquanto
|
|
132
|
+
roda, aparece o spinner com o tempo decorrido. Ferramentas de MCP usam a mesma
|
|
133
|
+
janela, com os argumentos e o resultado.
|
|
134
|
+
|
|
135
|
+
Quem escreve, edita ou executa pede aprovacao num modal com tres opcoes:
|
|
136
|
+
**Permitir uma vez**, **Permitir sempre** (roda tudo nesta sessao, sem perguntar
|
|
137
|
+
de novo — inclusive as proximas ferramentas do mesmo passo) e **Negar**. Navegue
|
|
138
|
+
com ↑/↓ (ou clique) e confirme com Enter; `a` libera sempre, `y` uma vez,
|
|
139
|
+
`n`/Esc nega. Todo caminho fica travado dentro do diretorio de trabalho.
|
|
140
|
+
|
|
141
|
+
Para ja abrir liberado, use `kafox --allow-all` (ou `KAFOX_ALLOW_ALL=1`), e dentro
|
|
142
|
+
da sessao `/allow` liga e `/allow off` volta a perguntar. Com o modo ligado a
|
|
143
|
+
barra lateral mostra **rodando tudo**.
|
|
144
|
+
|
|
145
|
+
## Servidores MCP
|
|
146
|
+
|
|
147
|
+
Ferramentas de servidores MCP entram no mesmo loop, com o prefixo
|
|
148
|
+
`mcp__servidor__ferramenta`. Configure em `~/.kafox/mcp.json` (ou `./mcp.json` do
|
|
149
|
+
projeto, que vence):
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcp": {
|
|
154
|
+
"playwright": {
|
|
155
|
+
"type": "local",
|
|
156
|
+
"command": ["npx", "-y", "@playwright/mcp@latest"]
|
|
157
|
+
},
|
|
158
|
+
"remoto": {
|
|
159
|
+
"type": "remote",
|
|
160
|
+
"url": "https://exemplo.com/mcp",
|
|
161
|
+
"headers": { "authorization": "Bearer ..." }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
O servidor local roda por stdio e o remoto fala HTTP/SSE. Use `enabled: false`
|
|
168
|
+
para desligar um servidor. Na TUI a barra lateral mostra o status de cada um (e o
|
|
169
|
+
motivo do erro quando falha) e `/mcp` lista tudo; `/mcp reload` reconecta.
|
|
52
170
|
|
|
53
171
|
## Configuracao
|
|
54
172
|
|
|
55
173
|
- `KAFOX_API_URL` ou `~/.kafox/config.json` (`api_url`) troca a API.
|
|
56
174
|
- `~/.kafox/config.json` guarda `model`, `theme`, `background` e a API key criada
|
|
57
175
|
automaticamente no primeiro `ask`.
|
|
176
|
+
- `~/.kafox/mcp.json` guarda os servidores MCP.
|
|
58
177
|
|
|
59
178
|
A CLI so responde se a conta tiver saldo; sem credito ela avisa e nao chama o
|
|
60
179
|
gateway.
|
|
61
180
|
|
|
62
181
|
## Escopo
|
|
63
182
|
|
|
64
|
-
O kafox
|
|
65
|
-
|
|
66
|
-
servidores MCP.
|
|
183
|
+
O kafox autentica na conta, lista modelos, mostra saldo, conversa pelo gateway e
|
|
184
|
+
roda tarefas no projeto: le e edita arquivos, executa comandos (sempre com
|
|
185
|
+
aprovacao) e chama servidores MCP configurados.
|
package/bin/kafox.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const chalk = require("chalk");
|
|
2
3
|
const { Command } = require("commander");
|
|
3
4
|
const { version } = require("../package.json");
|
|
4
5
|
const { runREPL, compact, clearContext, showStatus } = require("../src/cli");
|
|
@@ -6,27 +7,97 @@ const { login, logout, whoami, createKey } = require("../src/auth");
|
|
|
6
7
|
const { listModels, showBalance, showPricing } = require("../src/models");
|
|
7
8
|
const { ask } = require("../src/gateway");
|
|
8
9
|
const { runTheme, setContentBackground } = require("../src/theme");
|
|
10
|
+
const { runHeadless } = require("../src/runner");
|
|
9
11
|
|
|
10
12
|
const program = new Command();
|
|
11
13
|
|
|
12
14
|
program
|
|
13
15
|
.name("kafox")
|
|
14
16
|
.description("Kafox CLI in opencode style: terminal UI, account login, models, balance, themes")
|
|
17
|
+
.option(
|
|
18
|
+
"-y, --allow-all",
|
|
19
|
+
"abre ja aprovando tudo: ferramentas de arquivo, comando e MCP sem pedir aprovacao"
|
|
20
|
+
)
|
|
21
|
+
.option(
|
|
22
|
+
"--resume [codigo]",
|
|
23
|
+
"retoma uma conversa salva (o codigo aparece no Ctrl+C e no `kafox history`)"
|
|
24
|
+
)
|
|
25
|
+
.option("-c, --continue", "retoma a conversa mais recente")
|
|
15
26
|
.version(version);
|
|
16
27
|
|
|
28
|
+
/** `kafox history`: titulo de cada conversa e como retomar. */
|
|
29
|
+
function printHistory() {
|
|
30
|
+
const sessions = require("../src/sessions");
|
|
31
|
+
const chats = sessions.list();
|
|
32
|
+
if (!chats.length) {
|
|
33
|
+
console.log("Nenhuma conversa salva ainda.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const chat of chats) {
|
|
37
|
+
console.log(`- ${chalk.bold(chat.title)}`);
|
|
38
|
+
console.log(` kafox resume ${chat.id}`);
|
|
39
|
+
console.log("");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
program.command("login").description("Login with your Kafox account (device code SSO)").action(login);
|
|
18
44
|
program.command("logout").description("End the CLI session").action(logout);
|
|
19
45
|
program.command("whoami").description("Show the account linked to this CLI").action(whoami);
|
|
20
|
-
program.command("models").description("List available models
|
|
46
|
+
program.command("models").description("List available models").action(listModels);
|
|
21
47
|
program.command("balance").description("Show current balance").action(showBalance);
|
|
22
48
|
program.command("pricing").description("Show recharge packages").action(showPricing);
|
|
23
49
|
program.command("key").description("Create an API key for this CLI").action(createKey);
|
|
24
50
|
program.command("ask").argument("[question...]", "Question").option("-m, --model <id>", "Model").action(ask);
|
|
51
|
+
program
|
|
52
|
+
.command("agent")
|
|
53
|
+
.description("Run the agent without the TUI (tools, subagents, scripting)")
|
|
54
|
+
.argument("<objective...>", "Objetivo do agente")
|
|
55
|
+
.option("-m, --model <id>", "Model")
|
|
56
|
+
// -y/--allow-all é global (declarado no programa): declarar de novo aqui
|
|
57
|
+
// fazia o commander esconder a flag e o agente nunca rodava liberado.
|
|
58
|
+
.option("--max-steps <n>", "limite de passos (padrao: sem limite)", "0")
|
|
59
|
+
.option("--json", "imprime só o resultado em JSON")
|
|
60
|
+
.option("-q, --quiet", "esconde o progresso das ferramentas")
|
|
61
|
+
.action(async (objective, options) => {
|
|
62
|
+
const globals = program.opts();
|
|
63
|
+
const payload = await runHeadless({
|
|
64
|
+
prompt: objective.join(" "),
|
|
65
|
+
model: options.model,
|
|
66
|
+
allowAll: Boolean(options.allowAll || globals.allowAll),
|
|
67
|
+
maxSteps: Number(options.maxSteps) > 0 ? Number(options.maxSteps) : Infinity,
|
|
68
|
+
json: Boolean(options.json),
|
|
69
|
+
quiet: Boolean(options.quiet),
|
|
70
|
+
});
|
|
71
|
+
if (!payload.ok) process.exitCode = 1;
|
|
72
|
+
});
|
|
25
73
|
program.command("theme").description("Open the theme selector").action(runTheme);
|
|
26
74
|
program.command("background").description("Set terminal background image (URL)").argument("[url]", "Image URL").action(setContentBackground);
|
|
75
|
+
program
|
|
76
|
+
.command("history")
|
|
77
|
+
.alias("historico")
|
|
78
|
+
.description("Lista todas as conversas salvas")
|
|
79
|
+
.action(printHistory);
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command("resume [codigo]")
|
|
83
|
+
.description("Retoma uma conversa salva (sem codigo abre o seletor na tela)")
|
|
84
|
+
.action((codigo) =>
|
|
85
|
+
runREPL({ allowAll: Boolean(program.opts().allowAll), resume: codigo || "pick" })
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command("continue")
|
|
90
|
+
.alias("c")
|
|
91
|
+
.description("Continua a conversa mais recente")
|
|
92
|
+
.action(() => runREPL({ allowAll: Boolean(program.opts().allowAll), resume: "last" }));
|
|
27
93
|
program.command("compact").description("Compact the current conversation context").action(compact);
|
|
28
94
|
program.command("clear").description("Clear the conversation context").action(clearContext);
|
|
29
95
|
program.command("status").description("Show account, balance and gateway status").action(showStatus);
|
|
30
96
|
|
|
31
|
-
program.action(
|
|
97
|
+
program.action((options) =>
|
|
98
|
+
runREPL({
|
|
99
|
+
allowAll: Boolean(options.allowAll),
|
|
100
|
+
resume: options.continue ? "last" : options.resume || null,
|
|
101
|
+
})
|
|
102
|
+
);
|
|
32
103
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kafox-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kafox CLI
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Kafox CLI: TUI com streaming, thinking, edit com diff, agent tools (files/shell), MCP e temas",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kafox": "bin/kafox.js"
|
|
7
7
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"inquirer": "8.2.6"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node --test \"test/*.test.js\""
|
|
23
|
+
"test": "node --test \"test/*.test.js\"",
|
|
24
|
+
"smoke": "node test/e2e/tui.js"
|
|
24
25
|
}
|
|
25
26
|
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
const { chat } = require("./gateway");
|
|
2
|
+
const {
|
|
3
|
+
TOOL_SPECS,
|
|
4
|
+
APPROVAL_REQUIRED,
|
|
5
|
+
executeTool,
|
|
6
|
+
describeToolCall,
|
|
7
|
+
previewToolChange,
|
|
8
|
+
summarizeOutput,
|
|
9
|
+
} = require("./tools");
|
|
10
|
+
const {
|
|
11
|
+
SUBAGENT_TOOL,
|
|
12
|
+
SUBAGENT_TOOL_NAME,
|
|
13
|
+
MAX_DEPTH,
|
|
14
|
+
normalizeTasks,
|
|
15
|
+
subagentPrompt,
|
|
16
|
+
describeSubagentCall,
|
|
17
|
+
recordRun,
|
|
18
|
+
DEFAULT_MAX_STEPS,
|
|
19
|
+
} = require("./subagents");
|
|
20
|
+
|
|
21
|
+
// Sem teto de passos: o agente continua ate terminar (ou ate o usuario parar
|
|
22
|
+
// com Esc). `maxSteps` so vale quando alguem pede um limite de proposito.
|
|
23
|
+
const MAX_STEPS = Infinity;
|
|
24
|
+
const MAX_SUBAGENT_REPORT_CHARS = 6000;
|
|
25
|
+
|
|
26
|
+
function parseArgs(raw) {
|
|
27
|
+
if (!raw) return {};
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function systemPrompt(cwd, model) {
|
|
37
|
+
return [
|
|
38
|
+
"Voce e o kafox, uma CLI de terminal que conversa e executa tarefas no projeto do usuario.",
|
|
39
|
+
`Diretorio de trabalho: ${cwd}`,
|
|
40
|
+
`Modelo: ${model || "auto"}`,
|
|
41
|
+
"Ferramentas locais: read_file, list_dir, write_file, edit_file, run_command.",
|
|
42
|
+
"Para mudar um arquivo existente use edit_file (troca de trecho exato); write_file e para arquivo novo ou reescrita completa.",
|
|
43
|
+
"Ferramentas com nome mcp__<servidor>__<ferramenta> vem de servidores MCP conectados: use quando ajudarem.",
|
|
44
|
+
`spawn_subagent lanca subagentes com objetivo proprio em contexto separado (ate 3 em paralelo). Use para investigar varios pontos ao mesmo tempo, para auditoria/teste ou para tarefa longa; cada subagente devolve so o relatorio final.`,
|
|
45
|
+
"Nunca invente conteudo de arquivo: leia antes de editar. Prefira caminhos relativos.",
|
|
46
|
+
"Quem escreve ou executa passa por aprovacao do usuario, entao explique em uma frase o que vai fazer.",
|
|
47
|
+
"Responda no idioma do usuario, de forma direta, com Markdown.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Roda um subagente: contexto proprio, ferramentas limitadas e relatorio final.
|
|
53
|
+
* Os eventos (onSubagentEvent) deixam a TUI mostrar o progresso ao vivo.
|
|
54
|
+
*/
|
|
55
|
+
async function runSubagentTask({
|
|
56
|
+
task,
|
|
57
|
+
index,
|
|
58
|
+
total,
|
|
59
|
+
model,
|
|
60
|
+
cwd,
|
|
61
|
+
approve,
|
|
62
|
+
autoAllowed,
|
|
63
|
+
signal,
|
|
64
|
+
depth,
|
|
65
|
+
onSubagentEvent,
|
|
66
|
+
}) {
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
const baseTools = TOOL_SPECS.filter((spec) => task.tools.includes(spec.function.name));
|
|
69
|
+
// Um nivel de subagente: o filho nao abre netos (evita recursao descontrolada).
|
|
70
|
+
const nested = [];
|
|
71
|
+
|
|
72
|
+
onSubagentEvent?.({
|
|
73
|
+
index,
|
|
74
|
+
total,
|
|
75
|
+
objective: task.objective,
|
|
76
|
+
phase: "start",
|
|
77
|
+
tools: task.tools,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let lastText = "";
|
|
81
|
+
try {
|
|
82
|
+
const result = await runAgent({
|
|
83
|
+
messages: [
|
|
84
|
+
{
|
|
85
|
+
role: "system",
|
|
86
|
+
content: subagentPrompt({ objective: task.objective, context: task.context, cwd, depth }),
|
|
87
|
+
},
|
|
88
|
+
{ role: "user", content: task.objective },
|
|
89
|
+
],
|
|
90
|
+
model,
|
|
91
|
+
cwd,
|
|
92
|
+
baseTools,
|
|
93
|
+
extraTools: nested,
|
|
94
|
+
maxSteps: task.maxSteps || DEFAULT_MAX_STEPS,
|
|
95
|
+
approve: approve
|
|
96
|
+
? (name, args, context) =>
|
|
97
|
+
approve(name, args, { ...context, subagent: { index, objective: task.objective } })
|
|
98
|
+
: undefined,
|
|
99
|
+
autoApprove: autoAllowed,
|
|
100
|
+
signal,
|
|
101
|
+
depth: depth + 1,
|
|
102
|
+
subagents: depth + 1 < MAX_DEPTH,
|
|
103
|
+
onSubagentEvent,
|
|
104
|
+
onText: (chunk) => {
|
|
105
|
+
lastText += chunk;
|
|
106
|
+
onSubagentEvent?.({ index, total, phase: "text", text: chunk, objective: task.objective });
|
|
107
|
+
},
|
|
108
|
+
onReasoning: (chunk) =>
|
|
109
|
+
onSubagentEvent?.({ index, total, phase: "thought", text: chunk, objective: task.objective }),
|
|
110
|
+
onToolStart: ({ name, label }) =>
|
|
111
|
+
onSubagentEvent?.({ index, total, phase: "tool", tool: name, text: label, objective: task.objective }),
|
|
112
|
+
onToolResult: ({ name, ok }) =>
|
|
113
|
+
onSubagentEvent?.({ index, total, phase: "tool-done", tool: name, ok, objective: task.objective }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const report = String(result.text || lastText || "").trim();
|
|
117
|
+
const body = report
|
|
118
|
+
? report.slice(0, MAX_SUBAGENT_REPORT_CHARS)
|
|
119
|
+
: result.ok
|
|
120
|
+
? "(subagente terminou sem texto final)"
|
|
121
|
+
: `(subagente falhou: ${result.error || "erro desconhecido"})`;
|
|
122
|
+
recordRun({
|
|
123
|
+
objective: task.objective,
|
|
124
|
+
tools: task.tools,
|
|
125
|
+
ok: Boolean(result.ok),
|
|
126
|
+
ms: Date.now() - started,
|
|
127
|
+
steps: result.steps || 0,
|
|
128
|
+
report: body,
|
|
129
|
+
});
|
|
130
|
+
onSubagentEvent?.({
|
|
131
|
+
index,
|
|
132
|
+
total,
|
|
133
|
+
objective: task.objective,
|
|
134
|
+
phase: "done",
|
|
135
|
+
ok: Boolean(result.ok),
|
|
136
|
+
ms: Date.now() - started,
|
|
137
|
+
});
|
|
138
|
+
return { index, objective: task.objective, ok: Boolean(result.ok), body, ms: Date.now() - started };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
onSubagentEvent?.({
|
|
141
|
+
index,
|
|
142
|
+
total,
|
|
143
|
+
objective: task.objective,
|
|
144
|
+
phase: "done",
|
|
145
|
+
ok: false,
|
|
146
|
+
ms: Date.now() - started,
|
|
147
|
+
});
|
|
148
|
+
recordRun({
|
|
149
|
+
objective: task.objective,
|
|
150
|
+
tools: task.tools,
|
|
151
|
+
ok: false,
|
|
152
|
+
ms: Date.now() - started,
|
|
153
|
+
report: error.message,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
index,
|
|
157
|
+
objective: task.objective,
|
|
158
|
+
ok: false,
|
|
159
|
+
body: `(subagente falhou: ${error.message})`,
|
|
160
|
+
ms: Date.now() - started,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Dispara as tarefas (o que for independente roda em paralelo) e junta os relatorios. */
|
|
166
|
+
async function runSubagents({ args, model, cwd, approve, autoAllowed, signal, depth, onSubagentEvent }) {
|
|
167
|
+
const tasks = normalizeTasks(args);
|
|
168
|
+
if (!tasks.length) {
|
|
169
|
+
return "Nenhuma tarefa recebida: informe `tasks: [{objective}]` (ou `objective`) para o subagente.";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const results = await Promise.all(
|
|
173
|
+
tasks.map((task, index) =>
|
|
174
|
+
runSubagentTask({
|
|
175
|
+
task,
|
|
176
|
+
index: index + 1,
|
|
177
|
+
total: tasks.length,
|
|
178
|
+
model: args?.model || model,
|
|
179
|
+
cwd,
|
|
180
|
+
approve,
|
|
181
|
+
autoAllowed,
|
|
182
|
+
signal,
|
|
183
|
+
depth,
|
|
184
|
+
onSubagentEvent,
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return results
|
|
190
|
+
.map(
|
|
191
|
+
(result) =>
|
|
192
|
+
`[subagente ${result.index}/${results.length}] objetivo: ${result.objective}\n` +
|
|
193
|
+
`status: ${result.ok ? "ok" : "falhou"} · ${result.ms}ms\n${result.body}`
|
|
194
|
+
)
|
|
195
|
+
.join("\n\n---\n\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Roda o loop: modelo -> tool calls -> resultados -> modelo, ate a resposta final.
|
|
200
|
+
* Ferramenta que escreve, edita, executa ou vem de MCP passa por `approve`
|
|
201
|
+
* (async, true/false), a menos que `autoApprove` esteja ligado ("rodar tudo").
|
|
202
|
+
*/
|
|
203
|
+
async function runAgent({
|
|
204
|
+
messages = [],
|
|
205
|
+
model,
|
|
206
|
+
cwd = process.cwd(),
|
|
207
|
+
approve,
|
|
208
|
+
autoApprove = false,
|
|
209
|
+
extraTools = [],
|
|
210
|
+
baseTools = TOOL_SPECS,
|
|
211
|
+
mcp = null,
|
|
212
|
+
signal,
|
|
213
|
+
maxSteps = Infinity,
|
|
214
|
+
depth = 0,
|
|
215
|
+
subagents = true,
|
|
216
|
+
onText,
|
|
217
|
+
onReasoning,
|
|
218
|
+
onToolStart,
|
|
219
|
+
onToolResult,
|
|
220
|
+
onSubagentEvent,
|
|
221
|
+
onRetry,
|
|
222
|
+
} = {}) {
|
|
223
|
+
const conversation = messages.map((message) => ({ ...message }));
|
|
224
|
+
if (!conversation.length || conversation[0].role !== "system") {
|
|
225
|
+
conversation.unshift({ role: "system", content: systemPrompt(cwd, model) });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// O subagente entra na lista de ferramentas enquanto a profundidade permitir.
|
|
229
|
+
const canSpawn = subagents && depth < MAX_DEPTH;
|
|
230
|
+
const tools = [
|
|
231
|
+
...baseTools,
|
|
232
|
+
...extraTools,
|
|
233
|
+
...(canSpawn ? [SUBAGENT_TOOL] : []),
|
|
234
|
+
];
|
|
235
|
+
// `autoApprove` pode ser um valor ou uma funcao: o "Permitir sempre" da TUI
|
|
236
|
+
// liga o modo no meio do loop, e o resto da execucao ja nao pergunta mais.
|
|
237
|
+
const autoAllowed = typeof autoApprove === "function" ? autoApprove : () => Boolean(autoApprove);
|
|
238
|
+
let last = { text: "", reasoning: "", ms: 0, usage: null };
|
|
239
|
+
|
|
240
|
+
for (let step = 0; step < (maxSteps || Infinity); step += 1) {
|
|
241
|
+
// Esc duas vezes na TUI aborta o signal: nao comeca outro passo.
|
|
242
|
+
if (signal?.aborted) {
|
|
243
|
+
return { ok: true, aborted: true, text: last.text, messages: conversation, steps: step };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const started = Date.now();
|
|
247
|
+
const result = await chat({
|
|
248
|
+
messages: conversation,
|
|
249
|
+
model,
|
|
250
|
+
tools,
|
|
251
|
+
signal,
|
|
252
|
+
onDelta: onText,
|
|
253
|
+
onReasoning,
|
|
254
|
+
onRetry,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!result.ok) {
|
|
258
|
+
return { ...result, aborted: result.code === "aborted", messages: conversation, text: last.text };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Stream cortado no meio: guarda o que ja chegou e encerra o loop.
|
|
262
|
+
if (result.aborted) {
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
aborted: true,
|
|
266
|
+
text: result.text || "",
|
|
267
|
+
reasoning: result.reasoning || "",
|
|
268
|
+
ms: result.ms,
|
|
269
|
+
messages: conversation,
|
|
270
|
+
steps: step + 1,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
last = {
|
|
275
|
+
text: result.text,
|
|
276
|
+
reasoning: result.reasoning,
|
|
277
|
+
ms: result.ms,
|
|
278
|
+
usage: result.usage,
|
|
279
|
+
thoughtMs: Date.now() - started,
|
|
280
|
+
};
|
|
281
|
+
const assistant = { role: "assistant", content: result.text || "" };
|
|
282
|
+
if (result.toolCalls.length) assistant.tool_calls = result.toolCalls;
|
|
283
|
+
conversation.push(assistant);
|
|
284
|
+
|
|
285
|
+
if (!result.toolCalls.length) {
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
...last,
|
|
289
|
+
keyCreated: result.keyCreated,
|
|
290
|
+
messages: conversation,
|
|
291
|
+
steps: step + 1,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const call of result.toolCalls) {
|
|
296
|
+
const name = call?.function?.name || "";
|
|
297
|
+
const args = parseArgs(call?.function?.arguments);
|
|
298
|
+
const fromMcp = Boolean(mcp?.has?.(name));
|
|
299
|
+
const isSubagent = name === SUBAGENT_TOOL_NAME;
|
|
300
|
+
// O subagente em si nao pede aprovacao: quem escreve/executa dentro dele
|
|
301
|
+
// continua passando pelo portao normal.
|
|
302
|
+
const needsApproval = !isSubagent && (APPROVAL_REQUIRED.has(name) || fromMcp);
|
|
303
|
+
const callId = call.id || `call_${step}_${name}`;
|
|
304
|
+
|
|
305
|
+
let change = null;
|
|
306
|
+
try {
|
|
307
|
+
change = previewToolChange(name, args, { cwd });
|
|
308
|
+
} catch {
|
|
309
|
+
change = null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const label = isSubagent ? describeSubagentCall(args) : describeToolCall(name, args);
|
|
313
|
+
const tasks = isSubagent ? normalizeTasks(args) : null;
|
|
314
|
+
|
|
315
|
+
onToolStart?.({
|
|
316
|
+
id: callId,
|
|
317
|
+
name,
|
|
318
|
+
args,
|
|
319
|
+
label,
|
|
320
|
+
change,
|
|
321
|
+
fromMcp,
|
|
322
|
+
needsApproval,
|
|
323
|
+
subagent: tasks ? { count: tasks.length, objectives: tasks.map((task) => task.objective) } : null,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const gate = needsApproval && !autoAllowed();
|
|
327
|
+
let allowed = true;
|
|
328
|
+
if (gate) {
|
|
329
|
+
allowed = approve ? Boolean(await approve(name, args, { change, fromMcp })) : false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const toolStarted = Date.now();
|
|
333
|
+
let output;
|
|
334
|
+
if (!allowed) {
|
|
335
|
+
output = "O usuario negou a execucao desta ferramenta. Peca instrucoes se precisar.";
|
|
336
|
+
} else if (isSubagent) {
|
|
337
|
+
output = await runSubagents({
|
|
338
|
+
args,
|
|
339
|
+
model,
|
|
340
|
+
cwd,
|
|
341
|
+
approve,
|
|
342
|
+
autoAllowed,
|
|
343
|
+
signal,
|
|
344
|
+
depth,
|
|
345
|
+
onSubagentEvent: (event) => onSubagentEvent?.({ ...event, id: callId }),
|
|
346
|
+
});
|
|
347
|
+
} else {
|
|
348
|
+
try {
|
|
349
|
+
output = await executeTool(name, args, { cwd, mcp });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
output = `Erro: ${error.message}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
onToolResult?.({
|
|
356
|
+
id: callId,
|
|
357
|
+
name,
|
|
358
|
+
args,
|
|
359
|
+
output,
|
|
360
|
+
change,
|
|
361
|
+
ok: allowed && !String(output).startsWith("Erro:"),
|
|
362
|
+
skipped: !allowed,
|
|
363
|
+
fromMcp,
|
|
364
|
+
ms: Date.now() - toolStarted,
|
|
365
|
+
summary: summarizeOutput(output),
|
|
366
|
+
subagent: tasks ? { count: tasks.length } : null,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
conversation.push({
|
|
370
|
+
role: "tool",
|
|
371
|
+
tool_call_id: callId,
|
|
372
|
+
content: String(output),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
error: `Limite de ${maxSteps} passos do agente atingido.`,
|
|
380
|
+
...last,
|
|
381
|
+
messages: conversation,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = { runAgent, runSubagents, MAX_STEPS, systemPrompt };
|