primocode 7.0.2 → 7.1.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 +78 -76
- package/bin/primocode.js +243 -225
- package/lib/act.js +127 -74
- package/lib/api.js +110 -17
- package/lib/browser.js +492 -492
- package/lib/config.js +105 -52
- package/lib/desktop.js +142 -142
- package/lib/tools.js +147 -104
- package/lib/ui.js +165 -161
- package/package.json +5 -3
package/lib/act.js
CHANGED
|
@@ -1,104 +1,157 @@
|
|
|
1
|
-
// Loop do agente autônomo — streaming via /api/act/stream
|
|
1
|
+
// Loop do agente autônomo — streaming via /api/act/stream.
|
|
2
|
+
//
|
|
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.
|
|
2
7
|
|
|
3
8
|
'use strict';
|
|
4
9
|
|
|
5
10
|
const { c, actionHeader, Spinner } = require('./ui');
|
|
6
11
|
const tools = require('./tools');
|
|
7
12
|
const api = require('./api');
|
|
13
|
+
const cfg = require('./config');
|
|
8
14
|
|
|
9
15
|
const MAX_ITERATIONS = 15;
|
|
10
16
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
function logResult(kind, result) {
|
|
18
|
+
const prefix = c.muted(' ⎿ ');
|
|
19
|
+
if (!result) return;
|
|
20
|
+
if (result.ok === false) { console.log(prefix + c.err(result.error || `exit ${result.code ?? 'erro'}`)); return; }
|
|
21
|
+
|
|
22
|
+
switch (kind) {
|
|
23
|
+
case 'write_file':
|
|
24
|
+
console.log(prefix + c.muted(`${result.created ? 'created' : 'updated'} · ${tools.fmtBytes(result.bytes)}`));
|
|
25
|
+
return;
|
|
26
|
+
case 'read_file':
|
|
27
|
+
console.log(prefix + c.muted(`${tools.fmtBytes(result.bytes || 0)}${result.truncated ? ' (truncated)' : ''}`));
|
|
28
|
+
return;
|
|
29
|
+
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}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
case 'run_shell':
|
|
36
|
+
console.log(prefix + c.ok('exit 0'));
|
|
37
|
+
return;
|
|
38
|
+
case 'open_in_browser':
|
|
39
|
+
case 'run_in_new_terminal':
|
|
40
|
+
console.log(prefix + c.muted(result.opener || 'ok'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
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.
|
|
47
|
+
function summarizeForModel(result) {
|
|
48
|
+
const json = JSON.stringify(result);
|
|
49
|
+
return json.length > 4000 ? json.slice(0, 4000) + '…(truncado)' : json;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildMemoryContext(projectName) {
|
|
53
|
+
const parts = [];
|
|
54
|
+
const geral = cfg.readGeneralMemory();
|
|
55
|
+
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
56
|
+
const projeto = cfg.readProjectMemory(projectName);
|
|
57
|
+
if (projeto.trim()) parts.push(`[${projectName}/memory.md]\n${projeto}`);
|
|
58
|
+
return parts.join('\n\n');
|
|
29
59
|
}
|
|
30
60
|
|
|
31
61
|
async function runAct(userPrompt, opts) {
|
|
32
|
-
const { server, model, history, projectDir, projectName } = opts || {};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
const { server, model, token, history, projectDir, projectName } = opts || {};
|
|
63
|
+
|
|
64
|
+
// Conversa completa desta tarefa: histórico recente + memória + pedido.
|
|
65
|
+
const messages = [];
|
|
66
|
+
const memory = buildMemoryContext(projectName);
|
|
67
|
+
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
68
|
+
for (const m of (history || []).slice(-20)) {
|
|
69
|
+
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
70
|
+
}
|
|
71
|
+
messages.push({ role: 'user', content: userPrompt });
|
|
72
|
+
|
|
73
|
+
let finalText = '';
|
|
74
|
+
|
|
75
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
45
76
|
const sp = new Spinner('Primo Code pensando').start();
|
|
46
77
|
|
|
78
|
+
const toolCalls = [];
|
|
79
|
+
let assistantText = '';
|
|
80
|
+
let streamError = null;
|
|
81
|
+
let streaming = false;
|
|
82
|
+
|
|
47
83
|
try {
|
|
48
|
-
await api.requestAct(server, messages, model,
|
|
49
|
-
if (evt.
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (evt.tool_call) {
|
|
53
|
-
sp.stop();
|
|
54
|
-
const tc = evt.tool_call;
|
|
55
|
-
console.log(actionHeader(tc.name, tc.arguments));
|
|
56
|
-
logger.event(tc.name, tc.arguments);
|
|
57
|
-
|
|
58
|
-
// Executar tool localmente
|
|
59
|
-
tools.execute(tc.name, tc.arguments).then(result => {
|
|
60
|
-
logger.event(tc.name + '_done', null, result);
|
|
61
|
-
toolResults.push({ tool_call_id: tc.id || 'tc_' + iteration, result });
|
|
62
|
-
});
|
|
63
|
-
sp.start();
|
|
64
|
-
}
|
|
65
|
-
if (evt.assistant_message) {
|
|
66
|
-
assistantContent = evt.assistant_message.content || '';
|
|
67
|
-
}
|
|
84
|
+
await api.requestAct(server, { messages, model, token, projectDir, projectName }, (evt) => {
|
|
85
|
+
if (evt.erro) { streamError = evt.erro; return; }
|
|
86
|
+
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
68
87
|
if (evt.delta) {
|
|
69
|
-
sp.stop();
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
if (!streaming) { sp.stop(); streaming = true; }
|
|
89
|
+
assistantText += evt.delta;
|
|
90
|
+
process.stdout.write(c.white(evt.delta));
|
|
72
91
|
}
|
|
92
|
+
if (evt.assistant_message) assistantText = evt.assistant_message.content || assistantText;
|
|
73
93
|
});
|
|
94
|
+
} catch (e) {
|
|
74
95
|
sp.stop();
|
|
96
|
+
console.log(c.err(`\nErro: ${e.message || e}`));
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
sp.stop();
|
|
75
100
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
101
|
+
if (streamError) {
|
|
102
|
+
console.log(c.err(`\nErro: ${streamError}`));
|
|
103
|
+
return { assistantMessage: { role: 'assistant', content: `Erro: ${streamError}` } };
|
|
104
|
+
}
|
|
81
105
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
// Sem tool_calls: o modelo respondeu e acabou.
|
|
107
|
+
if (!toolCalls.length) {
|
|
108
|
+
finalText = assistantText.trim();
|
|
109
|
+
if (finalText && !streaming) console.log('\n' + c.white(finalText));
|
|
110
|
+
else if (streaming) process.stdout.write('\n');
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (streaming) process.stdout.write('\n');
|
|
115
|
+
|
|
116
|
+
// Registra na conversa o turno do assistente COM os tool_calls.
|
|
117
|
+
// A API exige que cada mensagem role:'tool' venha depois deste turno.
|
|
118
|
+
messages.push({
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content: assistantText || null,
|
|
121
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
122
|
+
id: tc.id,
|
|
123
|
+
type: 'function',
|
|
124
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments || {}) },
|
|
125
|
+
})),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let finished = null;
|
|
129
|
+
for (const tc of toolCalls) {
|
|
130
|
+
if (tc.name === 'finish') {
|
|
131
|
+
finished = (tc.arguments && tc.arguments.summary) || 'Concluído.';
|
|
132
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: 'ok' });
|
|
90
133
|
continue;
|
|
91
134
|
}
|
|
92
135
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
throw e;
|
|
136
|
+
console.log(actionHeader(tc.name, tc.arguments));
|
|
137
|
+
const result = await tools.execute(tc.name, tc.arguments, { projectDir });
|
|
138
|
+
logResult(tc.name, result);
|
|
139
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
98
140
|
}
|
|
141
|
+
|
|
142
|
+
if (finished) { finalText = finished; console.log('\n' + c.white(finished)); break; }
|
|
99
143
|
}
|
|
100
144
|
|
|
101
|
-
|
|
145
|
+
if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
|
|
146
|
+
|
|
147
|
+
if (projectName) {
|
|
148
|
+
cfg.appendProjectMemory(projectName, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
userMessage: { role: 'user', content: userPrompt },
|
|
153
|
+
assistantMessage: { role: 'assistant', content: finalText },
|
|
154
|
+
};
|
|
102
155
|
}
|
|
103
156
|
|
|
104
157
|
module.exports = { runAct };
|
package/lib/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cliente HTTP do primocode-server.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
'use strict';
|
|
@@ -8,35 +8,128 @@ const https = require('https');
|
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const { URL } = require('url');
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Erro cru do servidor não diz o que fazer. Estes dois dizem.
|
|
12
|
+
function explainHttpError(status, msg) {
|
|
13
|
+
if (status === 401) {
|
|
14
|
+
return `${msg}\nEste servidor pede um token. Configure com:\n export PRIMOCODE_TOKEN="seu-token"\nou adicione "token" em ~/.primocode/config.json`;
|
|
15
|
+
}
|
|
16
|
+
if (status === 429) {
|
|
17
|
+
return `${msg}\nSe você é o dono do servidor, configure PRIMOCODE_TOKEN para não ter limite.`;
|
|
18
|
+
}
|
|
19
|
+
return msg;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildOptions(url, method, extraHeaders, bodyLength, token) {
|
|
23
|
+
const headers = { ...extraHeaders };
|
|
24
|
+
if (bodyLength != null) {
|
|
25
|
+
headers['Content-Type'] = 'application/json';
|
|
26
|
+
headers['Content-Length'] = bodyLength;
|
|
27
|
+
}
|
|
28
|
+
if (token) headers['x-primocode-token'] = token;
|
|
29
|
+
return {
|
|
30
|
+
hostname: url.hostname,
|
|
31
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
32
|
+
path: url.pathname + url.search,
|
|
33
|
+
method,
|
|
34
|
+
headers,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function request(serverUrl, endpoint, payload, token) {
|
|
12
39
|
return new Promise((resolve, reject) => {
|
|
13
40
|
const url = new URL(endpoint, serverUrl);
|
|
14
41
|
const data = JSON.stringify(payload);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
43
|
+
const req = client.request(buildOptions(url, 'POST', {}, Buffer.byteLength(data), token), (res) => {
|
|
44
|
+
let body = '';
|
|
45
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
let json;
|
|
48
|
+
try { json = JSON.parse(body); }
|
|
49
|
+
catch { return reject(new Error(`Resposta inválida (HTTP ${res.statusCode}): ${body.slice(0, 200)}`)); }
|
|
50
|
+
if (res.statusCode >= 400) reject(new Error(json.erro || json.error || `HTTP ${res.statusCode}`));
|
|
51
|
+
else resolve(json);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
req.on('error', reject);
|
|
55
|
+
req.write(data);
|
|
56
|
+
req.end();
|
|
19
57
|
});
|
|
20
58
|
}
|
|
21
59
|
|
|
22
|
-
function requestStream(serverUrl, endpoint, payload, onEvent) {
|
|
60
|
+
function requestStream(serverUrl, endpoint, payload, onEvent, token) {
|
|
23
61
|
return new Promise((resolve, reject) => {
|
|
24
62
|
const url = new URL(endpoint, serverUrl);
|
|
25
63
|
const data = JSON.stringify(payload);
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
req.
|
|
64
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
65
|
+
const options = buildOptions(url, 'POST', { Accept: 'text/event-stream' }, Buffer.byteLength(data), token);
|
|
66
|
+
|
|
67
|
+
const req = client.request(options, (res) => {
|
|
68
|
+
// Erro HTTP não vem como SSE — o corpo é um JSON de erro.
|
|
69
|
+
if (res.statusCode >= 400) {
|
|
70
|
+
let body = '';
|
|
71
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
let msg = `HTTP ${res.statusCode}`;
|
|
74
|
+
try { const j = JSON.parse(body); msg = j.erro || j.error || msg; } catch {}
|
|
75
|
+
reject(new Error(explainHttpError(res.statusCode, msg)));
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let settled = false;
|
|
81
|
+
const finish = () => { if (!settled) { settled = true; resolve(); } };
|
|
82
|
+
|
|
83
|
+
let buffer = '';
|
|
84
|
+
res.setEncoding('utf8');
|
|
85
|
+
res.on('data', (chunk) => {
|
|
86
|
+
buffer += chunk;
|
|
87
|
+
const lines = buffer.split('\n');
|
|
88
|
+
buffer = lines.pop();
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (!line.startsWith('data: ')) continue;
|
|
91
|
+
let evt;
|
|
92
|
+
try { evt = JSON.parse(line.slice(6)); } catch { continue; }
|
|
93
|
+
onEvent(evt);
|
|
94
|
+
if (evt.done) finish();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
res.on('end', finish);
|
|
98
|
+
res.on('error', reject);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.on('error', reject);
|
|
102
|
+
req.write(data);
|
|
103
|
+
req.end();
|
|
30
104
|
});
|
|
31
105
|
}
|
|
32
106
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
107
|
+
// Agente autônomo. `payload` carrega a conversa completa (messages).
|
|
108
|
+
function requestAct(serverUrl, payload, onEvent) {
|
|
109
|
+
const { token, ...body } = payload;
|
|
110
|
+
return requestStream(serverUrl, '/api/act/stream', body, onEvent, token);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Chat puro.
|
|
114
|
+
function requestChat(serverUrl, payload, onEvent) {
|
|
115
|
+
const { token, ...body } = payload;
|
|
116
|
+
return requestStream(serverUrl, '/api/chat/stream', body, onEvent, token);
|
|
36
117
|
}
|
|
37
118
|
|
|
38
|
-
function getHealth(serverUrl) {
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
119
|
+
function getHealth(serverUrl, token) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const url = new URL('/health', serverUrl);
|
|
122
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
123
|
+
const req = client.get(buildOptions(url, 'GET', {}, null, token), (res) => {
|
|
124
|
+
let body = '';
|
|
125
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
126
|
+
res.on('end', () => {
|
|
127
|
+
try { resolve(JSON.parse(body)); }
|
|
128
|
+
catch { reject(new Error(`Resposta inválida do health check (HTTP ${res.statusCode})`)); }
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
req.on('error', reject);
|
|
132
|
+
});
|
|
40
133
|
}
|
|
41
134
|
|
|
42
|
-
module.exports = { request, requestStream, requestAct, getHealth };
|
|
135
|
+
module.exports = { request, requestStream, requestAct, requestChat, getHealth };
|