@burtson-labs/bandit-engine 2.0.59 → 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-URKUD3OL.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-V5QRXIIO.mjs → chunk-MFDMM5MS.mjs} +357 -19
  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 +374 -14
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +10 -10
  22. package/dist/management/management.js +374 -14
  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-NLBCURUN.mjs.map} +0 -0
  37. /package/dist/{chunk-URKUD3OL.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
package/dist/index.mjs CHANGED
@@ -1,25 +1,25 @@
1
1
  import {
2
2
  chat_default
3
- } from "./chunk-V5QRXIIO.mjs";
3
+ } from "./chunk-MFDMM5MS.mjs";
4
4
  import {
5
5
  chat_provider_default
6
- } from "./chunk-POTQI33D.mjs";
6
+ } from "./chunk-D55E6ZDV.mjs";
7
7
  import "./chunk-ONQMRE2G.mjs";
8
8
  import {
9
9
  management_default,
10
10
  useGatewayHealth,
11
11
  useGatewayMemory,
12
12
  useGatewayModels
13
- } from "./chunk-URKUD3OL.mjs";
14
- import "./chunk-KM7FUWCM.mjs";
15
- import "./chunk-UFSEYVRS.mjs";
16
- import "./chunk-QPBG6JQE.mjs";
13
+ } from "./chunk-3AWAL2YH.mjs";
14
+ import "./chunk-SRCCNBHF.mjs";
15
+ import "./chunk-VTC6AIWY.mjs";
16
+ import "./chunk-6QTTNYF2.mjs";
17
17
  import {
18
18
  defineCustomElement
19
19
  } from "./chunk-IXIM7BNO.mjs";
20
20
  import {
21
21
  chat_modal_default
22
- } from "./chunk-WL7NV4WJ.mjs";
22
+ } from "./chunk-PY7A3J5T.mjs";
23
23
  import {
24
24
  FeedbackButton,
25
25
  FeedbackModal,
@@ -37,7 +37,7 @@ import {
37
37
  useTTS,
38
38
  useVoiceStore,
39
39
  voiceService
40
- } from "./chunk-KNBWR4DS.mjs";
40
+ } from "./chunk-5WQMMCZQ.mjs";
41
41
  import {
42
42
  DEFAULT_TIER_FEATURES,
43
43
  FeatureFlagContext,
@@ -57,10 +57,10 @@ import {
57
57
  useVectorStore,
58
58
  vectorDatabaseService,
59
59
  vectorMigrationService
60
- } from "./chunk-557E5VZ2.mjs";
60
+ } from "./chunk-EUBVBTB3.mjs";
61
61
  import {
62
62
  usePackageSettingsStore
63
- } from "./chunk-7ZDS33S2.mjs";
63
+ } from "./chunk-IPMTNREZ.mjs";
64
64
  import "./chunk-H3BYFEIE.mjs";
65
65
  import {
66
66
  DebugLogger,
@@ -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,330 @@ ${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
+
21349
21677
  // src/chat/hooks/useMemoryEnhancer.tsx
21350
21678
  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
21679
  var init_useMemoryEnhancer = __esm({
@@ -22087,6 +22415,7 @@ var init_useAIProvider = __esm({
22087
22415
  import_react44 = require("react");
22088
22416
  init_knowledgeStore();
22089
22417
  init_aiProviderStore();
22418
+ init_telemetry();
22090
22419
  init_conversationStore();
22091
22420
  init_useMemoryEnhancer();
22092
22421
  init_useVectorStore();
@@ -22851,6 +23180,9 @@ ${protocol}`;
22851
23180
  setStreamBuffer(latestDisplayMessage);
22852
23181
  }, delay);
22853
23182
  };
23183
+ syncTelemetry();
23184
+ telemetryStartTurn(question, modelName);
23185
+ telemetryEvent("tool_loop:llm_start");
22854
23186
  const stream = provider.chat(request);
22855
23187
  const initialPlaceholderQuestion = lastEntry?.question;
22856
23188
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -22864,7 +23196,10 @@ ${protocol}`;
22864
23196
  const sub = stream.subscribe({
22865
23197
  next: (data) => {
22866
23198
  if (!data?.message?.content && !data?.message?.tool_calls) return;
22867
- if (data.message.content) fullMessage += data.message.content;
23199
+ if (data.message.content) {
23200
+ fullMessage += data.message.content;
23201
+ telemetryEvent("tool_loop:llm_chunk", { chunk: data.message.content });
23202
+ }
22868
23203
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
22869
23204
  setIsThinking?.(inThinkBlock);
22870
23205
  const visibleMessage = stripThinking(fullMessage);
@@ -22896,6 +23231,7 @@ ${protocol}`;
22896
23231
  setIsThinking?.(false);
22897
23232
  setPendingMessage(null);
22898
23233
  setLogoVisible(false);
23234
+ telemetryEndTurn({ error: err?.message || "stream error" });
22899
23235
  if (onError) {
22900
23236
  onError(err);
22901
23237
  }
@@ -22903,6 +23239,7 @@ ${protocol}`;
22903
23239
  complete: async () => {
22904
23240
  try {
22905
23241
  setIsThinking?.(false);
23242
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
22906
23243
  latestDisplayMessage = stripThinking(fullMessage);
22907
23244
  if (!sawToolBlock) {
22908
23245
  flushNow();
@@ -22911,6 +23248,7 @@ ${protocol}`;
22911
23248
  let enhancedMessage = fullMessage;
22912
23249
  const summarizableResults = [];
22913
23250
  const inlineImageBlocks = [];
23251
+ const collectedSources = [];
22914
23252
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
22915
23253
  debugLogger.info("Detected tool calls in AI response", {
22916
23254
  toolCallCount: toolCallMatches.length,
@@ -22945,10 +23283,21 @@ ${protocol}`;
22945
23283
  });
22946
23284
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
22947
23285
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
23286
+ clearFlushTimer();
23287
+ 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";
23288
+ const toolPreamble = stripToolBlocks(fullMessage).trim();
23289
+ setStreamBuffer(toolPreamble ? `${toolPreamble}
23290
+
23291
+ _${toolStatus}_` : `_${toolStatus}_`);
23292
+ telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
22948
23293
  const result = await executeMCPTool({
22949
23294
  toolName: functionName,
22950
23295
  parameters: parsedParams
22951
23296
  });
23297
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
23298
+ name: functionName,
23299
+ isError: !result.success
23300
+ });
22952
23301
  let resultText = "";
22953
23302
  if (result.success) {
22954
23303
  if (functionName === "web_search" || functionName === "web-search") {
@@ -22962,18 +23311,16 @@ ${protocol}`;
22962
23311
  blocks.push(
22963
23312
  items.slice(0, 6).map((item, index) => {
22964
23313
  const title = item.title?.trim() || "Untitled";
22965
- const url = item.url?.trim();
23314
+ const url = item.url?.trim() || "";
22966
23315
  const snippet = item.content?.trim();
22967
- let line = `${index + 1}. **${title}**`;
22968
- if (url) line += `
22969
- ${url}`;
23316
+ if (url) collectedSources.push({ title, url });
23317
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
22970
23318
  if (snippet) {
22971
23319
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
22972
- line += `
22973
- ${truncated}`;
23320
+ line += ` \u2014 ${truncated}`;
22974
23321
  }
22975
23322
  return line;
22976
- }).join("\n\n")
23323
+ }).join("\n")
22977
23324
  );
22978
23325
  }
22979
23326
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -23055,7 +23402,7 @@ ${r.output}`).join("\n\n");
23055
23402
 
23056
23403
  ${toolResultsText}
23057
23404
 
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.`
23405
+ 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
23406
  }
23060
23407
  ];
23061
23408
  const summaryRequest = {
@@ -23069,7 +23416,7 @@ Using these results together with your own knowledge, answer my original questio
23069
23416
  setStreamBuffer(
23070
23417
  summaryPreamble ? `${summaryPreamble}
23071
23418
 
23072
- _Working on it\u2026_` : "_Working on it\u2026_"
23419
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
23073
23420
  );
23074
23421
  const summaryText = await new Promise((resolve) => {
23075
23422
  let acc = "";
@@ -23110,7 +23457,18 @@ _Working on it\u2026_` : "_Working on it\u2026_"
23110
23457
  }, 3e4);
23111
23458
  });
23112
23459
  if (summaryText.trim()) {
23113
- enhancedMessage = summaryText + (inlineImageBlocks.length ? `
23460
+ const sourcesMd = collectedSources.length ? `
23461
+
23462
+ **Sources**
23463
+ ${collectedSources.slice(0, 6).map((s) => {
23464
+ let domain = s.url;
23465
+ try {
23466
+ domain = new URL(s.url).hostname.replace(/^www\./, "");
23467
+ } catch {
23468
+ }
23469
+ return `- [${s.title || domain}](${s.url}) \u2014 ${domain}`;
23470
+ }).join("\n")}` : "";
23471
+ enhancedMessage = summaryText + sourcesMd + (inlineImageBlocks.length ? `
23114
23472
 
23115
23473
  ${inlineImageBlocks.join("\n\n")}` : "");
23116
23474
  }
@@ -23159,6 +23517,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
23159
23517
  }
23160
23518
  setInputValue("");
23161
23519
  setPastedImages([]);
23520
+ telemetryEndTurn();
23162
23521
  setTimeout(() => {
23163
23522
  clearFlushTimer();
23164
23523
  setPendingMessage(null);
@@ -23174,6 +23533,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
23174
23533
  overrideComponentStatus("Idle");
23175
23534
  setIsSubmitting(false);
23176
23535
  setIsStreaming(false);
23536
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
23177
23537
  }
23178
23538
  }
23179
23539
  });
@@ -26032,10 +26392,10 @@ var init_enhanced_mobile_conversations_modal = __esm({
26032
26392
  (0, import_react52.useEffect)(() => {
26033
26393
  setDeletedConversationIds((prev) => {
26034
26394
  let changed = false;
26035
- const active = new Set(conversations.map((conv) => conv.id));
26395
+ const active2 = new Set(conversations.map((conv) => conv.id));
26036
26396
  const next = /* @__PURE__ */ new Set();
26037
26397
  prev.forEach((id) => {
26038
- if (active.has(id)) {
26398
+ if (active2.has(id)) {
26039
26399
  next.add(id);
26040
26400
  } else {
26041
26401
  changed = true;