@burtson-labs/bandit-engine 2.0.58 → 2.0.60

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-NLBCURUN.mjs +16 -0
  2. package/dist/chat-provider.js.map +1 -1
  3. package/dist/chat-provider.mjs +5 -5
  4. package/dist/{chunk-VU5N57QZ.mjs → chunk-3AWAL2YH.mjs} +9 -9
  5. package/dist/{chunk-KNBWR4DS.mjs → chunk-5WQMMCZQ.mjs} +3 -3
  6. package/dist/{chunk-QPBG6JQE.mjs → chunk-6QTTNYF2.mjs} +2 -2
  7. package/dist/{chunk-POTQI33D.mjs → chunk-D55E6ZDV.mjs} +5 -5
  8. package/dist/{chunk-557E5VZ2.mjs → chunk-EUBVBTB3.mjs} +2 -2
  9. package/dist/{chunk-7ZDS33S2.mjs → chunk-IPMTNREZ.mjs} +2 -2
  10. package/dist/{chunk-7ZDS33S2.mjs.map → chunk-IPMTNREZ.mjs.map} +1 -1
  11. package/dist/{chunk-N7GCS2BH.mjs → chunk-MFDMM5MS.mjs} +388 -22
  12. package/dist/chunk-MFDMM5MS.mjs.map +1 -0
  13. package/dist/{chunk-WL7NV4WJ.mjs → chunk-PY7A3J5T.mjs} +4 -4
  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 +405 -17
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +10 -10
  22. package/dist/management/management.js +405 -17
  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-MXC6O7M5.mjs +0 -16
  34. package/dist/chunk-KM7FUWCM.mjs.map +0 -1
  35. package/dist/chunk-N7GCS2BH.mjs.map +0 -1
  36. /package/dist/{chat-MXC6O7M5.mjs.map → chat-NLBCURUN.mjs.map} +0 -0
  37. /package/dist/{chunk-VU5N57QZ.mjs.map → chunk-3AWAL2YH.mjs.map} +0 -0
  38. /package/dist/{chunk-KNBWR4DS.mjs.map → chunk-5WQMMCZQ.mjs.map} +0 -0
  39. /package/dist/{chunk-QPBG6JQE.mjs.map → chunk-6QTTNYF2.mjs.map} +0 -0
  40. /package/dist/{chunk-POTQI33D.mjs.map → chunk-D55E6ZDV.mjs.map} +0 -0
  41. /package/dist/{chunk-557E5VZ2.mjs.map → chunk-EUBVBTB3.mjs.map} +0 -0
  42. /package/dist/{chunk-WL7NV4WJ.mjs.map → chunk-PY7A3J5T.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,313 @@ 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
+
1391
1698
  // src/chat/hooks/useMemoryEnhancer.tsx
1392
1699
  import { lastValueFrom, map as map2 } from "rxjs";
1393
1700
  var MEMORY_LIMIT = 100;
@@ -2021,7 +2328,14 @@ var executeMCPTool = async (toolCall) => {
2021
2328
  requestOptions.body = JSON.stringify(toolCall.parameters);
2022
2329
  }
2023
2330
  }
2024
- const response = await fetch(url, requestOptions);
2331
+ const controller = new AbortController();
2332
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
2333
+ let response;
2334
+ try {
2335
+ response = await fetch(url, { ...requestOptions, signal: controller.signal });
2336
+ } finally {
2337
+ clearTimeout(timeoutId);
2338
+ }
2025
2339
  const data = await response.json();
2026
2340
  if (!response.ok) {
2027
2341
  debugLogger.error("MCP tool execution failed", {
@@ -2828,6 +3142,9 @@ ${protocol}`;
2828
3142
  setStreamBuffer(latestDisplayMessage);
2829
3143
  }, delay);
2830
3144
  };
3145
+ syncTelemetry();
3146
+ telemetryStartTurn(question, modelName);
3147
+ telemetryEvent("tool_loop:llm_start");
2831
3148
  const stream = provider.chat(request);
2832
3149
  const initialPlaceholderQuestion = lastEntry?.question;
2833
3150
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -2841,7 +3158,10 @@ ${protocol}`;
2841
3158
  const sub = stream.subscribe({
2842
3159
  next: (data) => {
2843
3160
  if (!data?.message?.content && !data?.message?.tool_calls) return;
2844
- if (data.message.content) fullMessage += data.message.content;
3161
+ if (data.message.content) {
3162
+ fullMessage += data.message.content;
3163
+ telemetryEvent("tool_loop:llm_chunk", { chunk: data.message.content });
3164
+ }
2845
3165
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
2846
3166
  setIsThinking?.(inThinkBlock);
2847
3167
  const visibleMessage = stripThinking(fullMessage);
@@ -2873,6 +3193,7 @@ ${protocol}`;
2873
3193
  setIsThinking?.(false);
2874
3194
  setPendingMessage(null);
2875
3195
  setLogoVisible(false);
3196
+ telemetryEndTurn({ error: err?.message || "stream error" });
2876
3197
  if (onError) {
2877
3198
  onError(err);
2878
3199
  }
@@ -2880,6 +3201,7 @@ ${protocol}`;
2880
3201
  complete: async () => {
2881
3202
  try {
2882
3203
  setIsThinking?.(false);
3204
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
2883
3205
  latestDisplayMessage = stripThinking(fullMessage);
2884
3206
  if (!sawToolBlock) {
2885
3207
  flushNow();
@@ -2888,6 +3210,7 @@ ${protocol}`;
2888
3210
  let enhancedMessage = fullMessage;
2889
3211
  const summarizableResults = [];
2890
3212
  const inlineImageBlocks = [];
3213
+ const collectedSources = [];
2891
3214
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
2892
3215
  debugLogger.info("Detected tool calls in AI response", {
2893
3216
  toolCallCount: toolCallMatches.length,
@@ -2922,10 +3245,21 @@ ${protocol}`;
2922
3245
  });
2923
3246
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
2924
3247
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
3248
+ clearFlushTimer();
3249
+ 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";
3250
+ const toolPreamble = stripToolBlocks(fullMessage).trim();
3251
+ setStreamBuffer(toolPreamble ? `${toolPreamble}
3252
+
3253
+ _${toolStatus}_` : `_${toolStatus}_`);
3254
+ telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
2925
3255
  const result = await executeMCPTool({
2926
3256
  toolName: functionName,
2927
3257
  parameters: parsedParams
2928
3258
  });
3259
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
3260
+ name: functionName,
3261
+ isError: !result.success
3262
+ });
2929
3263
  let resultText = "";
2930
3264
  if (result.success) {
2931
3265
  if (functionName === "web_search" || functionName === "web-search") {
@@ -2939,18 +3273,16 @@ ${protocol}`;
2939
3273
  blocks.push(
2940
3274
  items.slice(0, 6).map((item, index) => {
2941
3275
  const title = item.title?.trim() || "Untitled";
2942
- const url = item.url?.trim();
3276
+ const url = item.url?.trim() || "";
2943
3277
  const snippet = item.content?.trim();
2944
- let line = `${index + 1}. **${title}**`;
2945
- if (url) line += `
2946
- ${url}`;
3278
+ if (url) collectedSources.push({ title, url });
3279
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
2947
3280
  if (snippet) {
2948
3281
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
2949
- line += `
2950
- ${truncated}`;
3282
+ line += ` \u2014 ${truncated}`;
2951
3283
  }
2952
3284
  return line;
2953
- }).join("\n\n")
3285
+ }).join("\n")
2954
3286
  );
2955
3287
  }
2956
3288
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -3032,7 +3364,7 @@ ${r.output}`).join("\n\n");
3032
3364
 
3033
3365
  ${toolResultsText}
3034
3366
 
3035
- 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.`
3367
+ 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.`
3036
3368
  }
3037
3369
  ];
3038
3370
  const summaryRequest = {
@@ -3042,9 +3374,22 @@ Using these results together with your own knowledge, answer my original questio
3042
3374
  options: { num_predict: tokenLimit + 250 }
3043
3375
  };
3044
3376
  clearFlushTimer();
3045
- setStreamBuffer("");
3377
+ const summaryPreamble = stripToolBlocks(fullMessage).trim();
3378
+ setStreamBuffer(
3379
+ summaryPreamble ? `${summaryPreamble}
3380
+
3381
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
3382
+ );
3046
3383
  const summaryText = await new Promise((resolve) => {
3047
3384
  let acc = "";
3385
+ let settled = false;
3386
+ let timer;
3387
+ const done = (value) => {
3388
+ if (settled) return;
3389
+ settled = true;
3390
+ if (timer) clearTimeout(timer);
3391
+ resolve(value);
3392
+ };
3048
3393
  const summarySub = provider.chat(summaryRequest).subscribe({
3049
3394
  next: (data) => {
3050
3395
  if (data?.message?.content) {
@@ -3059,14 +3404,33 @@ Using these results together with your own knowledge, answer my original questio
3059
3404
  debugLogger.error("Summarization pass failed", {
3060
3405
  error: summaryErr instanceof Error ? summaryErr.message : String(summaryErr)
3061
3406
  });
3062
- resolve("");
3407
+ done("");
3063
3408
  },
3064
- complete: () => resolve(stripThinking(acc).trim())
3409
+ complete: () => done(stripThinking(acc).trim())
3065
3410
  });
3066
3411
  currentSubRef.current = summarySub;
3412
+ timer = setTimeout(() => {
3413
+ debugLogger.warn("Summarization pass timed out; using inline tool output");
3414
+ try {
3415
+ summarySub.unsubscribe();
3416
+ } catch {
3417
+ }
3418
+ done("");
3419
+ }, 3e4);
3067
3420
  });
3068
3421
  if (summaryText.trim()) {
3069
- enhancedMessage = summaryText + (inlineImageBlocks.length ? `
3422
+ const sourcesMd = collectedSources.length ? `
3423
+
3424
+ **Sources**
3425
+ ${collectedSources.slice(0, 6).map((s) => {
3426
+ let domain = s.url;
3427
+ try {
3428
+ domain = new URL(s.url).hostname.replace(/^www\./, "");
3429
+ } catch {
3430
+ }
3431
+ return `- [${s.title || domain}](${s.url}) \u2014 ${domain}`;
3432
+ }).join("\n")}` : "";
3433
+ enhancedMessage = summaryText + sourcesMd + (inlineImageBlocks.length ? `
3070
3434
 
3071
3435
  ${inlineImageBlocks.join("\n\n")}` : "");
3072
3436
  }
@@ -3115,6 +3479,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
3115
3479
  }
3116
3480
  setInputValue("");
3117
3481
  setPastedImages([]);
3482
+ telemetryEndTurn();
3118
3483
  setTimeout(() => {
3119
3484
  clearFlushTimer();
3120
3485
  setPendingMessage(null);
@@ -3130,6 +3495,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
3130
3495
  overrideComponentStatus("Idle");
3131
3496
  setIsSubmitting(false);
3132
3497
  setIsStreaming(false);
3498
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
3133
3499
  }
3134
3500
  }
3135
3501
  });
@@ -6031,10 +6397,10 @@ var EnhancedMobileConversationsModal = ({
6031
6397
  useEffect10(() => {
6032
6398
  setDeletedConversationIds((prev) => {
6033
6399
  let changed = false;
6034
- const active = new Set(conversations.map((conv) => conv.id));
6400
+ const active2 = new Set(conversations.map((conv) => conv.id));
6035
6401
  const next = /* @__PURE__ */ new Set();
6036
6402
  prev.forEach((id) => {
6037
- if (active.has(id)) {
6403
+ if (active2.has(id)) {
6038
6404
  next.add(id);
6039
6405
  } else {
6040
6406
  changed = true;
@@ -9220,4 +9586,4 @@ var chat_default = Chat;
9220
9586
  export {
9221
9587
  chat_default
9222
9588
  };
9223
- //# sourceMappingURL=chunk-N7GCS2BH.mjs.map
9589
+ //# sourceMappingURL=chunk-MFDMM5MS.mjs.map