@burtson-labs/bandit-engine 2.0.59 → 2.0.61

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.
Files changed (44) hide show
  1. package/dist/chat-E23BSMK5.mjs +16 -0
  2. package/dist/chat-provider.js.map +1 -1
  3. package/dist/chat-provider.mjs +5 -5
  4. package/dist/{chunk-KNBWR4DS.mjs → chunk-5WQMMCZQ.mjs} +3 -3
  5. package/dist/{chunk-QPBG6JQE.mjs → chunk-6QTTNYF2.mjs} +2 -2
  6. package/dist/{chunk-POTQI33D.mjs → chunk-D55E6ZDV.mjs} +5 -5
  7. package/dist/{chunk-557E5VZ2.mjs → chunk-EUBVBTB3.mjs} +2 -2
  8. package/dist/{chunk-V5QRXIIO.mjs → chunk-G7U2FNUK.mjs} +494 -20
  9. package/dist/chunk-G7U2FNUK.mjs.map +1 -0
  10. package/dist/{chunk-7ZDS33S2.mjs → chunk-IPMTNREZ.mjs} +2 -2
  11. package/dist/{chunk-7ZDS33S2.mjs.map → chunk-IPMTNREZ.mjs.map} +1 -1
  12. package/dist/{chunk-WL7NV4WJ.mjs → chunk-PY7A3J5T.mjs} +4 -4
  13. package/dist/{chunk-URKUD3OL.mjs → chunk-SKMJ43NN.mjs} +9 -9
  14. package/dist/{chunk-KM7FUWCM.mjs → chunk-SRCCNBHF.mjs} +7 -3
  15. package/dist/chunk-SRCCNBHF.mjs.map +1 -0
  16. package/dist/{chunk-UFSEYVRS.mjs → chunk-VTC6AIWY.mjs} +3 -3
  17. package/dist/index.d.mts +2 -2
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +522 -16
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +13 -13
  22. package/dist/management/management.js +522 -16
  23. package/dist/management/management.js.map +1 -1
  24. package/dist/management/management.mjs +8 -8
  25. package/dist/modals/chat-modal/chat-modal.js.map +1 -1
  26. package/dist/modals/chat-modal/chat-modal.mjs +4 -4
  27. package/dist/{modelStore-XWFHNTBT.mjs → modelStore-FBPBG7TI.mjs} +2 -2
  28. package/dist/{public-BzsEWB08.d.mts → public-nrOOzXCZ.d.mts} +10 -0
  29. package/dist/{public-BzsEWB08.d.ts → public-nrOOzXCZ.d.ts} +10 -0
  30. package/dist/public-types.d.mts +1 -1
  31. package/dist/public-types.d.ts +1 -1
  32. package/package.json +1 -1
  33. package/dist/chat-3J4GDGWW.mjs +0 -16
  34. package/dist/chunk-KM7FUWCM.mjs.map +0 -1
  35. package/dist/chunk-V5QRXIIO.mjs.map +0 -1
  36. /package/dist/{chat-3J4GDGWW.mjs.map → chat-E23BSMK5.mjs.map} +0 -0
  37. /package/dist/{chunk-KNBWR4DS.mjs.map → chunk-5WQMMCZQ.mjs.map} +0 -0
  38. /package/dist/{chunk-QPBG6JQE.mjs.map → chunk-6QTTNYF2.mjs.map} +0 -0
  39. /package/dist/{chunk-POTQI33D.mjs.map → chunk-D55E6ZDV.mjs.map} +0 -0
  40. /package/dist/{chunk-557E5VZ2.mjs.map → chunk-EUBVBTB3.mjs.map} +0 -0
  41. /package/dist/{chunk-WL7NV4WJ.mjs.map → chunk-PY7A3J5T.mjs.map} +0 -0
  42. /package/dist/{chunk-URKUD3OL.mjs.map → chunk-SKMJ43NN.mjs.map} +0 -0
  43. /package/dist/{chunk-UFSEYVRS.mjs.map → chunk-VTC6AIWY.mjs.map} +0 -0
  44. /package/dist/{modelStore-XWFHNTBT.mjs.map → modelStore-FBPBG7TI.mjs.map} +0 -0
package/dist/index.js CHANGED
@@ -14862,7 +14862,11 @@ ${listMarkdown}`;
14862
14862
  href,
14863
14863
  target: "_blank",
14864
14864
  rel: "noopener noreferrer",
14865
- style: { color: theme.palette.primary.main, textDecoration: "underline" },
14865
+ style: {
14866
+ color: theme.palette.info?.main ?? theme.palette.primary.main,
14867
+ textDecoration: "underline",
14868
+ textUnderlineOffset: "2px"
14869
+ },
14866
14870
  ...props,
14867
14871
  children
14868
14872
  }
@@ -19107,6 +19111,388 @@ ${sanitize(
19107
19111
  }
19108
19112
  });
19109
19113
 
19114
+ // src/services/telemetry/otlpExporter.ts
19115
+ function redactSecretsString(s) {
19116
+ return s.replace(/\b(?:sk|tvly|ghp|gho|pk|rk)[-_][A-Za-z0-9]{8,}\b/gi, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted-jwt]").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer [redacted]");
19117
+ }
19118
+ function resolveTelemetryConfig(opts) {
19119
+ if (!opts.telemetry?.enabled) return null;
19120
+ const endpoint = (opts.telemetry.endpoint ?? "https://otlp.burtson.ai").replace(/\/+$/, "");
19121
+ const headers = { ...opts.telemetry.headers ?? {} };
19122
+ const hasAuth = Object.keys(headers).some((k) => k.toLowerCase() === "authorization");
19123
+ if (!hasAuth && opts.banditApiKey) {
19124
+ headers["Authorization"] = `Bearer ${opts.banditApiKey}`;
19125
+ }
19126
+ const mode = opts.telemetry.mode ?? "metrics+traces";
19127
+ return { endpoint, headers, mode, serviceName: opts.telemetry.serviceName ?? "bandit-web" };
19128
+ }
19129
+ function toAttrs(rec) {
19130
+ const out = [];
19131
+ for (const [key, v] of Object.entries(rec)) {
19132
+ if (v === void 0 || v === null || v === "") continue;
19133
+ if (typeof v === "boolean") out.push({ key, value: { boolValue: v } });
19134
+ else if (typeof v === "number")
19135
+ out.push({ key, value: Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v } });
19136
+ else out.push({ key, value: { stringValue: v } });
19137
+ }
19138
+ return out;
19139
+ }
19140
+ function hex(bytes) {
19141
+ const arr = new Uint8Array(bytes);
19142
+ if (webCrypto?.getRandomValues) webCrypto.getRandomValues(arr);
19143
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
19144
+ }
19145
+ function histogramPoint(value, bounds, attrs, startMs, endMs) {
19146
+ const counts = new Array(bounds.length + 1).fill(0);
19147
+ let idx = bounds.findIndex((b) => value <= b);
19148
+ if (idx === -1) idx = bounds.length;
19149
+ counts[idx] = 1;
19150
+ return {
19151
+ attributes: toAttrs(attrs),
19152
+ startTimeUnixNano: nano(startMs),
19153
+ timeUnixNano: nano(endMs),
19154
+ count: "1",
19155
+ sum: value,
19156
+ bucketCounts: counts.map(String),
19157
+ explicitBounds: bounds
19158
+ };
19159
+ }
19160
+ function sumPoint(value, attrs, startMs, endMs) {
19161
+ return {
19162
+ attributes: toAttrs(attrs),
19163
+ startTimeUnixNano: nano(startMs),
19164
+ timeUnixNano: nano(endMs),
19165
+ asInt: String(Math.round(value))
19166
+ };
19167
+ }
19168
+ var webCrypto, nano, TTFT_BUCKETS, DURATION_BUCKETS, clip, TelemetryExporter;
19169
+ var init_otlpExporter = __esm({
19170
+ "src/services/telemetry/otlpExporter.ts"() {
19171
+ "use strict";
19172
+ init_debugLogger();
19173
+ webCrypto = globalThis.crypto;
19174
+ nano = (ms) => String(Math.round(ms * 1e6));
19175
+ TTFT_BUCKETS = [0.1, 0.25, 0.5, 1, 2, 5, 10, 30];
19176
+ DURATION_BUCKETS = [0.5, 1, 2, 5, 10, 30, 60, 120, 300];
19177
+ clip = (s, n = 120) => redactSecretsString(s.slice(0, n)).slice(0, n);
19178
+ TelemetryExporter = class {
19179
+ cfg;
19180
+ now;
19181
+ traceId = "";
19182
+ turn = null;
19183
+ llm = null;
19184
+ llmFirstChunkMs = 0;
19185
+ openTools = [];
19186
+ completedSpans = [];
19187
+ model = "";
19188
+ turnChunkChars = 0;
19189
+ turnTokens = 0;
19190
+ ttftSeconds = null;
19191
+ constructor(cfg, opts) {
19192
+ this.cfg = cfg;
19193
+ this.now = opts?.now ?? (() => Date.now());
19194
+ }
19195
+ startTurn(goal, model) {
19196
+ this.traceId = hex(16);
19197
+ this.model = model;
19198
+ this.turnChunkChars = 0;
19199
+ this.turnTokens = 0;
19200
+ this.ttftSeconds = null;
19201
+ this.llm = null;
19202
+ this.llmFirstChunkMs = 0;
19203
+ this.openTools = [];
19204
+ this.completedSpans = [];
19205
+ this.turn = {
19206
+ spanId: hex(8),
19207
+ name: "agent.turn",
19208
+ startMs: this.now(),
19209
+ attrs: { "gen_ai.request.model": model, "bandit.turn.goal": clip(goal, 160) }
19210
+ };
19211
+ }
19212
+ /** Fed from the chat turn lifecycle. Best-effort; swallows bad payloads. */
19213
+ onEvent(type, payload) {
19214
+ if (!this.turn) return;
19215
+ try {
19216
+ const p = payload ?? {};
19217
+ switch (type) {
19218
+ case "tool_loop:llm_start":
19219
+ this.llm = {
19220
+ spanId: hex(8),
19221
+ parentSpanId: this.turn.spanId,
19222
+ name: "llm.generate",
19223
+ startMs: this.now(),
19224
+ attrs: { "gen_ai.request.model": this.model }
19225
+ };
19226
+ this.llmFirstChunkMs = 0;
19227
+ break;
19228
+ case "tool_loop:llm_chunk": {
19229
+ const chunk = typeof p.chunk === "string" ? p.chunk : "";
19230
+ if (this.llm && this.llmFirstChunkMs === 0 && chunk.length > 0) {
19231
+ this.llmFirstChunkMs = this.now();
19232
+ const ttft = (this.llmFirstChunkMs - this.llm.startMs) / 1e3;
19233
+ if (this.ttftSeconds === null) this.ttftSeconds = ttft;
19234
+ this.llm.attrs["bandit.llm.ttft_seconds"] = ttft;
19235
+ }
19236
+ this.turnChunkChars += chunk.length;
19237
+ this.turnTokens = Math.floor(this.turnChunkChars / 4);
19238
+ break;
19239
+ }
19240
+ case "tool_loop:llm_response":
19241
+ if (this.llm) {
19242
+ this.llm.endMs = this.now();
19243
+ if (typeof p.responseLength === "number") this.llm.attrs["bandit.llm.response_chars"] = p.responseLength;
19244
+ this.completedSpans.push(this.llm);
19245
+ this.llm = null;
19246
+ }
19247
+ break;
19248
+ case "tool_loop:tool_execute": {
19249
+ const name = typeof p.name === "string" ? p.name : "tool";
19250
+ const params = p.params ?? {};
19251
+ const primary = params.query ?? params.url ?? params.prompt ?? params.topic ?? "";
19252
+ this.openTools.push({
19253
+ spanId: hex(8),
19254
+ parentSpanId: this.turn.spanId,
19255
+ name: `tool.${name}`,
19256
+ startMs: this.now(),
19257
+ attrs: { "bandit.tool.name": name, "bandit.tool.primary": primary ? clip(primary) : void 0 }
19258
+ });
19259
+ break;
19260
+ }
19261
+ case "tool_loop:tool_result":
19262
+ case "tool_loop:tool_error": {
19263
+ const name = typeof p.name === "string" ? p.name : void 0;
19264
+ const span = this.takeOpenTool(name);
19265
+ if (span) {
19266
+ span.endMs = this.now();
19267
+ if (type === "tool_loop:tool_error" || p.isError === true) span.error = "tool error";
19268
+ this.completedSpans.push(span);
19269
+ }
19270
+ break;
19271
+ }
19272
+ }
19273
+ } catch {
19274
+ }
19275
+ }
19276
+ takeOpenTool(name) {
19277
+ if (name) {
19278
+ for (let i = this.openTools.length - 1; i >= 0; i -= 1) {
19279
+ if (this.openTools[i].name === `tool.${name}`) return this.openTools.splice(i, 1)[0];
19280
+ }
19281
+ }
19282
+ return this.openTools.shift();
19283
+ }
19284
+ /** Close the turn, build OTLP traces + metrics, and flush. Never rejects. */
19285
+ async endTurn(outcome) {
19286
+ const turn = this.turn;
19287
+ if (!turn) return;
19288
+ this.turn = null;
19289
+ const end = this.now();
19290
+ if (this.llm && !this.llm.endMs) {
19291
+ this.llm.endMs = end;
19292
+ this.completedSpans.push(this.llm);
19293
+ this.llm = null;
19294
+ }
19295
+ for (const t of this.openTools.splice(0)) {
19296
+ t.endMs = end;
19297
+ t.error = t.error ?? "incomplete";
19298
+ this.completedSpans.push(t);
19299
+ }
19300
+ turn.endMs = end;
19301
+ if (outcome?.error) turn.error = outcome.error;
19302
+ const traceId = this.traceId;
19303
+ const spans = [turn, ...this.completedSpans];
19304
+ const jobs = [];
19305
+ if (this.cfg.mode === "metrics+traces") jobs.push(this.post("/v1/traces", this.buildTraces(traceId, spans)));
19306
+ jobs.push(this.post("/v1/metrics", this.buildMetrics(turn)));
19307
+ try {
19308
+ await Promise.all(jobs);
19309
+ } catch {
19310
+ }
19311
+ }
19312
+ buildTraces(traceId, spans) {
19313
+ return {
19314
+ resourceSpans: [
19315
+ {
19316
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
19317
+ scopeSpans: [
19318
+ {
19319
+ scope: { name: this.cfg.serviceName },
19320
+ spans: spans.map((s) => ({
19321
+ traceId,
19322
+ spanId: s.spanId,
19323
+ parentSpanId: s.parentSpanId,
19324
+ name: s.name,
19325
+ kind: 1,
19326
+ startTimeUnixNano: nano(s.startMs),
19327
+ endTimeUnixNano: nano(s.endMs ?? s.startMs),
19328
+ attributes: toAttrs(s.attrs),
19329
+ status: s.error ? { code: 2, message: s.error.slice(0, 200) } : { code: 1 }
19330
+ }))
19331
+ }
19332
+ ]
19333
+ }
19334
+ ]
19335
+ };
19336
+ }
19337
+ buildMetrics(turn) {
19338
+ const start = turn.startMs;
19339
+ const end = turn.endMs ?? this.now();
19340
+ const metrics = [];
19341
+ if (this.turnTokens > 0) {
19342
+ metrics.push({
19343
+ name: "bandit.llm.tokens",
19344
+ sum: {
19345
+ aggregationTemporality: 1,
19346
+ isMonotonic: true,
19347
+ dataPoints: [sumPoint(this.turnTokens, { type: "output", "gen_ai.request.model": this.model }, start, end)]
19348
+ }
19349
+ });
19350
+ }
19351
+ if (this.ttftSeconds !== null) {
19352
+ metrics.push({
19353
+ name: "bandit.llm.ttft",
19354
+ unit: "s",
19355
+ histogram: {
19356
+ aggregationTemporality: 1,
19357
+ dataPoints: [histogramPoint(this.ttftSeconds, TTFT_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
19358
+ }
19359
+ });
19360
+ }
19361
+ metrics.push({
19362
+ name: "bandit.turn.duration",
19363
+ unit: "s",
19364
+ histogram: {
19365
+ aggregationTemporality: 1,
19366
+ dataPoints: [histogramPoint((end - start) / 1e3, DURATION_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
19367
+ }
19368
+ });
19369
+ return {
19370
+ resourceMetrics: [
19371
+ {
19372
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
19373
+ scopeMetrics: [{ scope: { name: this.cfg.serviceName }, metrics }]
19374
+ }
19375
+ ]
19376
+ };
19377
+ }
19378
+ async post(path, body) {
19379
+ const doFetch = globalThis.fetch;
19380
+ if (!doFetch) return;
19381
+ const ctrl = new AbortController();
19382
+ const timer = setTimeout(() => ctrl.abort(), 4e3);
19383
+ try {
19384
+ await doFetch(`${this.cfg.endpoint}${path}`, {
19385
+ method: "POST",
19386
+ headers: { "Content-Type": "application/json", ...this.cfg.headers },
19387
+ body: JSON.stringify(body),
19388
+ signal: ctrl.signal
19389
+ });
19390
+ } catch (e) {
19391
+ debugLogger.debug("[telemetry] OTLP post failed", {
19392
+ path,
19393
+ error: e instanceof Error ? e.message : String(e)
19394
+ });
19395
+ } finally {
19396
+ clearTimeout(timer);
19397
+ }
19398
+ }
19399
+ };
19400
+ }
19401
+ });
19402
+
19403
+ // src/services/telemetry/index.ts
19404
+ function syncTelemetry() {
19405
+ try {
19406
+ const settings = usePackageSettingsStore.getState().settings;
19407
+ const cfg = resolveTelemetryConfig({
19408
+ telemetry: settings?.telemetry,
19409
+ banditApiKey: authenticationService.getToken() ?? void 0
19410
+ });
19411
+ active = cfg ? new TelemetryExporter(cfg) : null;
19412
+ } catch {
19413
+ active = null;
19414
+ }
19415
+ return active !== null;
19416
+ }
19417
+ function telemetryStartTurn(goal, model) {
19418
+ active?.startTurn(goal, model);
19419
+ }
19420
+ function telemetryEvent(type, payload) {
19421
+ active?.onEvent(type, payload);
19422
+ }
19423
+ function telemetryEndTurn(outcome) {
19424
+ void active?.endTurn(outcome);
19425
+ }
19426
+ var active;
19427
+ var init_telemetry = __esm({
19428
+ "src/services/telemetry/index.ts"() {
19429
+ "use strict";
19430
+ init_otlpExporter();
19431
+ init_packageSettingsStore();
19432
+ init_authenticationService();
19433
+ init_otlpExporter();
19434
+ active = null;
19435
+ }
19436
+ });
19437
+
19438
+ // src/store/engineStore.ts
19439
+ var import_zustand15, STORAGE_KEY2, readStored, useEngineStore;
19440
+ var init_engineStore = __esm({
19441
+ "src/store/engineStore.ts"() {
19442
+ "use strict";
19443
+ import_zustand15 = require("zustand");
19444
+ init_packageSettingsStore();
19445
+ init_authenticationService();
19446
+ init_debugLogger();
19447
+ STORAGE_KEY2 = "bandit.selectedEngine";
19448
+ readStored = () => {
19449
+ try {
19450
+ return typeof window !== "undefined" ? window.localStorage.getItem(STORAGE_KEY2) : null;
19451
+ } catch {
19452
+ return null;
19453
+ }
19454
+ };
19455
+ useEngineStore = (0, import_zustand15.create)((set, get) => ({
19456
+ selectedEngine: readStored(),
19457
+ engines: [],
19458
+ loaded: false,
19459
+ setSelectedEngine: (id) => {
19460
+ set({ selectedEngine: id });
19461
+ try {
19462
+ window.localStorage.setItem(STORAGE_KEY2, id);
19463
+ } catch {
19464
+ }
19465
+ },
19466
+ getSelectedEngine: () => get().selectedEngine || usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core",
19467
+ fetchEngines: async () => {
19468
+ const settings = usePackageSettingsStore.getState().settings;
19469
+ const base = settings?.gatewayApiUrl?.replace(/\/$/, "") ?? "";
19470
+ if (!base || settings?.playgroundMode || base.toLowerCase().startsWith("playground://")) {
19471
+ set({ loaded: true });
19472
+ return;
19473
+ }
19474
+ try {
19475
+ const headers = { "Content-Type": "application/json" };
19476
+ const token = authenticationService.getToken();
19477
+ if (token) headers["Authorization"] = `Bearer ${token}`;
19478
+ const res = await fetch(`${base}/models`, { headers });
19479
+ const data = await res.json();
19480
+ if (res.ok && Array.isArray(data?.models)) {
19481
+ set({ engines: data.models, loaded: true });
19482
+ } else {
19483
+ set({ loaded: true });
19484
+ }
19485
+ } catch (error) {
19486
+ debugLogger.error("Failed to fetch engines", {
19487
+ error: error instanceof Error ? error.message : String(error)
19488
+ });
19489
+ set({ loaded: true });
19490
+ }
19491
+ }
19492
+ }));
19493
+ }
19494
+ });
19495
+
19110
19496
  // src/chat/hooks/useMemoryEnhancer.tsx
19111
19497
  var import_rxjs20, MEMORY_LIMIT, MIN_MEMORY_WORDS, MERGE_THRESHOLD, REJECT_ECHO_THRESHOLD, REJECT_DUPLICATE_THRESHOLD, CONTEXTUAL_DIVERGENCE_THRESHOLD, normalizeText, isStructurallyDuplicate, isAboutBandit, hasEngagementValue, isMemoryTooShortOrGeneric, isPersonalText, mergeMemory, isVoiceShifted, sanitizeMemory, sanitizeMemoryText, shouldAcceptMemory, isContextuallyDivergent, useMemoryEnhancer;
19112
19498
  var init_useMemoryEnhancer = __esm({
@@ -19848,12 +20234,13 @@ var init_useAIProvider = __esm({
19848
20234
  import_react22 = require("react");
19849
20235
  init_knowledgeStore();
19850
20236
  init_aiProviderStore();
20237
+ init_telemetry();
20238
+ init_engineStore();
19851
20239
  init_conversationStore();
19852
20240
  init_useMemoryEnhancer();
19853
20241
  init_useVectorStore();
19854
20242
  init_embeddingService();
19855
20243
  init_useMoodEngine();
19856
- init_packageSettingsStore();
19857
20244
  init_prompts();
19858
20245
  init_preferencesStore();
19859
20246
  init_mcp();
@@ -20217,7 +20604,7 @@ var init_useAIProvider = __esm({
20217
20604
  question: pendingQuestion,
20218
20605
  images: pendingImages
20219
20606
  });
20220
- const modelName = usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core:4b-it-qat";
20607
+ const modelName = useEngineStore.getState().getSelectedEngine();
20221
20608
  const CONFIG = modelConfigs[modelName] ?? modelConfigs["bandit-core:4b-it-qat"];
20222
20609
  const base64Images = imageList.map((img) => img.split(",")[1]);
20223
20610
  const latestEntries = history.slice(-CONFIG.historyMessages);
@@ -20612,6 +20999,9 @@ ${protocol}`;
20612
20999
  setStreamBuffer(latestDisplayMessage);
20613
21000
  }, delay);
20614
21001
  };
21002
+ syncTelemetry();
21003
+ telemetryStartTurn(question, modelName);
21004
+ telemetryEvent("tool_loop:llm_start");
20615
21005
  const stream = provider.chat(request);
20616
21006
  const initialPlaceholderQuestion = lastEntry?.question;
20617
21007
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -20625,7 +21015,10 @@ ${protocol}`;
20625
21015
  const sub = stream.subscribe({
20626
21016
  next: (data) => {
20627
21017
  if (!data?.message?.content && !data?.message?.tool_calls) return;
20628
- if (data.message.content) fullMessage += data.message.content;
21018
+ if (data.message.content) {
21019
+ fullMessage += data.message.content;
21020
+ telemetryEvent("tool_loop:llm_chunk", { chunk: data.message.content });
21021
+ }
20629
21022
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
20630
21023
  setIsThinking?.(inThinkBlock);
20631
21024
  const visibleMessage = stripThinking(fullMessage);
@@ -20657,6 +21050,7 @@ ${protocol}`;
20657
21050
  setIsThinking?.(false);
20658
21051
  setPendingMessage(null);
20659
21052
  setLogoVisible(false);
21053
+ telemetryEndTurn({ error: err?.message || "stream error" });
20660
21054
  if (onError) {
20661
21055
  onError(err);
20662
21056
  }
@@ -20664,6 +21058,7 @@ ${protocol}`;
20664
21058
  complete: async () => {
20665
21059
  try {
20666
21060
  setIsThinking?.(false);
21061
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
20667
21062
  latestDisplayMessage = stripThinking(fullMessage);
20668
21063
  if (!sawToolBlock) {
20669
21064
  flushNow();
@@ -20672,6 +21067,7 @@ ${protocol}`;
20672
21067
  let enhancedMessage = fullMessage;
20673
21068
  const summarizableResults = [];
20674
21069
  const inlineImageBlocks = [];
21070
+ const collectedSources = [];
20675
21071
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
20676
21072
  debugLogger.info("Detected tool calls in AI response", {
20677
21073
  toolCallCount: toolCallMatches.length,
@@ -20706,10 +21102,21 @@ ${protocol}`;
20706
21102
  });
20707
21103
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
20708
21104
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
21105
+ clearFlushTimer();
21106
+ const toolStatus = functionName === "web_search" || functionName === "web-search" ? "Searching the web\u2026" : functionName === "web_fetch" || functionName === "web-fetch" ? "Reading the page\u2026" : functionName === "image_generation" || functionName === "image-generation" ? "Generating the image\u2026" : "Working on it\u2026";
21107
+ const toolPreamble = stripToolBlocks(fullMessage).trim();
21108
+ setStreamBuffer(toolPreamble ? `${toolPreamble}
21109
+
21110
+ _${toolStatus}_` : `_${toolStatus}_`);
21111
+ telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
20709
21112
  const result = await executeMCPTool({
20710
21113
  toolName: functionName,
20711
21114
  parameters: parsedParams
20712
21115
  });
21116
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
21117
+ name: functionName,
21118
+ isError: !result.success
21119
+ });
20713
21120
  let resultText = "";
20714
21121
  if (result.success) {
20715
21122
  if (functionName === "web_search" || functionName === "web-search") {
@@ -20723,18 +21130,16 @@ ${protocol}`;
20723
21130
  blocks.push(
20724
21131
  items.slice(0, 6).map((item, index) => {
20725
21132
  const title = item.title?.trim() || "Untitled";
20726
- const url = item.url?.trim();
21133
+ const url = item.url?.trim() || "";
20727
21134
  const snippet = item.content?.trim();
20728
- let line = `${index + 1}. **${title}**`;
20729
- if (url) line += `
20730
- ${url}`;
21135
+ if (url) collectedSources.push({ title, url });
21136
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
20731
21137
  if (snippet) {
20732
21138
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
20733
- line += `
20734
- ${truncated}`;
21139
+ line += ` \u2014 ${truncated}`;
20735
21140
  }
20736
21141
  return line;
20737
- }).join("\n\n")
21142
+ }).join("\n")
20738
21143
  );
20739
21144
  }
20740
21145
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -20816,7 +21221,7 @@ ${r.output}`).join("\n\n");
20816
21221
 
20817
21222
  ${toolResultsText}
20818
21223
 
20819
- Using these results together with your own knowledge, answer my original question concisely and in a natural, well-formatted way. Cite source URLs inline when you rely on them. Do NOT output tool_code or call any tools again.`
21224
+ Using these results together with your own knowledge, answer my original question concisely and in a natural, well-formatted way. Reference sources by name where relevant, but do NOT paste raw URLs or a list of links \u2014 a clean Sources section is added automatically below your answer. Do NOT output tool_code or call any tools again.`
20820
21225
  }
20821
21226
  ];
20822
21227
  const summaryRequest = {
@@ -20830,7 +21235,7 @@ Using these results together with your own knowledge, answer my original questio
20830
21235
  setStreamBuffer(
20831
21236
  summaryPreamble ? `${summaryPreamble}
20832
21237
 
20833
- _Working on it\u2026_` : "_Working on it\u2026_"
21238
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
20834
21239
  );
20835
21240
  const summaryText = await new Promise((resolve) => {
20836
21241
  let acc = "";
@@ -20871,7 +21276,18 @@ _Working on it\u2026_` : "_Working on it\u2026_"
20871
21276
  }, 3e4);
20872
21277
  });
20873
21278
  if (summaryText.trim()) {
20874
- enhancedMessage = summaryText + (inlineImageBlocks.length ? `
21279
+ const sourcesMd = collectedSources.length ? `
21280
+
21281
+ **Sources**
21282
+ ${collectedSources.slice(0, 6).map((s) => {
21283
+ let domain = s.url;
21284
+ try {
21285
+ domain = new URL(s.url).hostname.replace(/^www\./, "");
21286
+ } catch {
21287
+ }
21288
+ return `- [${s.title || domain}](${s.url}) \u2014 ${domain}`;
21289
+ }).join("\n")}` : "";
21290
+ enhancedMessage = summaryText + sourcesMd + (inlineImageBlocks.length ? `
20875
21291
 
20876
21292
  ${inlineImageBlocks.join("\n\n")}` : "");
20877
21293
  }
@@ -20920,6 +21336,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
20920
21336
  }
20921
21337
  setInputValue("");
20922
21338
  setPastedImages([]);
21339
+ telemetryEndTurn();
20923
21340
  setTimeout(() => {
20924
21341
  clearFlushTimer();
20925
21342
  setPendingMessage(null);
@@ -20935,6 +21352,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
20935
21352
  overrideComponentStatus("Idle");
20936
21353
  setIsSubmitting(false);
20937
21354
  setIsStreaming(false);
21355
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
20938
21356
  }
20939
21357
  }
20940
21358
  });
@@ -23793,10 +24211,10 @@ var init_enhanced_mobile_conversations_modal = __esm({
23793
24211
  (0, import_react30.useEffect)(() => {
23794
24212
  setDeletedConversationIds((prev) => {
23795
24213
  let changed = false;
23796
- const active = new Set(conversations.map((conv) => conv.id));
24214
+ const active2 = new Set(conversations.map((conv) => conv.id));
23797
24215
  const next = /* @__PURE__ */ new Set();
23798
24216
  prev.forEach((id) => {
23799
- if (active.has(id)) {
24217
+ if (active2.has(id)) {
23800
24218
  next.add(id);
23801
24219
  } else {
23802
24220
  changed = true;
@@ -24418,6 +24836,7 @@ var init_chat_app_bar = __esm({
24418
24836
  init_packageSettingsStore();
24419
24837
  init_useFeatures();
24420
24838
  init_conversationSyncStore();
24839
+ init_engineStore();
24421
24840
  import_shallow2 = require("zustand/shallow");
24422
24841
  import_jsx_runtime24 = require("react/jsx-runtime");
24423
24842
  CDN_BASE = "https://cdn.burtson.ai/";
@@ -24467,6 +24886,7 @@ var init_chat_app_bar = __esm({
24467
24886
  menuText
24468
24887
  } = theme.palette.chat.appBar;
24469
24888
  const [modelAnchorEl, setModelAnchorEl] = (0, import_react31.useState)(null);
24889
+ const [engineAnchorEl, setEngineAnchorEl] = (0, import_react31.useState)(null);
24470
24890
  const [voiceAnchorEl, setVoiceAnchorEl] = (0, import_react31.useState)(null);
24471
24891
  const [modalOpen, setModalOpen] = (0, import_react31.useState)(false);
24472
24892
  const [confirmModelChangeOpen, setConfirmModelChangeOpen] = (0, import_react31.useState)(false);
@@ -24587,6 +25007,14 @@ var init_chat_app_bar = __esm({
24587
25007
  const selectedModel = useModelStore((s) => s.selectedModel);
24588
25008
  const currentModel = useModelStore((s) => s.availableModels.find((m) => m.name === selectedModel));
24589
25009
  const currentAvatar = currentModel?.avatarBase64 || modelAvatars2[selectedModel] || banditHead3;
25010
+ const engines = useEngineStore((s) => s.engines);
25011
+ const selectedEngine = useEngineStore((s) => s.selectedEngine);
25012
+ const effectiveEngineId = selectedEngine || usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core";
25013
+ const currentEngine = engines.find((e) => e.id === effectiveEngineId);
25014
+ const engineLabel = currentEngine?.displayName?.replace(/^Bandit /, "") || "Engine";
25015
+ (0, import_react31.useEffect)(() => {
25016
+ useEngineStore.getState().fetchEngines();
25017
+ }, []);
24590
25018
  const pendingModelAvatar = useModelStore.getState().availableModels.find((m) => m.name === pendingModel)?.avatarBase64 || modelAvatars2[pendingModel || ""] || banditHead3;
24591
25019
  const resolvedHomeUrl = preferences.homeUrl?.trim() || packageSettings?.homeUrl?.trim() || "";
24592
25020
  const homeTooltip = (() => {
@@ -24899,6 +25327,84 @@ var init_chat_app_bar = __esm({
24899
25327
  )
24900
25328
  }
24901
25329
  ) }),
25330
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Tooltip, { title: `Engine: ${currentEngine?.displayName ?? effectiveEngineId}`, arrow: true, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
25331
+ import_material23.IconButton,
25332
+ {
25333
+ onClick: (e) => setEngineAnchorEl(e.currentTarget),
25334
+ sx: pillButtonStyles,
25335
+ "aria-label": `Change base model (engine). Currently ${effectiveEngineId}`,
25336
+ children: [
25337
+ currentEngine?.cloud ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(CloudDoneIcon, { fontSize: "small" }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(CloudOffIcon, { fontSize: "small" }),
25338
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Typography, { variant: "caption", sx: { ml: 0.75, fontWeight: 600, whiteSpace: "nowrap" }, children: engineLabel })
25339
+ ]
25340
+ }
25341
+ ) }),
25342
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
25343
+ import_material23.Menu,
25344
+ {
25345
+ anchorEl: engineAnchorEl,
25346
+ open: Boolean(engineAnchorEl),
25347
+ onClose: () => setEngineAnchorEl(null),
25348
+ transformOrigin: { horizontal: "right", vertical: "top" },
25349
+ anchorOrigin: { horizontal: "right", vertical: "bottom" },
25350
+ children: [
25351
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Typography, { variant: "overline", sx: { px: 2, color: theme.palette.text.secondary }, children: "Engine \xB7 base model" }),
25352
+ engines.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.MenuItem, { disabled: true, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Typography, { variant: "body2", children: "No engines available" }) }),
25353
+ engines.map((engine) => {
25354
+ const badges = [
25355
+ engine.vision && "vision",
25356
+ engine.tools && "tools",
25357
+ engine.thinking && "thinking",
25358
+ engine.cloud && "cloud"
25359
+ ].filter(Boolean);
25360
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
25361
+ import_material23.MenuItem,
25362
+ {
25363
+ selected: engine.id === effectiveEngineId,
25364
+ disabled: !engine.available,
25365
+ onClick: () => {
25366
+ useEngineStore.getState().setSelectedEngine(engine.id);
25367
+ setEngineAnchorEl(null);
25368
+ },
25369
+ sx: {
25370
+ display: "flex",
25371
+ flexDirection: "column",
25372
+ alignItems: "flex-start",
25373
+ gap: 0.5,
25374
+ py: 1,
25375
+ px: 2,
25376
+ maxWidth: 360,
25377
+ whiteSpace: "normal"
25378
+ },
25379
+ children: [
25380
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_material23.Box, { sx: { display: "flex", alignItems: "center", gap: 1, width: "100%" }, children: [
25381
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Typography, { variant: "body2", sx: { fontWeight: 600, flex: 1 }, children: engine.displayName }),
25382
+ engine.id === effectiveEngineId && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Box, { sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: theme.palette.primary.main } })
25383
+ ] }),
25384
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Typography, { variant: "caption", sx: { color: theme.palette.text.secondary }, children: engine.available ? engine.description : engine.unavailableReason || "Unavailable" }),
25385
+ badges.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_material23.Box, { sx: { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.25 }, children: badges.map((b) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
25386
+ import_material23.Box,
25387
+ {
25388
+ sx: {
25389
+ fontSize: "0.65rem",
25390
+ px: 0.75,
25391
+ py: 0.1,
25392
+ borderRadius: 1,
25393
+ bgcolor: theme.palette.primary.main + "22",
25394
+ color: theme.palette.primary.main
25395
+ },
25396
+ children: b
25397
+ },
25398
+ b
25399
+ )) })
25400
+ ]
25401
+ },
25402
+ engine.id
25403
+ );
25404
+ })
25405
+ ]
25406
+ }
25407
+ ),
24902
25408
  /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
24903
25409
  import_material23.Menu,
24904
25410
  {