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/README.md +91 -107
- package/bin/primocode.js +274 -476
- package/lib/act.js +43 -471
- package/lib/api.js +5 -3
- package/lib/config.js +99 -35
- package/lib/desktop.js +142 -0
- package/lib/tools.js +68 -467
- package/lib/ui.js +135 -78
- package/package.json +7 -18
- package/lib/mascot.js +0 -121
- package/lib/memory.js +0 -179
- package/lib/mouse.js +0 -144
- package/lib/platform.js +0 -245
package/lib/act.js
CHANGED
|
@@ -1,500 +1,72 @@
|
|
|
1
|
-
//
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (kind === '
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) }
|
|
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' }
|
|
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
|
|
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); });
|