primocode 4.3.1 → 7.0.1

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/lib/act.js CHANGED
@@ -1,500 +1,72 @@
1
- // Modo Primo Code — faz tudo sozinho: código, arquivos, terminal, navegador
2
- // REGRA FUNDAMENTAL: NUNCA entregue código no CMD/terminal.
3
- // SEMPRE use write_file para criar arquivos.
4
- // Se for executável (HTML, etc.), use open_in_browser para abrir.
5
- // Se for Python/JS, use run_in_new_terminal para executar.
6
-
1
+ // Loop do agente autônomo — vai e volta entre server e execução local.
7
2
 
8
3
  'use strict';
9
4
 
10
- const path = require('path');
11
- const { c, actionHeader, toolLine, Spinner } = require('./ui');
5
+ const { c, actionHeader, Spinner } = require('./ui');
12
6
  const tools = require('./tools');
13
7
  const api = require('./api');
14
8
 
15
9
  const MAX_ITERATIONS = 15;
16
10
 
17
- function fmtArgs(name, args) {
18
- if (!args) return '';
19
- switch (name) {
20
- case 'read_file': return args.path || '';
21
- case 'write_file': return `${args.path || ''} · ${(args.content || '').length} chars`;
22
- case 'list_dir': return args.path || '.';
23
- case 'run_shell': return args.command || '';
24
- case 'run_in_new_terminal':return `${args.command || ''}${args.cwd ? ' @ ' + args.cwd : ''}`;
25
- case 'open_in_browser': return args.target || '';
26
- case 'browser_open': return args.url || '';
27
- case 'browser_navigate': return args.url || '';
28
- case 'browser_click': return args.selector || args.text || (args.x != null ? `${args.x},${args.y}` : '');
29
- case 'browser_type': return `${args.selector ? args.selector + ' · ' : ''}${(args.text || '').length} chars`;
30
- case 'browser_key': return args.key || '';
31
- case 'browser_wait_for': return args.selector || '';
32
- case 'browser_wait': return `${args.ms || 1000}ms`;
33
- case 'browser_screenshot': return args.path || '(auto)';
34
- case 'browser_eval': return (args.js || '').slice(0, 60);
35
- case 'browser_scroll': return `dy=${args.dy || 400}`;
36
- case 'browser_url': return '';
37
- case 'finish': return args.summary || '';
38
- default: return JSON.stringify(args).slice(0, 80);
39
- }
40
- }
41
-
42
- // Nomes simples das ações
43
- const NICE = {
44
- read_file: 'Read', write_file: 'Write', list_dir: 'List',
45
- run_shell: 'Bash', run_in_new_terminal: 'Terminal', open_in_browser: 'Open',
46
- browser_open: 'Browser', browser_navigate: 'Navigate', browser_click: 'Click',
47
- browser_clickText: 'ClickText', browser_type: 'Type', browser_key: 'Key',
48
- browser_wait_for: 'Wait', browser_wait: 'Wait', browser_screenshot: 'Screenshot',
49
- browser_eval: 'Eval', browser_scroll: 'Scroll', browser_url: 'URL', finish: 'Finish',
50
- };
51
-
52
11
  function makeLogger() {
53
12
  return {
54
13
  event(kind, arg, result) {
55
14
  if (kind === 'run_shell') return;
56
15
  if (kind === 'run_shell_done') {
57
- if (result) {
58
- const s = result.ok ? c.ok(' exit 0') : c.err(` exit ${result.code}${result.killed ? ' timeout' : ''}`);
59
- process.stdout.write('\n' + s + '\n');
60
- }
16
+ if (result) { const s = result.ok ? c.ok(' exit 0') : c.err(` exit ${result.code}${result.killed?' timeout':''}`); process.stdout.write('\n'+s+'\n'); }
61
17
  return;
62
18
  }
63
19
  const prefix = c.muted(' ⎿ ');
64
- if (kind === 'write_file' && result && result.ok) {
65
- console.log(prefix + c.muted(`${result.created ? 'created' : 'updated'} · ${tools.fmtBytes(result.bytes)}`));
66
- return;
67
- }
68
- if (kind === 'read_file' && result && result.ok) {
69
- console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes || 0)}${result.truncated ? ' (truncated)' : ''}`));
70
- return;
71
- }
72
- if (kind === 'list_dir' && result && result.ok) {
73
- const lines = result.tree.split('\n').slice(0, 8);
74
- for (const l of lines) console.log(prefix + c.muted(l));
75
- const total = result.tree.split('\n').length;
76
- if (total > 8) console.log(prefix + c.muted(`… +${total - 8}`));
77
- return;
78
- }
79
- if (kind === 'open_in_browser' && result && result.ok) {
80
- console.log(prefix + c.muted(`${result.opener || 'browser'}`));
81
- return;
82
- }
83
- if (kind === 'run_in_new_terminal' && result && result.ok) {
84
- console.log(prefix + c.muted(`${result.opener}`));
85
- return;
86
- }
87
- if (kind === 'finish') {
88
- console.log(prefix + c.muted(arg || 'ok'));
89
- return;
90
- }
91
- if (result && result.ok === false) {
92
- console.log(prefix + c.err(result.error || 'failed'));
93
- }
20
+ if (kind === 'write_file' && result && result.ok) { console.log(prefix + c.muted(`${result.created?'created':'updated'} · ${tools.fmtBytes(result.bytes)}`)); return; }
21
+ if (kind === 'read_file' && result && result.ok) { console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes||0)}${result.truncated?' (truncated)':''}`)); return; }
22
+ if (kind === 'list_dir' && result && result.ok) { const lines = result.tree.split('\n').slice(0,8); for (const l of lines) console.log(prefix + c.muted(l)); const total = result.tree.split('\n').length; if (total>8) console.log(prefix + c.muted(`… +${total-8}`)); return; }
23
+ if (kind === 'open_in_browser' && result && result.ok) { console.log(prefix + c.muted(`${result.opener||'browser'}`)); return; }
24
+ if (kind === 'run_in_new_terminal' && result && result.ok) { console.log(prefix + c.muted(`${result.opener}`)); return; }
25
+ if (kind === 'finish') { console.log(prefix + c.muted(arg || 'ok')); return; }
26
+ if (result && result.ok === false) console.log(prefix + c.err(result.error || 'failed'));
94
27
  },
95
28
  };
96
29
  }
97
30
 
98
- /**
99
- * Executa uma tarefa em modo Act (agente autônomo com tools).
100
- * @param {string} userPrompt - texto do usuário
101
- * @param {object} opts - { server, model, history: [{role,content}, ...] }
102
- * @returns {Promise<{userMessage, assistantMessage}>} para o caller atualizar seu histórico
103
- */
104
31
  async function runAct(userPrompt, opts) {
105
- const { server, model, history: chatHistory = [] } = opts;
106
-
107
- // Injeta contexto do CWD e inclui histórico curto de conversa
108
- const cwd = process.cwd();
109
- const cwdBase = path.basename(cwd);
110
- const enriched = `[contexto: diretório atual = ${cwd} (basename: '${cwdBase}')]\n\n${userPrompt}`;
111
-
112
- // Estado do loop
113
- // Começamos com as mensagens do chat (para memória) + o novo user prompt.
114
- let messages = [];
115
- if (Array.isArray(chatHistory) && chatHistory.length) {
116
- // Só passamos user/assistant sem tool_calls (o histórico de tool calls foi episódico).
117
- for (const m of chatHistory) {
118
- if (m && (m.role === 'user' || m.role === 'assistant') && m.content) {
119
- messages.push({ role: m.role, content: m.content });
120
- }
32
+ const { server, model, history, projectDir, projectName } = opts || {};
33
+ const logger = makeLogger();
34
+ const messages = [
35
+ ...(Array.isArray(history) ? history.filter(m => m && m.role && m.content).slice(-20) : []),
36
+ { role: 'user', content: userPrompt },
37
+ ];
38
+ const toolResults = [];
39
+ let iteration = 0;
40
+
41
+ while (iteration < MAX_ITERATIONS) {
42
+ iteration++;
43
+ const sp = new Spinner('pensando').start();
44
+ let response;
45
+ try { response = await api.requestAct(server, messages, model); }
46
+ catch (e) { sp.stop(); console.log(c.err(`\nErro no servidor: ${e.message||e}`)); throw e; }
47
+ sp.stop();
48
+
49
+ if (!response.tool_calls || !response.tool_calls.length) {
50
+ const content = response.content || '';
51
+ console.log('\n' + c.white(content));
52
+ return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content } };
121
53
  }
122
- }
123
-
124
- let promptForCall = enriched;
125
- let toolResults = null;
126
- let finished = false;
127
- const log = makeLogger();
128
- const recentCalls = [];
129
- let finalAssistantText = '';
130
- let sp = null;
131
- let planCache = ''; // cache do plano do Planner entre turnos
132
- let planShown = false; // evita mostrar plano múltiplas vezes
133
- const filesWritten = new Set(); // arquivos criados/modificados pelo Construtor 1 (para refino)
134
- const deferredOpens = []; // open_in_browser adiados até depois do refino
135
-
136
- for (let iter = 1; iter <= MAX_ITERATIONS && !finished; iter++) {
137
- let assistantContent = '';
138
- let assistantMessage = null;
139
- let toolCalls = [];
140
- let serverErr = null;
141
- let hasToolCalls = false;
142
- let streamingStarted = false;
143
- let printedUpTo = 0; // quantos chars de assistantContent já foram impressos
144
- let suppressRest = false; // uma vez detectado marker, para de imprimir
145
- let retryNeeded = false; // server pedidu retry (ex: finish rejeitado)
146
- let rejectedNote = null; // motivo da rejeição, injetado como tool_result no próximo turno
147
- let subagentsPayload = null; // resultado de spawn_parallel (injetado como tool_result)
148
- let subagentsToolCallId = null;
149
- let subagentsTotal = 0;
150
- let subagentsDone = 0;
151
-
152
- // Spinner enquanto aguarda primeiro byte
153
- sp = new Spinner('Pensando').start();
154
54
 
155
- try {
156
- await api.requestStream(server, '/api/act/stream', {
157
- prompt: promptForCall,
158
- history: messages,
159
- tool_results: toolResults,
160
- model,
161
- plan_cache: planCache,
162
- }, (evt) => {
163
- if (evt.phase === 'inspiring') {
164
- if (sp) sp.stop();
165
- sp = new Spinner('Buscando referências').start();
166
- }
167
- if (evt.inspiration_urls && Array.isArray(evt.inspiration_urls) && evt.inspiration_urls.length > 0) {
168
- if (sp) { sp.stop(); sp = null; }
169
- console.log('\n' + c.muted('◆ Inspirando-se em:'));
170
- for (const u of evt.inspiration_urls) console.log(c.muted(' · ') + c.accent(u));
171
- }
172
- if (evt.phase === 'planning' && !planShown) {
173
- if (sp) sp.stop();
174
- sp = new Spinner('Pensando').start();
175
- }
176
- if (evt.plan) {
177
- planCache = evt.plan;
178
- if (sp) { sp.stop(); sp = null; }
179
- planShown = true;
180
- sp = new Spinner('Executando').start();
181
- }
182
- if (evt.subagents_spawning) {
183
- subagentsTotal = evt.subagents_spawning;
184
- if (sp) sp.stop();
185
- console.log('\n' + c.muted(`◆ Spawn de ${subagentsTotal} subagentes em paralelo`));
186
- sp = new Spinner(`Subagentes 0/${subagentsTotal}`).start();
187
- }
188
- if (evt.subagent_done) {
189
- subagentsDone++;
190
- const d = evt.subagent_done;
191
- if (sp) { sp.stop(); sp = null; }
192
- const status = d.ok ? c.ok('✓') : c.err('✗');
193
- console.log(` ${status} ${c.muted(d.title)} ${c.muted('· ' + (d.bytes || 0) + ' bytes')}`);
194
- if (subagentsDone < subagentsTotal) sp = new Spinner(`Subagentes ${subagentsDone}/${subagentsTotal}`).start();
195
- }
196
- if (evt.subagents_result) {
197
- subagentsPayload = evt.subagents_result;
198
- subagentsToolCallId = evt.tool_call_id;
199
- if (sp) { sp.stop(); sp = null; }
200
- }
201
- if (evt.erro) { serverErr = evt.erro; return; }
202
- if (evt.delta) {
203
- if (!streamingStarted) {
204
- sp.stop();
205
- process.stdout.write('\n');
206
- streamingStarted = true;
207
- }
208
- assistantContent += evt.delta;
209
-
210
- if (suppressRest) return;
211
-
212
- // Se aparece começo de marker suspeito, marca e para de imprimir daqui em diante.
213
- // Também "rebobina" cortando o que já foi acumulado até antes do marker
214
- // (não conseguimos apagar do terminal, mas o texto final ficará limpo).
215
- const leakRe = /<\||【|\n\s*\{\s*"(name|type|summary|arguments|command|path)"|\bfinish\s*\(|\bwrite_file\s*\(|\brun_shell\s*\(|\bopen_in_browser\s*\(|\bbrowser_\w+\s*\(/;
216
- const m = assistantContent.match(leakRe);
217
- if (m && m.index !== undefined) {
218
- const safeEnd = m.index;
219
- if (safeEnd > printedUpTo) {
220
- process.stdout.write(c.white(assistantContent.slice(printedUpTo, safeEnd)));
221
- printedUpTo = safeEnd;
222
- }
223
- suppressRest = true;
224
- return;
225
- }
226
-
227
- // Imprime até len-2 para dar margem se um marker começar a formar
228
- const target = Math.max(printedUpTo, assistantContent.length - 2);
229
- if (target > printedUpTo) {
230
- process.stdout.write(c.white(assistantContent.slice(printedUpTo, target)));
231
- printedUpTo = target;
232
- }
233
- }
234
- if (evt.tool_call) toolCalls.push(evt.tool_call);
235
- if (evt.assistant_message) assistantMessage = evt.assistant_message;
236
- if (evt.tool_rejected) rejectedNote = evt.tool_rejected;
237
- if (evt.retry_needed) retryNeeded = true;
238
- if (evt.done) hasToolCalls = !!evt.has_tool_calls;
239
- if (evt.instability) { serverErr = 'servidor com rate-limit; tente novamente em alguns minutos'; return; }
240
- });
241
- } catch (e) {
242
- if (sp) sp.stop();
243
- console.log(c.err(`\nerro: ${e.message}`));
244
- return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: `[erro: ${e.message}]` } };
55
+ for (const tc of response.tool_calls) {
56
+ const name = tc.function?.name || tc.name;
57
+ let args = {}; try { args = JSON.parse(tc.function?.arguments || tc.arguments || '{}'); } catch {}
58
+ console.log(actionHeader(name, args));
59
+ logger.event(name, null, null);
60
+ let result; try { result = await tools.execute(name, args); } catch (e) { result = { ok: false, error: e.message || String(e) }; }
61
+ logger.event(name + '_done', null, result);
62
+ toolResults.push({ tool_call_id: tc.id, role: 'tool', name, content: JSON.stringify(result) });
245
63
  }
246
64
 
247
- if (sp && !streamingStarted) sp.stop();
248
-
249
- // Flush do buffer de 2 chars retido para evitar corte de markers
250
- if (streamingStarted && !suppressRest && printedUpTo < assistantContent.length) {
251
- process.stdout.write(c.white(assistantContent.slice(printedUpTo)));
252
- printedUpTo = assistantContent.length;
253
- }
254
-
255
- if (serverErr) {
256
- console.log(c.err(`\nerro: ${serverErr}`));
257
- return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: `[erro: ${serverErr}]` } };
258
- }
259
-
260
- if (streamingStarted) process.stdout.write('\n');
261
-
262
- // Atualiza messages internas do loop (com tool_calls, para o modelo saber o que pediu)
263
- if (assistantMessage) messages.push(assistantMessage);
264
- else messages.push({ role: 'assistant', content: assistantContent });
265
-
266
- // No primeiro turno, inserimos o user antes do assistant (foi só passado como prompt)
267
- if (iter === 1) {
268
- const last = messages.pop();
269
- messages.push({ role: 'user', content: enriched });
270
- messages.push(last);
271
- }
272
-
273
- promptForCall = null;
274
-
275
- if (assistantContent) {
276
- let cleaned = assistantContent
277
- .replace(/<\|[\s\S]*$/g, '')
278
- .replace(/【[\s\S]*$/g, '')
279
- .replace(/\n\s*\{\s*"(name|type|summary|arguments|command|path)"[\s\S]*$/, '')
280
- .replace(/^\s*\{\s*"(name|type|summary|arguments)"[\s\S]*$/, '')
281
- // Corta chamadas de função vazadas em texto
282
- .replace(/\b(finish|write_file|read_file|list_dir|run_shell|run_in_new_terminal|open_in_browser|browser_\w+)\s*\([\s\S]*$/g, '')
283
- .trim();
284
- if (cleaned) finalAssistantText = cleaned;
285
- }
286
-
287
- // Subagentes rodaram — injeta resultado como tool_result e segue loop
288
- if (subagentsPayload && subagentsToolCallId) {
289
- toolResults = [{ tool_call_id: subagentsToolCallId, result: JSON.stringify(subagentsPayload) }];
290
- promptForCall = null;
291
- continue;
292
- }
293
-
294
- // Server pediu retry (ex: finish sem trabalho no primeiro turno)
295
- if (retryNeeded && rejectedNote) {
296
- // Injeta pseudo tool_result educativo pro próximo turno
297
- const rejectedId = assistantMessage?.tool_calls?.[0]?.id;
298
- if (rejectedId) {
299
- toolResults = [{ tool_call_id: rejectedId, result: JSON.stringify({ ok: false, error: rejectedNote.reason }) }];
300
- }
301
- // Novo prompt reforçando a ordem
302
- promptForCall = `[sistema] Seu último ${rejectedNote.name}() foi rejeitado: ${rejectedNote.reason}\n\nExecute o pedido original agora, usando as ferramentas corretas antes de qualquer finish.`;
303
- continue;
304
- }
305
-
306
- if (!hasToolCalls || toolCalls.length === 0) {
307
- finished = true;
308
- break;
309
- }
310
-
311
- // Executa cada tool
312
- const results = [];
313
- for (const tc of toolCalls) {
314
- const name = tc.name;
315
- const args = tc.arguments || {};
316
- const nice = NICE[name] || name;
317
- const detail = fmtArgs(name, args);
318
- console.log('\n' + toolLine(nice, detail));
319
-
320
- let res;
321
- // Interceptação: se o Construtor 1 quer abrir arquivo E há refino planejado,
322
- // adia a abertura pra DEPOIS do refino (não mostra ao usuário versão bugada).
323
- // Só adiamos se já houver pelo menos 1 arquivo escrito (senão não faz sentido refinar).
324
- const shouldDeferOpen = (name === 'open_in_browser' && args.target && filesWritten.size > 0);
325
- if (shouldDeferOpen) {
326
- deferredOpens.push(args.target);
327
- res = { ok: true, text_for_model: JSON.stringify({ ok: true, deferred: true, note: 'abertura adiada para depois do refino' }) };
328
- console.log(c.muted(' ⎿ adiado até depois do refino'));
329
- } else {
330
- try {
331
- res = await tools.executeTool(name, args, log);
332
- } catch (e) {
333
- res = { ok: false, text_for_model: JSON.stringify({ ok: false, error: e.message }) };
334
- console.log(c.err(` exception: ${e.message}`));
335
- }
336
- }
337
- results.push({ tool_call_id: tc.id, result: res.text_for_model });
338
-
339
- // Rastreia arquivos escritos para depois passar ao Construtor 2 (refino)
340
- if (name === 'write_file' && args.path) {
341
- try { filesWritten.add(String(args.path)); } catch { /* noop */ }
342
- }
343
-
344
- if (res.finish) finished = true;
345
-
346
- const sig = name + '|' + JSON.stringify(args);
347
- recentCalls.push(sig);
348
- if (recentCalls.length > 4) recentCalls.shift();
349
- if (recentCalls.filter(s => s === sig).length >= 3) {
350
- console.log('\n' + c.warn('loop detectado — encerrando.'));
351
- finished = true;
352
- break;
353
- }
354
- }
355
-
356
- toolResults = results;
357
- if (finished) break;
358
- }
359
-
360
- if (!finished) console.log('\n' + c.warn(`limite de ${MAX_ITERATIONS} iterações atingido.`));
361
-
362
- // ═══ FASE C — Refino (Construtor 2) ═══
363
- // Só refina se o Construtor 1 realmente criou/modificou arquivos.
364
- // Não refina para saudações/perguntas conceituais.
365
- if (filesWritten.size > 0) {
366
- const refineFiles = Array.from(filesWritten).slice(0, 8); // cap defensivo
367
- console.log('\n' + c.muted('◆ Refinando (Construtor 2)'));
368
- let refineSp = new Spinner('Melhorando').start();
369
- let refineIter = 0;
370
- let refineDone = false;
371
- let refineToolResults = null;
372
- const REFINE_MAX = 6;
373
- // Histórico ISOLADO do refino — nunca inclui o histórico do executor
374
- // (isso quebra o Groq porque as tool_calls antigas não têm tool_results correspondentes).
375
- const refineMessages = [];
376
-
377
- while (!refineDone && refineIter < REFINE_MAX) {
378
- refineIter++;
379
- let assistantMessage = null;
380
- let toolCalls = [];
381
- let serverErr = null;
382
- let hasToolCalls = false;
383
- let streamingStarted = false;
384
- let printedUpTo = 0;
385
- let suppressRest = false;
386
- let assistantContent = '';
387
-
388
- try {
389
- await api.requestStream(server, '/api/act/stream', {
390
- refine: true,
391
- refine_files: refineFiles,
392
- refine_original_prompt: userPrompt,
393
- refine_history: refineMessages,
394
- tool_results: refineToolResults,
395
- model,
396
- }, (evt) => {
397
- if (evt.erro) { serverErr = evt.erro; return; }
398
- if (evt.delta) {
399
- if (!streamingStarted) {
400
- if (refineSp) { refineSp.stop(); refineSp = null; }
401
- process.stdout.write('\n');
402
- streamingStarted = true;
403
- }
404
- assistantContent += evt.delta;
405
- if (suppressRest) return;
406
- const leakRe = /<\||【|\n\s*\{\s*"(name|type|summary|arguments|command|path)"|\bfinish\s*\(|\bwrite_file\s*\(|\brun_shell\s*\(|\bopen_in_browser\s*\(|\bbrowser_\w+\s*\(/;
407
- const mm = assistantContent.match(leakRe);
408
- if (mm && mm.index !== undefined) {
409
- const safeEnd = mm.index;
410
- if (safeEnd > printedUpTo) {
411
- process.stdout.write(c.white(assistantContent.slice(printedUpTo, safeEnd)));
412
- printedUpTo = safeEnd;
413
- }
414
- suppressRest = true;
415
- return;
416
- }
417
- const target = Math.max(printedUpTo, assistantContent.length - 2);
418
- if (target > printedUpTo) {
419
- process.stdout.write(c.white(assistantContent.slice(printedUpTo, target)));
420
- printedUpTo = target;
421
- }
422
- }
423
- if (evt.tool_call) toolCalls.push(evt.tool_call);
424
- if (evt.assistant_message) assistantMessage = evt.assistant_message;
425
- if (evt.done) hasToolCalls = !!evt.has_tool_calls;
426
- if (evt.instability) { serverErr = 'refino indisponível (rate-limit)'; return; }
427
- if (evt.refine_skipped) { refineDone = true; return; }
428
- });
429
- } catch (e) {
430
- if (refineSp) refineSp.stop();
431
- console.log(c.warn(`\n(refino pulado: ${e.message})`));
432
- break;
433
- }
434
-
435
- if (refineSp && !streamingStarted) refineSp.stop();
436
- if (streamingStarted && !suppressRest && printedUpTo < assistantContent.length) {
437
- process.stdout.write(c.white(assistantContent.slice(printedUpTo)));
438
- }
439
- if (streamingStarted) process.stdout.write('\n');
440
-
441
- if (serverErr) { console.log(c.warn(`\n(refino falhou: ${serverErr})`)); break; }
442
-
443
- // Guarda o assistant_message NO HISTÓRICO ISOLADO do refino (não no messages global)
444
- if (assistantMessage) refineMessages.push(assistantMessage);
445
- if (!hasToolCalls || toolCalls.length === 0) { refineDone = true; break; }
446
-
447
- const results = [];
448
- for (const tc of toolCalls) {
449
- const name = tc.name;
450
- const args = tc.arguments || {};
451
- const nice = NICE[name] || name;
452
- const detail = fmtArgs(name, args);
453
- console.log('\n' + toolLine(nice, detail));
454
- let res;
455
- try { res = await tools.executeTool(name, args, log); }
456
- catch (e) { res = { ok: false, text_for_model: JSON.stringify({ ok: false, error: e.message }) }; console.log(c.err(` exception: ${e.message}`)); }
457
- results.push({ tool_call_id: tc.id, result: res.text_for_model });
458
- // O refino também precisa adicionar tool result no histórico isolado
459
- refineMessages.push({ role: 'tool', tool_call_id: tc.id, content: res.text_for_model });
460
- if (res.finish) refineDone = true;
461
- }
462
- refineToolResults = null; // já injetamos no refineMessages, não duplicar
463
-
464
- // Prepara spinner do próximo turno de refino se ainda não terminou
465
- if (!refineDone) refineSp = new Spinner('Melhorando').start();
466
- }
467
- if (refineSp) refineSp.stop();
468
-
469
- // Executa aberturas adiadas agora que o refino terminou
470
- if (deferredOpens.length > 0) {
471
- for (const target of deferredOpens) {
472
- console.log('\n' + toolLine('Open', target));
473
- try {
474
- const res = await tools.executeTool('open_in_browser', { target }, log);
475
- if (!res.ok && res.text_for_model) {
476
- console.log(c.muted(' ⎿ ' + (res.error || 'ok')));
477
- }
478
- } catch (e) {
479
- console.log(c.err(` ⎿ falha: ${e.message}`));
480
- }
481
- }
482
- }
483
- } else if (deferredOpens.length > 0) {
484
- // Não houve refino mas há aberturas adiadas — executa direto (fallback)
485
- for (const target of deferredOpens) {
486
- console.log('\n' + toolLine('Open', target));
487
- try { await tools.executeTool('open_in_browser', { target }, log); }
488
- catch (e) { console.log(c.err(` ⎿ falha: ${e.message}`)); }
489
- }
65
+ messages.push({ role: 'assistant', content: response.content || '', tool_calls: response.tool_calls });
66
+ for (const tr of toolResults) messages.push({ role: 'tool', tool_call_id: tr.tool_call_id, name: tr.name, content: tr.content });
490
67
  }
491
-
492
- // Retorna a mensagem final para o caller anexar ao histórico do chat
493
- // Consolidamos toda a atividade + texto final para o usuário poder referenciar depois
494
- return {
495
- userMessage: { role: 'user', content: userPrompt },
496
- assistantMessage: { role: 'assistant', content: finalAssistantText || '[ação concluída]' },
497
- };
68
+ console.log(c.err('\nLimite de iterações atingido.'));
69
+ return { userMessage: { role: 'user', content: userPrompt }, assistantMessage: { role: 'assistant', content: 'Limite de iterações atingido.' } };
498
70
  }
499
71
 
500
72
  module.exports = { runAct };
package/lib/api.js CHANGED
@@ -12,7 +12,7 @@ function request(serverUrl, endpoint, payload) {
12
12
  return new Promise((resolve, reject) => {
13
13
  const url = new URL(endpoint, serverUrl);
14
14
  const data = JSON.stringify(payload);
15
- const options = { hostname: url.hostname, port: url.port || (url.protocol==='https:'?443:80), path: url.pathname+url.search, method: 'POST', headers: { 'Content-Type':'application/json', 'Content-Length':Buffer.byteLength(data) }, timeout: 60000 };
15
+ const options = { hostname: url.hostname, port: url.port || (url.protocol==='https:'?443:80), path: url.pathname+url.search, method: 'POST', headers: { 'Content-Type':'application/json', 'Content-Length':Buffer.byteLength(data) } };
16
16
  const client = url.protocol==='https:' ? https : http;
17
17
  const req = client.request(options, (res) => { let body=''; res.on('data', c=>body+=c); res.on('end', () => { try { const json=JSON.parse(body); if (res.statusCode>=400) reject(new Error(json.erro||json.error||`HTTP ${res.statusCode}`)); else resolve(json); } catch { reject(new Error(`Resposta inválida: ${body.slice(0,200)}`)); } }); });
18
18
  req.on('error', reject); req.write(data); req.end();
@@ -23,14 +23,16 @@ function requestStream(serverUrl, endpoint, payload, onEvent) {
23
23
  return new Promise((resolve, reject) => {
24
24
  const url = new URL(endpoint, serverUrl);
25
25
  const data = JSON.stringify(payload);
26
- const options = { hostname: url.hostname, port: url.port || (url.protocol==='https:'?443:80), path: url.pathname+url.search, method: 'POST', headers: { 'Content-Type':'application/json', 'Content-Length':Buffer.byteLength(data), 'Accept':'text/event-stream' }, timeout: 60000 };
26
+ const options = { hostname: url.hostname, port: url.port || (url.protocol==='https:'?443:80), path: url.pathname+url.search, method: 'POST', headers: { 'Content-Type':'application/json', 'Content-Length':Buffer.byteLength(data), 'Accept':'text/event-stream' } };
27
27
  const client = url.protocol==='https:' ? https : http;
28
28
  const req = client.request(options, (res) => { let buffer=''; res.on('data', chunk => { buffer+=chunk; const lines=buffer.split('\n'); buffer=lines.pop(); for (const line of lines) if (line.startsWith('data: ')) { try { const evt=JSON.parse(line.slice(6)); onEvent(evt); if (evt.done) resolve(); } catch {} } }); res.on('end', () => resolve()); });
29
29
  req.on('error', reject); req.write(data); req.end();
30
30
  });
31
31
  }
32
32
 
33
- function requestAct(serverUrl, messages, model) { return request(serverUrl, '/api/act', { messages, model }); }
33
+ function requestAct(serverUrl, messages, model, extras) {
34
+ return request(serverUrl, '/api/act', { messages, model, ...(extras || {}) });
35
+ }
34
36
 
35
37
  function getHealth(serverUrl) {
36
38
  return new Promise((resolve, reject) => { const url=new URL('/health',serverUrl); const client=url.protocol==='https:'?https:http; const req=client.get(url, (res)=>{ let body=''; res.on('data',c=>body+=c); res.on('end',()=>{ try { resolve(JSON.parse(body)); } catch { reject(new Error('Resposta inválida do health check')); } }); }); req.on('error',reject); });