@nomad-e/bluma-cli 0.1.24 → 0.1.25
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 +52 -10
- package/dist/main.js +380 -166
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,22 +231,33 @@ The JSON payload must follow this envelope:
|
|
|
231
231
|
|
|
232
232
|
```json
|
|
233
233
|
{
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
234
|
+
"session_id": "conv-uuid-stable", // Recomendado: mesma sessão entre jobs (histórico + workspace)
|
|
235
|
+
"message_id": "job-123", // Opcional mas recomendado
|
|
236
|
+
"from_agent": "sandbox-api",
|
|
237
|
+
"to_agent": "bluma",
|
|
238
|
+
"action": "generate_app",
|
|
239
|
+
"context": {
|
|
239
240
|
"user_request": "Criar dashboard de vendas",
|
|
240
241
|
"erp_models": ["sale.order"],
|
|
241
242
|
"permissions": ["sales"]
|
|
242
243
|
},
|
|
243
|
-
"
|
|
244
|
+
"user_context": {
|
|
245
|
+
"userId": "13",
|
|
246
|
+
"userName": "Nome",
|
|
247
|
+
"userEmail": "user@example.com",
|
|
248
|
+
"companyId": "4",
|
|
249
|
+
"companyName": "Empresa",
|
|
250
|
+
"conversationId": null
|
|
251
|
+
},
|
|
252
|
+
"metadata": {
|
|
244
253
|
"sandbox": true,
|
|
245
254
|
"caller": "agiweb"
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
257
|
```
|
|
249
258
|
|
|
259
|
+
O campo **`user_context`** (opcional) é enviado ao FactorRouter nos headers `X-User-*` / `X-Company-*` para custos e auditoria. `context.user_request` (primeiros 300 caracteres) vai em `X-User-Message` (URL-encoded).
|
|
260
|
+
|
|
250
261
|
Internally, BluMa will:
|
|
251
262
|
|
|
252
263
|
- Initialize the agent with a dedicated `eventBus`.
|
|
@@ -479,10 +490,41 @@ Notes
|
|
|
479
490
|
---
|
|
480
491
|
|
|
481
492
|
## <a name="configuration-and-environment-variables"></a>Configuration and Environment Variables
|
|
482
|
-
You must create a `.env` file (copy if needed from `.env.example`) with the following variable:
|
|
483
|
-
- `OPENROUTER_API_KEY` (required; get from [openrouter.ai](https://openrouter.ai))
|
|
484
493
|
|
|
485
|
-
|
|
494
|
+
**Recommended:** set **`FACTOR_ROUTER_KEY`** and **`FACTOR_ROUTER_URL`** in your **user or system environment** (shell profile, Windows User env, CI secrets, etc.) so every process sees them.
|
|
495
|
+
|
|
496
|
+
BluMa also **loads** `~/.bluma/.env` if that file exists (optional merge via `dotenv`); use `.env.example` as a template only if you prefer a local file.
|
|
497
|
+
|
|
498
|
+
**LLM routing** uses the FactorRouter gateway (OpenAI-compatible API):
|
|
499
|
+
|
|
500
|
+
- `FACTOR_ROUTER_KEY` (required) — e.g. `sk-fai-...` from your FactorRouter admin
|
|
501
|
+
- `FACTOR_ROUTER_URL` (required) — gateway base URL (e.g. `http://host:8003/router-api`; the client appends `/v1` if missing)
|
|
502
|
+
|
|
503
|
+
These replace legacy `NOMAD_API_KEY`, `NOMAD_BASE_URL`, and `MODEL_NOMAD` (the router picks the model; requests use `model: "auto"`).
|
|
504
|
+
|
|
505
|
+
Optional: `BLUMA_SANDBOX`, `BLUMA_SANDBOX_NAME`, MCP tokens, etc.
|
|
506
|
+
|
|
507
|
+
### FactorRouter — headers HTTP (CLI vs sandbox)
|
|
508
|
+
|
|
509
|
+
O SDK OpenAI (`openai` npm) envia metadados no **2.º argumento** da chamada `chat.completions.create(body, { headers })` — são **headers HTTP normais**, não um campo `extra_headers` no JSON do body.
|
|
510
|
+
|
|
511
|
+
**Modo CLI interativo** (Ink, sem envelope): em cada request ao gateway são acrescentados:
|
|
512
|
+
|
|
513
|
+
| Header | Conteúdo típico (exemplo) |
|
|
514
|
+
|--------|---------------------------|
|
|
515
|
+
| `X-Turn-Id` | UUID novo por turno (igual em todo o loop de tools desse turno) |
|
|
516
|
+
| `X-Session-Id` | ID da sessão BluMa (`~/.bluma/sessions/…`) |
|
|
517
|
+
| `X-Conversation-Id` | `null` |
|
|
518
|
+
| `X-User-Message` | Primeiros 300 caracteres do pedido (URL-encoded) |
|
|
519
|
+
| `X-User-Id` | MAC da 1.ª interface não-interna, ou `host:<hostname>` se não houver MAC útil |
|
|
520
|
+
| `X-User-Name` | Utilizador do SO (`os.userInfo().username`, URL-encoded) |
|
|
521
|
+
| `X-User-Email` | `null` |
|
|
522
|
+
| `X-Company-Id` | Igual a `X-User-Id` (identificador da máquina) |
|
|
523
|
+
| `X-Company-Name` | Igual (URL-encoded) |
|
|
524
|
+
|
|
525
|
+
**Privacidade (desenvolvedores):** na CLI, estes valores servem para **agregação de custos** no FactorRouter. Não substituem o utilizador real no **agent mode**: aí prevalece o bloco `user_context` do JSON (sandbox / Severino).
|
|
526
|
+
|
|
527
|
+
**Agent mode** (`bluma agent`): os mesmos nomes de header; valores vêm do envelope (`session_id`, `user_context`, `context.user_request`). Se `user_context` for omitido, user/company ficam `null` nos headers (não se usa a heurística MAC da CLI).
|
|
486
528
|
|
|
487
529
|
Advanced config files are located in `src/app/agent/config/`.
|
|
488
530
|
|
|
@@ -495,7 +537,7 @@ Advanced config files are located in `src/app/agent/config/`.
|
|
|
495
537
|
- Bundler: esbuild, with `esbuild-plugin-node-externals`
|
|
496
538
|
- Test Runner: Jest 30 + babel-jest
|
|
497
539
|
- Transpilers: Babel presets (env, react, typescript)
|
|
498
|
-
- LLM/Agent:
|
|
540
|
+
- LLM/Agent: FactorRouter (OpenAI-compatible API); MCP via `@modelcontextprotocol/sdk`
|
|
499
541
|
- Config loading: dotenv
|
|
500
542
|
- Utilities: uuid, diff, react-devtools-core
|
|
501
543
|
|
package/dist/main.js
CHANGED
|
@@ -332,7 +332,7 @@ import React11 from "react";
|
|
|
332
332
|
import { render } from "ink";
|
|
333
333
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
334
334
|
import fs14 from "fs";
|
|
335
|
-
import { v4 as
|
|
335
|
+
import { v4 as uuidv46 } from "uuid";
|
|
336
336
|
|
|
337
337
|
// src/app/ui/App.tsx
|
|
338
338
|
import { useState as useState7, useEffect as useEffect6, useRef as useRef5, useCallback as useCallback2, memo as memo11 } from "react";
|
|
@@ -1485,7 +1485,7 @@ var ConfirmationPrompt = memo4(ConfirmationPromptComponent);
|
|
|
1485
1485
|
// src/app/agent/agent.ts
|
|
1486
1486
|
import * as dotenv from "dotenv";
|
|
1487
1487
|
import path16 from "path";
|
|
1488
|
-
import
|
|
1488
|
+
import os10 from "os";
|
|
1489
1489
|
|
|
1490
1490
|
// src/app/agent/tool_invoker.ts
|
|
1491
1491
|
import { promises as fs8 } from "fs";
|
|
@@ -3704,6 +3704,7 @@ var AdvancedFeedbackSystem = class {
|
|
|
3704
3704
|
|
|
3705
3705
|
// src/app/agent/bluma/core/bluma.ts
|
|
3706
3706
|
import path15 from "path";
|
|
3707
|
+
import { v4 as uuidv43 } from "uuid";
|
|
3707
3708
|
|
|
3708
3709
|
// src/app/agent/session_manager/session_manager.ts
|
|
3709
3710
|
import path12 from "path";
|
|
@@ -4913,21 +4914,243 @@ function createApiContextWindow(fullHistory, maxTurns) {
|
|
|
4913
4914
|
return finalContext;
|
|
4914
4915
|
}
|
|
4915
4916
|
|
|
4917
|
+
// src/app/agent/core/llm/llm.ts
|
|
4918
|
+
import os8 from "os";
|
|
4919
|
+
import OpenAI from "openai";
|
|
4920
|
+
function defaultBlumaUserContextInput(sessionId, userMessage) {
|
|
4921
|
+
const msg = String(userMessage || "").slice(0, 300);
|
|
4922
|
+
return {
|
|
4923
|
+
sessionId,
|
|
4924
|
+
conversationId: null,
|
|
4925
|
+
userMessage: msg,
|
|
4926
|
+
userId: null,
|
|
4927
|
+
userName: null,
|
|
4928
|
+
userEmail: null,
|
|
4929
|
+
companyId: null,
|
|
4930
|
+
companyName: null
|
|
4931
|
+
};
|
|
4932
|
+
}
|
|
4933
|
+
function getPreferredMacAddress() {
|
|
4934
|
+
try {
|
|
4935
|
+
const ifaces = os8.networkInterfaces();
|
|
4936
|
+
for (const name of Object.keys(ifaces)) {
|
|
4937
|
+
const addrs = ifaces[name];
|
|
4938
|
+
if (!addrs) continue;
|
|
4939
|
+
for (const addr of addrs) {
|
|
4940
|
+
if (addr.internal) continue;
|
|
4941
|
+
const mac = (addr.mac || "").toLowerCase();
|
|
4942
|
+
if (mac && mac !== "00:00:00:00:00:00") {
|
|
4943
|
+
return mac;
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
} catch {
|
|
4948
|
+
}
|
|
4949
|
+
try {
|
|
4950
|
+
return `host:${os8.hostname()}`;
|
|
4951
|
+
} catch {
|
|
4952
|
+
return "unknown";
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
function defaultInteractiveCliUserContextInput(sessionId, userMessage) {
|
|
4956
|
+
const base = defaultBlumaUserContextInput(sessionId, userMessage);
|
|
4957
|
+
const machineId = getPreferredMacAddress();
|
|
4958
|
+
let userName = null;
|
|
4959
|
+
try {
|
|
4960
|
+
userName = os8.userInfo().username || null;
|
|
4961
|
+
} catch {
|
|
4962
|
+
userName = null;
|
|
4963
|
+
}
|
|
4964
|
+
return {
|
|
4965
|
+
...base,
|
|
4966
|
+
userId: machineId,
|
|
4967
|
+
userName,
|
|
4968
|
+
userEmail: null,
|
|
4969
|
+
companyId: machineId,
|
|
4970
|
+
companyName: machineId
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
function encodeHeader(value, maxLen = 300) {
|
|
4974
|
+
if (value == null || value === "") return "null";
|
|
4975
|
+
return encodeURIComponent(String(value).slice(0, maxLen));
|
|
4976
|
+
}
|
|
4977
|
+
function buildFactorHeaders(ctx) {
|
|
4978
|
+
return {
|
|
4979
|
+
"X-Turn-Id": ctx.turnId,
|
|
4980
|
+
"X-Session-Id": ctx.sessionId,
|
|
4981
|
+
"X-Conversation-Id": ctx.conversationId ?? "null",
|
|
4982
|
+
"X-User-Message": encodeHeader(ctx.userMessage),
|
|
4983
|
+
"X-User-Id": ctx.userId ?? "null",
|
|
4984
|
+
"X-User-Name": encodeHeader(ctx.userName),
|
|
4985
|
+
"X-User-Email": ctx.userEmail ?? "null",
|
|
4986
|
+
"X-Company-Id": ctx.companyId ?? "null",
|
|
4987
|
+
"X-Company-Name": encodeHeader(ctx.companyName)
|
|
4988
|
+
};
|
|
4989
|
+
}
|
|
4990
|
+
function normalizeFactorBaseUrl(raw) {
|
|
4991
|
+
let u = raw.trim().replace(/\/$/, "");
|
|
4992
|
+
u = u.replace(/^http:\/\/http:\/\//i, "http://");
|
|
4993
|
+
if (!u.endsWith("/v1")) {
|
|
4994
|
+
u = `${u}/v1`;
|
|
4995
|
+
}
|
|
4996
|
+
return u;
|
|
4997
|
+
}
|
|
4998
|
+
async function notifyFactorRouterTurnEnd(params) {
|
|
4999
|
+
const frUrl = (process.env.FACTOR_ROUTER_URL ?? "").trim();
|
|
5000
|
+
const frKey = (process.env.FACTOR_ROUTER_KEY ?? "").trim();
|
|
5001
|
+
if (!frUrl || !frKey) return;
|
|
5002
|
+
const base = normalizeFactorBaseUrl(frUrl);
|
|
5003
|
+
const url = `${base}/turns/${encodeURIComponent(params.turnId)}/end`;
|
|
5004
|
+
try {
|
|
5005
|
+
await fetch(url, {
|
|
5006
|
+
method: "POST",
|
|
5007
|
+
headers: {
|
|
5008
|
+
Authorization: `Bearer ${frKey}`,
|
|
5009
|
+
"Content-Type": "application/json",
|
|
5010
|
+
...buildFactorHeaders(params.userContext)
|
|
5011
|
+
},
|
|
5012
|
+
body: JSON.stringify({ reason: params.reason ?? "message_result" })
|
|
5013
|
+
});
|
|
5014
|
+
} catch {
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
function hasLegacyNomadEnvVars() {
|
|
5018
|
+
return Boolean(
|
|
5019
|
+
(process.env.NOMAD_API_KEY || "").trim() || (process.env.NOMAD_BASE_URL || "").trim() || (process.env.MODEL_NOMAD || "").trim()
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
5022
|
+
var LLMService = class {
|
|
5023
|
+
client;
|
|
5024
|
+
constructor() {
|
|
5025
|
+
const frUrl = (process.env.FACTOR_ROUTER_URL ?? "").trim();
|
|
5026
|
+
const frKey = (process.env.FACTOR_ROUTER_KEY ?? "").trim();
|
|
5027
|
+
if (!frUrl || !frKey) {
|
|
5028
|
+
let msg = "FACTOR_ROUTER_URL and FACTOR_ROUTER_KEY must be set in the environment (e.g. export in your shell profile or OS user variables).";
|
|
5029
|
+
if (hasLegacyNomadEnvVars()) {
|
|
5030
|
+
msg += " You still have NOMAD_API_KEY, NOMAD_BASE_URL, and/or MODEL_NOMAD set \u2014 remove those and use FactorRouter variables instead.";
|
|
5031
|
+
}
|
|
5032
|
+
throw new Error(msg);
|
|
5033
|
+
}
|
|
5034
|
+
const baseURL = normalizeFactorBaseUrl(frUrl);
|
|
5035
|
+
this.client = new OpenAI({
|
|
5036
|
+
apiKey: frKey,
|
|
5037
|
+
baseURL
|
|
5038
|
+
});
|
|
5039
|
+
}
|
|
5040
|
+
requestHeaders(ctx) {
|
|
5041
|
+
return buildFactorHeaders(ctx);
|
|
5042
|
+
}
|
|
5043
|
+
async chatCompletion(params) {
|
|
5044
|
+
if (!params.userContext) {
|
|
5045
|
+
throw new Error("LLMService.chatCompletion: userContext \xE9 obrigat\xF3rio");
|
|
5046
|
+
}
|
|
5047
|
+
const tools = params.tools;
|
|
5048
|
+
const hasTools = Array.isArray(tools) && tools.length > 0;
|
|
5049
|
+
const resp = await this.client.chat.completions.create(
|
|
5050
|
+
{
|
|
5051
|
+
model: "auto",
|
|
5052
|
+
messages: params.messages,
|
|
5053
|
+
tools: hasTools ? tools : void 0,
|
|
5054
|
+
tool_choice: hasTools ? "auto" : void 0,
|
|
5055
|
+
parallel_tool_calls: params.parallel_tool_calls ?? false,
|
|
5056
|
+
temperature: params.temperature ?? 0,
|
|
5057
|
+
max_tokens: params.max_tokens
|
|
5058
|
+
},
|
|
5059
|
+
{ headers: this.requestHeaders(params.userContext) }
|
|
5060
|
+
);
|
|
5061
|
+
return resp;
|
|
5062
|
+
}
|
|
5063
|
+
/**
|
|
5064
|
+
* Streaming — mesmo turnId em todas as chamadas do loop de tools (via params.userContext).
|
|
5065
|
+
*/
|
|
5066
|
+
async *chatCompletionStream(params) {
|
|
5067
|
+
if (!params.userContext) {
|
|
5068
|
+
throw new Error("LLMService.chatCompletionStream: userContext \xE9 obrigat\xF3rio");
|
|
5069
|
+
}
|
|
5070
|
+
const tools = params.tools;
|
|
5071
|
+
const hasTools = Array.isArray(tools) && tools.length > 0;
|
|
5072
|
+
const stream = await this.client.chat.completions.create(
|
|
5073
|
+
{
|
|
5074
|
+
model: "auto",
|
|
5075
|
+
messages: params.messages,
|
|
5076
|
+
tools: hasTools ? tools : void 0,
|
|
5077
|
+
tool_choice: hasTools ? "auto" : void 0,
|
|
5078
|
+
parallel_tool_calls: params.parallel_tool_calls ?? false,
|
|
5079
|
+
temperature: params.temperature ?? 0,
|
|
5080
|
+
max_tokens: params.max_tokens,
|
|
5081
|
+
stream: true
|
|
5082
|
+
},
|
|
5083
|
+
{ headers: this.requestHeaders(params.userContext) }
|
|
5084
|
+
);
|
|
5085
|
+
const toolCallsAccumulator = /* @__PURE__ */ new Map();
|
|
5086
|
+
for await (const chunk of stream) {
|
|
5087
|
+
const choice = chunk.choices[0];
|
|
5088
|
+
if (!choice) continue;
|
|
5089
|
+
const delta = choice.delta;
|
|
5090
|
+
if (delta?.tool_calls) {
|
|
5091
|
+
for (const tc of delta.tool_calls) {
|
|
5092
|
+
const idx = tc.index;
|
|
5093
|
+
if (!toolCallsAccumulator.has(idx)) {
|
|
5094
|
+
toolCallsAccumulator.set(idx, {
|
|
5095
|
+
id: tc.id || "",
|
|
5096
|
+
type: tc.type || "function",
|
|
5097
|
+
function: { name: "", arguments: "" }
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
5100
|
+
const acc = toolCallsAccumulator.get(idx);
|
|
5101
|
+
if (tc.id) acc.id = tc.id;
|
|
5102
|
+
if (tc.function?.name) acc.function.name = tc.function.name;
|
|
5103
|
+
if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
const reasoning = delta?.reasoning_content || delta?.reasoning || "";
|
|
5107
|
+
yield {
|
|
5108
|
+
delta: delta?.content || "",
|
|
5109
|
+
reasoning,
|
|
5110
|
+
tool_calls: choice.finish_reason === "tool_calls" ? Array.from(toolCallsAccumulator.values()) : void 0,
|
|
5111
|
+
finish_reason: choice.finish_reason
|
|
5112
|
+
};
|
|
5113
|
+
}
|
|
5114
|
+
if (toolCallsAccumulator.size > 0) {
|
|
5115
|
+
yield {
|
|
5116
|
+
delta: "",
|
|
5117
|
+
tool_calls: Array.from(toolCallsAccumulator.values()),
|
|
5118
|
+
finish_reason: "tool_calls"
|
|
5119
|
+
};
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
/** Compat: modelo efectivo é decidido pelo gateway (`auto`). */
|
|
5123
|
+
getDefaultModel() {
|
|
5124
|
+
return "auto";
|
|
5125
|
+
}
|
|
5126
|
+
};
|
|
5127
|
+
|
|
4916
5128
|
// src/app/agent/core/llm/tool_call_normalizer.ts
|
|
4917
5129
|
import { randomUUID } from "crypto";
|
|
4918
5130
|
var ToolCallNormalizer = class {
|
|
5131
|
+
/**
|
|
5132
|
+
* Com tool_calls e sem texto visível: content deve ser null (API OpenAI-compatible), nunca "" nem undefined.
|
|
5133
|
+
*/
|
|
5134
|
+
static assistantContentWithToolCalls(content) {
|
|
5135
|
+
if (content === void 0 || content === null) return null;
|
|
5136
|
+
if (typeof content === "string") return content.trim() === "" ? null : content;
|
|
5137
|
+
return null;
|
|
5138
|
+
}
|
|
4919
5139
|
/**
|
|
4920
5140
|
* Normaliza a mensagem do assistant, convertendo diferentes formatos de tool calls
|
|
4921
5141
|
*/
|
|
4922
5142
|
static normalizeAssistantMessage(message2) {
|
|
4923
5143
|
if (message2.tool_calls && this.isOpenAIFormat(message2.tool_calls)) {
|
|
4924
|
-
return
|
|
5144
|
+
return {
|
|
5145
|
+
...message2,
|
|
5146
|
+
content: this.assistantContentWithToolCalls(message2.content)
|
|
5147
|
+
};
|
|
4925
5148
|
}
|
|
4926
5149
|
const toolCalls = this.extractToolCalls(message2);
|
|
4927
5150
|
if (toolCalls.length > 0) {
|
|
4928
5151
|
return {
|
|
4929
5152
|
role: message2.role || "assistant",
|
|
4930
|
-
content: message2.content
|
|
5153
|
+
content: this.assistantContentWithToolCalls(message2.content),
|
|
4931
5154
|
tool_calls: toolCalls
|
|
4932
5155
|
};
|
|
4933
5156
|
}
|
|
@@ -5064,7 +5287,6 @@ var ToolCallNormalizer = class {
|
|
|
5064
5287
|
// src/app/agent/bluma/core/bluma.ts
|
|
5065
5288
|
var BluMaAgent = class {
|
|
5066
5289
|
llm;
|
|
5067
|
-
model;
|
|
5068
5290
|
sessionId;
|
|
5069
5291
|
sessionFile = "";
|
|
5070
5292
|
history = [];
|
|
@@ -5074,11 +5296,12 @@ var BluMaAgent = class {
|
|
|
5074
5296
|
skillLoader;
|
|
5075
5297
|
maxContextTurns = 5;
|
|
5076
5298
|
isInterrupted = false;
|
|
5077
|
-
|
|
5299
|
+
/** Mesmo turnId durante processTurn + todo o loop de tool_calls (FactorRouter). */
|
|
5300
|
+
activeTurnContext = null;
|
|
5301
|
+
constructor(sessionId, eventBus, llm, mcpClient, feedbackSystem) {
|
|
5078
5302
|
this.sessionId = sessionId;
|
|
5079
5303
|
this.eventBus = eventBus;
|
|
5080
5304
|
this.llm = llm;
|
|
5081
|
-
this.model = model;
|
|
5082
5305
|
this.mcpClient = mcpClient;
|
|
5083
5306
|
this.feedbackSystem = feedbackSystem;
|
|
5084
5307
|
this.skillLoader = new SkillLoader(process.cwd());
|
|
@@ -5143,9 +5366,15 @@ var BluMaAgent = class {
|
|
|
5143
5366
|
getUiToolsDetailed() {
|
|
5144
5367
|
return this.mcpClient.getAvailableToolsDetailed();
|
|
5145
5368
|
}
|
|
5146
|
-
async processTurn(userInput) {
|
|
5369
|
+
async processTurn(userInput, userContextInput) {
|
|
5147
5370
|
this.isInterrupted = false;
|
|
5148
5371
|
const inputText = String(userInput.content || "").trim();
|
|
5372
|
+
const turnId = uuidv43();
|
|
5373
|
+
this.activeTurnContext = {
|
|
5374
|
+
...userContextInput,
|
|
5375
|
+
turnId,
|
|
5376
|
+
sessionId: userContextInput.sessionId || this.sessionId
|
|
5377
|
+
};
|
|
5149
5378
|
this.history.push({ role: "user", content: inputText });
|
|
5150
5379
|
if (inputText === "/init") {
|
|
5151
5380
|
this.eventBus.emit("dispatch", inputText);
|
|
@@ -5265,6 +5494,13 @@ var BluMaAgent = class {
|
|
|
5265
5494
|
try {
|
|
5266
5495
|
const resultObj = typeof toolResultContent === "string" ? JSON.parse(toolResultContent) : toolResultContent;
|
|
5267
5496
|
if (resultObj.message_type === "result") {
|
|
5497
|
+
if (this.activeTurnContext) {
|
|
5498
|
+
await notifyFactorRouterTurnEnd({
|
|
5499
|
+
turnId: this.activeTurnContext.turnId,
|
|
5500
|
+
userContext: this.activeTurnContext,
|
|
5501
|
+
reason: "message_result"
|
|
5502
|
+
});
|
|
5503
|
+
}
|
|
5268
5504
|
shouldContinueConversation = false;
|
|
5269
5505
|
this.eventBus.emit("backend_message", { type: "done", status: "completed" });
|
|
5270
5506
|
}
|
|
@@ -5302,6 +5538,12 @@ ${editData.error.display}`;
|
|
|
5302
5538
|
return `An unexpected error occurred while generating the edit preview: ${e.message}`;
|
|
5303
5539
|
}
|
|
5304
5540
|
}
|
|
5541
|
+
getLlmUserContext() {
|
|
5542
|
+
if (!this.activeTurnContext) {
|
|
5543
|
+
throw new Error("BluMaAgent: activeTurnContext ausente (processTurn n\xE3o iniciou o turno).");
|
|
5544
|
+
}
|
|
5545
|
+
return this.activeTurnContext;
|
|
5546
|
+
}
|
|
5305
5547
|
async _continueConversation() {
|
|
5306
5548
|
try {
|
|
5307
5549
|
if (this.isInterrupted) {
|
|
@@ -5330,12 +5572,11 @@ ${editData.error.display}`;
|
|
|
5330
5572
|
let hasEmittedStart = false;
|
|
5331
5573
|
let reasoningContent = "";
|
|
5332
5574
|
const stream = llmService.chatCompletionStream({
|
|
5333
|
-
model: this.model,
|
|
5334
5575
|
messages: contextWindow,
|
|
5335
5576
|
temperature: 0,
|
|
5336
5577
|
tools: this.mcpClient.getAvailableTools(),
|
|
5337
|
-
|
|
5338
|
-
|
|
5578
|
+
parallel_tool_calls: false,
|
|
5579
|
+
userContext: this.getLlmUserContext()
|
|
5339
5580
|
});
|
|
5340
5581
|
for await (const chunk of stream) {
|
|
5341
5582
|
if (this.isInterrupted) {
|
|
@@ -5368,11 +5609,14 @@ ${editData.error.display}`;
|
|
|
5368
5609
|
}
|
|
5369
5610
|
}
|
|
5370
5611
|
}
|
|
5612
|
+
const trimmedText = accumulatedContent.trim();
|
|
5613
|
+
const hasToolCalls = Boolean(toolCalls && toolCalls.length > 0);
|
|
5614
|
+
const content = trimmedText === "" ? null : accumulatedContent;
|
|
5371
5615
|
const message2 = {
|
|
5372
5616
|
role: "assistant",
|
|
5373
|
-
content
|
|
5617
|
+
content
|
|
5374
5618
|
};
|
|
5375
|
-
if (
|
|
5619
|
+
if (hasToolCalls) {
|
|
5376
5620
|
message2.tool_calls = toolCalls;
|
|
5377
5621
|
}
|
|
5378
5622
|
const normalizedMessage = ToolCallNormalizer.normalizeAssistantMessage(message2);
|
|
@@ -5420,7 +5664,7 @@ ${editData.error.display}`;
|
|
|
5420
5664
|
this.eventBus.emit("backend_message", { type: "confirmation_request", tool_calls: validToolCalls });
|
|
5421
5665
|
}
|
|
5422
5666
|
}
|
|
5423
|
-
} else if (
|
|
5667
|
+
} else if (trimmedText) {
|
|
5424
5668
|
this.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
|
|
5425
5669
|
const feedback = this.feedbackSystem.generateFeedback({
|
|
5426
5670
|
event: "protocol_violation_direct_text",
|
|
@@ -5435,12 +5679,11 @@ ${editData.error.display}`;
|
|
|
5435
5679
|
}
|
|
5436
5680
|
async _handleNonStreamingResponse(contextWindow) {
|
|
5437
5681
|
const response = await this.llm.chatCompletion({
|
|
5438
|
-
model: this.model,
|
|
5439
5682
|
messages: contextWindow,
|
|
5440
5683
|
temperature: 0,
|
|
5441
5684
|
tools: this.mcpClient.getAvailableTools(),
|
|
5442
|
-
|
|
5443
|
-
|
|
5685
|
+
parallel_tool_calls: false,
|
|
5686
|
+
userContext: this.getLlmUserContext()
|
|
5444
5687
|
});
|
|
5445
5688
|
if (this.isInterrupted) {
|
|
5446
5689
|
this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
|
|
@@ -5493,7 +5736,7 @@ ${editData.error.display}`;
|
|
|
5493
5736
|
this.eventBus.emit("backend_message", { type: "confirmation_request", tool_calls: validToolCalls });
|
|
5494
5737
|
}
|
|
5495
5738
|
}
|
|
5496
|
-
} else if (message2.content) {
|
|
5739
|
+
} else if (typeof message2.content === "string" && message2.content.trim()) {
|
|
5497
5740
|
this.eventBus.emit("backend_message", { type: "assistant_message", content: message2.content });
|
|
5498
5741
|
const feedback = this.feedbackSystem.generateFeedback({
|
|
5499
5742
|
event: "protocol_violation_direct_text",
|
|
@@ -5509,105 +5752,6 @@ ${editData.error.display}`;
|
|
|
5509
5752
|
}
|
|
5510
5753
|
};
|
|
5511
5754
|
|
|
5512
|
-
// src/app/agent/core/llm/llm.ts
|
|
5513
|
-
import OpenAI from "openai";
|
|
5514
|
-
var LLMService = class {
|
|
5515
|
-
client;
|
|
5516
|
-
defaultModel;
|
|
5517
|
-
constructor(config2) {
|
|
5518
|
-
this.client = new OpenAI({
|
|
5519
|
-
apiKey: config2.apiKey,
|
|
5520
|
-
baseURL: config2.baseUrl || "",
|
|
5521
|
-
defaultHeaders: {
|
|
5522
|
-
"HTTP-Referer": "https://bluma.ai",
|
|
5523
|
-
// Optional. Site URL for rankings on openrouter.ai.
|
|
5524
|
-
"X-Title": "Bluma"
|
|
5525
|
-
// Optional. Site title for rankings on openrouter.ai.
|
|
5526
|
-
}
|
|
5527
|
-
});
|
|
5528
|
-
this.defaultModel = config2.model || "";
|
|
5529
|
-
}
|
|
5530
|
-
/**
|
|
5531
|
-
* Chamada tradicional (não-streaming) - retorna resposta completa
|
|
5532
|
-
*/
|
|
5533
|
-
async chatCompletion(params) {
|
|
5534
|
-
const resp = await this.client.chat.completions.create({
|
|
5535
|
-
model: params.model || this.defaultModel,
|
|
5536
|
-
messages: params.messages,
|
|
5537
|
-
tools: params.tools,
|
|
5538
|
-
//tool_choice: params.tool_choice,
|
|
5539
|
-
parallel_tool_calls: params.parallel_tool_calls,
|
|
5540
|
-
temperature: params.temperature,
|
|
5541
|
-
max_tokens: params.max_tokens
|
|
5542
|
-
});
|
|
5543
|
-
return resp;
|
|
5544
|
-
}
|
|
5545
|
-
/**
|
|
5546
|
-
* Chamada com streaming - retorna chunks em tempo real
|
|
5547
|
-
*
|
|
5548
|
-
* Uso:
|
|
5549
|
-
* ```
|
|
5550
|
-
* for await (const chunk of llm.chatCompletionStream(params)) {
|
|
5551
|
-
* process.stdout.write(chunk.delta);
|
|
5552
|
-
* }
|
|
5553
|
-
* ```
|
|
5554
|
-
*/
|
|
5555
|
-
async *chatCompletionStream(params) {
|
|
5556
|
-
const stream = await this.client.chat.completions.create({
|
|
5557
|
-
model: params.model || this.defaultModel,
|
|
5558
|
-
messages: params.messages,
|
|
5559
|
-
tools: params.tools,
|
|
5560
|
-
//tool_choice: params.tool_choice,
|
|
5561
|
-
parallel_tool_calls: params.parallel_tool_calls,
|
|
5562
|
-
temperature: params.temperature,
|
|
5563
|
-
max_tokens: params.max_tokens,
|
|
5564
|
-
stream: true
|
|
5565
|
-
});
|
|
5566
|
-
const toolCallsAccumulator = /* @__PURE__ */ new Map();
|
|
5567
|
-
for await (const chunk of stream) {
|
|
5568
|
-
const choice = chunk.choices[0];
|
|
5569
|
-
if (!choice) continue;
|
|
5570
|
-
const delta = choice.delta;
|
|
5571
|
-
if (delta?.tool_calls) {
|
|
5572
|
-
for (const tc of delta.tool_calls) {
|
|
5573
|
-
const idx = tc.index;
|
|
5574
|
-
if (!toolCallsAccumulator.has(idx)) {
|
|
5575
|
-
toolCallsAccumulator.set(idx, {
|
|
5576
|
-
id: tc.id || "",
|
|
5577
|
-
type: tc.type || "function",
|
|
5578
|
-
function: { name: "", arguments: "" }
|
|
5579
|
-
});
|
|
5580
|
-
}
|
|
5581
|
-
const acc = toolCallsAccumulator.get(idx);
|
|
5582
|
-
if (tc.id) acc.id = tc.id;
|
|
5583
|
-
if (tc.function?.name) acc.function.name = tc.function.name;
|
|
5584
|
-
if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
|
|
5585
|
-
}
|
|
5586
|
-
}
|
|
5587
|
-
const reasoning = delta?.reasoning_content || delta?.reasoning || "";
|
|
5588
|
-
yield {
|
|
5589
|
-
delta: delta?.content || "",
|
|
5590
|
-
reasoning,
|
|
5591
|
-
tool_calls: choice.finish_reason === "tool_calls" ? Array.from(toolCallsAccumulator.values()) : void 0,
|
|
5592
|
-
finish_reason: choice.finish_reason
|
|
5593
|
-
};
|
|
5594
|
-
}
|
|
5595
|
-
if (toolCallsAccumulator.size > 0) {
|
|
5596
|
-
yield {
|
|
5597
|
-
delta: "",
|
|
5598
|
-
tool_calls: Array.from(toolCallsAccumulator.values()),
|
|
5599
|
-
finish_reason: "tool_calls"
|
|
5600
|
-
};
|
|
5601
|
-
}
|
|
5602
|
-
}
|
|
5603
|
-
/**
|
|
5604
|
-
* Retorna o modelo padrão configurado
|
|
5605
|
-
*/
|
|
5606
|
-
getDefaultModel() {
|
|
5607
|
-
return this.defaultModel;
|
|
5608
|
-
}
|
|
5609
|
-
};
|
|
5610
|
-
|
|
5611
5755
|
// src/app/agent/subagents/registry.ts
|
|
5612
5756
|
var subAgentRegistry = {};
|
|
5613
5757
|
function registerSubAgent(subAgent) {
|
|
@@ -5619,8 +5763,14 @@ function getSubAgentByCommand(cmd) {
|
|
|
5619
5763
|
return subAgentRegistry[cmd];
|
|
5620
5764
|
}
|
|
5621
5765
|
|
|
5766
|
+
// src/app/agent/subagents/init/init_subagent.ts
|
|
5767
|
+
import { v4 as uuidv45 } from "uuid";
|
|
5768
|
+
|
|
5769
|
+
// src/app/agent/subagents/base_llm_subagent.ts
|
|
5770
|
+
import { v4 as uuidv44 } from "uuid";
|
|
5771
|
+
|
|
5622
5772
|
// src/app/agent/subagents/init/init_system_prompt.ts
|
|
5623
|
-
import
|
|
5773
|
+
import os9 from "os";
|
|
5624
5774
|
var SYSTEM_PROMPT2 = `
|
|
5625
5775
|
|
|
5626
5776
|
### YOU ARE BluMa CLI \u2014 INIT SUBAGENT \u2014 AUTONOMOUS SENIOR SOFTWARE ENGINEER @ NOMADENGENUITY
|
|
@@ -5783,12 +5933,12 @@ Rule Summary:
|
|
|
5783
5933
|
function getInitPrompt() {
|
|
5784
5934
|
const now = /* @__PURE__ */ new Date();
|
|
5785
5935
|
const collectedData = {
|
|
5786
|
-
os_type:
|
|
5787
|
-
os_version:
|
|
5788
|
-
architecture:
|
|
5936
|
+
os_type: os9.type(),
|
|
5937
|
+
os_version: os9.release(),
|
|
5938
|
+
architecture: os9.arch(),
|
|
5789
5939
|
workdir: process.cwd(),
|
|
5790
5940
|
shell_type: process.env.SHELL || process.env.COMSPEC || "Unknown",
|
|
5791
|
-
username:
|
|
5941
|
+
username: os9.userInfo().username || "Unknown",
|
|
5792
5942
|
current_date: now.toISOString().split("T")[0],
|
|
5793
5943
|
// Formato YYYY-MM-DD
|
|
5794
5944
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Unknown",
|
|
@@ -5822,6 +5972,8 @@ var BaseLLMSubAgent = class {
|
|
|
5822
5972
|
sessionFile = "";
|
|
5823
5973
|
maxContextTurns = 160;
|
|
5824
5974
|
isInterrupted = false;
|
|
5975
|
+
/** Um turnId por execute(); reutilizado em todo o loop de tools do subagente. */
|
|
5976
|
+
subagentTurnContext = null;
|
|
5825
5977
|
async execute(input, ctx) {
|
|
5826
5978
|
this.ctx = ctx;
|
|
5827
5979
|
this.isInterrupted = false;
|
|
@@ -5829,7 +5981,15 @@ var BaseLLMSubAgent = class {
|
|
|
5829
5981
|
this.isInterrupted = true;
|
|
5830
5982
|
});
|
|
5831
5983
|
await this.initializeHistory();
|
|
5832
|
-
|
|
5984
|
+
const rawUser = typeof input === "string" ? input : JSON.stringify(input);
|
|
5985
|
+
const base = ctx.blumaUserContextInput ?? defaultBlumaUserContextInput(`subagent:${this.id}`, rawUser.slice(0, 300));
|
|
5986
|
+
const turnId = uuidv44();
|
|
5987
|
+
this.subagentTurnContext = {
|
|
5988
|
+
...base,
|
|
5989
|
+
turnId,
|
|
5990
|
+
sessionId: base.sessionId || `subagent:${this.id}`
|
|
5991
|
+
};
|
|
5992
|
+
this.history.push({ role: "user", content: rawUser });
|
|
5833
5993
|
await this._continueConversation();
|
|
5834
5994
|
return { history: this.history };
|
|
5835
5995
|
}
|
|
@@ -5868,12 +6028,14 @@ ${editData.error.display}`;
|
|
|
5868
6028
|
return;
|
|
5869
6029
|
}
|
|
5870
6030
|
const contextWindow = this.history.slice(-this.maxContextTurns);
|
|
6031
|
+
if (!this.subagentTurnContext) {
|
|
6032
|
+
throw new Error("BaseLLMSubAgent: subagentTurnContext n\xE3o inicializado");
|
|
6033
|
+
}
|
|
5871
6034
|
const response = await this.ctx.llm.chatCompletion({
|
|
5872
|
-
model: this.ctx.policy?.llmDeployment || "default",
|
|
5873
6035
|
messages: contextWindow,
|
|
5874
6036
|
tools: this.ctx.mcpClient.getAvailableTools(),
|
|
5875
|
-
|
|
5876
|
-
|
|
6037
|
+
parallel_tool_calls: false,
|
|
6038
|
+
userContext: this.subagentTurnContext
|
|
5877
6039
|
});
|
|
5878
6040
|
if (this.isInterrupted) {
|
|
5879
6041
|
this.emitEvent("info", { message: "SubAgent task cancelled byuserr." });
|
|
@@ -5963,6 +6125,13 @@ var InitAgentImpl = class extends BaseLLMSubAgent {
|
|
|
5963
6125
|
this.isInterrupted = true;
|
|
5964
6126
|
});
|
|
5965
6127
|
await this.initializeHistory();
|
|
6128
|
+
const preview = typeof input === "string" ? input.slice(0, 300) : JSON.stringify(input ?? "").slice(0, 300);
|
|
6129
|
+
const base = ctx.blumaUserContextInput ?? defaultBlumaUserContextInput(`subagent:${this.id}`, preview);
|
|
6130
|
+
this.subagentTurnContext = {
|
|
6131
|
+
...base,
|
|
6132
|
+
turnId: uuidv45(),
|
|
6133
|
+
sessionId: base.sessionId || `subagent:${this.id}`
|
|
6134
|
+
};
|
|
5966
6135
|
const seed = `
|
|
5967
6136
|
Scan the current project repository comprehensively.
|
|
5968
6137
|
Map the directory structure (excluding heavy/ignored folders), infer the technology stack from manifests and configs, identify entry points and useful scripts, and analyze module relationships and architectural patterns.
|
|
@@ -5985,7 +6154,6 @@ var SubAgentsBluMa = class {
|
|
|
5985
6154
|
mcpClient;
|
|
5986
6155
|
toolInvoker;
|
|
5987
6156
|
llm;
|
|
5988
|
-
model;
|
|
5989
6157
|
projectRoot;
|
|
5990
6158
|
policy;
|
|
5991
6159
|
logger;
|
|
@@ -5994,7 +6162,6 @@ var SubAgentsBluMa = class {
|
|
|
5994
6162
|
this.mcpClient = params.mcpClient;
|
|
5995
6163
|
this.toolInvoker = params.toolInvoker;
|
|
5996
6164
|
this.llm = params.llm;
|
|
5997
|
-
this.model = params.model;
|
|
5998
6165
|
this.projectRoot = params.projectRoot || process.cwd();
|
|
5999
6166
|
this.policy = params.policy;
|
|
6000
6167
|
this.logger = params.logger;
|
|
@@ -6002,7 +6169,7 @@ var SubAgentsBluMa = class {
|
|
|
6002
6169
|
// Recebe dados do front (ex.: { content: inputText } vindo de /init)
|
|
6003
6170
|
// e faz o roteamento para o subagente adequado com base no comando.
|
|
6004
6171
|
async registerAndDispatch(frontPayload) {
|
|
6005
|
-
const { command, content, ...rest } = frontPayload || {};
|
|
6172
|
+
const { command, content, userContext, ...rest } = frontPayload || {};
|
|
6006
6173
|
const resolvedCommand = this.resolveCommand({ command, content });
|
|
6007
6174
|
if (!resolvedCommand) {
|
|
6008
6175
|
this.emit("error", { message: "Nenhum comando/subagente correspondente encontrado." });
|
|
@@ -6013,16 +6180,18 @@ var SubAgentsBluMa = class {
|
|
|
6013
6180
|
this.emit("error", { message: `Subagente n\xE3o registrado para ${resolvedCommand}` });
|
|
6014
6181
|
return { ok: false, error: "unregistered_subagent" };
|
|
6015
6182
|
}
|
|
6183
|
+
const inputForAgent = content ?? JSON.stringify({ content, ...rest });
|
|
6184
|
+
const msgPreview = String(inputForAgent).slice(0, 300);
|
|
6016
6185
|
const ctx = {
|
|
6017
6186
|
projectRoot: this.projectRoot,
|
|
6018
6187
|
eventBus: this.eventBus,
|
|
6019
6188
|
mcpClient: this.mcpClient,
|
|
6020
6189
|
toolInvoker: this.toolInvoker,
|
|
6021
6190
|
llm: this.llm,
|
|
6022
|
-
policy: { llmDeployment:
|
|
6023
|
-
logger: this.logger
|
|
6191
|
+
policy: { llmDeployment: "auto", ...this.policy || {} },
|
|
6192
|
+
logger: this.logger,
|
|
6193
|
+
blumaUserContextInput: userContext ?? defaultBlumaUserContextInput(`subagent:${resolvedCommand}`, msgPreview)
|
|
6024
6194
|
};
|
|
6025
|
-
const inputForAgent = content ?? JSON.stringify({ content, ...rest });
|
|
6026
6195
|
this.emit("info", { message: `[SubAgentsBluMa] Dispatch -> ${resolvedCommand}` });
|
|
6027
6196
|
return subAgent.execute(inputForAgent, ctx);
|
|
6028
6197
|
}
|
|
@@ -6055,17 +6224,18 @@ var RouteManager = class {
|
|
|
6055
6224
|
}
|
|
6056
6225
|
async handleRoute(payload) {
|
|
6057
6226
|
const inputText = String(payload.content || "").trim();
|
|
6227
|
+
const { userContext } = payload;
|
|
6058
6228
|
for (const [path18, handler] of this.routeHandlers) {
|
|
6059
6229
|
if (inputText === path18 || inputText.startsWith(`${path18} `)) {
|
|
6060
|
-
return handler({ content: inputText });
|
|
6230
|
+
return handler({ content: inputText, userContext });
|
|
6061
6231
|
}
|
|
6062
6232
|
}
|
|
6063
|
-
await this.core.processTurn({ content: inputText });
|
|
6233
|
+
await this.core.processTurn({ content: inputText }, userContext);
|
|
6064
6234
|
}
|
|
6065
6235
|
};
|
|
6066
6236
|
|
|
6067
6237
|
// src/app/agent/agent.ts
|
|
6068
|
-
var globalEnvPath = path16.join(
|
|
6238
|
+
var globalEnvPath = path16.join(os10.homedir(), ".bluma", ".env");
|
|
6069
6239
|
dotenv.config({ path: globalEnvPath });
|
|
6070
6240
|
var Agent = class {
|
|
6071
6241
|
sessionId;
|
|
@@ -6074,7 +6244,6 @@ var Agent = class {
|
|
|
6074
6244
|
feedbackSystem;
|
|
6075
6245
|
llm;
|
|
6076
6246
|
routeManager;
|
|
6077
|
-
model;
|
|
6078
6247
|
core;
|
|
6079
6248
|
subAgents;
|
|
6080
6249
|
toolInvoker;
|
|
@@ -6085,55 +6254,77 @@ var Agent = class {
|
|
|
6085
6254
|
this.toolInvoker = nativeToolInvoker;
|
|
6086
6255
|
this.mcpClient = new MCPClient(nativeToolInvoker, eventBus);
|
|
6087
6256
|
this.feedbackSystem = new AdvancedFeedbackSystem();
|
|
6088
|
-
const
|
|
6089
|
-
const
|
|
6090
|
-
this.model = process.env.MODEL_NOMAD || "";
|
|
6091
|
-
const requiredEnvVars = ["NOMAD_API_KEY", "NOMAD_BASE_URL"];
|
|
6092
|
-
const missingEnvVars = requiredEnvVars.filter((v) => !process.env[v]);
|
|
6257
|
+
const requiredEnvVars = ["FACTOR_ROUTER_KEY", "FACTOR_ROUTER_URL"];
|
|
6258
|
+
const missingEnvVars = requiredEnvVars.filter((v) => !process.env[v]?.trim());
|
|
6093
6259
|
if (missingEnvVars.length > 0) {
|
|
6094
6260
|
const platform = process.platform;
|
|
6095
6261
|
let guidance = "";
|
|
6096
6262
|
if (platform === "win32") {
|
|
6097
6263
|
guidance = [
|
|
6098
|
-
"
|
|
6264
|
+
"Current PowerShell session (quick test):",
|
|
6099
6265
|
...missingEnvVars.map((v) => ` $env:${v}="<your-value>"`),
|
|
6100
|
-
"
|
|
6101
|
-
|
|
6266
|
+
"",
|
|
6267
|
+
"Persistent (User environment \u2014 restart terminal after):",
|
|
6268
|
+
...missingEnvVars.map(
|
|
6269
|
+
(v) => ` [System.Environment]::SetEnvironmentVariable("${v}", "<your-value>", "User")`
|
|
6270
|
+
)
|
|
6102
6271
|
].join("\n");
|
|
6103
|
-
} else if (platform === "darwin" || platform === "linux") {
|
|
6272
|
+
} else if (platform === "darwin" || platform === "linux" || platform === "freebsd") {
|
|
6273
|
+
const osLabel = platform === "darwin" ? "macOS" : platform === "linux" ? "Linux" : "BSD";
|
|
6274
|
+
const sh = (process.env.SHELL || "").toLowerCase();
|
|
6275
|
+
let rcFile = "~/.profile";
|
|
6276
|
+
if (sh.includes("zsh")) rcFile = "~/.zshrc";
|
|
6277
|
+
else if (sh.includes("bash")) rcFile = platform === "darwin" ? "~/.bash_profile" : "~/.bashrc";
|
|
6104
6278
|
guidance = [
|
|
6105
|
-
|
|
6106
|
-
...missingEnvVars.map((v) => `
|
|
6107
|
-
"
|
|
6279
|
+
`${osLabel} \u2014 current shell only (quick test):`,
|
|
6280
|
+
...missingEnvVars.map((v) => ` export ${v}="<your-value>"`),
|
|
6281
|
+
"",
|
|
6282
|
+
`${osLabel} \u2014 persistent (append to ${rcFile} for your current $SHELL):`,
|
|
6283
|
+
...missingEnvVars.map((v) => ` echo 'export ${v}="<your-value>"' >> ${rcFile}`),
|
|
6284
|
+
` source ${rcFile} # or open a new terminal`
|
|
6108
6285
|
].join("\n");
|
|
6109
6286
|
} else {
|
|
6110
|
-
guidance =
|
|
6287
|
+
guidance = [
|
|
6288
|
+
`${platform} \u2014 current shell:`,
|
|
6289
|
+
...missingEnvVars.map((v) => ` export ${v}="<your-value>"`)
|
|
6290
|
+
].join("\n");
|
|
6111
6291
|
}
|
|
6112
|
-
const
|
|
6292
|
+
const messageParts = [
|
|
6113
6293
|
`Missing required environment variables: ${missingEnvVars.join(", ")}.`,
|
|
6114
|
-
`Configure them globally using the commands below, or set them in: ${globalEnvPath}`,
|
|
6115
6294
|
"",
|
|
6116
|
-
|
|
6117
|
-
]
|
|
6295
|
+
"Set them in your user or system environment so the BluMa process inherits them."
|
|
6296
|
+
];
|
|
6297
|
+
if (hasLegacyNomadEnvVars()) {
|
|
6298
|
+
messageParts.push(
|
|
6299
|
+
"",
|
|
6300
|
+
"You still have NOMAD_API_KEY, NOMAD_BASE_URL, and/or MODEL_NOMAD set. Remove those and use FACTOR_ROUTER_KEY and FACTOR_ROUTER_URL instead."
|
|
6301
|
+
);
|
|
6302
|
+
}
|
|
6303
|
+
messageParts.push("", guidance);
|
|
6304
|
+
const message2 = messageParts.join("\n");
|
|
6118
6305
|
this.eventBus.emit("backend_message", {
|
|
6119
6306
|
type: "error",
|
|
6120
6307
|
code: "missing_env",
|
|
6121
6308
|
missing: missingEnvVars,
|
|
6122
|
-
path: globalEnvPath,
|
|
6123
6309
|
message: message2
|
|
6124
6310
|
});
|
|
6125
6311
|
throw new Error(message2);
|
|
6126
6312
|
}
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6313
|
+
try {
|
|
6314
|
+
this.llm = new LLMService();
|
|
6315
|
+
} catch (e) {
|
|
6316
|
+
const message2 = e?.message || String(e);
|
|
6317
|
+
this.eventBus.emit("backend_message", {
|
|
6318
|
+
type: "error",
|
|
6319
|
+
code: "llm_config",
|
|
6320
|
+
message: message2
|
|
6321
|
+
});
|
|
6322
|
+
throw e;
|
|
6323
|
+
}
|
|
6132
6324
|
this.core = new BluMaAgent(
|
|
6133
6325
|
this.sessionId,
|
|
6134
6326
|
this.eventBus,
|
|
6135
6327
|
this.llm,
|
|
6136
|
-
this.model,
|
|
6137
6328
|
this.mcpClient,
|
|
6138
6329
|
this.feedbackSystem
|
|
6139
6330
|
);
|
|
@@ -6142,7 +6333,6 @@ var Agent = class {
|
|
|
6142
6333
|
mcpClient: this.mcpClient,
|
|
6143
6334
|
toolInvoker: this.toolInvoker,
|
|
6144
6335
|
llm: this.llm,
|
|
6145
|
-
model: this.model,
|
|
6146
6336
|
projectRoot: process.cwd()
|
|
6147
6337
|
});
|
|
6148
6338
|
this.routeManager = new RouteManager(this.subAgents, this.core);
|
|
@@ -6156,12 +6346,13 @@ var Agent = class {
|
|
|
6156
6346
|
getUiToolsDetailed() {
|
|
6157
6347
|
return this.core.getUiToolsDetailed();
|
|
6158
6348
|
}
|
|
6159
|
-
async processTurn(userInput) {
|
|
6349
|
+
async processTurn(userInput, userContextInput) {
|
|
6160
6350
|
const inputText = String(userInput.content || "").trim();
|
|
6351
|
+
const resolvedUserContext = userContextInput ?? defaultInteractiveCliUserContextInput(this.sessionId, inputText.slice(0, 300));
|
|
6161
6352
|
if (inputText === "/init" || inputText.startsWith("/init ")) {
|
|
6162
6353
|
this.routeManager.registerRoute("/init", this.dispatchToSubAgent);
|
|
6163
6354
|
}
|
|
6164
|
-
await this.routeManager.handleRoute({ content: inputText });
|
|
6355
|
+
await this.routeManager.handleRoute({ content: inputText, userContext: resolvedUserContext });
|
|
6165
6356
|
}
|
|
6166
6357
|
async handleToolResponse(decisionData) {
|
|
6167
6358
|
await this.core.handleToolResponse(decisionData);
|
|
@@ -7709,6 +7900,18 @@ function stopTitleKeeper() {
|
|
|
7709
7900
|
}
|
|
7710
7901
|
|
|
7711
7902
|
// src/main.ts
|
|
7903
|
+
function extractUserMessage(envelope) {
|
|
7904
|
+
const c = envelope.context;
|
|
7905
|
+
if (c && typeof c === "object" && typeof c.user_request === "string") {
|
|
7906
|
+
return String(c.user_request).slice(0, 300);
|
|
7907
|
+
}
|
|
7908
|
+
if (typeof c === "string") return c.slice(0, 300);
|
|
7909
|
+
try {
|
|
7910
|
+
return JSON.stringify(c ?? {}).slice(0, 300);
|
|
7911
|
+
} catch {
|
|
7912
|
+
return "";
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7712
7915
|
function writeJsonl(event) {
|
|
7713
7916
|
try {
|
|
7714
7917
|
process.stdout.write(JSON.stringify(event) + "\n");
|
|
@@ -7763,7 +7966,18 @@ async function runAgentMode() {
|
|
|
7763
7966
|
}
|
|
7764
7967
|
}
|
|
7765
7968
|
const eventBus = new EventEmitter2();
|
|
7766
|
-
const sessionId = envelope.session_id || envelope.message_id ||
|
|
7969
|
+
const sessionId = envelope.session_id || envelope.message_id || uuidv46();
|
|
7970
|
+
const uc = envelope.user_context;
|
|
7971
|
+
const userContextInput = {
|
|
7972
|
+
sessionId,
|
|
7973
|
+
conversationId: uc?.conversationId ?? null,
|
|
7974
|
+
userMessage: extractUserMessage(envelope),
|
|
7975
|
+
userId: uc?.userId ?? null,
|
|
7976
|
+
userName: uc?.userName ?? null,
|
|
7977
|
+
userEmail: uc?.userEmail ?? null,
|
|
7978
|
+
companyId: uc?.companyId ?? null,
|
|
7979
|
+
companyName: uc?.companyName ?? null
|
|
7980
|
+
};
|
|
7767
7981
|
let lastAssistantMessage = null;
|
|
7768
7982
|
let reasoningBuffer = null;
|
|
7769
7983
|
let lastAttachments = null;
|
|
@@ -7848,7 +8062,7 @@ async function runAgentMode() {
|
|
|
7848
8062
|
action: envelope.action || "unknown",
|
|
7849
8063
|
context: envelope.context || {}
|
|
7850
8064
|
});
|
|
7851
|
-
await agent.processTurn({ content: userContent });
|
|
8065
|
+
await agent.processTurn({ content: userContent }, userContextInput);
|
|
7852
8066
|
if (!resultEmitted) {
|
|
7853
8067
|
resultEmitted = true;
|
|
7854
8068
|
writeJsonl({
|
|
@@ -7884,7 +8098,7 @@ function runCliMode() {
|
|
|
7884
8098
|
const BLUMA_TITLE = process.env.BLUMA_TITLE || "BluMa - NomadEngenuity";
|
|
7885
8099
|
startTitleKeeper(BLUMA_TITLE);
|
|
7886
8100
|
const eventBus = new EventEmitter2();
|
|
7887
|
-
const sessionId =
|
|
8101
|
+
const sessionId = uuidv46();
|
|
7888
8102
|
const props = {
|
|
7889
8103
|
eventBus,
|
|
7890
8104
|
sessionId
|