primocode 9.8.0-beta.0 → 9.8.0-beta.2
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 +73 -0
- package/lib/api.js +21 -0
- package/lib/claude-engine.js +630 -0
- package/lib/codex-engine.js +207 -0
- package/lib/local/cerebro.json +2116 -0
- package/lib/local/entrada.js +332 -0
- package/lib/local/motor-cli.js +90 -0
- package/lib/local/motor.js +264 -0
- package/lib/local/provedor.js +169 -0
- package/package.json +2 -2
|
@@ -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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motor-cli.js — o Claude Code ou o Codex da pessoa como cérebro do PrimoCode.
|
|
3
|
+
*
|
|
4
|
+
* "Coloca o /claude de novo, mas dentro do /api: aí coloca /claude e
|
|
5
|
+
* /codex." (24/09/2026)
|
|
6
|
+
*
|
|
7
|
+
* Sem chave: usa a assinatura que a pessoa já tem nessas ferramentas. O
|
|
8
|
+
* agente (o loop, as ferramentas, a tela) continua sendo o do PrimoCode; a
|
|
9
|
+
* ferramenta de fora só decide o próximo passo, pelo protocolo de bloco
|
|
10
|
+
* marcado de lib/claude-engine.js (o Codex usa o mesmo).
|
|
11
|
+
*
|
|
12
|
+
* Os motores só sabem o `requestAct`. Chat e resumo, aqui, são uma pergunta
|
|
13
|
+
* de texto simples ao mesmo CLI; o plano não divide (uma ida a mais a um CLI
|
|
14
|
+
* lento custaria mais do que a divisão poupa) e segue em passo único.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { execFileSync } = require('child_process');
|
|
20
|
+
const cerebro = require('./cerebro.json');
|
|
21
|
+
|
|
22
|
+
const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
|
|
23
|
+
|
|
24
|
+
function motorDe(id) {
|
|
25
|
+
if (id === 'codex') {
|
|
26
|
+
/* O padrão antigo do motor (gpt-5-mini) é recusado por conta de
|
|
27
|
+
assinatura do ChatGPT — medido: "o Codex não aceitou o modelo", e
|
|
28
|
+
uma segunda chamada em TODA rodada. Pelo /api, vai o modelo da
|
|
29
|
+
conta. Quem quiser outro ainda usa PRIMOCODE_CODEX_MODELO. */
|
|
30
|
+
if (process.env.PRIMOCODE_CODEX_MODELO === undefined) process.env.PRIMOCODE_CODEX_MODELO = '';
|
|
31
|
+
return require('../codex-engine.js');
|
|
32
|
+
}
|
|
33
|
+
return require('../claude-engine.js');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Uma pergunta de texto, resposta de texto. Sem protocolo, sem ferramenta. */
|
|
37
|
+
function perguntar(id, texto) {
|
|
38
|
+
if (id === 'codex') return String(motorDe('codex').chamarCodex(texto) || '').trim();
|
|
39
|
+
const claude = motorDe('claude-code');
|
|
40
|
+
const saida = execFileSync('claude', ['-p', '--output-format', 'json', ...claude.argsDoModelo()], {
|
|
41
|
+
encoding: 'utf-8', timeout: 120000, input: texto, maxBuffer: 8 * 1024 * 1024, ...SEM_APARECER,
|
|
42
|
+
});
|
|
43
|
+
try { return String(JSON.parse(saida).result || '').trim(); } catch { return String(saida || '').trim(); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function criar(id) {
|
|
47
|
+
const motor = motorDe(id);
|
|
48
|
+
return {
|
|
49
|
+
requestAct: (servidor, payload, emit) => motor.requestAct(servidor, payload, emit),
|
|
50
|
+
|
|
51
|
+
async requestChat(_servidor, { messages, prompt, history } = {}, emit) {
|
|
52
|
+
const turnos = Array.isArray(messages) ? messages
|
|
53
|
+
: [...(Array.isArray(history) ? history : []), ...(prompt ? [{ role: 'user', content: prompt }] : [])];
|
|
54
|
+
const conversa = turnos.filter((m) => m && m.role && typeof m.content === 'string')
|
|
55
|
+
.map((m) => `${m.role === 'user' ? 'PESSOA' : 'VOCÊ'}: ${m.content}`).join('\n');
|
|
56
|
+
try {
|
|
57
|
+
const resposta = perguntar(id, `${cerebro.SYSTEM_CHAT}\n\nResponda só à última fala da PESSOA.\n\n${conversa}`);
|
|
58
|
+
if (resposta) emit({ delta: resposta });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
emit({ erro: e.message });
|
|
61
|
+
}
|
|
62
|
+
emit({ done: true });
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async requestResumo(_servidor, { resumo, mensagens } = {}) {
|
|
66
|
+
const novas = (mensagens || []).filter((m) => m && m.role);
|
|
67
|
+
if (!novas.length) return { resumo: String(resumo || '') };
|
|
68
|
+
const corpo = (resumo ? `RESUMO ANTERIOR:\n${resumo}\n\n` : '') + 'MENSAGENS NOVAS:\n'
|
|
69
|
+
+ novas.map((m) => `${m.role === 'user' ? 'USUÁRIO' : m.role === 'assistant' ? 'AGENTE' : 'FERRAMENTA'}: `
|
|
70
|
+
+ String(typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).slice(0, 1200)).join('\n');
|
|
71
|
+
try {
|
|
72
|
+
return { resumo: perguntar(id, `${cerebro.SYSTEM_RESUMO}\n\n${corpo}`) || String(resumo || '') };
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { resumo: String(resumo || ''), erro: e.message };
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async requestPlano(_servidor, { prompt } = {}) {
|
|
79
|
+
return { passos: [{ titulo: 'tarefa', pedido: String(prompt || '').trim() }], dividiu: false };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A ferramenta está instalada? (sem rodar nada caro) */
|
|
85
|
+
function instalado(id) {
|
|
86
|
+
if (id === 'codex') return motorDe('codex').temCodex();
|
|
87
|
+
return motorDe('claude-code').temClaudeCode();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { criar, instalado, perguntar };
|