@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
package/dist/index.mjs CHANGED
@@ -1,25 +1,25 @@
1
1
  import {
2
2
  chat_default
3
- } from "./chunk-N7GCS2BH.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-VU5N57QZ.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({
@@ -22009,7 +22337,14 @@ var init_mcpService = __esm({
22009
22337
  requestOptions.body = JSON.stringify(toolCall.parameters);
22010
22338
  }
22011
22339
  }
22012
- const response = await fetch(url, requestOptions);
22340
+ const controller = new AbortController();
22341
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
22342
+ let response;
22343
+ try {
22344
+ response = await fetch(url, { ...requestOptions, signal: controller.signal });
22345
+ } finally {
22346
+ clearTimeout(timeoutId);
22347
+ }
22013
22348
  const data = await response.json();
22014
22349
  if (!response.ok) {
22015
22350
  debugLogger.error("MCP tool execution failed", {
@@ -22080,6 +22415,7 @@ var init_useAIProvider = __esm({
22080
22415
  import_react44 = require("react");
22081
22416
  init_knowledgeStore();
22082
22417
  init_aiProviderStore();
22418
+ init_telemetry();
22083
22419
  init_conversationStore();
22084
22420
  init_useMemoryEnhancer();
22085
22421
  init_useVectorStore();
@@ -22844,6 +23180,9 @@ ${protocol}`;
22844
23180
  setStreamBuffer(latestDisplayMessage);
22845
23181
  }, delay);
22846
23182
  };
23183
+ syncTelemetry();
23184
+ telemetryStartTurn(question, modelName);
23185
+ telemetryEvent("tool_loop:llm_start");
22847
23186
  const stream = provider.chat(request);
22848
23187
  const initialPlaceholderQuestion = lastEntry?.question;
22849
23188
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -22857,7 +23196,10 @@ ${protocol}`;
22857
23196
  const sub = stream.subscribe({
22858
23197
  next: (data) => {
22859
23198
  if (!data?.message?.content && !data?.message?.tool_calls) return;
22860
- 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
+ }
22861
23203
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
22862
23204
  setIsThinking?.(inThinkBlock);
22863
23205
  const visibleMessage = stripThinking(fullMessage);
@@ -22889,6 +23231,7 @@ ${protocol}`;
22889
23231
  setIsThinking?.(false);
22890
23232
  setPendingMessage(null);
22891
23233
  setLogoVisible(false);
23234
+ telemetryEndTurn({ error: err?.message || "stream error" });
22892
23235
  if (onError) {
22893
23236
  onError(err);
22894
23237
  }
@@ -22896,6 +23239,7 @@ ${protocol}`;
22896
23239
  complete: async () => {
22897
23240
  try {
22898
23241
  setIsThinking?.(false);
23242
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
22899
23243
  latestDisplayMessage = stripThinking(fullMessage);
22900
23244
  if (!sawToolBlock) {
22901
23245
  flushNow();
@@ -22904,6 +23248,7 @@ ${protocol}`;
22904
23248
  let enhancedMessage = fullMessage;
22905
23249
  const summarizableResults = [];
22906
23250
  const inlineImageBlocks = [];
23251
+ const collectedSources = [];
22907
23252
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
22908
23253
  debugLogger.info("Detected tool calls in AI response", {
22909
23254
  toolCallCount: toolCallMatches.length,
@@ -22938,10 +23283,21 @@ ${protocol}`;
22938
23283
  });
22939
23284
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
22940
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 });
22941
23293
  const result = await executeMCPTool({
22942
23294
  toolName: functionName,
22943
23295
  parameters: parsedParams
22944
23296
  });
23297
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
23298
+ name: functionName,
23299
+ isError: !result.success
23300
+ });
22945
23301
  let resultText = "";
22946
23302
  if (result.success) {
22947
23303
  if (functionName === "web_search" || functionName === "web-search") {
@@ -22955,18 +23311,16 @@ ${protocol}`;
22955
23311
  blocks.push(
22956
23312
  items.slice(0, 6).map((item, index) => {
22957
23313
  const title = item.title?.trim() || "Untitled";
22958
- const url = item.url?.trim();
23314
+ const url = item.url?.trim() || "";
22959
23315
  const snippet = item.content?.trim();
22960
- let line = `${index + 1}. **${title}**`;
22961
- if (url) line += `
22962
- ${url}`;
23316
+ if (url) collectedSources.push({ title, url });
23317
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
22963
23318
  if (snippet) {
22964
23319
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
22965
- line += `
22966
- ${truncated}`;
23320
+ line += ` \u2014 ${truncated}`;
22967
23321
  }
22968
23322
  return line;
22969
- }).join("\n\n")
23323
+ }).join("\n")
22970
23324
  );
22971
23325
  }
22972
23326
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -23048,7 +23402,7 @@ ${r.output}`).join("\n\n");
23048
23402
 
23049
23403
  ${toolResultsText}
23050
23404
 
23051
- 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.`
23052
23406
  }
23053
23407
  ];
23054
23408
  const summaryRequest = {
@@ -23058,9 +23412,22 @@ Using these results together with your own knowledge, answer my original questio
23058
23412
  options: { num_predict: tokenLimit + 250 }
23059
23413
  };
23060
23414
  clearFlushTimer();
23061
- setStreamBuffer("");
23415
+ const summaryPreamble = stripToolBlocks(fullMessage).trim();
23416
+ setStreamBuffer(
23417
+ summaryPreamble ? `${summaryPreamble}
23418
+
23419
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
23420
+ );
23062
23421
  const summaryText = await new Promise((resolve) => {
23063
23422
  let acc = "";
23423
+ let settled = false;
23424
+ let timer;
23425
+ const done = (value) => {
23426
+ if (settled) return;
23427
+ settled = true;
23428
+ if (timer) clearTimeout(timer);
23429
+ resolve(value);
23430
+ };
23064
23431
  const summarySub = provider.chat(summaryRequest).subscribe({
23065
23432
  next: (data) => {
23066
23433
  if (data?.message?.content) {
@@ -23075,14 +23442,33 @@ Using these results together with your own knowledge, answer my original questio
23075
23442
  debugLogger.error("Summarization pass failed", {
23076
23443
  error: summaryErr instanceof Error ? summaryErr.message : String(summaryErr)
23077
23444
  });
23078
- resolve("");
23445
+ done("");
23079
23446
  },
23080
- complete: () => resolve(stripThinking(acc).trim())
23447
+ complete: () => done(stripThinking(acc).trim())
23081
23448
  });
23082
23449
  currentSubRef.current = summarySub;
23450
+ timer = setTimeout(() => {
23451
+ debugLogger.warn("Summarization pass timed out; using inline tool output");
23452
+ try {
23453
+ summarySub.unsubscribe();
23454
+ } catch {
23455
+ }
23456
+ done("");
23457
+ }, 3e4);
23083
23458
  });
23084
23459
  if (summaryText.trim()) {
23085
- 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 ? `
23086
23472
 
23087
23473
  ${inlineImageBlocks.join("\n\n")}` : "");
23088
23474
  }
@@ -23131,6 +23517,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
23131
23517
  }
23132
23518
  setInputValue("");
23133
23519
  setPastedImages([]);
23520
+ telemetryEndTurn();
23134
23521
  setTimeout(() => {
23135
23522
  clearFlushTimer();
23136
23523
  setPendingMessage(null);
@@ -23146,6 +23533,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
23146
23533
  overrideComponentStatus("Idle");
23147
23534
  setIsSubmitting(false);
23148
23535
  setIsStreaming(false);
23536
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
23149
23537
  }
23150
23538
  }
23151
23539
  });
@@ -26004,10 +26392,10 @@ var init_enhanced_mobile_conversations_modal = __esm({
26004
26392
  (0, import_react52.useEffect)(() => {
26005
26393
  setDeletedConversationIds((prev) => {
26006
26394
  let changed = false;
26007
- const active = new Set(conversations.map((conv) => conv.id));
26395
+ const active2 = new Set(conversations.map((conv) => conv.id));
26008
26396
  const next = /* @__PURE__ */ new Set();
26009
26397
  prev.forEach((id) => {
26010
- if (active.has(id)) {
26398
+ if (active2.has(id)) {
26011
26399
  next.add(id);
26012
26400
  } else {
26013
26401
  changed = true;