primocode 9.7.0 → 9.8.0-beta.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 +5 -4
- package/bin/primocode.js +167 -11
- package/lib/act.js +30 -6
- package/lib/api.js +18 -0
- package/lib/browser.js +80 -6
- package/lib/local/cerebro.json +2116 -0
- package/lib/local/entrada.js +332 -0
- package/lib/local/motor.js +264 -0
- package/lib/local/provedor.js +151 -0
- package/lib/nome-projeto.js +97 -0
- package/lib/repeticao.js +65 -5
- package/lib/tools.js +15 -1
- package/lib/voz.js +7 -1
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +4 -2
- package/voz/primo_voz/transcrever.py +37 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orcamento-entrada.js — o que ENTRA no pedido, e quanto isso custa.
|
|
3
|
+
*
|
|
4
|
+
* ── O NÚMERO QUE MOTIVOU ESTE ARQUIVO ────────────────────────────────────
|
|
5
|
+
* Medido no servidor, com o pedido VAZIO — antes de o usuário digitar
|
|
6
|
+
* qualquer coisa:
|
|
7
|
+
*
|
|
8
|
+
* schema das 45 ferramentas ....... 4.326 tokens
|
|
9
|
+
* prompt do sistema ............... 2.365 tokens
|
|
10
|
+
* ────────────────────────────────────────────
|
|
11
|
+
* piso de qualquer pedido ......... 6.691 tokens
|
|
12
|
+
* teto do plano grátis ............ 6.000 tokens por minuto
|
|
13
|
+
*
|
|
14
|
+
* Ou seja: TODO pedido do modo primocode já nascia estourado. Não era a
|
|
15
|
+
* conversa que crescia demais — era o custo fixo que não cabia. O usuário via
|
|
16
|
+
* "a conversa passou do teto" no meio de uma tarefa e não tinha o que encurtar.
|
|
17
|
+
*
|
|
18
|
+
* ── A IDEIA ──────────────────────────────────────────────────────────────
|
|
19
|
+
* Ninguém precisa das 45 ferramentas para fazer um slide. Quem pede um vídeo
|
|
20
|
+
* não usa `desktop_click`; quem conserta um bug não usa `studio_criar`. Então
|
|
21
|
+
* cada pedido leva só o que aquele pedido pode usar — ferramentas E seções do
|
|
22
|
+
* prompt — e o que sobra do orçamento vira espaço para a conversa.
|
|
23
|
+
*
|
|
24
|
+
* O núcleo (ler, escrever, procurar, rodar, terminar) vai SEMPRE: é o que
|
|
25
|
+
* qualquer tarefa usa, e adivinhar errado aqui custaria a tarefa inteira.
|
|
26
|
+
*
|
|
27
|
+
* Puro de propósito: sem I/O e sem rede, para o teste medir o orçamento sem
|
|
28
|
+
* chave de API nenhuma.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A MESMA conta do orcamento.js — e isso não era verdade antes.
|
|
35
|
+
*
|
|
36
|
+
* Aqui era 4 caracteres por token e lá 2,6. Para o mesmo prompt e as mesmas
|
|
37
|
+
* ferramentas as duas discordavam em 2.070 tokens, e quem decidia o orçamento
|
|
38
|
+
* de entrada era a conta OTIMISTA: achava que 3.844 cabiam em 5.200 quando o
|
|
39
|
+
* custo real era ~5.900. JSON de schema é o pior caso — pontuação, aspas e
|
|
40
|
+
* identificador curto tokenizam muito pior que texto corrido.
|
|
41
|
+
*/
|
|
42
|
+
const CHARS_POR_TOKEN = 2.6;
|
|
43
|
+
function tokensDe(x) {
|
|
44
|
+
const texto = typeof x === 'string' ? x : JSON.stringify(x || '');
|
|
45
|
+
return Math.ceil(texto.length / CHARS_POR_TOKEN);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* O mesmo schema, com a descrição cortada na primeira frase.
|
|
50
|
+
*
|
|
51
|
+
* A primeira frase é a que diz o que a tool FAZ; o resto é conselho de uso,
|
|
52
|
+
* que também está no prompt. Num teto apertado isso vale 768 tokens (3.130 ->
|
|
53
|
+
* 2.362) sem tirar nenhuma ferramenta da mão do agente — e ferramenta a menos
|
|
54
|
+
* é tarefa que não sai.
|
|
55
|
+
*/
|
|
56
|
+
function enxugarSchema(tools) {
|
|
57
|
+
return (tools || []).map((t) => {
|
|
58
|
+
const d = t && t.function && t.function.description;
|
|
59
|
+
if (!d) return t;
|
|
60
|
+
const primeira = String(d).split(/(?<=\.)\s/)[0].slice(0, 160);
|
|
61
|
+
if (primeira.length >= String(d).length) return t;
|
|
62
|
+
return { ...t, function: { ...t.function, description: primeira } };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Os grupos ────────────────────────────────────────────────────────────
|
|
67
|
+
// `sempre: true` é o núcleo — vai em todo pedido, sem discussão.
|
|
68
|
+
const GRUPOS = [
|
|
69
|
+
{
|
|
70
|
+
nome: 'nucleo',
|
|
71
|
+
sempre: true,
|
|
72
|
+
tools: ['write_file', 'edit_file', 'multi_edit', 'read_file', 'list_dir',
|
|
73
|
+
'glob', 'grep', 'run_shell', 'write_todos', 'finish'],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
nome: 'estudio',
|
|
77
|
+
tools: ['studio_manual', 'studio_spec', 'studio_criar', 'studio_ler',
|
|
78
|
+
'studio_atualizar', 'studio_versoes', 'studio_listar', 'studio_midia'],
|
|
79
|
+
// O que o estúdio produz, e os apps que ele substitui.
|
|
80
|
+
gatilho: /\b(slides?|apresenta[çc][ãa]o|deck|v[íi]deo|reels?|anima[çc][ãa]o|post|banner|capa|thumbnail|arte|cartaz|logo|planilha|tabela|or[çc]amento|documento|relat[óo]rio|artigo|proposta|contrato|diagrama|fluxograma|organograma|mapa mental|narra[çc][ãa]o|trilha|est[úu]dio|canva|powerpoint|keynote|word|docs|excel|sheets|photoshop|figma|capcut|premiere|miro)\b/i,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
nome: 'navegador',
|
|
84
|
+
tools: ['browser_open', 'browser_click', 'browser_type', 'browser_key',
|
|
85
|
+
'browser_read', 'browser_screenshot', 'browser_scroll', 'browser_wait',
|
|
86
|
+
'browser_eval', 'open_in_browser'],
|
|
87
|
+
gatilho: /\b(site|p[áa]gina|navegador|browser|chrome|url|https?:|www\.|link|formul[áa]rio|login|clicar no bot[ãa]o|testar? (a p[áa]gina|no navegador)|pesquis|busca|internet|not[íi]cia|empresa|mercado)\b/i,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
nome: 'computador',
|
|
91
|
+
tools: ['desktop_open_app', 'desktop_windows', 'desktop_focus', 'desktop_elements',
|
|
92
|
+
'desktop_click_element', 'desktop_click', 'desktop_double_click', 'desktop_move',
|
|
93
|
+
'desktop_drag', 'desktop_type', 'desktop_key', 'desktop_scroll', 'desktop_size',
|
|
94
|
+
'desktop_screenshot'],
|
|
95
|
+
// Só quando a tarefa é MESMO no computador. É o grupo mais caro
|
|
96
|
+
// (1.141 tokens) e o menos usado no dia a dia.
|
|
97
|
+
gatilho: /\b(janela|[áa]rea de trabalho|desktop|abrir? o (app|programa|aplicativo)|clica(r)? (na tela|no [íi]cone)|[íi]cone|mouse|teclado|atalho|print da tela|screenshot|alt\+tab|ctrl\+|bloco de notas|calculadora|explorer|finder)\b/i,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
nome: 'pesquisa',
|
|
101
|
+
tools: ['pesquisar_referencia'],
|
|
102
|
+
gatilho: /\b(pesquis|busca|procur|internet|documenta[çc][ãa]o|refer[êe]ncia|empresa|mercado|not[íi]cia|quem [ée]|sobre a |sobre o |dados de|hist[óo]ria d)\b/i,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
/* O PIPELINE DE QUATRO PAPÉIS, e ele é caro de duas formas: o schema
|
|
106
|
+
dele é longo, e chamá-lo custa seis a oito agentes. Por isso o
|
|
107
|
+
gatilho pede o PAR — um verbo de construir colado num substantivo de
|
|
108
|
+
coisa inteira. "Melhore o site" e "conserte o app" não passam de
|
|
109
|
+
propósito: o pipeline constrói do zero, e mandá-lo mexer no que já
|
|
110
|
+
existe é pagar oito agentes para refazer o que estava pronto. */
|
|
111
|
+
nome: 'pipeline',
|
|
112
|
+
tools: ['construir_app'],
|
|
113
|
+
gatilho: /\b(fa[çc]a|fa[çc]er|faz|cria|crie|criar|monta|monte|montar|constr[óo]i|construa|construir|desenvolv[ae]|quero|preciso de)\b[^.!?]{0,24}\b(app|aplicativo|site|p[áa]gina|landing|jogo|game|ferramenta|dashboard|painel|calculadora|sistema|portf[óo]lio|loja|formul[áa]rio|cron[ôo]metro|agenda|lista de tarefas|to-?do)\b/i,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
/* O ARQUIVO do vídeo. Quem pede "faz um vídeo" recebe o link e o
|
|
117
|
+
botão de baixar; a ferramenta que gera o mp4 sem clique só entra
|
|
118
|
+
quando o pedido fala do arquivo — é o que mantém o pedido de vídeo
|
|
119
|
+
dentro do teto do minuto (medido: ela custa ~180 tokens). */
|
|
120
|
+
nome: 'exportar',
|
|
121
|
+
tools: ['studio_exportar'],
|
|
122
|
+
gatilho: /\b(mp4|webm|baix[ae]r?|download|exporta\w*|salv[ae]r? (o )?v[íi]deo|arquivo (do|de) v[íi]deo|me manda o v[íi]deo|manda o arquivo|renderiza\w*)\b/i,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
nome: 'delegar',
|
|
126
|
+
tools: ['spawn_agent'],
|
|
127
|
+
// Delegar só compensa em tarefa grande; em pedido curto é peso morto.
|
|
128
|
+
gatilho: /\b(v[áa]rios|v[áa]rias|todos os|todas as|em paralelo|projeto inteiro|refator|migra[çc][ãa]o|auditor)\b/i,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
nome: 'terminal',
|
|
132
|
+
tools: ['run_in_new_terminal'],
|
|
133
|
+
gatilho: /\b(servidor|dev|watch|npm run|yarn|vite|next|rodar? em segundo plano|localhost|porta \d+)\b/i,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
// Sem tool nova — só a seção de prompt com as regras de qualidade de
|
|
137
|
+
// código. Nasceu de reclamação real do dono: "ele faz coisas
|
|
138
|
+
// horríveis em relação a código". As regras custam ~200 tokens e só
|
|
139
|
+
// entram quando a tarefa É de código.
|
|
140
|
+
nome: 'codigo',
|
|
141
|
+
tools: [],
|
|
142
|
+
gatilho: /\b(c[óo]digo|app|site|jogo|fun[çc][ãa]o|bug|erro|script|html|css|javascript|typescript|python|node|api|backend|frontend|componente|refator|classe|m[óo]dulo|landing)\b/i,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Quais grupos esta tarefa pode usar.
|
|
148
|
+
*
|
|
149
|
+
* Olha o pedido do usuário — não a conversa inteira: o que o modelo escreveu
|
|
150
|
+
* no meio do caminho não deve mudar o que ele pode usar depois. `forcar`
|
|
151
|
+
* existe para o que já foi decidido fora daqui (o esforço alto sempre pesquisa).
|
|
152
|
+
*/
|
|
153
|
+
function gruposDe(texto, forcar = []) {
|
|
154
|
+
const t = String(texto || '');
|
|
155
|
+
const escolhidos = new Set(['nucleo', ...forcar]);
|
|
156
|
+
for (const g of GRUPOS) {
|
|
157
|
+
if (g.sempre) { escolhidos.add(g.nome); continue; }
|
|
158
|
+
if (g.gatilho && g.gatilho.test(t)) escolhidos.add(g.nome);
|
|
159
|
+
}
|
|
160
|
+
return escolhidos;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** As ferramentas dos grupos escolhidos, na ordem original da lista. */
|
|
164
|
+
function escolherFerramentas(todas, texto, opts = {}) {
|
|
165
|
+
const grupos = gruposDe(texto, opts.forcar);
|
|
166
|
+
const permitidas = new Set();
|
|
167
|
+
for (const g of GRUPOS) {
|
|
168
|
+
if (grupos.has(g.nome)) for (const t of g.tools) permitidas.add(t);
|
|
169
|
+
}
|
|
170
|
+
const escolhidas = (todas || []).filter((t) => permitidas.has(t.function && t.function.name));
|
|
171
|
+
// Rede de segurança: se a seleção falhar (nome novo que ninguém mapeou),
|
|
172
|
+
// é melhor mandar tudo do que mandar um agente sem ferramenta.
|
|
173
|
+
return escolhidas.length >= 5 ? escolhidas : todas;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* O que a conversa mostra que o agente JÁ APRENDEU.
|
|
178
|
+
*
|
|
179
|
+
* A seção "O ESTÚDIO" do prompt ensina o que a spec entrega — e a spec, quando
|
|
180
|
+
* já foi lida, está inteira na conversa. Manter as duas é pagar duas vezes
|
|
181
|
+
* pelo mesmo ensinamento, e são 1.113 tokens que fazem falta justamente na
|
|
182
|
+
* hora de escrever o documento.
|
|
183
|
+
*
|
|
184
|
+
* O mesmo vale para pesquisa: com as fontes já lidas, o texto sobre COMO
|
|
185
|
+
* pesquisar não muda mais nada.
|
|
186
|
+
*/
|
|
187
|
+
function jaAprendeu(messages) {
|
|
188
|
+
const nomePorId = new Map();
|
|
189
|
+
for (const m of messages || []) {
|
|
190
|
+
for (const tc of (m && m.tool_calls) || []) {
|
|
191
|
+
nomePorId.set(tc.id, (tc.function && tc.function.name) || tc.name);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const feitas = new Set();
|
|
195
|
+
for (const m of messages || []) {
|
|
196
|
+
if (m && m.role === 'tool' && nomePorId.has(m.tool_call_id)) feitas.add(nomePorId.get(m.tool_call_id));
|
|
197
|
+
}
|
|
198
|
+
const dispensaveis = new Set();
|
|
199
|
+
if (feitas.has('studio_spec') || feitas.has('studio_manual')) dispensaveis.add('estudio');
|
|
200
|
+
if (feitas.has('pesquisar_referencia')) dispensaveis.add('pesquisa');
|
|
201
|
+
return dispensaveis;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* O prompt, montado só com as seções que a tarefa usa.
|
|
206
|
+
*
|
|
207
|
+
* `secoes` é { nome: texto }. O núcleo entra sempre; as outras entram quando o
|
|
208
|
+
* grupo correspondente entrou.
|
|
209
|
+
*/
|
|
210
|
+
function montarPrompt(nucleo, secoes, texto, opts = {}) {
|
|
211
|
+
const grupos = gruposDe(texto, opts.forcar);
|
|
212
|
+
const dispensaveis = opts.jaAprendeu || new Set();
|
|
213
|
+
const partes = [nucleo];
|
|
214
|
+
for (const [nome, corpo] of Object.entries(secoes || {})) {
|
|
215
|
+
if (grupos.has(nome) && corpo && !dispensaveis.has(nome)) partes.push(corpo);
|
|
216
|
+
}
|
|
217
|
+
return partes.join('\n\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* O relatório do que este pedido vai custar, para o servidor registrar e para
|
|
222
|
+
* o teste provar que a economia existe.
|
|
223
|
+
*/
|
|
224
|
+
function relatorio({ prompt, ferramentas, messages }) {
|
|
225
|
+
const p = tokensDe(prompt);
|
|
226
|
+
const f = tokensDe(ferramentas);
|
|
227
|
+
const m = tokensDe(messages);
|
|
228
|
+
return { prompt: p, ferramentas: f, conversa: m, total: p + f + m, tools: (ferramentas || []).length };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Faz a CONVERSA caber no que sobrou do orçamento.
|
|
233
|
+
*
|
|
234
|
+
* A seleção de ferramentas resolve o custo fixo; isto resolve o variável. Numa
|
|
235
|
+
* tarefa de pesquisa o histórico cresce rápido (cada página lida são milhares
|
|
236
|
+
* de caracteres), e sem um teto ela volta a estourar no meio do trabalho —
|
|
237
|
+
* justamente depois de todo o esforço já gasto.
|
|
238
|
+
*
|
|
239
|
+
* Ordem das concessões, da menos dolorosa para a mais:
|
|
240
|
+
* 1. encurta os resultados de tool ANTIGOS (os novos são os que importam);
|
|
241
|
+
* 2. encurta mais ainda, se preciso;
|
|
242
|
+
* 3. em último caso, descarta as rodadas mais VELHAS — sempre o par
|
|
243
|
+
* inteiro (o turno do assistente com tool_calls + as respostas dele),
|
|
244
|
+
* porque a API recusa um role:'tool' órfão.
|
|
245
|
+
*
|
|
246
|
+
* O pedido do usuário e a última rodada nunca são descartados: sem eles o
|
|
247
|
+
* modelo não sabe mais o que estava fazendo.
|
|
248
|
+
*/
|
|
249
|
+
// Tools cuja resposta é DOCUMENTO FIXO e que o agente precisa ter em mãos na
|
|
250
|
+
// hora de escrever. Encolher isto é o que criou o loop original: sem o formato,
|
|
251
|
+
// o modelo pede a spec de novo, e de novo.
|
|
252
|
+
//
|
|
253
|
+
// O CLI já protege as mesmas mensagens antes de mandar (lib/repeticao.js), mas
|
|
254
|
+
// quem falava por último aqui era `caberNoTeto`, que não sabia disso e cortava
|
|
255
|
+
// assim mesmo. Dois compactadores brigando, e o do servidor ganhava.
|
|
256
|
+
const REFERENCIAS = new Set(['studio_manual', 'studio_spec']);
|
|
257
|
+
|
|
258
|
+
// Referência tem teto GENEROSO, não infinito.
|
|
259
|
+
//
|
|
260
|
+
// Proteger a spec inteira parecia certo — sem o formato o agente não escreve o
|
|
261
|
+
// documento — mas a spec do `corte` são 3.270 tokens, e com o prompt e as 17
|
|
262
|
+
// tools isso dá 7.126: estoura o minuto SOZINHO, e nenhum corte na conversa
|
|
263
|
+
// resolve. Protegida demais, ela deixava de caber. O começo do documento é o
|
|
264
|
+
// formato (que o agente precisa); o fim são exemplos e opções avançadas.
|
|
265
|
+
const LIMITE_REFERENCIA = 3500;
|
|
266
|
+
|
|
267
|
+
/** Os índices de role:'tool' que não podem encolher: a cópia mais nova de cada referência. */
|
|
268
|
+
function referenciasProtegidas(messages) {
|
|
269
|
+
const nomePorId = new Map();
|
|
270
|
+
for (const m of messages || []) {
|
|
271
|
+
for (const tc of (m && m.tool_calls) || []) {
|
|
272
|
+
nomePorId.set(tc.id, (tc.function && tc.function.name) || tc.name);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const ultima = new Map();
|
|
276
|
+
(messages || []).forEach((m, i) => {
|
|
277
|
+
if (!m || m.role !== 'tool') return;
|
|
278
|
+
const nome = nomePorId.get(m.tool_call_id);
|
|
279
|
+
if (REFERENCIAS.has(nome)) ultima.set(nome, i);
|
|
280
|
+
});
|
|
281
|
+
return new Set(ultima.values());
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function caberNoTeto(messages, sobra) {
|
|
285
|
+
const conta = (m) => tokensDe(m);
|
|
286
|
+
if (conta(messages) <= sobra) return { messages, cortou: false };
|
|
287
|
+
|
|
288
|
+
let atual = messages.map((m) => ({ ...m }));
|
|
289
|
+
|
|
290
|
+
// 1 e 2: encurtar resultados antigos, em dois apertos.
|
|
291
|
+
const protegidas = referenciasProtegidas(atual);
|
|
292
|
+
for (const limite of [400, 120]) {
|
|
293
|
+
const indices = atual.map((m, i) => (m.role === 'tool' ? i : -1)).filter((i) => i >= 0);
|
|
294
|
+
const manterInteiros = new Set(indices.slice(-2));
|
|
295
|
+
atual = atual.map((m, i) => {
|
|
296
|
+
const texto = String(m.content || '');
|
|
297
|
+
// Referência: teto próprio, muito maior que o dos outros — o
|
|
298
|
+
// formato vem no começo do documento.
|
|
299
|
+
if (protegidas.has(i)) {
|
|
300
|
+
if (texto.length <= LIMITE_REFERENCIA) return m;
|
|
301
|
+
return { ...m, content: texto.slice(0, LIMITE_REFERENCIA) + '…(spec cortada no limite do plano; o formato essencial está acima)' };
|
|
302
|
+
}
|
|
303
|
+
if (m.role !== 'tool' || manterInteiros.has(i)) return m;
|
|
304
|
+
if (texto.length <= limite) return m;
|
|
305
|
+
return { ...m, content: texto.slice(0, limite) + '…(encurtado para caber no limite do plano)' };
|
|
306
|
+
});
|
|
307
|
+
if (conta(atual) <= sobra) return { messages: atual, cortou: true, como: 'encurtou resultados antigos' };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 3: descartar as rodadas mais velhas, em pares completos.
|
|
311
|
+
while (conta(atual) > sobra) {
|
|
312
|
+
const aindaProtegidas = referenciasProtegidas(atual);
|
|
313
|
+
const primeiroAssistente = atual.findIndex((m, i) => {
|
|
314
|
+
if (!(m.role === 'assistant' && m.tool_calls)) return false;
|
|
315
|
+
// Descartar a rodada que trouxe a spec é recriar o loop original:
|
|
316
|
+
// sem o formato o agente pede a spec de novo, e de novo.
|
|
317
|
+
let f = i + 1;
|
|
318
|
+
while (f < atual.length && atual[f].role === 'tool') { if (aindaProtegidas.has(f)) return false; f++; }
|
|
319
|
+
return true;
|
|
320
|
+
});
|
|
321
|
+
if (primeiroAssistente === -1) break;
|
|
322
|
+
let fim = primeiroAssistente + 1;
|
|
323
|
+
while (fim < atual.length && atual[fim].role === 'tool') fim++;
|
|
324
|
+
// Nunca engolir tudo: para se sobrar só o pedido do usuário.
|
|
325
|
+
if (fim >= atual.length - 1) break;
|
|
326
|
+
atual = [...atual.slice(0, primeiroAssistente), ...atual.slice(fim)];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { messages: atual, cortou: true, como: 'descartou as rodadas mais antigas' };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = { GRUPOS, CHARS_POR_TOKEN, REFERENCIAS, LIMITE_REFERENCIA, tokensDe, enxugarSchema, referenciasProtegidas, jaAprendeu, gruposDe, escolherFerramentas, montarPrompt, relatorio, caberNoTeto };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motor.js — o agente falando DIRETO com a IA da pessoa, daqui da máquina.
|
|
3
|
+
*
|
|
4
|
+
* Faz o que o primocode-server faz nas rotas /api/act/stream, /api/chat/stream,
|
|
5
|
+
* /api/resumir e /api/plano, com o mesmo prompt e as mesmas ferramentas
|
|
6
|
+
* (lib/local/cerebro.json, copiado do servidor por scripts/cerebro-do-servidor.js),
|
|
7
|
+
* e devolve os MESMOS eventos. Para o loop do agente (lib/act.js), nada muda:
|
|
8
|
+
* ele não sabe se quem respondeu foi o servidor ou a API da pessoa.
|
|
9
|
+
*
|
|
10
|
+
* O que fica de fora de propósito: a fila de provedores e o orçamento de 6.000
|
|
11
|
+
* tokens por minuto. Os dois existem para caber na cota compartilhada — aqui
|
|
12
|
+
* a conta é da pessoa, e o teto é o dela.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const cerebro = require('./cerebro.json');
|
|
18
|
+
const entrada = require('./entrada.js');
|
|
19
|
+
const provedor = require('./provedor.js');
|
|
20
|
+
const catalogo = require('../catalogo.js');
|
|
21
|
+
|
|
22
|
+
// A conversa pode ocupar isto, em tokens, além do prompt e das ferramentas.
|
|
23
|
+
// Folgado: os modelos de hoje têm 128 mil ou mais de contexto.
|
|
24
|
+
const TETO_CONVERSA = Number(process.env.PRIMOCODE_TETO_LOCAL || 60000);
|
|
25
|
+
const SAIDA_MAXIMA = 8192;
|
|
26
|
+
const ESFORCO_PESQUISA = ['high', 'xhigh', 'max', 'ultracode'];
|
|
27
|
+
|
|
28
|
+
/* ── AS FERRAMENTAS QUE ESTE CLIENTE EXECUTA ─────────────────────────────
|
|
29
|
+
* O servidor ainda oferece o Estúdio antigo (studio_*), que este cliente não
|
|
30
|
+
* executa mais; e o cliente tem o Estúdio novo (estudio_*, planejar_producao),
|
|
31
|
+
* que o servidor não conhece. Local, a lista certa é a do CLIENTE: o schema
|
|
32
|
+
* vem do servidor quando existe lá, e do catálogo quando só existe aqui. */
|
|
33
|
+
function ferramentasDoCliente() {
|
|
34
|
+
const doServidor = new Map(cerebro.TOOLS.map((t) => [t.function.name, t]));
|
|
35
|
+
const lista = [];
|
|
36
|
+
for (const grupo of catalogo.GRUPOS) {
|
|
37
|
+
for (const item of grupo.itens) {
|
|
38
|
+
if (doServidor.has(item.tool)) { lista.push(doServidor.get(item.tool)); continue; }
|
|
39
|
+
lista.push({ type: 'function', function: {
|
|
40
|
+
name: item.tool,
|
|
41
|
+
description: `${item.faz}. Argumentos: ${item.args || 'nenhum'}`.slice(0, 1800),
|
|
42
|
+
parameters: { type: 'object', properties: {}, additionalProperties: true },
|
|
43
|
+
} });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!lista.some((t) => t.function.name === 'finish') && doServidor.has('finish')) lista.push(doServidor.get('finish'));
|
|
47
|
+
return lista;
|
|
48
|
+
}
|
|
49
|
+
let FERRAMENTAS = null;
|
|
50
|
+
let FERRAMENTAS_SUB = null;
|
|
51
|
+
function ferramentas(subagent) {
|
|
52
|
+
if (!FERRAMENTAS) {
|
|
53
|
+
FERRAMENTAS = ferramentasDoCliente();
|
|
54
|
+
FERRAMENTAS_SUB = FERRAMENTAS.filter((t) => !['spawn_agent', 'construir_app'].includes(t.function.name));
|
|
55
|
+
}
|
|
56
|
+
return subagent ? FERRAMENTAS_SUB : FERRAMENTAS;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** O erro da API, no formato que o loop já sabe tratar (429 espera, 401 avisa). */
|
|
60
|
+
async function erroHttp(r, p) {
|
|
61
|
+
let msg = '';
|
|
62
|
+
try {
|
|
63
|
+
const corpo = await r.text();
|
|
64
|
+
try {
|
|
65
|
+
const j = JSON.parse(corpo);
|
|
66
|
+
const e = Array.isArray(j) ? j[0] && j[0].error : j.error;
|
|
67
|
+
msg = (e && (e.message || e.status)) || j.message || corpo;
|
|
68
|
+
} catch { msg = corpo; }
|
|
69
|
+
} catch { /* sem corpo */ }
|
|
70
|
+
const quem = p ? p.nome : 'o provedor';
|
|
71
|
+
if (r.status === 401 || r.status === 403) {
|
|
72
|
+
return new Error(`${r.status} ${quem} recusou a chave. Rode /api para colar outra, ou /api off para voltar ao servidor.`);
|
|
73
|
+
}
|
|
74
|
+
const espera = r.headers && r.headers.get && r.headers.get('retry-after');
|
|
75
|
+
return new Error(`${r.status} ${quem}: ${String(msg).slice(0, 300)}${espera ? ` Please try again in ${espera}s.` : ''}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cabecalhos(p) {
|
|
79
|
+
const h = { 'Content-Type': 'application/json', Authorization: `Bearer ${p.chave}` };
|
|
80
|
+
if (p.id === 'openrouter') { h['HTTP-Referer'] = 'https://github.com/parisgroup-ai/primocode'; h['X-Title'] = 'PrimoCode'; }
|
|
81
|
+
return h;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Uma chamada inteira, sem stream (resumo, plano). */
|
|
85
|
+
async function completar(messages, { maxTokens = 600, temperatura = 0.2 } = {}) {
|
|
86
|
+
const p = provedor.atual();
|
|
87
|
+
const r = await fetch(`${p.base}/chat/completions`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: cabecalhos(p),
|
|
90
|
+
body: JSON.stringify({ model: p.modelo, messages, temperature: temperatura, max_tokens: maxTokens }),
|
|
91
|
+
signal: AbortSignal.timeout(90000),
|
|
92
|
+
});
|
|
93
|
+
if (!r.ok) throw await erroHttp(r, p);
|
|
94
|
+
const j = await r.json();
|
|
95
|
+
return String((j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || '').trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Lê o stream SSE do /chat/completions. Chama `aoPedaco(delta, fim)` a cada
|
|
100
|
+
* pedaço. Resolve quando acaba ou quando `sinal` corta.
|
|
101
|
+
*/
|
|
102
|
+
async function lerStream(corpo, { sinal, aoPedaco }) {
|
|
103
|
+
const p = provedor.atual();
|
|
104
|
+
let r;
|
|
105
|
+
try {
|
|
106
|
+
r = await fetch(`${p.base}/chat/completions`, {
|
|
107
|
+
method: 'POST', headers: cabecalhos(p), body: JSON.stringify({ ...corpo, model: p.modelo, stream: true }), signal: sinal,
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (sinal && sinal.aborted) return;
|
|
111
|
+
throw new Error(`ECONNRESET não consegui falar com ${p.nome}: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
if (!r.ok) throw await erroHttp(r, p);
|
|
114
|
+
const decod = new TextDecoder();
|
|
115
|
+
let buffer = '';
|
|
116
|
+
try {
|
|
117
|
+
for await (const pedaco of r.body) {
|
|
118
|
+
buffer += decod.decode(pedaco, { stream: true });
|
|
119
|
+
const linhas = buffer.split('\n');
|
|
120
|
+
buffer = linhas.pop();
|
|
121
|
+
for (const linha of linhas) {
|
|
122
|
+
const l = linha.trim();
|
|
123
|
+
if (!l.startsWith('data:')) continue;
|
|
124
|
+
const dado = l.slice(5).trim();
|
|
125
|
+
if (dado === '[DONE]') return;
|
|
126
|
+
let j;
|
|
127
|
+
try { j = JSON.parse(dado); } catch { continue; }
|
|
128
|
+
if (j.error) throw new Error(`${j.error.code || 400} ${j.error.message || JSON.stringify(j.error)}`);
|
|
129
|
+
const escolha = j.choices && j.choices[0];
|
|
130
|
+
if (escolha) aoPedaco(escolha.delta || {}, escolha.finish_reason);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (sinal && sinal.aborted) return;
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Junta os pedaços de tool_call. Gemini às vezes manda sem `index` e sem `id`. */
|
|
140
|
+
function juntarChamadas(acumuladas, pedacos) {
|
|
141
|
+
for (const tc of pedacos || []) {
|
|
142
|
+
let idx;
|
|
143
|
+
if (Number.isInteger(tc.index)) idx = tc.index;
|
|
144
|
+
else if (tc.id) {
|
|
145
|
+
idx = acumuladas.findIndex((a) => a && a.id === tc.id);
|
|
146
|
+
if (idx < 0) idx = acumuladas.length; // id novo: chamada nova
|
|
147
|
+
} else idx = Math.max(0, acumuladas.length - 1); // sem nada: continua a última
|
|
148
|
+
if (!acumuladas[idx]) acumuladas[idx] = { id: tc.id || '', name: '', arguments: '' };
|
|
149
|
+
const alvo = acumuladas[idx];
|
|
150
|
+
if (tc.id) alvo.id = tc.id;
|
|
151
|
+
if (tc.function && tc.function.name) alvo.name = tc.function.name;
|
|
152
|
+
if (tc.function && tc.function.arguments) {
|
|
153
|
+
const a = tc.function.arguments;
|
|
154
|
+
alvo.arguments += typeof a === 'string' ? a : JSON.stringify(a);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return acumuladas;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── /api/act/stream ─────────────────────────────────────────────────── */
|
|
161
|
+
async function requestAct(_servidor, body, emit) {
|
|
162
|
+
const { messages: convo = [], prompt, history, effort, projectDir, projectName, subagent, sinal } = body || {};
|
|
163
|
+
const pedido = [...convo].reverse().find((m) => m && m.role === 'user');
|
|
164
|
+
const texto = String((pedido && pedido.content) || prompt || '');
|
|
165
|
+
const forcar = effort && ESFORCO_PESQUISA.includes(String(effort)) ? ['pesquisa'] : [];
|
|
166
|
+
|
|
167
|
+
// Todas, sempre: o recorte por assunto do servidor existe para caber em
|
|
168
|
+
// 6.000 tokens por minuto, e ainda não conhece o Estúdio novo (estudio_*)
|
|
169
|
+
// — com ele, um pedido de vídeo chegaria sem a ferramenta de vídeo.
|
|
170
|
+
const tools = ferramentas(subagent);
|
|
171
|
+
const sistema = subagent
|
|
172
|
+
? cerebro.SYSTEM_SUB
|
|
173
|
+
: entrada.montarPrompt(cerebro.SYSTEM_NUCLEO, cerebro.SECOES, texto, { forcar, jaAprendeu: entrada.jaAprendeu(convo) });
|
|
174
|
+
const messages = [{ role: 'system', content: sistema }];
|
|
175
|
+
if (projectDir) {
|
|
176
|
+
messages.push({ role: 'system', content: `[Projeto: ${projectName || 'sem nome'}]\n[Pasta: ${projectDir}]\n`
|
|
177
|
+
+ 'Todo caminho que você passar para write_file/read_file/list_dir é relativo a esta pasta. '
|
|
178
|
+
+ 'Use caminhos relativos simples (ex: "index.html", "src/app.js"). Nunca use caminho absoluto.' });
|
|
179
|
+
}
|
|
180
|
+
const conversa = convo.length ? convo : [...(history || []), { role: 'user', content: String(prompt || '') }];
|
|
181
|
+
messages.push(...entrada.caberNoTeto(conversa.filter((m) => m && m.role), TETO_CONVERSA).messages);
|
|
182
|
+
|
|
183
|
+
let texto_ = '';
|
|
184
|
+
const chamadas = [];
|
|
185
|
+
await lerStream({ messages, tools, tool_choice: 'auto', temperature: 0.1, max_tokens: SAIDA_MAXIMA }, {
|
|
186
|
+
sinal,
|
|
187
|
+
aoPedaco: (delta) => {
|
|
188
|
+
if (delta.tool_calls) juntarChamadas(chamadas, delta.tool_calls);
|
|
189
|
+
if (delta.content && !chamadas.length) { texto_ += delta.content; emit({ delta: delta.content }); }
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
chamadas.forEach((tc, i) => {
|
|
193
|
+
if (!tc || !tc.name) return;
|
|
194
|
+
let args = {};
|
|
195
|
+
try { args = JSON.parse(tc.arguments || '{}'); } catch { /* o loop pede de novo */ }
|
|
196
|
+
emit({ tool_call: { id: tc.id || `local_${Date.now()}_${i}`, name: tc.name, arguments: args } });
|
|
197
|
+
});
|
|
198
|
+
if (texto_) emit({ assistant_message: { content: texto_ } });
|
|
199
|
+
emit({ done: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ── /api/chat/stream ────────────────────────────────────────────────── */
|
|
203
|
+
async function requestChat(_servidor, body, emit) {
|
|
204
|
+
const { messages, prompt, history, sinal } = body || {};
|
|
205
|
+
const turnos = Array.isArray(messages) ? messages
|
|
206
|
+
: [...(Array.isArray(history) ? history : []), ...(prompt ? [{ role: 'user', content: prompt }] : [])];
|
|
207
|
+
await lerStream({
|
|
208
|
+
messages: [{ role: 'system', content: cerebro.SYSTEM_CHAT }, ...turnos.filter((m) => m && m.role)],
|
|
209
|
+
temperature: 0.4, max_tokens: 1200,
|
|
210
|
+
}, { sinal, aoPedaco: (delta) => { if (delta.content) emit({ delta: delta.content }); } });
|
|
211
|
+
emit({ done: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── /api/resumir ────────────────────────────────────────────────────── */
|
|
215
|
+
async function requestResumo(_servidor, { resumo, mensagens } = {}) {
|
|
216
|
+
const novas = Array.isArray(mensagens) ? mensagens.filter((m) => m && m.role) : [];
|
|
217
|
+
if (!novas.length) return { resumo: String(resumo || '') };
|
|
218
|
+
const virarTexto = (m) => {
|
|
219
|
+
const quem = m.role === 'user' ? 'USUÁRIO' : m.role === 'assistant' ? 'AGENTE' : 'FERRAMENTA';
|
|
220
|
+
const conteudo = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
221
|
+
return `${quem}: ${conteudo.slice(0, 1200)}`;
|
|
222
|
+
};
|
|
223
|
+
const corpo = (resumo ? `RESUMO ANTERIOR:\n${resumo}\n\n` : '') + `MENSAGENS NOVAS:\n${novas.map(virarTexto).join('\n')}`;
|
|
224
|
+
try {
|
|
225
|
+
const limpo = await completar([{ role: 'system', content: cerebro.SYSTEM_RESUMO }, { role: 'user', content: corpo }], { maxTokens: 600 });
|
|
226
|
+
return { resumo: limpo || String(resumo || '') };
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return { resumo: String(resumo || ''), erro: e.message };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Os passos do plano, tolerando cerca de markdown. Mesma regra do servidor. */
|
|
233
|
+
function lerPassos(texto) {
|
|
234
|
+
const bruto = String(texto || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
|
|
235
|
+
const abre = bruto.indexOf('{');
|
|
236
|
+
const fecha = bruto.lastIndexOf('}');
|
|
237
|
+
if (abre === -1 || fecha <= abre) return null;
|
|
238
|
+
let j;
|
|
239
|
+
try { j = JSON.parse(bruto.slice(abre, fecha + 1)); } catch { return null; }
|
|
240
|
+
const lista = Array.isArray(j && j.passos) ? j.passos : null;
|
|
241
|
+
if (!lista || !lista.length) return null;
|
|
242
|
+
const limpos = lista
|
|
243
|
+
.map((p) => ({ titulo: String((p && p.titulo) || 'passo').slice(0, 60), pedido: String((p && p.pedido) || '').trim() }))
|
|
244
|
+
.filter((p) => p.pedido.length > 10)
|
|
245
|
+
.slice(0, 3);
|
|
246
|
+
return limpos.length ? limpos : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* ── /api/plano ──────────────────────────────────────────────────────── */
|
|
250
|
+
async function requestPlano(_servidor, { prompt } = {}) {
|
|
251
|
+
const cru = String(prompt || '').trim();
|
|
252
|
+
const passoUnico = [{ titulo: 'tarefa', pedido: cru }];
|
|
253
|
+
if (cru.length < 25) return { passos: passoUnico, dividiu: false };
|
|
254
|
+
try {
|
|
255
|
+
const passos = lerPassos(await completar(
|
|
256
|
+
[{ role: 'system', content: cerebro.SYSTEM_PLANO }, { role: 'user', content: cru }],
|
|
257
|
+
{ maxTokens: 800, temperatura: 0.1 })) || passoUnico;
|
|
258
|
+
return { passos, dividiu: passos.length > 1 };
|
|
259
|
+
} catch {
|
|
260
|
+
return { passos: passoUnico, dividiu: false };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { requestAct, requestChat, requestResumo, requestPlano, juntarChamadas, lerPassos, ferramentas, ativo: () => !!provedor.atual() };
|