primocode 7.2.0 → 7.4.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 CHANGED
@@ -1,16 +1,35 @@
1
- # PrimoCode v7.1.0
1
+ # PrimoCode v7.4.0
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
- Cria arquivos de verdade, roda comandos, abre o navegador — e lembra do que fez
5
- entre uma sessão e outra.
4
+ Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
5
+ e lembra do que fez entre uma sessão e outra.
6
6
 
7
7
  ```
8
- ╷
9
- ╭─┴─╮ PrimoCode
10
- │◕ ◕│ v7.1.0
11
- ╰───╯ projeto: jogo-de-carro
8
+ ╭◠╮
9
+ ╭─┴─┴─╮
10
+ ◖┫ ● ● ┣◗ PrimoCode v7.4.0
11
+ ┃ ‿ ┃ projeto: jogo-de-carro
12
+ ╰──┬──╯
13
+ ╰─┴─╯
12
14
  ```
13
15
 
16
+ Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
17
+ veja `/mascot` e `/name`.
18
+
19
+ ## Por que trocar o Claude Code pelo PrimoCode
20
+
21
+ | | PrimoCode | Assistentes gringos de terminal |
22
+ |---|---|---|
23
+ | **Idioma** | Nasceu em português do Brasil — fala como gente, não como manual traduzido | Inglês primeiro; PT-BR é tradução |
24
+ | **Chave de API** | **Não precisa.** Instala e usa — o modelo já vem hospedado | Exige conta e chave de API própria |
25
+ | **Controla a máquina** | Navegador (Chrome real) **e** desktop: mouse, teclado, tela | Em geral só arquivos e shell |
26
+ | **Memória** | Cada projeto lembra o que foi feito, entre sessões | Esquece ao fechar |
27
+ | **Instalação** | Node puro, **zero dependências** — segundos | Pacotes e dependências |
28
+ | **Cara própria** | Mascote, nome e temas que você escolhe | Fixo |
29
+ | **Custo** | Roda em modelos rápidos e baratos (Groq/Llama) | Cobrança por token da API |
30
+
31
+ Em uma frase: **é o poder de um agente de código no terminal, em português, controlando o seu computador de verdade — sem você precisar configurar chave de API nenhuma.**
32
+
14
33
  ## Instalação
15
34
 
16
35
  ```bash
@@ -61,6 +80,101 @@ fim. É por isso que a IA não esquece o projeto entre sessões.
61
80
 
62
81
  Alterne com `/mode primocode` ou `/mode chat`.
63
82
 
83
+ ## Ferramentas do agente
84
+
85
+ | Tool | Para quê |
86
+ |---|---|
87
+ | `write_file` | Cria arquivo novo ou reescreve inteiro |
88
+ | `edit_file` | **Troca um trecho exato.** É o que evita reescrever 800 linhas pra mudar uma |
89
+ | `multi_edit` | Várias trocas no mesmo arquivo, em ordem, tudo-ou-nada |
90
+ | `read_file` | Lê o arquivo, com `offset`/`limit` para os grandes |
91
+ | `glob` | Acha arquivos por padrão: `**/*.js`, `src/**/*.{ts,tsx}` |
92
+ | `grep` | Procura texto/regex **dentro** dos arquivos, com arquivo e linha |
93
+ | `list_dir` | Árvore de arquivos |
94
+ | `run_shell` | Roda comando na pasta do projeto |
95
+ | `run_in_new_terminal` | Sobe servidor de dev / watch em outro terminal |
96
+ | `write_todos` | Plano de tarefas da sessão, visível no terminal |
97
+ | `open_in_browser` | Abre HTML ou URL no navegador padrão |
98
+ | `spawn_agent` | Delega uma sub-tarefa a outro agente |
99
+
100
+ O `edit_file` é rigoroso de propósito: se o trecho não existe, ou aparece mais
101
+ de uma vez sem `replace_all`, ele **recusa** e devolve as linhas reais do
102
+ arquivo — em vez de editar o lugar errado.
103
+
104
+ ### Controle de navegador e desktop
105
+
106
+ O agente não só abre o navegador — ele **age** nele, e no computador também.
107
+
108
+ | Tool | Para quê |
109
+ |---|---|
110
+ | `browser_open` | Abre a página num Chrome controlado (CDP) e espera carregar |
111
+ | `browser_click` | Clica por texto visível, seletor CSS ou coordenada |
112
+ | `browser_type` | Digita num campo (por seletor) ou onde o foco estiver |
113
+ | `browser_key` | Enter, Tab, Escape, setas… |
114
+ | `browser_read` | Lê o texto visível da página (conferir o resultado) |
115
+ | `browser_screenshot` | Print da página, salvo no projeto |
116
+ | `browser_scroll` / `browser_wait` / `browser_eval` | Rolar, esperar seletor, rodar JS |
117
+ | `desktop_screenshot` | Print da tela inteira do usuário |
118
+ | `desktop_click` / `desktop_move` / `desktop_drag` | Mouse na tela real |
119
+ | `desktop_type` / `desktop_key` / `desktop_scroll` | Teclado e rolagem do sistema |
120
+ | `desktop_size` | Resolução da tela, para calcular coordenadas |
121
+
122
+ O navegador roda via Chrome DevTools Protocol (sem dependências npm).
123
+
124
+ **Requisitos por plataforma** (as ferramentas dão um erro claro se faltar algo):
125
+
126
+ | | Navegador | Desktop (mouse/teclado) | Screenshot da tela |
127
+ |---|---|---|---|
128
+ | **Windows** | Chrome ou Edge | nativo (PowerShell) | nativo (PowerShell) |
129
+ | **macOS** | Chrome/Chromium | `brew install cliclick` | nativo (`screencapture`) |
130
+ | **Linux** | Chrome/Chromium | `apt install xdotool` | `scrot`, `gnome-screenshot`, `spectacle`, `maim` ou ImageMagick |
131
+
132
+ Variáveis de ambiente úteis:
133
+ - `CHROME_PATH` / `PRIMOCODE_CHROME` — aponta para um Chrome fora do caminho padrão
134
+ - `PRIMOCODE_BROWSER_HEADLESS=1` — roda o navegador sem janela (CI/servidor)
135
+
136
+ ## Subagentes
137
+
138
+ Para tarefas com partes independentes, o agente delega:
139
+
140
+ ```
141
+ Primo Code site ❯ crie o visual do site
142
+
143
+ ◆ Agente criar o CSS
144
+ ⎿ Write style.css
145
+ ⎿ criado · 40 B
146
+ ◆ Criei style.css com fundo #111
147
+
148
+ Pronto: CSS delegado e criado
149
+ ```
150
+
151
+ O subagente tem conversa própria — **não vê o seu diálogo** — e devolve só o
152
+ resumo final. Ele não pode criar outro subagente, então a árvore nunca cresce
153
+ sem controle.
154
+
155
+ ## Nunca "pronto!" falso
156
+
157
+ Se uma ferramenta falha e o agente tenta encerrar dizendo que deu certo, o
158
+ PrimoCode barra e devolve a tarefa para ele:
159
+
160
+ ```
161
+ ⎿ Edit app.js
162
+ ⎿ Trecho não encontrado
163
+ → agente tenta finish("Porta alterada")
164
+ → BARRADO: "NÃO terminou. Estas tools falharam..."
165
+ ⎿ Edit app.js
166
+ ⎿ 1 troca · ±0 linhas ← agora foi de verdade
167
+ ```
168
+
169
+ ## Testes
170
+
171
+ ```bash
172
+ npm test
173
+ ```
174
+
175
+ 25 verificações sobre glob, grep, edit_file, leitura por partes e bloqueio de
176
+ caminho fora do projeto.
177
+
64
178
  ## Uso
65
179
 
66
180
  ```bash
@@ -83,6 +197,9 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
83
197
  | `/run <comando>` | Executa no shell, dentro da pasta do projeto |
84
198
  | `/open <arquivo\|url>` | Abre no navegador |
85
199
  | `/status` | Health check do servidor |
200
+ | `/name <nome>` | Como o Primo te chama na saudação |
201
+ | `/mascot primo\|fox\|cat` | Escolhe o mascote (mostra os três) |
202
+ | `/config` | Mostra suas preferências e onde ficam salvas |
86
203
  | `/clear` · `/cls` | Limpa conversa / tela |
87
204
  | `/uninstall` | Remove o PrimoCode (seus projetos ficam) |
88
205
  | `/exit` | Sair |
package/bin/primocode.js CHANGED
@@ -13,7 +13,8 @@ const fs = require('fs');
13
13
  const os = require('os');
14
14
 
15
15
  const pkg = require('../package.json');
16
- const { c, welcomeBox, Spinner } = require('../lib/ui');
16
+ const ui = require('../lib/ui');
17
+ const { c, welcomeBox, Spinner } = ui;
17
18
  const api = require('../lib/api');
18
19
  const tools = require('../lib/tools');
19
20
  const cfg = require('../lib/config');
@@ -35,6 +36,7 @@ const HELP = `
35
36
  ${c.bold('Comandos')}
36
37
  ${c.brand('/project')} lista projetos ou muda: /project <nome>
37
38
  ${c.brand('/mode')} alterna: /mode primocode|chat
39
+ ${c.brand('/model')} alterna: /model main|fast
38
40
  ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
39
41
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
40
42
  ${c.brand('/tree')} [n] árvore do projeto
@@ -42,6 +44,9 @@ ${c.bold('Comandos')}
42
44
  ${c.brand('/run')} <comando> executa no shell do projeto
43
45
  ${c.brand('/open')} <arquivo|url> abre no navegador
44
46
  ${c.brand('/status')} checa o servidor
47
+ ${c.brand('/name')} <nome> como o Primo te chama
48
+ ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
49
+ ${c.brand('/config')} mostra suas preferências
45
50
  ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
46
51
  ${c.brand('/uninstall')} remove o PrimoCode
47
52
  ${c.brand('/exit')} sair
@@ -178,7 +183,7 @@ async function runChat(promptForModel, userTextForMemory) {
178
183
  model: state.config.model,
179
184
  token: state.config.token,
180
185
  }, (evt) => {
181
- if (evt.erro) { if (!started) sp.stop(); console.log(c.err(`\nerro: ${evt.erro}`)); return; }
186
+ if (evt.erro) { if (!started) sp.stop(); console.log('\n' + c.err(ui.friendlyError(evt.erro))); return; }
182
187
  if (evt.delta) {
183
188
  if (!started) { sp.stop(); process.stdout.write('\n'); started = true; }
184
189
  text += evt.delta;
@@ -188,7 +193,7 @@ async function runChat(promptForModel, userTextForMemory) {
188
193
  });
189
194
  } catch (e) {
190
195
  sp.stop();
191
- console.log(c.err(`\n${e.message || e}`));
196
+ console.log('\n' + c.err(ui.friendlyError(e)));
192
197
  return;
193
198
  }
194
199
  sp.stop();
@@ -294,6 +299,47 @@ async function handleSlash(text) {
294
299
  break;
295
300
  }
296
301
 
302
+ case 'name': {
303
+ if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
304
+ state.config.userName = rest;
305
+ cfg.saveConfig({ userName: rest });
306
+ console.log(c.ok(`Feito! Vou te chamar de ${rest}.`));
307
+ break;
308
+ }
309
+
310
+ case 'mascot': {
311
+ const estilos = ['primo', 'fox', 'cat'];
312
+ const m = rest.toLowerCase();
313
+ if (!m) {
314
+ console.log('');
315
+ for (const e of estilos) {
316
+ console.log(` ${e === state.config.mascot ? c.brand('● ' + e) : c.muted('○ ' + e)}`);
317
+ ui.mascot(e).forEach((l) => console.log(' ' + l));
318
+ console.log('');
319
+ }
320
+ console.log(c.muted(' Escolha: /mascot <estilo>'));
321
+ break;
322
+ }
323
+ if (!estilos.includes(m)) { console.log(c.err(`Uso: /mascot ${estilos.join('|')}`)); break; }
324
+ state.config.mascot = m;
325
+ cfg.saveConfig({ mascot: m });
326
+ console.log('');
327
+ ui.mascot(m).forEach((l) => console.log(' ' + l));
328
+ console.log(c.ok(`\nMascote: ${m}`));
329
+ break;
330
+ }
331
+
332
+ case 'config': {
333
+ console.log('');
334
+ console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
335
+ console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
336
+ console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}`);
337
+ console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
338
+ console.log(` ${c.muted('servidor')} ${c.dim(state.config.server)}`);
339
+ console.log(c.muted(`\n ${cfg.CONFIG_FILE}`));
340
+ break;
341
+ }
342
+
297
343
  case 'clear': state.history = []; console.log(c.ok('Conversa limpa.')); break;
298
344
  case 'cls': process.stdout.write('\x1Bc'); break;
299
345
 
@@ -373,7 +419,10 @@ async function main() {
373
419
  process.exit(0);
374
420
  }
375
421
 
376
- console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null));
422
+ console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null, {
423
+ mascot: state.config.mascot,
424
+ userName: state.config.userName,
425
+ }));
377
426
  rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
378
427
  rl.prompt();
379
428
 
@@ -400,7 +449,7 @@ async function main() {
400
449
  if (isSlash(text)) await handleSlash(text);
401
450
  else await sendPrompt(text);
402
451
  } catch (e) {
403
- console.log(c.err(`\n${e.message || e}`));
452
+ console.log('\n' + c.err(ui.friendlyError(e)));
404
453
  }
405
454
  }
406
455
  busy = false;
package/lib/act.js CHANGED
@@ -1,52 +1,124 @@
1
1
  // Loop do agente autônomo — streaming via /api/act/stream.
2
2
  //
3
3
  // O contrato com o servidor é uma conversa completa (`messages`, estilo OpenAI).
4
- // Cada volta do loop: o servidor devolve tool_calls -> executamos AQUI, com await,
5
- // e devolvemos o resultado na próxima volta. Sem isso o modelo fica cego e repete
6
- // a mesma tool até estourar o limite.
4
+ // Cada volta: o servidor devolve tool_calls -> executamos AQUI, com await, e
5
+ // devolvemos o resultado na volta seguinte. Sem isso o modelo fica cego e
6
+ // repete a mesma tool até estourar o limite.
7
+ //
8
+ // spawn_agent roda este mesmo loop recursivamente, com conversa própria: o
9
+ // subagente não enxerga o diálogo do usuário e devolve só o resumo final.
7
10
 
8
11
  'use strict';
9
12
 
10
- const { c, actionHeader, Spinner } = require('./ui');
13
+ const { c, actionHeader, Spinner, friendlyError } = require('./ui');
11
14
  const tools = require('./tools');
12
15
  const api = require('./api');
13
16
  const cfg = require('./config');
14
17
 
15
18
  const MAX_ITERATIONS = 15;
19
+ const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
20
+
21
+ function ident(profundidade) {
22
+ return ' '.repeat(profundidade);
23
+ }
16
24
 
17
- function logResult(kind, result) {
18
- const prefix = c.muted(' ⎿ ');
25
+ function logResult(kind, result, profundidade) {
26
+ const p = ident(profundidade) + c.muted(' ⎿ ');
19
27
  if (!result) return;
20
- if (result.ok === false) { console.log(prefix + c.err(result.error || `exit ${result.code ?? 'erro'}`)); return; }
28
+ if (result.ok === false) { console.log(p + c.err(result.error || `exit ${result.code ?? 'erro'}`)); return; }
21
29
 
22
30
  switch (kind) {
23
31
  case 'write_file':
24
- console.log(prefix + c.muted(`${result.created ? 'created' : 'updated'} · ${tools.fmtBytes(result.bytes)}`));
32
+ console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`));
25
33
  return;
34
+ case 'edit_file': {
35
+ const delta = result.linhasDepois - result.linhasAntes;
36
+ const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
37
+ console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas'));
38
+ return;
39
+ }
26
40
  case 'read_file':
27
- console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes || 0)}${result.truncated ? ' (truncated)' : ''}`));
41
+ console.log(p + c.muted(result.de
42
+ ? `linhas ${result.de}–${result.ate} de ${result.linhas}`
43
+ : `${result.linhas} linhas · ${tools.fmtBytes(result.bytes || 0)}${result.truncado ? ' (cortado)' : ''}`));
28
44
  return;
29
45
  case 'list_dir': {
30
- const lines = (result.tree || '').split('\n').filter(Boolean);
31
- for (const l of lines.slice(0, 8)) console.log(prefix + c.muted(l));
32
- if (lines.length > 8) console.log(prefix + c.muted(`… +${lines.length - 8}`));
46
+ const linhas = (result.tree || '').split('\n').filter(Boolean);
47
+ for (const l of linhas.slice(0, 8)) console.log(p + c.muted(l));
48
+ if (linhas.length > 8) console.log(p + c.muted(`… +${linhas.length - 8}`));
33
49
  return;
34
50
  }
35
- case 'run_shell':
36
- console.log(prefix + c.ok('exit 0'));
51
+ case 'glob':
52
+ console.log(p + c.muted(`${result.total} arquivo${result.total === 1 ? '' : 's'}`));
53
+ for (const f of (result.arquivos || []).slice(0, 5)) console.log(p + c.dim(f));
54
+ if ((result.arquivos || []).length > 5) console.log(p + c.dim(`… +${result.arquivos.length - 5}`));
37
55
  return;
56
+ case 'grep': {
57
+ console.log(p + c.muted(`${result.total} ocorrência${result.total === 1 ? '' : 's'} em ${result.arquivosVistos} arquivos`));
58
+ for (const r of (result.resultados || []).slice(0, 5)) {
59
+ console.log(p + c.dim(`${r.arquivo}:${r.linha}`));
60
+ }
61
+ if ((result.resultados || []).length > 5) console.log(p + c.dim(`… +${result.resultados.length - 5}`));
62
+ return;
63
+ }
64
+ case 'run_shell': {
65
+ const saida = (result.stdout || '').trim().split('\n').filter(Boolean);
66
+ for (const l of saida.slice(0, 6)) console.log(p + c.muted(l.slice(0, 160)));
67
+ if (saida.length > 6) console.log(p + c.muted(`… +${saida.length - 6} linhas`));
68
+ if (!saida.length) console.log(p + c.ok('exit 0'));
69
+ return;
70
+ }
38
71
  case 'open_in_browser':
39
- case 'run_in_new_terminal':
40
- console.log(prefix + c.muted(result.opener || 'ok'));
72
+ console.log(p + c.muted(result.opener || 'aberto'));
73
+ return;
74
+ case 'multi_edit': {
75
+ const delta = result.linhasDepois - result.linhasAntes;
76
+ const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
77
+ console.log(p + c.muted(`${result.substituicoes} trocas · `) + sinal + c.muted(' linhas'));
78
+ return;
79
+ }
80
+ case 'write_todos': {
81
+ const marca = { completed: c.ok('✔'), in_progress: c.brand('▶'), pending: c.dim('○') };
82
+ for (const t of result.todos || []) {
83
+ const texto = t.status === 'completed' ? c.dim(t.content) : c.white(t.content);
84
+ console.log(p + `${marca[t.status] || c.dim('○')} ${texto}`);
85
+ }
86
+ console.log(p + c.muted(`${result.feitos}/${result.total} concluídas`));
87
+ return;
88
+ }
89
+ case 'browser_open':
90
+ console.log(p + c.muted(result.url || 'aberto'));
91
+ return;
92
+ case 'browser_read':
93
+ console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
94
+ return;
95
+ case 'browser_screenshot':
96
+ case 'desktop_screenshot':
97
+ console.log(p + c.muted(`${result.path} · ${tools.fmtBytes(result.bytes || 0)}`));
98
+ return;
99
+ case 'browser_click':
100
+ case 'browser_type':
101
+ case 'browser_key':
102
+ case 'browser_scroll':
103
+ case 'browser_wait':
104
+ case 'browser_eval':
105
+ case 'desktop_click':
106
+ case 'desktop_move':
107
+ case 'desktop_drag':
108
+ case 'desktop_type':
109
+ case 'desktop_key':
110
+ case 'desktop_scroll':
111
+ case 'desktop_size':
112
+ console.log(p + c.ok('ok') + (result.text || result.tag || result.selector || result.width ? c.muted(' · ' + (result.selector || result.tag || (result.width ? `${result.width}×${result.height}` : ''))) : ''));
41
113
  return;
42
114
  }
43
115
  }
44
116
 
45
- // O que mandamos de volta ao modelo. Cortamos saídas gigantes para não
46
- // estourar a janela de contexto na iteração seguinte.
117
+ // O que volta para o modelo. Cortamos saídas gigantes para não estourar a
118
+ // janela de contexto na iteração seguinte.
47
119
  function summarizeForModel(result) {
48
120
  const json = JSON.stringify(result);
49
- return json.length > 4000 ? json.slice(0, 4000) + '…(truncado)' : json;
121
+ return json.length > 6000 ? json.slice(0, 6000) + '…(truncado)' : json;
50
122
  }
51
123
 
52
124
  function buildMemoryContext(projectDir, projectName) {
@@ -59,24 +131,31 @@ function buildMemoryContext(projectDir, projectName) {
59
131
  }
60
132
 
61
133
  async function runAct(userPrompt, opts) {
62
- const { server, model, token, history, projectDir, projectName, ensureWorkDir } = opts || {};
134
+ const {
135
+ server, model, token, history, projectDir, projectName,
136
+ ensureWorkDir, profundidade = 0, subagent = false,
137
+ } = opts || {};
138
+
63
139
  // A pasta só nasce quando o agente for mesmo mexer em arquivo. Um "oi"
64
140
  // não deve deixar diretório vazio espalhado pelo disco.
65
141
  let workDirReady = !ensureWorkDir;
142
+ const falhasPendentes = [];
143
+ let tentativasDeEncerrar = 0;
66
144
 
67
- // Conversa completa desta tarefa: histórico recente + memória + pedido.
68
145
  const messages = [];
69
- const memory = buildMemoryContext(projectDir, projectName);
70
- if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
71
- for (const m of (history || []).slice(-20)) {
72
- if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
146
+ if (!subagent) {
147
+ const memory = buildMemoryContext(projectDir, projectName);
148
+ if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
149
+ for (const m of (history || []).slice(-20)) {
150
+ if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
151
+ }
73
152
  }
74
153
  messages.push({ role: 'user', content: userPrompt });
75
154
 
76
155
  let finalText = '';
77
156
 
78
157
  for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
79
- const sp = new Spinner('Primo Code pensando').start();
158
+ const sp = new Spinner(subagent ? 'subagente trabalhando' : 'Primo Code pensando').start();
80
159
 
81
160
  const toolCalls = [];
82
161
  let assistantText = '';
@@ -84,40 +163,46 @@ async function runAct(userPrompt, opts) {
84
163
  let streaming = false;
85
164
 
86
165
  try {
87
- await api.requestAct(server, { messages, model, token, projectDir, projectName }, (evt) => {
166
+ await api.requestAct(server, { messages, model, token, projectDir, projectName, subagent }, (evt) => {
88
167
  if (evt.erro) { streamError = evt.erro; return; }
89
168
  if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
90
169
  if (evt.delta) {
91
170
  if (!streaming) { sp.stop(); streaming = true; }
92
171
  assistantText += evt.delta;
93
- process.stdout.write(c.white(evt.delta));
172
+ if (!subagent) process.stdout.write(c.white(evt.delta));
94
173
  }
95
174
  if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
96
175
  });
97
176
  } catch (e) {
98
177
  sp.stop();
99
- console.log(c.err(`\nErro: ${e.message || e}`));
178
+ const amigavel = friendlyError(e);
179
+ if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
180
+ console.log('\n' + c.err(amigavel));
100
181
  throw e;
101
182
  }
102
183
  sp.stop();
103
184
 
104
185
  if (streamError) {
105
- console.log(c.err(`\nErro: ${streamError}`));
106
- return { assistantMessage: { role: 'assistant', content: `Erro: ${streamError}` } };
186
+ const amigavel = friendlyError(streamError);
187
+ if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` } };
188
+ console.log('\n' + c.err(amigavel));
189
+ return { assistantMessage: { role: 'assistant', content: amigavel } };
107
190
  }
108
191
 
109
192
  // Sem tool_calls: o modelo respondeu e acabou.
110
193
  if (!toolCalls.length) {
111
194
  finalText = assistantText.trim();
112
- if (finalText && !streaming) console.log('\n' + c.white(finalText));
113
- else if (streaming) process.stdout.write('\n');
195
+ if (!subagent) {
196
+ if (finalText && !streaming) console.log('\n' + c.white(finalText));
197
+ else if (streaming) process.stdout.write('\n');
198
+ }
114
199
  break;
115
200
  }
116
201
 
117
- if (streaming) process.stdout.write('\n');
202
+ if (streaming && !subagent) process.stdout.write('\n');
118
203
 
119
- // Registra na conversa o turno do assistente COM os tool_calls.
120
- // A API exige que cada mensagem role:'tool' venha depois deste turno.
204
+ // Registra o turno do assistente COM os tool_calls. A API exige que
205
+ // cada mensagem role:'tool' venha logo depois deste turno.
121
206
  messages.push({
122
207
  role: 'assistant',
123
208
  content: assistantText || null,
@@ -131,26 +216,55 @@ async function runAct(userPrompt, opts) {
131
216
  let finished = null;
132
217
  for (const tc of toolCalls) {
133
218
  if (tc.name === 'finish') {
219
+ // Encerrar com falha pendente é como o agente dizia "pronto!"
220
+ // sem ter mudado nada. Devolvemos a tarefa para ele.
221
+ if (falhasPendentes.length && tentativasDeEncerrar < 2) {
222
+ tentativasDeEncerrar++;
223
+ messages.push({
224
+ role: 'tool',
225
+ tool_call_id: tc.id,
226
+ content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.join('; ')}. `
227
+ + 'Conserte agora (leia o arquivo antes de editar) ou, se for impossível, diga claramente ao usuário o que falhou.',
228
+ });
229
+ continue;
230
+ }
134
231
  finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
135
232
  messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
136
233
  continue;
137
234
  }
138
235
 
236
+ if (tc.name === 'spawn_agent') {
237
+ const r = await runSubagent(tc.arguments || {}, {
238
+ server, model, token, projectDir, projectName, profundidade,
239
+ });
240
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
241
+ continue;
242
+ }
243
+
139
244
  if (!workDirReady) { ensureWorkDir(); workDirReady = true; }
140
245
 
141
- console.log(actionHeader(tc.name, tc.arguments));
246
+ console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
142
247
  const result = await tools.execute(tc.name, tc.arguments, { projectDir });
143
- logResult(tc.name, result);
248
+ logResult(tc.name, result, profundidade);
249
+
250
+ // Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
251
+ if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
252
+ else if (['write_file', 'edit_file'].includes(tc.name)) falhasPendentes.length = 0;
253
+
144
254
  messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
145
255
  }
146
256
 
147
- if (finished) { finalText = finished; console.log('\n' + c.white(finished)); break; }
257
+ if (finished) {
258
+ finalText = finished;
259
+ if (!subagent) console.log('\n' + c.white(finished));
260
+ break;
261
+ }
148
262
  }
149
263
 
150
264
  if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
151
265
 
152
266
  // Nada de arquivo mexido = nada para lembrar.
153
- if (projectName && workDirReady) {
267
+ if (!subagent && projectName && workDirReady) {
154
268
  cfg.appendProjectMemory(projectDir, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
155
269
  }
156
270
 
@@ -160,4 +274,27 @@ async function runAct(userPrompt, opts) {
160
274
  };
161
275
  }
162
276
 
277
+ async function runSubagent(args, ctx) {
278
+ const { server, model, token, projectDir, projectName, profundidade } = ctx;
279
+ const descricao = args.description || 'sub-tarefa';
280
+
281
+ if (profundidade + 1 >= MAX_PROFUNDIDADE) {
282
+ return 'Recusado: subagente não pode criar outro subagente. Faça você mesmo.';
283
+ }
284
+ if (!args.prompt) return 'Recusado: prompt do subagente vazio.';
285
+
286
+ console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(descricao)}`);
287
+
288
+ const r = await runAct(args.prompt, {
289
+ server, model, token, projectDir, projectName,
290
+ history: [],
291
+ profundidade: profundidade + 1,
292
+ subagent: true,
293
+ });
294
+
295
+ const resumo = r.assistantMessage.content || 'sem resultado';
296
+ console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.muted(resumo.slice(0, 120))}`);
297
+ return resumo;
298
+ }
299
+
163
300
  module.exports = { runAct };