@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
@@ -19448,7 +19448,11 @@ ${listMarkdown}`;
19448
19448
  href,
19449
19449
  target: "_blank",
19450
19450
  rel: "noopener noreferrer",
19451
- style: { color: theme.palette.primary.main, textDecoration: "underline" },
19451
+ style: {
19452
+ color: theme.palette.info?.main ?? theme.palette.primary.main,
19453
+ textDecoration: "underline",
19454
+ textUnderlineOffset: "2px"
19455
+ },
19452
19456
  ...props,
19453
19457
  children
19454
19458
  }
@@ -21346,6 +21350,388 @@ ${sanitize(
21346
21350
  }
21347
21351
  });
21348
21352
 
21353
+ // src/services/telemetry/otlpExporter.ts
21354
+ function redactSecretsString(s) {
21355
+ 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]");
21356
+ }
21357
+ function resolveTelemetryConfig(opts) {
21358
+ if (!opts.telemetry?.enabled) return null;
21359
+ const endpoint = (opts.telemetry.endpoint ?? "https://otlp.burtson.ai").replace(/\/+$/, "");
21360
+ const headers = { ...opts.telemetry.headers ?? {} };
21361
+ const hasAuth = Object.keys(headers).some((k) => k.toLowerCase() === "authorization");
21362
+ if (!hasAuth && opts.banditApiKey) {
21363
+ headers["Authorization"] = `Bearer ${opts.banditApiKey}`;
21364
+ }
21365
+ const mode = opts.telemetry.mode ?? "metrics+traces";
21366
+ return { endpoint, headers, mode, serviceName: opts.telemetry.serviceName ?? "bandit-web" };
21367
+ }
21368
+ function toAttrs(rec) {
21369
+ const out = [];
21370
+ for (const [key, v] of Object.entries(rec)) {
21371
+ if (v === void 0 || v === null || v === "") continue;
21372
+ if (typeof v === "boolean") out.push({ key, value: { boolValue: v } });
21373
+ else if (typeof v === "number")
21374
+ out.push({ key, value: Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v } });
21375
+ else out.push({ key, value: { stringValue: v } });
21376
+ }
21377
+ return out;
21378
+ }
21379
+ function hex(bytes) {
21380
+ const arr = new Uint8Array(bytes);
21381
+ if (webCrypto?.getRandomValues) webCrypto.getRandomValues(arr);
21382
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
21383
+ }
21384
+ function histogramPoint(value, bounds, attrs, startMs, endMs) {
21385
+ const counts = new Array(bounds.length + 1).fill(0);
21386
+ let idx = bounds.findIndex((b) => value <= b);
21387
+ if (idx === -1) idx = bounds.length;
21388
+ counts[idx] = 1;
21389
+ return {
21390
+ attributes: toAttrs(attrs),
21391
+ startTimeUnixNano: nano(startMs),
21392
+ timeUnixNano: nano(endMs),
21393
+ count: "1",
21394
+ sum: value,
21395
+ bucketCounts: counts.map(String),
21396
+ explicitBounds: bounds
21397
+ };
21398
+ }
21399
+ function sumPoint(value, attrs, startMs, endMs) {
21400
+ return {
21401
+ attributes: toAttrs(attrs),
21402
+ startTimeUnixNano: nano(startMs),
21403
+ timeUnixNano: nano(endMs),
21404
+ asInt: String(Math.round(value))
21405
+ };
21406
+ }
21407
+ var webCrypto, nano, TTFT_BUCKETS, DURATION_BUCKETS, clip, TelemetryExporter;
21408
+ var init_otlpExporter = __esm({
21409
+ "src/services/telemetry/otlpExporter.ts"() {
21410
+ "use strict";
21411
+ init_debugLogger();
21412
+ webCrypto = globalThis.crypto;
21413
+ nano = (ms) => String(Math.round(ms * 1e6));
21414
+ TTFT_BUCKETS = [0.1, 0.25, 0.5, 1, 2, 5, 10, 30];
21415
+ DURATION_BUCKETS = [0.5, 1, 2, 5, 10, 30, 60, 120, 300];
21416
+ clip = (s, n = 120) => redactSecretsString(s.slice(0, n)).slice(0, n);
21417
+ TelemetryExporter = class {
21418
+ cfg;
21419
+ now;
21420
+ traceId = "";
21421
+ turn = null;
21422
+ llm = null;
21423
+ llmFirstChunkMs = 0;
21424
+ openTools = [];
21425
+ completedSpans = [];
21426
+ model = "";
21427
+ turnChunkChars = 0;
21428
+ turnTokens = 0;
21429
+ ttftSeconds = null;
21430
+ constructor(cfg, opts) {
21431
+ this.cfg = cfg;
21432
+ this.now = opts?.now ?? (() => Date.now());
21433
+ }
21434
+ startTurn(goal, model) {
21435
+ this.traceId = hex(16);
21436
+ this.model = model;
21437
+ this.turnChunkChars = 0;
21438
+ this.turnTokens = 0;
21439
+ this.ttftSeconds = null;
21440
+ this.llm = null;
21441
+ this.llmFirstChunkMs = 0;
21442
+ this.openTools = [];
21443
+ this.completedSpans = [];
21444
+ this.turn = {
21445
+ spanId: hex(8),
21446
+ name: "agent.turn",
21447
+ startMs: this.now(),
21448
+ attrs: { "gen_ai.request.model": model, "bandit.turn.goal": clip(goal, 160) }
21449
+ };
21450
+ }
21451
+ /** Fed from the chat turn lifecycle. Best-effort; swallows bad payloads. */
21452
+ onEvent(type, payload) {
21453
+ if (!this.turn) return;
21454
+ try {
21455
+ const p = payload ?? {};
21456
+ switch (type) {
21457
+ case "tool_loop:llm_start":
21458
+ this.llm = {
21459
+ spanId: hex(8),
21460
+ parentSpanId: this.turn.spanId,
21461
+ name: "llm.generate",
21462
+ startMs: this.now(),
21463
+ attrs: { "gen_ai.request.model": this.model }
21464
+ };
21465
+ this.llmFirstChunkMs = 0;
21466
+ break;
21467
+ case "tool_loop:llm_chunk": {
21468
+ const chunk = typeof p.chunk === "string" ? p.chunk : "";
21469
+ if (this.llm && this.llmFirstChunkMs === 0 && chunk.length > 0) {
21470
+ this.llmFirstChunkMs = this.now();
21471
+ const ttft = (this.llmFirstChunkMs - this.llm.startMs) / 1e3;
21472
+ if (this.ttftSeconds === null) this.ttftSeconds = ttft;
21473
+ this.llm.attrs["bandit.llm.ttft_seconds"] = ttft;
21474
+ }
21475
+ this.turnChunkChars += chunk.length;
21476
+ this.turnTokens = Math.floor(this.turnChunkChars / 4);
21477
+ break;
21478
+ }
21479
+ case "tool_loop:llm_response":
21480
+ if (this.llm) {
21481
+ this.llm.endMs = this.now();
21482
+ if (typeof p.responseLength === "number") this.llm.attrs["bandit.llm.response_chars"] = p.responseLength;
21483
+ this.completedSpans.push(this.llm);
21484
+ this.llm = null;
21485
+ }
21486
+ break;
21487
+ case "tool_loop:tool_execute": {
21488
+ const name = typeof p.name === "string" ? p.name : "tool";
21489
+ const params = p.params ?? {};
21490
+ const primary = params.query ?? params.url ?? params.prompt ?? params.topic ?? "";
21491
+ this.openTools.push({
21492
+ spanId: hex(8),
21493
+ parentSpanId: this.turn.spanId,
21494
+ name: `tool.${name}`,
21495
+ startMs: this.now(),
21496
+ attrs: { "bandit.tool.name": name, "bandit.tool.primary": primary ? clip(primary) : void 0 }
21497
+ });
21498
+ break;
21499
+ }
21500
+ case "tool_loop:tool_result":
21501
+ case "tool_loop:tool_error": {
21502
+ const name = typeof p.name === "string" ? p.name : void 0;
21503
+ const span = this.takeOpenTool(name);
21504
+ if (span) {
21505
+ span.endMs = this.now();
21506
+ if (type === "tool_loop:tool_error" || p.isError === true) span.error = "tool error";
21507
+ this.completedSpans.push(span);
21508
+ }
21509
+ break;
21510
+ }
21511
+ }
21512
+ } catch {
21513
+ }
21514
+ }
21515
+ takeOpenTool(name) {
21516
+ if (name) {
21517
+ for (let i = this.openTools.length - 1; i >= 0; i -= 1) {
21518
+ if (this.openTools[i].name === `tool.${name}`) return this.openTools.splice(i, 1)[0];
21519
+ }
21520
+ }
21521
+ return this.openTools.shift();
21522
+ }
21523
+ /** Close the turn, build OTLP traces + metrics, and flush. Never rejects. */
21524
+ async endTurn(outcome) {
21525
+ const turn = this.turn;
21526
+ if (!turn) return;
21527
+ this.turn = null;
21528
+ const end = this.now();
21529
+ if (this.llm && !this.llm.endMs) {
21530
+ this.llm.endMs = end;
21531
+ this.completedSpans.push(this.llm);
21532
+ this.llm = null;
21533
+ }
21534
+ for (const t of this.openTools.splice(0)) {
21535
+ t.endMs = end;
21536
+ t.error = t.error ?? "incomplete";
21537
+ this.completedSpans.push(t);
21538
+ }
21539
+ turn.endMs = end;
21540
+ if (outcome?.error) turn.error = outcome.error;
21541
+ const traceId = this.traceId;
21542
+ const spans = [turn, ...this.completedSpans];
21543
+ const jobs = [];
21544
+ if (this.cfg.mode === "metrics+traces") jobs.push(this.post("/v1/traces", this.buildTraces(traceId, spans)));
21545
+ jobs.push(this.post("/v1/metrics", this.buildMetrics(turn)));
21546
+ try {
21547
+ await Promise.all(jobs);
21548
+ } catch {
21549
+ }
21550
+ }
21551
+ buildTraces(traceId, spans) {
21552
+ return {
21553
+ resourceSpans: [
21554
+ {
21555
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
21556
+ scopeSpans: [
21557
+ {
21558
+ scope: { name: this.cfg.serviceName },
21559
+ spans: spans.map((s) => ({
21560
+ traceId,
21561
+ spanId: s.spanId,
21562
+ parentSpanId: s.parentSpanId,
21563
+ name: s.name,
21564
+ kind: 1,
21565
+ startTimeUnixNano: nano(s.startMs),
21566
+ endTimeUnixNano: nano(s.endMs ?? s.startMs),
21567
+ attributes: toAttrs(s.attrs),
21568
+ status: s.error ? { code: 2, message: s.error.slice(0, 200) } : { code: 1 }
21569
+ }))
21570
+ }
21571
+ ]
21572
+ }
21573
+ ]
21574
+ };
21575
+ }
21576
+ buildMetrics(turn) {
21577
+ const start = turn.startMs;
21578
+ const end = turn.endMs ?? this.now();
21579
+ const metrics = [];
21580
+ if (this.turnTokens > 0) {
21581
+ metrics.push({
21582
+ name: "bandit.llm.tokens",
21583
+ sum: {
21584
+ aggregationTemporality: 1,
21585
+ isMonotonic: true,
21586
+ dataPoints: [sumPoint(this.turnTokens, { type: "output", "gen_ai.request.model": this.model }, start, end)]
21587
+ }
21588
+ });
21589
+ }
21590
+ if (this.ttftSeconds !== null) {
21591
+ metrics.push({
21592
+ name: "bandit.llm.ttft",
21593
+ unit: "s",
21594
+ histogram: {
21595
+ aggregationTemporality: 1,
21596
+ dataPoints: [histogramPoint(this.ttftSeconds, TTFT_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
21597
+ }
21598
+ });
21599
+ }
21600
+ metrics.push({
21601
+ name: "bandit.turn.duration",
21602
+ unit: "s",
21603
+ histogram: {
21604
+ aggregationTemporality: 1,
21605
+ dataPoints: [histogramPoint((end - start) / 1e3, DURATION_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
21606
+ }
21607
+ });
21608
+ return {
21609
+ resourceMetrics: [
21610
+ {
21611
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
21612
+ scopeMetrics: [{ scope: { name: this.cfg.serviceName }, metrics }]
21613
+ }
21614
+ ]
21615
+ };
21616
+ }
21617
+ async post(path, body) {
21618
+ const doFetch = globalThis.fetch;
21619
+ if (!doFetch) return;
21620
+ const ctrl = new AbortController();
21621
+ const timer = setTimeout(() => ctrl.abort(), 4e3);
21622
+ try {
21623
+ await doFetch(`${this.cfg.endpoint}${path}`, {
21624
+ method: "POST",
21625
+ headers: { "Content-Type": "application/json", ...this.cfg.headers },
21626
+ body: JSON.stringify(body),
21627
+ signal: ctrl.signal
21628
+ });
21629
+ } catch (e) {
21630
+ debugLogger.debug("[telemetry] OTLP post failed", {
21631
+ path,
21632
+ error: e instanceof Error ? e.message : String(e)
21633
+ });
21634
+ } finally {
21635
+ clearTimeout(timer);
21636
+ }
21637
+ }
21638
+ };
21639
+ }
21640
+ });
21641
+
21642
+ // src/services/telemetry/index.ts
21643
+ function syncTelemetry() {
21644
+ try {
21645
+ const settings = usePackageSettingsStore.getState().settings;
21646
+ const cfg = resolveTelemetryConfig({
21647
+ telemetry: settings?.telemetry,
21648
+ banditApiKey: authenticationService.getToken() ?? void 0
21649
+ });
21650
+ active = cfg ? new TelemetryExporter(cfg) : null;
21651
+ } catch {
21652
+ active = null;
21653
+ }
21654
+ return active !== null;
21655
+ }
21656
+ function telemetryStartTurn(goal, model) {
21657
+ active?.startTurn(goal, model);
21658
+ }
21659
+ function telemetryEvent(type, payload) {
21660
+ active?.onEvent(type, payload);
21661
+ }
21662
+ function telemetryEndTurn(outcome) {
21663
+ void active?.endTurn(outcome);
21664
+ }
21665
+ var active;
21666
+ var init_telemetry = __esm({
21667
+ "src/services/telemetry/index.ts"() {
21668
+ "use strict";
21669
+ init_otlpExporter();
21670
+ init_packageSettingsStore();
21671
+ init_authenticationService();
21672
+ init_otlpExporter();
21673
+ active = null;
21674
+ }
21675
+ });
21676
+
21677
+ // src/store/engineStore.ts
21678
+ var import_zustand15, STORAGE_KEY2, readStored, useEngineStore;
21679
+ var init_engineStore = __esm({
21680
+ "src/store/engineStore.ts"() {
21681
+ "use strict";
21682
+ import_zustand15 = require("zustand");
21683
+ init_packageSettingsStore();
21684
+ init_authenticationService();
21685
+ init_debugLogger();
21686
+ STORAGE_KEY2 = "bandit.selectedEngine";
21687
+ readStored = () => {
21688
+ try {
21689
+ return typeof window !== "undefined" ? window.localStorage.getItem(STORAGE_KEY2) : null;
21690
+ } catch {
21691
+ return null;
21692
+ }
21693
+ };
21694
+ useEngineStore = (0, import_zustand15.create)((set, get) => ({
21695
+ selectedEngine: readStored(),
21696
+ engines: [],
21697
+ loaded: false,
21698
+ setSelectedEngine: (id) => {
21699
+ set({ selectedEngine: id });
21700
+ try {
21701
+ window.localStorage.setItem(STORAGE_KEY2, id);
21702
+ } catch {
21703
+ }
21704
+ },
21705
+ getSelectedEngine: () => get().selectedEngine || usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core",
21706
+ fetchEngines: async () => {
21707
+ const settings = usePackageSettingsStore.getState().settings;
21708
+ const base = settings?.gatewayApiUrl?.replace(/\/$/, "") ?? "";
21709
+ if (!base || settings?.playgroundMode || base.toLowerCase().startsWith("playground://")) {
21710
+ set({ loaded: true });
21711
+ return;
21712
+ }
21713
+ try {
21714
+ const headers = { "Content-Type": "application/json" };
21715
+ const token = authenticationService.getToken();
21716
+ if (token) headers["Authorization"] = `Bearer ${token}`;
21717
+ const res = await fetch(`${base}/models`, { headers });
21718
+ const data = await res.json();
21719
+ if (res.ok && Array.isArray(data?.models)) {
21720
+ set({ engines: data.models, loaded: true });
21721
+ } else {
21722
+ set({ loaded: true });
21723
+ }
21724
+ } catch (error) {
21725
+ debugLogger.error("Failed to fetch engines", {
21726
+ error: error instanceof Error ? error.message : String(error)
21727
+ });
21728
+ set({ loaded: true });
21729
+ }
21730
+ }
21731
+ }));
21732
+ }
21733
+ });
21734
+
21349
21735
  // src/chat/hooks/useMemoryEnhancer.tsx
21350
21736
  var import_rxjs23, 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;
21351
21737
  var init_useMemoryEnhancer = __esm({
@@ -22087,12 +22473,13 @@ var init_useAIProvider = __esm({
22087
22473
  import_react44 = require("react");
22088
22474
  init_knowledgeStore();
22089
22475
  init_aiProviderStore();
22476
+ init_telemetry();
22477
+ init_engineStore();
22090
22478
  init_conversationStore();
22091
22479
  init_useMemoryEnhancer();
22092
22480
  init_useVectorStore();
22093
22481
  init_embeddingService();
22094
22482
  init_useMoodEngine();
22095
- init_packageSettingsStore();
22096
22483
  init_prompts();
22097
22484
  init_preferencesStore();
22098
22485
  init_mcp();
@@ -22456,7 +22843,7 @@ var init_useAIProvider = __esm({
22456
22843
  question: pendingQuestion,
22457
22844
  images: pendingImages
22458
22845
  });
22459
- const modelName = usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core:4b-it-qat";
22846
+ const modelName = useEngineStore.getState().getSelectedEngine();
22460
22847
  const CONFIG = modelConfigs[modelName] ?? modelConfigs["bandit-core:4b-it-qat"];
22461
22848
  const base64Images = imageList.map((img) => img.split(",")[1]);
22462
22849
  const latestEntries = history.slice(-CONFIG.historyMessages);
@@ -22851,6 +23238,9 @@ ${protocol}`;
22851
23238
  setStreamBuffer(latestDisplayMessage);
22852
23239
  }, delay);
22853
23240
  };
23241
+ syncTelemetry();
23242
+ telemetryStartTurn(question, modelName);
23243
+ telemetryEvent("tool_loop:llm_start");
22854
23244
  const stream = provider.chat(request);
22855
23245
  const initialPlaceholderQuestion = lastEntry?.question;
22856
23246
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -22864,7 +23254,10 @@ ${protocol}`;
22864
23254
  const sub = stream.subscribe({
22865
23255
  next: (data) => {
22866
23256
  if (!data?.message?.content && !data?.message?.tool_calls) return;
22867
- if (data.message.content) fullMessage += data.message.content;
23257
+ if (data.message.content) {
23258
+ fullMessage += data.message.content;
23259
+ telemetryEvent("tool_loop:llm_chunk", { chunk: data.message.content });
23260
+ }
22868
23261
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
22869
23262
  setIsThinking?.(inThinkBlock);
22870
23263
  const visibleMessage = stripThinking(fullMessage);
@@ -22896,6 +23289,7 @@ ${protocol}`;
22896
23289
  setIsThinking?.(false);
22897
23290
  setPendingMessage(null);
22898
23291
  setLogoVisible(false);
23292
+ telemetryEndTurn({ error: err?.message || "stream error" });
22899
23293
  if (onError) {
22900
23294
  onError(err);
22901
23295
  }
@@ -22903,6 +23297,7 @@ ${protocol}`;
22903
23297
  complete: async () => {
22904
23298
  try {
22905
23299
  setIsThinking?.(false);
23300
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
22906
23301
  latestDisplayMessage = stripThinking(fullMessage);
22907
23302
  if (!sawToolBlock) {
22908
23303
  flushNow();
@@ -22911,6 +23306,7 @@ ${protocol}`;
22911
23306
  let enhancedMessage = fullMessage;
22912
23307
  const summarizableResults = [];
22913
23308
  const inlineImageBlocks = [];
23309
+ const collectedSources = [];
22914
23310
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
22915
23311
  debugLogger.info("Detected tool calls in AI response", {
22916
23312
  toolCallCount: toolCallMatches.length,
@@ -22945,10 +23341,21 @@ ${protocol}`;
22945
23341
  });
22946
23342
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
22947
23343
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
23344
+ clearFlushTimer();
23345
+ 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";
23346
+ const toolPreamble = stripToolBlocks(fullMessage).trim();
23347
+ setStreamBuffer(toolPreamble ? `${toolPreamble}
23348
+
23349
+ _${toolStatus}_` : `_${toolStatus}_`);
23350
+ telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
22948
23351
  const result = await executeMCPTool({
22949
23352
  toolName: functionName,
22950
23353
  parameters: parsedParams
22951
23354
  });
23355
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
23356
+ name: functionName,
23357
+ isError: !result.success
23358
+ });
22952
23359
  let resultText = "";
22953
23360
  if (result.success) {
22954
23361
  if (functionName === "web_search" || functionName === "web-search") {
@@ -22962,18 +23369,16 @@ ${protocol}`;
22962
23369
  blocks.push(
22963
23370
  items.slice(0, 6).map((item, index) => {
22964
23371
  const title = item.title?.trim() || "Untitled";
22965
- const url = item.url?.trim();
23372
+ const url = item.url?.trim() || "";
22966
23373
  const snippet = item.content?.trim();
22967
- let line = `${index + 1}. **${title}**`;
22968
- if (url) line += `
22969
- ${url}`;
23374
+ if (url) collectedSources.push({ title, url });
23375
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
22970
23376
  if (snippet) {
22971
23377
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
22972
- line += `
22973
- ${truncated}`;
23378
+ line += ` \u2014 ${truncated}`;
22974
23379
  }
22975
23380
  return line;
22976
- }).join("\n\n")
23381
+ }).join("\n")
22977
23382
  );
22978
23383
  }
22979
23384
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -23055,7 +23460,7 @@ ${r.output}`).join("\n\n");
23055
23460
 
23056
23461
  ${toolResultsText}
23057
23462
 
23058
- 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.`
23463
+ 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.`
23059
23464
  }
23060
23465
  ];
23061
23466
  const summaryRequest = {
@@ -23069,7 +23474,7 @@ Using these results together with your own knowledge, answer my original questio
23069
23474
  setStreamBuffer(
23070
23475
  summaryPreamble ? `${summaryPreamble}
23071
23476
 
23072
- _Working on it\u2026_` : "_Working on it\u2026_"
23477
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
23073
23478
  );
23074
23479
  const summaryText = await new Promise((resolve) => {
23075
23480
  let acc = "";
@@ -23110,7 +23515,18 @@ _Working on it\u2026_` : "_Working on it\u2026_"
23110
23515
  }, 3e4);
23111
23516
  });
23112
23517
  if (summaryText.trim()) {
23113
- enhancedMessage = summaryText + (inlineImageBlocks.length ? `
23518
+ const sourcesMd = collectedSources.length ? `
23519
+
23520
+ **Sources**
23521
+ ${collectedSources.slice(0, 6).map((s) => {
23522
+ let domain = s.url;
23523
+ try {
23524
+ domain = new URL(s.url).hostname.replace(/^www\./, "");
23525
+ } catch {
23526
+ }
23527
+ return `- [${s.title || domain}](${s.url}) \u2014 ${domain}`;
23528
+ }).join("\n")}` : "";
23529
+ enhancedMessage = summaryText + sourcesMd + (inlineImageBlocks.length ? `
23114
23530
 
23115
23531
  ${inlineImageBlocks.join("\n\n")}` : "");
23116
23532
  }
@@ -23159,6 +23575,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
23159
23575
  }
23160
23576
  setInputValue("");
23161
23577
  setPastedImages([]);
23578
+ telemetryEndTurn();
23162
23579
  setTimeout(() => {
23163
23580
  clearFlushTimer();
23164
23581
  setPendingMessage(null);
@@ -23174,6 +23591,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
23174
23591
  overrideComponentStatus("Idle");
23175
23592
  setIsSubmitting(false);
23176
23593
  setIsStreaming(false);
23594
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
23177
23595
  }
23178
23596
  }
23179
23597
  });
@@ -26032,10 +26450,10 @@ var init_enhanced_mobile_conversations_modal = __esm({
26032
26450
  (0, import_react52.useEffect)(() => {
26033
26451
  setDeletedConversationIds((prev) => {
26034
26452
  let changed = false;
26035
- const active = new Set(conversations.map((conv) => conv.id));
26453
+ const active2 = new Set(conversations.map((conv) => conv.id));
26036
26454
  const next = /* @__PURE__ */ new Set();
26037
26455
  prev.forEach((id) => {
26038
- if (active.has(id)) {
26456
+ if (active2.has(id)) {
26039
26457
  next.add(id);
26040
26458
  } else {
26041
26459
  changed = true;
@@ -26657,6 +27075,7 @@ var init_chat_app_bar = __esm({
26657
27075
  init_packageSettingsStore();
26658
27076
  init_useFeatures();
26659
27077
  init_conversationSyncStore();
27078
+ init_engineStore();
26660
27079
  import_shallow2 = require("zustand/shallow");
26661
27080
  import_jsx_runtime42 = require("react/jsx-runtime");
26662
27081
  CDN_BASE2 = "https://cdn.burtson.ai/";
@@ -26706,6 +27125,7 @@ var init_chat_app_bar = __esm({
26706
27125
  menuText
26707
27126
  } = theme.palette.chat.appBar;
26708
27127
  const [modelAnchorEl, setModelAnchorEl] = (0, import_react53.useState)(null);
27128
+ const [engineAnchorEl, setEngineAnchorEl] = (0, import_react53.useState)(null);
26709
27129
  const [voiceAnchorEl, setVoiceAnchorEl] = (0, import_react53.useState)(null);
26710
27130
  const [modalOpen, setModalOpen] = (0, import_react53.useState)(false);
26711
27131
  const [confirmModelChangeOpen, setConfirmModelChangeOpen] = (0, import_react53.useState)(false);
@@ -26826,6 +27246,14 @@ var init_chat_app_bar = __esm({
26826
27246
  const selectedModel = useModelStore((s) => s.selectedModel);
26827
27247
  const currentModel = useModelStore((s) => s.availableModels.find((m) => m.name === selectedModel));
26828
27248
  const currentAvatar = currentModel?.avatarBase64 || modelAvatars3[selectedModel] || banditHead5;
27249
+ const engines = useEngineStore((s) => s.engines);
27250
+ const selectedEngine = useEngineStore((s) => s.selectedEngine);
27251
+ const effectiveEngineId = selectedEngine || usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core";
27252
+ const currentEngine = engines.find((e) => e.id === effectiveEngineId);
27253
+ const engineLabel = currentEngine?.displayName?.replace(/^Bandit /, "") || "Engine";
27254
+ (0, import_react53.useEffect)(() => {
27255
+ useEngineStore.getState().fetchEngines();
27256
+ }, []);
26829
27257
  const pendingModelAvatar = useModelStore.getState().availableModels.find((m) => m.name === pendingModel)?.avatarBase64 || modelAvatars3[pendingModel || ""] || banditHead5;
26830
27258
  const resolvedHomeUrl = preferences.homeUrl?.trim() || packageSettings?.homeUrl?.trim() || "";
26831
27259
  const homeTooltip = (() => {
@@ -27138,6 +27566,84 @@ var init_chat_app_bar = __esm({
27138
27566
  )
27139
27567
  }
27140
27568
  ) }),
27569
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Tooltip, { title: `Engine: ${currentEngine?.displayName ?? effectiveEngineId}`, arrow: true, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
27570
+ import_material43.IconButton,
27571
+ {
27572
+ onClick: (e) => setEngineAnchorEl(e.currentTarget),
27573
+ sx: pillButtonStyles,
27574
+ "aria-label": `Change base model (engine). Currently ${effectiveEngineId}`,
27575
+ children: [
27576
+ currentEngine?.cloud ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(CloudDoneIcon, { fontSize: "small" }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(CloudOffIcon, { fontSize: "small" }),
27577
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Typography, { variant: "caption", sx: { ml: 0.75, fontWeight: 600, whiteSpace: "nowrap" }, children: engineLabel })
27578
+ ]
27579
+ }
27580
+ ) }),
27581
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
27582
+ import_material43.Menu,
27583
+ {
27584
+ anchorEl: engineAnchorEl,
27585
+ open: Boolean(engineAnchorEl),
27586
+ onClose: () => setEngineAnchorEl(null),
27587
+ transformOrigin: { horizontal: "right", vertical: "top" },
27588
+ anchorOrigin: { horizontal: "right", vertical: "bottom" },
27589
+ children: [
27590
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Typography, { variant: "overline", sx: { px: 2, color: theme.palette.text.secondary }, children: "Engine \xB7 base model" }),
27591
+ engines.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.MenuItem, { disabled: true, children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Typography, { variant: "body2", children: "No engines available" }) }),
27592
+ engines.map((engine) => {
27593
+ const badges = [
27594
+ engine.vision && "vision",
27595
+ engine.tools && "tools",
27596
+ engine.thinking && "thinking",
27597
+ engine.cloud && "cloud"
27598
+ ].filter(Boolean);
27599
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
27600
+ import_material43.MenuItem,
27601
+ {
27602
+ selected: engine.id === effectiveEngineId,
27603
+ disabled: !engine.available,
27604
+ onClick: () => {
27605
+ useEngineStore.getState().setSelectedEngine(engine.id);
27606
+ setEngineAnchorEl(null);
27607
+ },
27608
+ sx: {
27609
+ display: "flex",
27610
+ flexDirection: "column",
27611
+ alignItems: "flex-start",
27612
+ gap: 0.5,
27613
+ py: 1,
27614
+ px: 2,
27615
+ maxWidth: 360,
27616
+ whiteSpace: "normal"
27617
+ },
27618
+ children: [
27619
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_material43.Box, { sx: { display: "flex", alignItems: "center", gap: 1, width: "100%" }, children: [
27620
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Typography, { variant: "body2", sx: { fontWeight: 600, flex: 1 }, children: engine.displayName }),
27621
+ engine.id === effectiveEngineId && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Box, { sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: theme.palette.primary.main } })
27622
+ ] }),
27623
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Typography, { variant: "caption", sx: { color: theme.palette.text.secondary }, children: engine.available ? engine.description : engine.unavailableReason || "Unavailable" }),
27624
+ badges.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_material43.Box, { sx: { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.25 }, children: badges.map((b) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
27625
+ import_material43.Box,
27626
+ {
27627
+ sx: {
27628
+ fontSize: "0.65rem",
27629
+ px: 0.75,
27630
+ py: 0.1,
27631
+ borderRadius: 1,
27632
+ bgcolor: theme.palette.primary.main + "22",
27633
+ color: theme.palette.primary.main
27634
+ },
27635
+ children: b
27636
+ },
27637
+ b
27638
+ )) })
27639
+ ]
27640
+ },
27641
+ engine.id
27642
+ );
27643
+ })
27644
+ ]
27645
+ }
27646
+ ),
27141
27647
  /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
27142
27648
  import_material43.Menu,
27143
27649
  {