bimmo-cli 1.2.4 → 2.0.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "1.2.4",
4
- "description": "🌿 CLI universal para IAs com interface verde/lavanda, modo agente (Auto-Edit) e contexto inteligente de projeto.",
3
+ "version": "2.0.3",
4
+ "description": "🌿 Plataforma de IA universal com modo Normal, Agentes Especialistas e Enxames (Swarms). Suporte a Auto-Edit, Contexto Inteligente e interrupção inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
@@ -15,7 +15,10 @@
15
15
  "grok",
16
16
  "deepseek",
17
17
  "openrouter",
18
+ "zai",
18
19
  "agent",
20
+ "swarm",
21
+ "orchestration",
19
22
  "multimodal",
20
23
  "terminal"
21
24
  ],
package/src/config.js CHANGED
@@ -29,6 +29,7 @@ export async function configure() {
29
29
  choices: [
30
30
  { name: 'Criar novo perfil de IA', value: 'create' },
31
31
  { name: 'Selecionar perfil ativo', value: 'select' },
32
+ { name: 'Gerenciar Agentes Especialistas', value: 'agents' },
32
33
  { name: 'Configurar chave Tavily', value: 'tavily' },
33
34
  { name: 'Sair', value: 'exit' }
34
35
  ]
@@ -37,6 +38,8 @@ export async function configure() {
37
38
 
38
39
  if (action === 'exit') return;
39
40
 
41
+ if (action === 'agents') return configureAgents();
42
+
40
43
  if (action === 'tavily') {
41
44
  const { tavilyKey } = await inquirer.prompt([{
42
45
  type: 'input',
@@ -74,7 +77,7 @@ export async function configure() {
74
77
  {
75
78
  type: 'input',
76
79
  name: 'profileName',
77
- message: 'Dê um nome para este perfil (ex: MeuGPT, ClaudeTrabalho):',
80
+ message: 'Dê um nome para este perfil:',
78
81
  validate: i => i.length > 0 || 'Nome obrigatório'
79
82
  },
80
83
  {
@@ -120,6 +123,50 @@ export async function configure() {
120
123
  console.log(chalk.green(`\n✅ Perfil "${answers.profileName}" criado e ativado!`));
121
124
  }
122
125
 
126
+ async function configureAgents() {
127
+ const agents = config.get('agents') || {};
128
+ const profiles = config.get('profiles') || {};
129
+ const profileList = Object.keys(profiles);
130
+
131
+ const { action } = await inquirer.prompt([{
132
+ type: 'list',
133
+ name: 'action',
134
+ message: 'Gerenciar Agentes:',
135
+ choices: ['Criar Agente', 'Listar Agentes', 'Remover Agente', 'Voltar']
136
+ }]);
137
+
138
+ if (action === 'Voltar') return configure();
139
+
140
+ if (action === 'Criar Agente') {
141
+ if (profileList.length === 0) {
142
+ console.log(chalk.red('Crie um perfil de IA primeiro.'));
143
+ return configure();
144
+ }
145
+
146
+ const answers = await inquirer.prompt([
147
+ { type: 'input', name: 'name', message: 'Nome do Agente (ex: Arquiteto, Revisor):' },
148
+ { type: 'list', name: 'profile', message: 'Qual perfil este agente usará?', choices: profileList },
149
+ { type: 'input', name: 'modelOverride', message: 'Sobrescrever modelo (vazio para manter o do perfil):' },
150
+ { type: 'list', name: 'mode', message: 'Modo padrão:', choices: ['chat', 'plan', 'edit'] },
151
+ { type: 'editor', name: 'role', message: 'Descreva a Task/Papel deste agente (System Prompt):' }
152
+ ]);
153
+
154
+ agents[answers.name] = answers;
155
+ config.set('agents', agents);
156
+ console.log(chalk.green(`✓ Agente "${answers.name}" criado!`));
157
+ }
158
+
159
+ if (action === 'Listar Agentes') {
160
+ console.log(chalk.blue('\nAgentes Configurados:'));
161
+ Object.keys(agents).forEach(name => {
162
+ console.log(`- ${chalk.bold(name)} [${agents[name].profile}] (Modo: ${agents[name].mode})`);
163
+ });
164
+ console.log('');
165
+ }
166
+
167
+ return configureAgents();
168
+ }
169
+
123
170
  export function getConfig() {
124
171
  return config.store;
125
172
  }
package/src/interface.js CHANGED
@@ -7,10 +7,12 @@ import ora from 'ora';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import mime from 'mime-types';
10
+ import readline from 'readline';
10
11
 
11
12
  import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
12
13
  import { createProvider } from './providers/factory.js';
13
14
  import { getProjectContext } from './project-context.js';
15
+ import { SwarmOrchestrator } from './orchestrator.js';
14
16
 
15
17
  marked.use(new TerminalRenderer({
16
18
  heading: chalk.hex('#c084fc').bold,
@@ -23,7 +25,10 @@ const gray = chalk.gray;
23
25
  const bold = chalk.bold;
24
26
  const yellow = chalk.yellow;
25
27
 
26
- let currentMode = 'chat'; // 'chat', 'plan', 'edit'
28
+ let currentMode = 'chat';
29
+ let activePersona = null;
30
+ let exitCounter = 0;
31
+ let exitTimer = null;
27
32
 
28
33
  async function processInput(input) {
29
34
  const parts = input.split(' ');
@@ -91,10 +96,11 @@ async function processInput(input) {
91
96
  }
92
97
 
93
98
  function getModeStyle() {
99
+ const personaLabel = activePersona ? `[${activePersona.toUpperCase()}] ` : '';
94
100
  switch (currentMode) {
95
- case 'plan': return yellow.bold(' [PLAN] ');
96
- case 'edit': return chalk.red.bold(' [EDIT] ');
97
- default: return lavender.bold(' [CHAT] ');
101
+ case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
102
+ case 'edit': return chalk.red.bold(`${personaLabel}[EDIT] `);
103
+ default: return lavender.bold(`${personaLabel}[CHAT] `);
98
104
  }
99
105
  }
100
106
 
@@ -109,77 +115,120 @@ export async function startInteractive() {
109
115
  }
110
116
 
111
117
  let provider = createProvider(config);
112
- const messages = [];
118
+ const orchestrator = new SwarmOrchestrator(config);
119
+ let messages = [];
120
+
121
+ const resetMessages = () => {
122
+ messages = [];
123
+ const projectContext = getProjectContext();
124
+ messages.push({ role: 'system', content: projectContext });
125
+ if (activePersona) {
126
+ const agent = (config.agents || {})[activePersona];
127
+ if (agent) {
128
+ messages.push({ role: 'system', content: `Sua persona atual é: ${agent.name}. Sua tarefa: ${agent.role}` });
129
+ }
130
+ }
131
+ };
113
132
 
114
- // 1. Injeta o sistema de contexto inteligente do projeto
115
- const projectContext = getProjectContext();
116
- messages.push({
117
- role: 'system',
118
- content: projectContext
119
- });
133
+ resetMessages();
120
134
 
121
135
  console.clear();
122
136
  console.log(lavender(figlet.textSync('bimmo')));
123
137
  console.log(lavender('─'.repeat(60)));
124
- console.log(green(` Perfil Ativo: ${bold(config.activeProfile || 'Padrão')} (${config.provider.toUpperCase()})`));
138
+ console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
125
139
  console.log(green(` Modelo: ${bold(config.model)}`));
126
- console.log(gray(' /chat | /plan | /edit | /switch [perfil] | /model [novo] | /help'));
140
+ console.log(gray(' /chat | /plan | /edit | /swarm | /use [agente] | /help'));
127
141
  console.log(lavender('─'.repeat(60)) + '\n');
128
142
 
129
- console.log(lavender('👋 Olá! Sou seu agente BIMMO. No que posso atuar?\n'));
143
+ console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
144
+
145
+ // Handler Global de SIGINT para o modo ocioso (Idle)
146
+ process.on('SIGINT', () => {
147
+ exitCounter++;
148
+ if (exitCounter === 1) {
149
+ console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
150
+ if (exitTimer) clearTimeout(exitTimer);
151
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
152
+ } else {
153
+ console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
154
+ process.exit(0);
155
+ }
156
+ });
157
+
158
+ readline.emitKeypressEvents(process.stdin);
159
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
130
160
 
131
161
  while (true) {
132
162
  const modeIndicator = getModeStyle();
133
- const { input } = await inquirer.prompt([
134
- {
135
- type: 'input',
136
- name: 'input',
137
- message: modeIndicator + green('Você'),
138
- prefix: '',
139
- }
140
- ]);
163
+ let input;
164
+
165
+ try {
166
+ const answers = await inquirer.prompt([
167
+ {
168
+ type: 'input',
169
+ name: 'input',
170
+ message: modeIndicator + green('Você'),
171
+ prefix: '→',
172
+ }
173
+ ]);
174
+ input = answers.input;
175
+ } catch (e) {
176
+ // Inquirer joga erro no Ctrl+C se não for tratado
177
+ continue;
178
+ }
141
179
 
142
180
  const rawInput = input.trim();
143
181
  const cmd = rawInput.toLowerCase();
144
182
 
145
183
  if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') {
146
184
  console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
147
- break;
185
+ process.exit(0);
148
186
  }
149
187
 
150
188
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
151
189
  if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
152
190
  if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red('⚠️ Modo EDIT.\n')); continue; }
153
191
 
154
- // /switch [perfil] -> Troca Perfil + Chave + Provedor + Modelo instantaneamente
155
192
  if (cmd.startsWith('/switch ')) {
156
193
  const profileName = rawInput.split(' ')[1];
157
194
  if (profileName && switchProfile(profileName)) {
158
- config = getConfig(); // Atualiza config local
159
- provider = createProvider(config); // Recria provedor com nova chave/url
160
- console.log(green(`\n✓ Trocado para o perfil "${bold(profileName)}"!`));
161
- console.log(gray(` IA: ${config.provider.toUpperCase()} | Modelo: ${config.model}\n`));
162
- } else {
163
- console.log(chalk.red(`\n✖ Perfil "${profileName}" não encontrado.\n`));
195
+ config = getConfig();
196
+ provider = createProvider(config);
197
+ console.log(green(`\n✓ Perfil "${bold(profileName)}" ativado!`));
198
+ continue;
164
199
  }
200
+ console.log(chalk.red(`\n✖ Perfil não encontrado.\n`));
165
201
  continue;
166
202
  }
167
203
 
168
- // /model [modelo] -> Troca apenas o modelo do Perfil atual
169
- if (cmd.startsWith('/model ')) {
170
- const newModel = rawInput.split(' ')[1];
171
- if (newModel) {
172
- updateActiveModel(newModel);
173
- config.model = newModel;
174
- provider = createProvider(config);
175
- console.log(green(`\n✓ Modelo atualizado para: ${bold(newModel)}\n`));
204
+ if (cmd.startsWith('/use ')) {
205
+ const agentName = rawInput.split(' ')[1];
206
+ const agents = config.agents || {};
207
+ if (agentName === 'normal' || agentName === 'default') {
208
+ activePersona = null;
209
+ console.log(lavender(`\n✓ Voltando para o Modo Normal.\n`));
210
+ resetMessages();
211
+ continue;
212
+ }
213
+ if (agents[agentName]) {
214
+ activePersona = agentName;
215
+ const agent = agents[agentName];
216
+ if (switchProfile(agent.profile)) {
217
+ config = getConfig();
218
+ provider = createProvider(config);
219
+ }
220
+ currentMode = agent.mode || 'chat';
221
+ console.log(green(`\n✓ Agora você está falando com o Agente: ${bold(agentName)}`));
222
+ console.log(gray(` Task: ${agent.role.substring(0, 100)}...\n`));
223
+ resetMessages();
224
+ } else {
225
+ console.log(chalk.red(`\n✖ Agente "${agentName}" não encontrado.\n`));
176
226
  }
177
227
  continue;
178
228
  }
179
229
 
180
230
  if (cmd === '/clear') {
181
- messages.length = 0;
182
- messages.push({ role: 'system', content: getProjectContext() });
231
+ resetMessages();
183
232
  console.clear();
184
233
  console.log(lavender('✓ Histórico limpo, contexto preservado.\n'));
185
234
  continue;
@@ -187,61 +236,73 @@ export async function startInteractive() {
187
236
 
188
237
  if (cmd === '/help') {
189
238
  console.log(gray(`
190
- Comandos Disponíveis:
239
+ Comandos de Modo:
191
240
  /chat /plan /edit → Mudar modo de operação
192
- /switch [nome] Mudar PERFIL (Troca Chave/API/IA completa)
193
- /model [nome] Mudar apenas o MODELO da IA atual
194
- /init Inicializar .bimmorc.json neste projeto
195
- /config → Gerenciar perfis e chaves
196
- /clear → Resetar conversa (mantém contexto base)
197
- @caminho Anexar arquivos ou imagens
241
+ /use [agente] Usar um Agente Especialista
242
+ /use normal Voltar para o chat normal
243
+ /swarm Rodar fluxos complexos
244
+
245
+ Gerenciamento:
246
+ /switch [nome] Mudar perfil de IA completo
247
+ /model [nome] → Mudar modelo atual
248
+ /config → Perfis e Agentes
249
+ /init → Inicializar .bimmorc.json
250
+ @arquivo → Ler arquivo ou imagem
198
251
  `));
199
252
  continue;
200
253
  }
201
254
 
202
- if (cmd === '/init') {
203
- const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
204
- if (fs.existsSync(bimmoRcPath)) {
205
- const { overwrite } = await inquirer.prompt([{
206
- type: 'confirm',
207
- name: 'overwrite',
208
- message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
209
- default: false
210
- }]);
211
- if (!overwrite) continue;
212
- }
255
+ if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
213
256
 
214
- const initialConfig = {
215
- projectName: path.basename(process.cwd()),
216
- rules: [
217
- "Siga as convenções de código existentes.",
218
- "Prefira código limpo e modular.",
219
- "Sempre valide mudanças antes de aplicar no modo EDIT."
220
- ],
221
- preferredTech: [],
222
- architecture: "Não especificada",
223
- ignorePatterns: ["node_modules", "dist", ".git"]
224
- };
225
-
226
- fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
227
- console.log(green(`\n✅ Arquivo .bimmorc.json criado com sucesso em: ${bold(bimmoRcPath)}\n`));
257
+ if (cmd === '/swarm') {
258
+ const agents = config.agents || {};
259
+ const agentList = Object.keys(agents);
260
+ if (agentList.length < 2) {
261
+ console.log(chalk.yellow('\nCrie pelo menos 2 Agentes em /config primeiro.\n'));
262
+ continue;
263
+ }
264
+ const { swarmAction } = await inquirer.prompt([{
265
+ type: 'list',
266
+ name: 'swarmAction',
267
+ message: 'Tipo de Enxame:',
268
+ choices: ['Sequencial (A → B)', 'Hierárquico (Líder + Workers)', 'Voltar']
269
+ }]);
270
+ if (swarmAction === 'Voltar') continue;
271
+ const { goal } = await inquirer.prompt([{ type: 'input', name: 'goal', message: 'Objetivo do enxame:' }]);
228
272
 
229
- // Recarrega o contexto para a conversa atual
230
- messages.push({
231
- role: 'system',
232
- content: `Novo contexto inicializado via /init:\n${JSON.stringify(initialConfig, null, 2)}`
233
- });
273
+ try {
274
+ let result;
275
+ if (swarmAction.includes('Sequencial')) {
276
+ const { selectedAgents } = await inquirer.prompt([{ type: 'checkbox', name: 'selectedAgents', message: 'Ordem dos agentes:', choices: agentList }]);
277
+ result = await orchestrator.runSequential(selectedAgents, goal);
278
+ } else {
279
+ const { manager } = await inquirer.prompt([{ type: 'list', name: 'manager', message: 'Líder:', choices: agentList }]);
280
+ const { workers } = await inquirer.prompt([{ type: 'checkbox', name: 'workers', message: 'Workers:', choices: agentList.filter(a => a !== manager) }]);
281
+ result = await orchestrator.runHierarchical(manager, workers, goal);
282
+ }
283
+ console.log(lavender('\n=== RESULTADO FINAL ===\n') + marked(result));
284
+ } catch (e) {
285
+ console.error(chalk.red(`\nErro: ${e.message}`));
286
+ }
234
287
  continue;
235
288
  }
236
289
 
237
- if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
238
-
239
290
  if (rawInput === '') continue;
240
291
 
241
- // Injeção dinâmica de instruções de modo
292
+ const controller = new AbortController();
293
+
294
+ // Handler local para SIGINT durante o processamento da IA
295
+ const localInterruptHandler = () => {
296
+ controller.abort();
297
+ };
298
+
299
+ // Remove temporariamente o handler global de saída
300
+ process.removeAllListeners('SIGINT');
301
+ process.on('SIGINT', localInterruptHandler);
302
+
242
303
  let modeInstr = "";
243
- if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Descreva e analise, mas NÃO altere arquivos.";
244
- else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Você tem permissão para usar write_file e run_command AGORA.";
304
+ if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
305
+ else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Aplique as mudanças agora.";
245
306
 
246
307
  const content = await processInput(rawInput);
247
308
  messages.push({
@@ -250,22 +311,41 @@ Comandos Disponíveis:
250
311
  });
251
312
 
252
313
  const spinner = ora({
253
- text: lavender(`bimmo (${currentMode}) pensando...`),
314
+ text: lavender(`bimmo pensando... (Ctrl+C para interromper)`),
254
315
  color: currentMode === 'edit' ? 'red' : 'magenta'
255
316
  }).start();
256
317
 
257
318
  try {
258
- const responseText = await provider.sendMessage(messages);
319
+ let responseText = await provider.sendMessage(messages, { signal: controller.signal });
259
320
  spinner.stop();
321
+ const cleanedText = responseText.replace(/<\/?[^>]+(>|$)/g, "");
260
322
  messages.push({ role: 'assistant', content: responseText });
261
-
262
323
  console.log('\n' + lavender('bimmo') + getModeStyle());
263
324
  console.log(lavender('─'.repeat(50)));
264
- console.log(marked(responseText));
325
+ console.log(marked(cleanedText));
265
326
  console.log(gray('─'.repeat(50)) + '\n');
266
327
  } catch (err) {
267
328
  spinner.stop();
268
- console.error(chalk.red('✖ Erro Crítico:') + ' ' + err.message + '\n');
329
+ if (controller.signal.aborted || err.name === 'AbortError') {
330
+ console.log(yellow('\n\n⚠️ Operação interrompida pelo usuário.\n'));
331
+ messages.pop();
332
+ } else {
333
+ console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
334
+ }
335
+ } finally {
336
+ // Restaura o handler global de saída
337
+ process.removeListener('SIGINT', localInterruptHandler);
338
+ process.on('SIGINT', () => {
339
+ exitCounter++;
340
+ if (exitCounter === 1) {
341
+ console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
342
+ if (exitTimer) clearTimeout(exitTimer);
343
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
344
+ } else {
345
+ console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
346
+ process.exit(0);
347
+ }
348
+ });
269
349
  }
270
350
  }
271
- }
351
+ }
@@ -0,0 +1,84 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createProvider } from './providers/factory.js';
4
+ import { getProjectContext } from './project-context.js';
5
+
6
+ export class SwarmOrchestrator {
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.agents = config.agents || {};
10
+ this.profiles = config.profiles || {};
11
+ }
12
+
13
+ /**
14
+ * Executa uma tarefa sequencialmente através de uma lista de agentes.
15
+ * O output de um agente vira o contexto do próximo.
16
+ */
17
+ async runSequential(agentNames, goal, options = {}) {
18
+ let currentContext = goal;
19
+ const results = [];
20
+
21
+ console.log(chalk.cyan(`\n🚀 Iniciando Enxame Sequencial: ${agentNames.join(' → ')}\n`));
22
+
23
+ for (const name of agentNames) {
24
+ const agent = this.agents[name];
25
+ if (!agent) throw new Error(`Agente ${name} não encontrado.`);
26
+
27
+ const profile = this.profiles[agent.profile];
28
+ if (!profile) throw new Error(`Perfil ${agent.profile} do agente ${name} não encontrado.`);
29
+
30
+ const agentConfig = {
31
+ ...profile,
32
+ model: agent.modelOverride || profile.model
33
+ };
34
+
35
+ const provider = createProvider(agentConfig);
36
+ const spinner = ora({
37
+ text: chalk.magenta(`Agente [${name}] trabalhando...`),
38
+ color: agent.mode === 'edit' ? 'red' : 'magenta'
39
+ }).start();
40
+
41
+ const messages = [
42
+ { role: 'system', content: `Você é o agente ${name}. Sua tarefa específica é: ${agent.role}\n\nMODO ATUAL: ${agent.mode.toUpperCase()}\n${getProjectContext()}` },
43
+ { role: 'user', content: `CONTEXTO ATUAL:\n${currentContext}\n\nOBJETIVO DO ENXAME:\n${goal}\n\nPor favor, execute sua parte e retorne o resultado final processado.` }
44
+ ];
45
+
46
+ try {
47
+ const response = await provider.sendMessage(messages, { signal: options.signal });
48
+ spinner.succeed(chalk.green(`Agente [${name}] concluído.`));
49
+
50
+ currentContext = response;
51
+ results.push({ agent: name, output: response });
52
+ } catch (err) {
53
+ spinner.fail(chalk.red(`Agente [${name}] falhou: ${err.message}`));
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ return currentContext;
59
+ }
60
+
61
+ /**
62
+ * Executa uma tarefa hierárquica.
63
+ * Um Manager recebe o objetivo, define o que cada Worker deve fazer, e consolida.
64
+ */
65
+ async runHierarchical(managerName, workerNames, goal, options = {}) {
66
+ console.log(chalk.cyan(`\n👑 Iniciando Enxame Hierárquico (Líder: ${managerName})\n`));
67
+
68
+ // Passo 1: Manager analisa e delega
69
+ const managerOutput = await this.runSequential([managerName], `Analise o objetivo abaixo e descreva o que cada um dos seguintes agentes deve fazer: ${workerNames.join(', ')}.\n\nOBJETIVO: ${goal}`);
70
+
71
+ // Passo 2: Workers executam baseado na delegação do Manager (em paralelo para velocidade)
72
+ console.log(chalk.blue(`\n👷 Workers entrando em ação...\n`));
73
+ const workerPromises = workerNames.map(name =>
74
+ this.runSequential([name], `Baseado no plano do Manager:\n${managerOutput}\n\nExecute sua tarefa para o objetivo: ${goal}`, options)
75
+ );
76
+
77
+ const workerResults = await Promise.all(workerPromises);
78
+
79
+ // Passo 3: Manager consolida tudo
80
+ const finalResult = await this.runSequential([managerName], `Aqui estão os resultados dos workers:\n${workerResults.join('\n---\n')}\n\nPor favor, consolide tudo em um resultado final perfeito para o objetivo: ${goal}`, options);
81
+
82
+ return finalResult;
83
+ }
84
+ }
@@ -21,7 +21,7 @@ export class AnthropicProvider extends BaseProvider {
21
21
  });
22
22
  }
23
23
 
24
- async sendMessage(messages) {
24
+ async sendMessage(messages, options = {}) {
25
25
  const systemMessage = messages.find(m => m.role === 'system');
26
26
  const userMessages = messages
27
27
  .filter(m => m.role !== 'system')
@@ -30,7 +30,6 @@ export class AnthropicProvider extends BaseProvider {
30
30
  content: this.formatContent(m.content)
31
31
  }));
32
32
 
33
- // Converte tools do agent.js para o formato da Anthropic
34
33
  const anthropicTools = tools.map(t => ({
35
34
  name: t.name,
36
35
  description: t.description,
@@ -44,17 +43,18 @@ export class AnthropicProvider extends BaseProvider {
44
43
  messages: userMessages,
45
44
  tools: anthropicTools,
46
45
  temperature: 0.7
47
- });
46
+ }, { signal: options.signal });
48
47
 
49
48
  if (response.stop_reason === 'tool_use') {
50
49
  const toolUse = response.content.find(p => p.type === 'tool_use');
51
50
  const tool = tools.find(t => t.name === toolUse.name);
52
51
 
53
52
  if (tool) {
53
+ if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
54
+
54
55
  console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
55
56
  const result = await tool.execute(toolUse.input);
56
57
 
57
- // Adiciona a resposta da IA e o resultado da tool ao histórico
58
58
  const nextMessages = [
59
59
  ...messages,
60
60
  { role: 'assistant', content: response.content },
@@ -68,10 +68,10 @@ export class AnthropicProvider extends BaseProvider {
68
68
  }
69
69
  ];
70
70
 
71
- return this.sendMessage(nextMessages);
71
+ return this.sendMessage(nextMessages, options);
72
72
  }
73
73
  }
74
74
 
75
75
  return response.content[0].text;
76
76
  }
77
- }
77
+ }
@@ -3,7 +3,7 @@ export class BaseProvider {
3
3
  this.config = config;
4
4
  }
5
5
 
6
- async sendMessage(messages) {
6
+ async sendMessage(messages, options = {}) {
7
7
  throw new Error('Método sendMessage deve ser implementado');
8
8
  }
9
- }
9
+ }
@@ -18,7 +18,7 @@ export class GeminiProvider extends BaseProvider {
18
18
  });
19
19
  }
20
20
 
21
- async sendMessage(messages) {
21
+ async sendMessage(messages, options = {}) {
22
22
  const systemPrompt = messages.find(m => m.role === 'system')?.content;
23
23
  const history = messages
24
24
  .filter(m => m.role !== 'system')
@@ -30,7 +30,6 @@ export class GeminiProvider extends BaseProvider {
30
30
 
31
31
  const lastMessageContent = this.formatContent(messages[messages.length - 1].content);
32
32
 
33
- // Converte tools do agent.js para o formato do Gemini
34
33
  const geminiTools = tools.map(t => ({
35
34
  functionDeclarations: [{
36
35
  name: t.name,
@@ -53,19 +52,19 @@ export class GeminiProvider extends BaseProvider {
53
52
  const result = await chat.sendMessage(lastMessageContent);
54
53
  const response = await result.response;
55
54
 
56
- // Processamento de Tool Calls no Gemini
57
55
  const call = response.candidates[0].content.parts.find(p => p.functionCall);
58
56
  if (call) {
57
+ if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
58
+
59
59
  const tool = tools.find(t => t.name === call.functionCall.name);
60
60
  if (tool) {
61
61
  console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
62
- const result = await tool.execute(call.functionCall.args);
62
+ const toolResult = await tool.execute(call.functionCall.args);
63
63
 
64
- // No Gemini, enviamos o resultado de volta para o chat
65
64
  const resultResponse = await chat.sendMessage([{
66
65
  functionResponse: {
67
66
  name: call.functionCall.name,
68
- response: { content: result }
67
+ response: { content: toolResult }
69
68
  }
70
69
  }]);
71
70
  return resultResponse.response.text();
@@ -74,4 +73,4 @@ export class GeminiProvider extends BaseProvider {
74
73
 
75
74
  return response.text();
76
75
  }
77
- }
76
+ }
@@ -1,5 +1,6 @@
1
1
  import OpenAI from 'openai';
2
2
  import { BaseProvider } from './base.js';
3
+ import { tools } from '../agent.js';
3
4
 
4
5
  export class GrokProvider extends BaseProvider {
5
6
  constructor(config) {
@@ -33,15 +34,15 @@ export class GrokProvider extends BaseProvider {
33
34
  });
34
35
  }
35
36
 
36
- async sendMessage(messages) {
37
+ async sendMessage(messages, options = {}) {
37
38
  const formattedMessages = this.formatMessages(messages);
38
39
 
39
40
  const response = await this.client.chat.completions.create({
40
41
  model: this.config.model,
41
42
  messages: formattedMessages,
42
43
  temperature: 0.7
43
- });
44
+ }, { signal: options.signal });
44
45
 
45
46
  return response.choices[0].message.content;
46
47
  }
47
- }
48
+ }
@@ -13,7 +13,7 @@ export class OllamaProvider extends BaseProvider {
13
13
 
14
14
  const images = msg.content
15
15
  .filter(part => part.type === 'image')
16
- .map(part => part.data); // Ollama espera apenas a string base64
16
+ .map(part => part.data);
17
17
 
18
18
  const text = msg.content
19
19
  .filter(part => part.type === 'text')
@@ -28,7 +28,7 @@ export class OllamaProvider extends BaseProvider {
28
28
  });
29
29
  }
30
30
 
31
- async sendMessage(messages) {
31
+ async sendMessage(messages, options = {}) {
32
32
  const formattedMessages = this.formatMessages(messages);
33
33
 
34
34
  const response = await this.client.chat({
@@ -38,7 +38,7 @@ export class OllamaProvider extends BaseProvider {
38
38
  options: {
39
39
  temperature: 0.7,
40
40
  }
41
- });
41
+ }, { signal: options.signal });
42
42
 
43
43
  const text = response.message?.content;
44
44
 
@@ -48,4 +48,4 @@ export class OllamaProvider extends BaseProvider {
48
48
 
49
49
  return text;
50
50
  }
51
- }
51
+ }
@@ -34,7 +34,7 @@ export class OpenAIProvider extends BaseProvider {
34
34
  });
35
35
  }
36
36
 
37
- async sendMessage(messages) {
37
+ async sendMessage(messages, options = {}) {
38
38
  const formattedMessages = this.formatMessages(messages);
39
39
 
40
40
  const openAiTools = tools.map(t => ({
@@ -46,25 +46,26 @@ export class OpenAIProvider extends BaseProvider {
46
46
  }
47
47
  }));
48
48
 
49
- const options = {
49
+ const requestOptions = {
50
50
  model: this.config.model,
51
51
  messages: formattedMessages,
52
52
  temperature: 0.7
53
53
  };
54
54
 
55
- // Só envia tools se houver alguma definida para evitar erros em alguns provedores
56
55
  if (openAiTools.length > 0) {
57
- options.tools = openAiTools;
58
- options.tool_choice = 'auto';
56
+ requestOptions.tools = openAiTools;
57
+ requestOptions.tool_choice = 'auto';
59
58
  }
60
59
 
61
- const response = await this.client.chat.completions.create(options);
60
+ const response = await this.client.chat.completions.create(requestOptions, { signal: options.signal });
62
61
 
63
62
  const message = response.choices[0].message;
64
63
 
65
64
  if (message.tool_calls) {
66
65
  const toolResults = [];
67
66
  for (const toolCall of message.tool_calls) {
67
+ if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
68
+
68
69
  const tool = tools.find(t => t.name === toolCall.function.name);
69
70
  if (tool) {
70
71
  console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
@@ -80,9 +81,9 @@ export class OpenAIProvider extends BaseProvider {
80
81
  }
81
82
 
82
83
  const nextMessages = [...formattedMessages, message, ...toolResults];
83
- return this.sendMessage(nextMessages);
84
+ return this.sendMessage(nextMessages, options);
84
85
  }
85
86
 
86
87
  return message.content;
87
88
  }
88
- }
89
+ }