@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
@@ -3,10 +3,10 @@ import {
3
3
  } from "./chunk-ONQMRE2G.mjs";
4
4
  import {
5
5
  StreamingMarkdown_default
6
- } from "./chunk-KM7FUWCM.mjs";
6
+ } from "./chunk-SRCCNBHF.mjs";
7
7
  import {
8
8
  useMCPToolsStore
9
- } from "./chunk-QPBG6JQE.mjs";
9
+ } from "./chunk-6QTTNYF2.mjs";
10
10
  import {
11
11
  AddIcon,
12
12
  ArrowDownwardIcon,
@@ -42,7 +42,7 @@ import {
42
42
  useNotificationService,
43
43
  useTTS,
44
44
  useVoiceStore
45
- } from "./chunk-KNBWR4DS.mjs";
45
+ } from "./chunk-5WQMMCZQ.mjs";
46
46
  import {
47
47
  authenticationService,
48
48
  brandingService_default,
@@ -67,13 +67,13 @@ import {
67
67
  useMemoryStore,
68
68
  useProjectStore,
69
69
  useVectorStore
70
- } from "./chunk-557E5VZ2.mjs";
70
+ } from "./chunk-EUBVBTB3.mjs";
71
71
  import {
72
72
  indexedDBService_default,
73
73
  useModelStore,
74
74
  usePackageSettingsStore,
75
75
  usePreferencesStore
76
- } from "./chunk-7ZDS33S2.mjs";
76
+ } from "./chunk-IPMTNREZ.mjs";
77
77
  import {
78
78
  useAIProviderStore
79
79
  } from "./chunk-H3BYFEIE.mjs";
@@ -1388,6 +1388,362 @@ var chat_input_default = ChatInput;
1388
1388
  // src/chat/hooks/useAIProvider.tsx
1389
1389
  import { useCallback, useRef as useRef3 } from "react";
1390
1390
 
1391
+ // src/services/telemetry/otlpExporter.ts
1392
+ function redactSecretsString(s) {
1393
+ 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]");
1394
+ }
1395
+ function resolveTelemetryConfig(opts) {
1396
+ if (!opts.telemetry?.enabled) return null;
1397
+ const endpoint = (opts.telemetry.endpoint ?? "https://otlp.burtson.ai").replace(/\/+$/, "");
1398
+ const headers = { ...opts.telemetry.headers ?? {} };
1399
+ const hasAuth = Object.keys(headers).some((k) => k.toLowerCase() === "authorization");
1400
+ if (!hasAuth && opts.banditApiKey) {
1401
+ headers["Authorization"] = `Bearer ${opts.banditApiKey}`;
1402
+ }
1403
+ const mode = opts.telemetry.mode ?? "metrics+traces";
1404
+ return { endpoint, headers, mode, serviceName: opts.telemetry.serviceName ?? "bandit-web" };
1405
+ }
1406
+ function toAttrs(rec) {
1407
+ const out = [];
1408
+ for (const [key, v] of Object.entries(rec)) {
1409
+ if (v === void 0 || v === null || v === "") continue;
1410
+ if (typeof v === "boolean") out.push({ key, value: { boolValue: v } });
1411
+ else if (typeof v === "number")
1412
+ out.push({ key, value: Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v } });
1413
+ else out.push({ key, value: { stringValue: v } });
1414
+ }
1415
+ return out;
1416
+ }
1417
+ var webCrypto = globalThis.crypto;
1418
+ function hex(bytes) {
1419
+ const arr = new Uint8Array(bytes);
1420
+ if (webCrypto?.getRandomValues) webCrypto.getRandomValues(arr);
1421
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
1422
+ }
1423
+ var nano = (ms) => String(Math.round(ms * 1e6));
1424
+ var TTFT_BUCKETS = [0.1, 0.25, 0.5, 1, 2, 5, 10, 30];
1425
+ var DURATION_BUCKETS = [0.5, 1, 2, 5, 10, 30, 60, 120, 300];
1426
+ function histogramPoint(value, bounds, attrs, startMs, endMs) {
1427
+ const counts = new Array(bounds.length + 1).fill(0);
1428
+ let idx = bounds.findIndex((b) => value <= b);
1429
+ if (idx === -1) idx = bounds.length;
1430
+ counts[idx] = 1;
1431
+ return {
1432
+ attributes: toAttrs(attrs),
1433
+ startTimeUnixNano: nano(startMs),
1434
+ timeUnixNano: nano(endMs),
1435
+ count: "1",
1436
+ sum: value,
1437
+ bucketCounts: counts.map(String),
1438
+ explicitBounds: bounds
1439
+ };
1440
+ }
1441
+ function sumPoint(value, attrs, startMs, endMs) {
1442
+ return {
1443
+ attributes: toAttrs(attrs),
1444
+ startTimeUnixNano: nano(startMs),
1445
+ timeUnixNano: nano(endMs),
1446
+ asInt: String(Math.round(value))
1447
+ };
1448
+ }
1449
+ var clip = (s, n = 120) => redactSecretsString(s.slice(0, n)).slice(0, n);
1450
+ var TelemetryExporter = class {
1451
+ cfg;
1452
+ now;
1453
+ traceId = "";
1454
+ turn = null;
1455
+ llm = null;
1456
+ llmFirstChunkMs = 0;
1457
+ openTools = [];
1458
+ completedSpans = [];
1459
+ model = "";
1460
+ turnChunkChars = 0;
1461
+ turnTokens = 0;
1462
+ ttftSeconds = null;
1463
+ constructor(cfg, opts) {
1464
+ this.cfg = cfg;
1465
+ this.now = opts?.now ?? (() => Date.now());
1466
+ }
1467
+ startTurn(goal, model) {
1468
+ this.traceId = hex(16);
1469
+ this.model = model;
1470
+ this.turnChunkChars = 0;
1471
+ this.turnTokens = 0;
1472
+ this.ttftSeconds = null;
1473
+ this.llm = null;
1474
+ this.llmFirstChunkMs = 0;
1475
+ this.openTools = [];
1476
+ this.completedSpans = [];
1477
+ this.turn = {
1478
+ spanId: hex(8),
1479
+ name: "agent.turn",
1480
+ startMs: this.now(),
1481
+ attrs: { "gen_ai.request.model": model, "bandit.turn.goal": clip(goal, 160) }
1482
+ };
1483
+ }
1484
+ /** Fed from the chat turn lifecycle. Best-effort; swallows bad payloads. */
1485
+ onEvent(type, payload) {
1486
+ if (!this.turn) return;
1487
+ try {
1488
+ const p = payload ?? {};
1489
+ switch (type) {
1490
+ case "tool_loop:llm_start":
1491
+ this.llm = {
1492
+ spanId: hex(8),
1493
+ parentSpanId: this.turn.spanId,
1494
+ name: "llm.generate",
1495
+ startMs: this.now(),
1496
+ attrs: { "gen_ai.request.model": this.model }
1497
+ };
1498
+ this.llmFirstChunkMs = 0;
1499
+ break;
1500
+ case "tool_loop:llm_chunk": {
1501
+ const chunk = typeof p.chunk === "string" ? p.chunk : "";
1502
+ if (this.llm && this.llmFirstChunkMs === 0 && chunk.length > 0) {
1503
+ this.llmFirstChunkMs = this.now();
1504
+ const ttft = (this.llmFirstChunkMs - this.llm.startMs) / 1e3;
1505
+ if (this.ttftSeconds === null) this.ttftSeconds = ttft;
1506
+ this.llm.attrs["bandit.llm.ttft_seconds"] = ttft;
1507
+ }
1508
+ this.turnChunkChars += chunk.length;
1509
+ this.turnTokens = Math.floor(this.turnChunkChars / 4);
1510
+ break;
1511
+ }
1512
+ case "tool_loop:llm_response":
1513
+ if (this.llm) {
1514
+ this.llm.endMs = this.now();
1515
+ if (typeof p.responseLength === "number") this.llm.attrs["bandit.llm.response_chars"] = p.responseLength;
1516
+ this.completedSpans.push(this.llm);
1517
+ this.llm = null;
1518
+ }
1519
+ break;
1520
+ case "tool_loop:tool_execute": {
1521
+ const name = typeof p.name === "string" ? p.name : "tool";
1522
+ const params = p.params ?? {};
1523
+ const primary = params.query ?? params.url ?? params.prompt ?? params.topic ?? "";
1524
+ this.openTools.push({
1525
+ spanId: hex(8),
1526
+ parentSpanId: this.turn.spanId,
1527
+ name: `tool.${name}`,
1528
+ startMs: this.now(),
1529
+ attrs: { "bandit.tool.name": name, "bandit.tool.primary": primary ? clip(primary) : void 0 }
1530
+ });
1531
+ break;
1532
+ }
1533
+ case "tool_loop:tool_result":
1534
+ case "tool_loop:tool_error": {
1535
+ const name = typeof p.name === "string" ? p.name : void 0;
1536
+ const span = this.takeOpenTool(name);
1537
+ if (span) {
1538
+ span.endMs = this.now();
1539
+ if (type === "tool_loop:tool_error" || p.isError === true) span.error = "tool error";
1540
+ this.completedSpans.push(span);
1541
+ }
1542
+ break;
1543
+ }
1544
+ }
1545
+ } catch {
1546
+ }
1547
+ }
1548
+ takeOpenTool(name) {
1549
+ if (name) {
1550
+ for (let i = this.openTools.length - 1; i >= 0; i -= 1) {
1551
+ if (this.openTools[i].name === `tool.${name}`) return this.openTools.splice(i, 1)[0];
1552
+ }
1553
+ }
1554
+ return this.openTools.shift();
1555
+ }
1556
+ /** Close the turn, build OTLP traces + metrics, and flush. Never rejects. */
1557
+ async endTurn(outcome) {
1558
+ const turn = this.turn;
1559
+ if (!turn) return;
1560
+ this.turn = null;
1561
+ const end = this.now();
1562
+ if (this.llm && !this.llm.endMs) {
1563
+ this.llm.endMs = end;
1564
+ this.completedSpans.push(this.llm);
1565
+ this.llm = null;
1566
+ }
1567
+ for (const t of this.openTools.splice(0)) {
1568
+ t.endMs = end;
1569
+ t.error = t.error ?? "incomplete";
1570
+ this.completedSpans.push(t);
1571
+ }
1572
+ turn.endMs = end;
1573
+ if (outcome?.error) turn.error = outcome.error;
1574
+ const traceId = this.traceId;
1575
+ const spans = [turn, ...this.completedSpans];
1576
+ const jobs = [];
1577
+ if (this.cfg.mode === "metrics+traces") jobs.push(this.post("/v1/traces", this.buildTraces(traceId, spans)));
1578
+ jobs.push(this.post("/v1/metrics", this.buildMetrics(turn)));
1579
+ try {
1580
+ await Promise.all(jobs);
1581
+ } catch {
1582
+ }
1583
+ }
1584
+ buildTraces(traceId, spans) {
1585
+ return {
1586
+ resourceSpans: [
1587
+ {
1588
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
1589
+ scopeSpans: [
1590
+ {
1591
+ scope: { name: this.cfg.serviceName },
1592
+ spans: spans.map((s) => ({
1593
+ traceId,
1594
+ spanId: s.spanId,
1595
+ parentSpanId: s.parentSpanId,
1596
+ name: s.name,
1597
+ kind: 1,
1598
+ startTimeUnixNano: nano(s.startMs),
1599
+ endTimeUnixNano: nano(s.endMs ?? s.startMs),
1600
+ attributes: toAttrs(s.attrs),
1601
+ status: s.error ? { code: 2, message: s.error.slice(0, 200) } : { code: 1 }
1602
+ }))
1603
+ }
1604
+ ]
1605
+ }
1606
+ ]
1607
+ };
1608
+ }
1609
+ buildMetrics(turn) {
1610
+ const start = turn.startMs;
1611
+ const end = turn.endMs ?? this.now();
1612
+ const metrics = [];
1613
+ if (this.turnTokens > 0) {
1614
+ metrics.push({
1615
+ name: "bandit.llm.tokens",
1616
+ sum: {
1617
+ aggregationTemporality: 1,
1618
+ isMonotonic: true,
1619
+ dataPoints: [sumPoint(this.turnTokens, { type: "output", "gen_ai.request.model": this.model }, start, end)]
1620
+ }
1621
+ });
1622
+ }
1623
+ if (this.ttftSeconds !== null) {
1624
+ metrics.push({
1625
+ name: "bandit.llm.ttft",
1626
+ unit: "s",
1627
+ histogram: {
1628
+ aggregationTemporality: 1,
1629
+ dataPoints: [histogramPoint(this.ttftSeconds, TTFT_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
1630
+ }
1631
+ });
1632
+ }
1633
+ metrics.push({
1634
+ name: "bandit.turn.duration",
1635
+ unit: "s",
1636
+ histogram: {
1637
+ aggregationTemporality: 1,
1638
+ dataPoints: [histogramPoint((end - start) / 1e3, DURATION_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
1639
+ }
1640
+ });
1641
+ return {
1642
+ resourceMetrics: [
1643
+ {
1644
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
1645
+ scopeMetrics: [{ scope: { name: this.cfg.serviceName }, metrics }]
1646
+ }
1647
+ ]
1648
+ };
1649
+ }
1650
+ async post(path, body) {
1651
+ const doFetch = globalThis.fetch;
1652
+ if (!doFetch) return;
1653
+ const ctrl = new AbortController();
1654
+ const timer = setTimeout(() => ctrl.abort(), 4e3);
1655
+ try {
1656
+ await doFetch(`${this.cfg.endpoint}${path}`, {
1657
+ method: "POST",
1658
+ headers: { "Content-Type": "application/json", ...this.cfg.headers },
1659
+ body: JSON.stringify(body),
1660
+ signal: ctrl.signal
1661
+ });
1662
+ } catch (e) {
1663
+ debugLogger.debug("[telemetry] OTLP post failed", {
1664
+ path,
1665
+ error: e instanceof Error ? e.message : String(e)
1666
+ });
1667
+ } finally {
1668
+ clearTimeout(timer);
1669
+ }
1670
+ }
1671
+ };
1672
+
1673
+ // src/services/telemetry/index.ts
1674
+ var active = null;
1675
+ function syncTelemetry() {
1676
+ try {
1677
+ const settings = usePackageSettingsStore.getState().settings;
1678
+ const cfg = resolveTelemetryConfig({
1679
+ telemetry: settings?.telemetry,
1680
+ banditApiKey: authenticationService.getToken() ?? void 0
1681
+ });
1682
+ active = cfg ? new TelemetryExporter(cfg) : null;
1683
+ } catch {
1684
+ active = null;
1685
+ }
1686
+ return active !== null;
1687
+ }
1688
+ function telemetryStartTurn(goal, model) {
1689
+ active?.startTurn(goal, model);
1690
+ }
1691
+ function telemetryEvent(type, payload) {
1692
+ active?.onEvent(type, payload);
1693
+ }
1694
+ function telemetryEndTurn(outcome) {
1695
+ void active?.endTurn(outcome);
1696
+ }
1697
+
1698
+ // src/store/engineStore.ts
1699
+ import { create as create2 } from "zustand";
1700
+ var STORAGE_KEY = "bandit.selectedEngine";
1701
+ var readStored = () => {
1702
+ try {
1703
+ return typeof window !== "undefined" ? window.localStorage.getItem(STORAGE_KEY) : null;
1704
+ } catch {
1705
+ return null;
1706
+ }
1707
+ };
1708
+ var useEngineStore = create2((set, get) => ({
1709
+ selectedEngine: readStored(),
1710
+ engines: [],
1711
+ loaded: false,
1712
+ setSelectedEngine: (id) => {
1713
+ set({ selectedEngine: id });
1714
+ try {
1715
+ window.localStorage.setItem(STORAGE_KEY, id);
1716
+ } catch {
1717
+ }
1718
+ },
1719
+ getSelectedEngine: () => get().selectedEngine || usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core",
1720
+ fetchEngines: async () => {
1721
+ const settings = usePackageSettingsStore.getState().settings;
1722
+ const base = settings?.gatewayApiUrl?.replace(/\/$/, "") ?? "";
1723
+ if (!base || settings?.playgroundMode || base.toLowerCase().startsWith("playground://")) {
1724
+ set({ loaded: true });
1725
+ return;
1726
+ }
1727
+ try {
1728
+ const headers = { "Content-Type": "application/json" };
1729
+ const token = authenticationService.getToken();
1730
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1731
+ const res = await fetch(`${base}/models`, { headers });
1732
+ const data = await res.json();
1733
+ if (res.ok && Array.isArray(data?.models)) {
1734
+ set({ engines: data.models, loaded: true });
1735
+ } else {
1736
+ set({ loaded: true });
1737
+ }
1738
+ } catch (error) {
1739
+ debugLogger.error("Failed to fetch engines", {
1740
+ error: error instanceof Error ? error.message : String(error)
1741
+ });
1742
+ set({ loaded: true });
1743
+ }
1744
+ }
1745
+ }));
1746
+
1391
1747
  // src/chat/hooks/useMemoryEnhancer.tsx
1392
1748
  import { lastValueFrom, map as map2 } from "rxjs";
1393
1749
  var MEMORY_LIMIT = 100;
@@ -2440,7 +2796,7 @@ var useAIProvider = ({
2440
2796
  question: pendingQuestion,
2441
2797
  images: pendingImages
2442
2798
  });
2443
- const modelName = usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core:4b-it-qat";
2799
+ const modelName = useEngineStore.getState().getSelectedEngine();
2444
2800
  const CONFIG = modelConfigs[modelName] ?? modelConfigs["bandit-core:4b-it-qat"];
2445
2801
  const base64Images = imageList.map((img) => img.split(",")[1]);
2446
2802
  const latestEntries = history.slice(-CONFIG.historyMessages);
@@ -2835,6 +3191,9 @@ ${protocol}`;
2835
3191
  setStreamBuffer(latestDisplayMessage);
2836
3192
  }, delay);
2837
3193
  };
3194
+ syncTelemetry();
3195
+ telemetryStartTurn(question, modelName);
3196
+ telemetryEvent("tool_loop:llm_start");
2838
3197
  const stream = provider.chat(request);
2839
3198
  const initialPlaceholderQuestion = lastEntry?.question;
2840
3199
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -2848,7 +3207,10 @@ ${protocol}`;
2848
3207
  const sub = stream.subscribe({
2849
3208
  next: (data) => {
2850
3209
  if (!data?.message?.content && !data?.message?.tool_calls) return;
2851
- if (data.message.content) fullMessage += data.message.content;
3210
+ if (data.message.content) {
3211
+ fullMessage += data.message.content;
3212
+ telemetryEvent("tool_loop:llm_chunk", { chunk: data.message.content });
3213
+ }
2852
3214
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
2853
3215
  setIsThinking?.(inThinkBlock);
2854
3216
  const visibleMessage = stripThinking(fullMessage);
@@ -2880,6 +3242,7 @@ ${protocol}`;
2880
3242
  setIsThinking?.(false);
2881
3243
  setPendingMessage(null);
2882
3244
  setLogoVisible(false);
3245
+ telemetryEndTurn({ error: err?.message || "stream error" });
2883
3246
  if (onError) {
2884
3247
  onError(err);
2885
3248
  }
@@ -2887,6 +3250,7 @@ ${protocol}`;
2887
3250
  complete: async () => {
2888
3251
  try {
2889
3252
  setIsThinking?.(false);
3253
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
2890
3254
  latestDisplayMessage = stripThinking(fullMessage);
2891
3255
  if (!sawToolBlock) {
2892
3256
  flushNow();
@@ -2895,6 +3259,7 @@ ${protocol}`;
2895
3259
  let enhancedMessage = fullMessage;
2896
3260
  const summarizableResults = [];
2897
3261
  const inlineImageBlocks = [];
3262
+ const collectedSources = [];
2898
3263
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
2899
3264
  debugLogger.info("Detected tool calls in AI response", {
2900
3265
  toolCallCount: toolCallMatches.length,
@@ -2929,10 +3294,21 @@ ${protocol}`;
2929
3294
  });
2930
3295
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
2931
3296
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
3297
+ clearFlushTimer();
3298
+ 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";
3299
+ const toolPreamble = stripToolBlocks(fullMessage).trim();
3300
+ setStreamBuffer(toolPreamble ? `${toolPreamble}
3301
+
3302
+ _${toolStatus}_` : `_${toolStatus}_`);
3303
+ telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
2932
3304
  const result = await executeMCPTool({
2933
3305
  toolName: functionName,
2934
3306
  parameters: parsedParams
2935
3307
  });
3308
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
3309
+ name: functionName,
3310
+ isError: !result.success
3311
+ });
2936
3312
  let resultText = "";
2937
3313
  if (result.success) {
2938
3314
  if (functionName === "web_search" || functionName === "web-search") {
@@ -2946,18 +3322,16 @@ ${protocol}`;
2946
3322
  blocks.push(
2947
3323
  items.slice(0, 6).map((item, index) => {
2948
3324
  const title = item.title?.trim() || "Untitled";
2949
- const url = item.url?.trim();
3325
+ const url = item.url?.trim() || "";
2950
3326
  const snippet = item.content?.trim();
2951
- let line = `${index + 1}. **${title}**`;
2952
- if (url) line += `
2953
- ${url}`;
3327
+ if (url) collectedSources.push({ title, url });
3328
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
2954
3329
  if (snippet) {
2955
3330
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
2956
- line += `
2957
- ${truncated}`;
3331
+ line += ` \u2014 ${truncated}`;
2958
3332
  }
2959
3333
  return line;
2960
- }).join("\n\n")
3334
+ }).join("\n")
2961
3335
  );
2962
3336
  }
2963
3337
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -3039,7 +3413,7 @@ ${r.output}`).join("\n\n");
3039
3413
 
3040
3414
  ${toolResultsText}
3041
3415
 
3042
- 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.`
3416
+ 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.`
3043
3417
  }
3044
3418
  ];
3045
3419
  const summaryRequest = {
@@ -3053,7 +3427,7 @@ Using these results together with your own knowledge, answer my original questio
3053
3427
  setStreamBuffer(
3054
3428
  summaryPreamble ? `${summaryPreamble}
3055
3429
 
3056
- _Working on it\u2026_` : "_Working on it\u2026_"
3430
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
3057
3431
  );
3058
3432
  const summaryText = await new Promise((resolve) => {
3059
3433
  let acc = "";
@@ -3094,7 +3468,18 @@ _Working on it\u2026_` : "_Working on it\u2026_"
3094
3468
  }, 3e4);
3095
3469
  });
3096
3470
  if (summaryText.trim()) {
3097
- enhancedMessage = summaryText + (inlineImageBlocks.length ? `
3471
+ const sourcesMd = collectedSources.length ? `
3472
+
3473
+ **Sources**
3474
+ ${collectedSources.slice(0, 6).map((s) => {
3475
+ let domain = s.url;
3476
+ try {
3477
+ domain = new URL(s.url).hostname.replace(/^www\./, "");
3478
+ } catch {
3479
+ }
3480
+ return `- [${s.title || domain}](${s.url}) \u2014 ${domain}`;
3481
+ }).join("\n")}` : "";
3482
+ enhancedMessage = summaryText + sourcesMd + (inlineImageBlocks.length ? `
3098
3483
 
3099
3484
  ${inlineImageBlocks.join("\n\n")}` : "");
3100
3485
  }
@@ -3143,6 +3528,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
3143
3528
  }
3144
3529
  setInputValue("");
3145
3530
  setPastedImages([]);
3531
+ telemetryEndTurn();
3146
3532
  setTimeout(() => {
3147
3533
  clearFlushTimer();
3148
3534
  setPendingMessage(null);
@@ -3158,6 +3544,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
3158
3544
  overrideComponentStatus("Idle");
3159
3545
  setIsSubmitting(false);
3160
3546
  setIsStreaming(false);
3547
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
3161
3548
  }
3162
3549
  }
3163
3550
  });
@@ -6059,10 +6446,10 @@ var EnhancedMobileConversationsModal = ({
6059
6446
  useEffect10(() => {
6060
6447
  setDeletedConversationIds((prev) => {
6061
6448
  let changed = false;
6062
- const active = new Set(conversations.map((conv) => conv.id));
6449
+ const active2 = new Set(conversations.map((conv) => conv.id));
6063
6450
  const next = /* @__PURE__ */ new Set();
6064
6451
  prev.forEach((id) => {
6065
- if (active.has(id)) {
6452
+ if (active2.has(id)) {
6066
6453
  next.add(id);
6067
6454
  } else {
6068
6455
  changed = true;
@@ -6711,6 +7098,7 @@ var ChatAppBar = ({
6711
7098
  menuText
6712
7099
  } = theme.palette.chat.appBar;
6713
7100
  const [modelAnchorEl, setModelAnchorEl] = useState12(null);
7101
+ const [engineAnchorEl, setEngineAnchorEl] = useState12(null);
6714
7102
  const [voiceAnchorEl, setVoiceAnchorEl] = useState12(null);
6715
7103
  const [modalOpen, setModalOpen] = useState12(false);
6716
7104
  const [confirmModelChangeOpen, setConfirmModelChangeOpen] = useState12(false);
@@ -6831,6 +7219,14 @@ var ChatAppBar = ({
6831
7219
  const selectedModel = useModelStore((s) => s.selectedModel);
6832
7220
  const currentModel = useModelStore((s) => s.availableModels.find((m) => m.name === selectedModel));
6833
7221
  const currentAvatar = currentModel?.avatarBase64 || modelAvatars[selectedModel] || banditHead;
7222
+ const engines = useEngineStore((s) => s.engines);
7223
+ const selectedEngine = useEngineStore((s) => s.selectedEngine);
7224
+ const effectiveEngineId = selectedEngine || usePackageSettingsStore.getState().settings?.defaultModel || "bandit-core";
7225
+ const currentEngine = engines.find((e) => e.id === effectiveEngineId);
7226
+ const engineLabel = currentEngine?.displayName?.replace(/^Bandit /, "") || "Engine";
7227
+ useEffect11(() => {
7228
+ useEngineStore.getState().fetchEngines();
7229
+ }, []);
6834
7230
  const pendingModelAvatar = useModelStore.getState().availableModels.find((m) => m.name === pendingModel)?.avatarBase64 || modelAvatars[pendingModel || ""] || banditHead;
6835
7231
  const resolvedHomeUrl = preferences.homeUrl?.trim() || packageSettings?.homeUrl?.trim() || "";
6836
7232
  const homeTooltip = (() => {
@@ -7143,6 +7539,84 @@ var ChatAppBar = ({
7143
7539
  )
7144
7540
  }
7145
7541
  ) }),
7542
+ /* @__PURE__ */ jsx13(Tooltip4, { title: `Engine: ${currentEngine?.displayName ?? effectiveEngineId}`, arrow: true, children: /* @__PURE__ */ jsxs10(
7543
+ IconButton9,
7544
+ {
7545
+ onClick: (e) => setEngineAnchorEl(e.currentTarget),
7546
+ sx: pillButtonStyles,
7547
+ "aria-label": `Change base model (engine). Currently ${effectiveEngineId}`,
7548
+ children: [
7549
+ currentEngine?.cloud ? /* @__PURE__ */ jsx13(CloudDoneIcon, { fontSize: "small" }) : /* @__PURE__ */ jsx13(CloudOffIcon, { fontSize: "small" }),
7550
+ /* @__PURE__ */ jsx13(Typography8, { variant: "caption", sx: { ml: 0.75, fontWeight: 600, whiteSpace: "nowrap" }, children: engineLabel })
7551
+ ]
7552
+ }
7553
+ ) }),
7554
+ /* @__PURE__ */ jsxs10(
7555
+ Menu5,
7556
+ {
7557
+ anchorEl: engineAnchorEl,
7558
+ open: Boolean(engineAnchorEl),
7559
+ onClose: () => setEngineAnchorEl(null),
7560
+ transformOrigin: { horizontal: "right", vertical: "top" },
7561
+ anchorOrigin: { horizontal: "right", vertical: "bottom" },
7562
+ children: [
7563
+ /* @__PURE__ */ jsx13(Typography8, { variant: "overline", sx: { px: 2, color: theme.palette.text.secondary }, children: "Engine \xB7 base model" }),
7564
+ engines.length === 0 && /* @__PURE__ */ jsx13(MenuItem5, { disabled: true, children: /* @__PURE__ */ jsx13(Typography8, { variant: "body2", children: "No engines available" }) }),
7565
+ engines.map((engine) => {
7566
+ const badges = [
7567
+ engine.vision && "vision",
7568
+ engine.tools && "tools",
7569
+ engine.thinking && "thinking",
7570
+ engine.cloud && "cloud"
7571
+ ].filter(Boolean);
7572
+ return /* @__PURE__ */ jsxs10(
7573
+ MenuItem5,
7574
+ {
7575
+ selected: engine.id === effectiveEngineId,
7576
+ disabled: !engine.available,
7577
+ onClick: () => {
7578
+ useEngineStore.getState().setSelectedEngine(engine.id);
7579
+ setEngineAnchorEl(null);
7580
+ },
7581
+ sx: {
7582
+ display: "flex",
7583
+ flexDirection: "column",
7584
+ alignItems: "flex-start",
7585
+ gap: 0.5,
7586
+ py: 1,
7587
+ px: 2,
7588
+ maxWidth: 360,
7589
+ whiteSpace: "normal"
7590
+ },
7591
+ children: [
7592
+ /* @__PURE__ */ jsxs10(Box10, { sx: { display: "flex", alignItems: "center", gap: 1, width: "100%" }, children: [
7593
+ /* @__PURE__ */ jsx13(Typography8, { variant: "body2", sx: { fontWeight: 600, flex: 1 }, children: engine.displayName }),
7594
+ engine.id === effectiveEngineId && /* @__PURE__ */ jsx13(Box10, { sx: { width: 8, height: 8, borderRadius: "50%", bgcolor: theme.palette.primary.main } })
7595
+ ] }),
7596
+ /* @__PURE__ */ jsx13(Typography8, { variant: "caption", sx: { color: theme.palette.text.secondary }, children: engine.available ? engine.description : engine.unavailableReason || "Unavailable" }),
7597
+ badges.length > 0 && /* @__PURE__ */ jsx13(Box10, { sx: { display: "flex", gap: 0.5, flexWrap: "wrap", mt: 0.25 }, children: badges.map((b) => /* @__PURE__ */ jsx13(
7598
+ Box10,
7599
+ {
7600
+ sx: {
7601
+ fontSize: "0.65rem",
7602
+ px: 0.75,
7603
+ py: 0.1,
7604
+ borderRadius: 1,
7605
+ bgcolor: theme.palette.primary.main + "22",
7606
+ color: theme.palette.primary.main
7607
+ },
7608
+ children: b
7609
+ },
7610
+ b
7611
+ )) })
7612
+ ]
7613
+ },
7614
+ engine.id
7615
+ );
7616
+ })
7617
+ ]
7618
+ }
7619
+ ),
7146
7620
  /* @__PURE__ */ jsx13(
7147
7621
  Menu5,
7148
7622
  {
@@ -9248,4 +9722,4 @@ var chat_default = Chat;
9248
9722
  export {
9249
9723
  chat_default
9250
9724
  };
9251
- //# sourceMappingURL=chunk-V5QRXIIO.mjs.map
9725
+ //# sourceMappingURL=chunk-G7U2FNUK.mjs.map