@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.js CHANGED
@@ -14862,7 +14862,11 @@ ${listMarkdown}`;
14862
14862
  href,
14863
14863
  target: "_blank",
14864
14864
  rel: "noopener noreferrer",
14865
- style: { color: theme.palette.primary.main, textDecoration: "underline" },
14865
+ style: {
14866
+ color: theme.palette.info?.main ?? theme.palette.primary.main,
14867
+ textDecoration: "underline",
14868
+ textUnderlineOffset: "2px"
14869
+ },
14866
14870
  ...props,
14867
14871
  children
14868
14872
  }
@@ -19107,6 +19111,330 @@ ${sanitize(
19107
19111
  }
19108
19112
  });
19109
19113
 
19114
+ // src/services/telemetry/otlpExporter.ts
19115
+ function redactSecretsString(s) {
19116
+ 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]");
19117
+ }
19118
+ function resolveTelemetryConfig(opts) {
19119
+ if (!opts.telemetry?.enabled) return null;
19120
+ const endpoint = (opts.telemetry.endpoint ?? "https://otlp.burtson.ai").replace(/\/+$/, "");
19121
+ const headers = { ...opts.telemetry.headers ?? {} };
19122
+ const hasAuth = Object.keys(headers).some((k) => k.toLowerCase() === "authorization");
19123
+ if (!hasAuth && opts.banditApiKey) {
19124
+ headers["Authorization"] = `Bearer ${opts.banditApiKey}`;
19125
+ }
19126
+ const mode = opts.telemetry.mode ?? "metrics+traces";
19127
+ return { endpoint, headers, mode, serviceName: opts.telemetry.serviceName ?? "bandit-web" };
19128
+ }
19129
+ function toAttrs(rec) {
19130
+ const out = [];
19131
+ for (const [key, v] of Object.entries(rec)) {
19132
+ if (v === void 0 || v === null || v === "") continue;
19133
+ if (typeof v === "boolean") out.push({ key, value: { boolValue: v } });
19134
+ else if (typeof v === "number")
19135
+ out.push({ key, value: Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v } });
19136
+ else out.push({ key, value: { stringValue: v } });
19137
+ }
19138
+ return out;
19139
+ }
19140
+ function hex(bytes) {
19141
+ const arr = new Uint8Array(bytes);
19142
+ if (webCrypto?.getRandomValues) webCrypto.getRandomValues(arr);
19143
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
19144
+ }
19145
+ function histogramPoint(value, bounds, attrs, startMs, endMs) {
19146
+ const counts = new Array(bounds.length + 1).fill(0);
19147
+ let idx = bounds.findIndex((b) => value <= b);
19148
+ if (idx === -1) idx = bounds.length;
19149
+ counts[idx] = 1;
19150
+ return {
19151
+ attributes: toAttrs(attrs),
19152
+ startTimeUnixNano: nano(startMs),
19153
+ timeUnixNano: nano(endMs),
19154
+ count: "1",
19155
+ sum: value,
19156
+ bucketCounts: counts.map(String),
19157
+ explicitBounds: bounds
19158
+ };
19159
+ }
19160
+ function sumPoint(value, attrs, startMs, endMs) {
19161
+ return {
19162
+ attributes: toAttrs(attrs),
19163
+ startTimeUnixNano: nano(startMs),
19164
+ timeUnixNano: nano(endMs),
19165
+ asInt: String(Math.round(value))
19166
+ };
19167
+ }
19168
+ var webCrypto, nano, TTFT_BUCKETS, DURATION_BUCKETS, clip, TelemetryExporter;
19169
+ var init_otlpExporter = __esm({
19170
+ "src/services/telemetry/otlpExporter.ts"() {
19171
+ "use strict";
19172
+ init_debugLogger();
19173
+ webCrypto = globalThis.crypto;
19174
+ nano = (ms) => String(Math.round(ms * 1e6));
19175
+ TTFT_BUCKETS = [0.1, 0.25, 0.5, 1, 2, 5, 10, 30];
19176
+ DURATION_BUCKETS = [0.5, 1, 2, 5, 10, 30, 60, 120, 300];
19177
+ clip = (s, n = 120) => redactSecretsString(s.slice(0, n)).slice(0, n);
19178
+ TelemetryExporter = class {
19179
+ cfg;
19180
+ now;
19181
+ traceId = "";
19182
+ turn = null;
19183
+ llm = null;
19184
+ llmFirstChunkMs = 0;
19185
+ openTools = [];
19186
+ completedSpans = [];
19187
+ model = "";
19188
+ turnChunkChars = 0;
19189
+ turnTokens = 0;
19190
+ ttftSeconds = null;
19191
+ constructor(cfg, opts) {
19192
+ this.cfg = cfg;
19193
+ this.now = opts?.now ?? (() => Date.now());
19194
+ }
19195
+ startTurn(goal, model) {
19196
+ this.traceId = hex(16);
19197
+ this.model = model;
19198
+ this.turnChunkChars = 0;
19199
+ this.turnTokens = 0;
19200
+ this.ttftSeconds = null;
19201
+ this.llm = null;
19202
+ this.llmFirstChunkMs = 0;
19203
+ this.openTools = [];
19204
+ this.completedSpans = [];
19205
+ this.turn = {
19206
+ spanId: hex(8),
19207
+ name: "agent.turn",
19208
+ startMs: this.now(),
19209
+ attrs: { "gen_ai.request.model": model, "bandit.turn.goal": clip(goal, 160) }
19210
+ };
19211
+ }
19212
+ /** Fed from the chat turn lifecycle. Best-effort; swallows bad payloads. */
19213
+ onEvent(type, payload) {
19214
+ if (!this.turn) return;
19215
+ try {
19216
+ const p = payload ?? {};
19217
+ switch (type) {
19218
+ case "tool_loop:llm_start":
19219
+ this.llm = {
19220
+ spanId: hex(8),
19221
+ parentSpanId: this.turn.spanId,
19222
+ name: "llm.generate",
19223
+ startMs: this.now(),
19224
+ attrs: { "gen_ai.request.model": this.model }
19225
+ };
19226
+ this.llmFirstChunkMs = 0;
19227
+ break;
19228
+ case "tool_loop:llm_chunk": {
19229
+ const chunk = typeof p.chunk === "string" ? p.chunk : "";
19230
+ if (this.llm && this.llmFirstChunkMs === 0 && chunk.length > 0) {
19231
+ this.llmFirstChunkMs = this.now();
19232
+ const ttft = (this.llmFirstChunkMs - this.llm.startMs) / 1e3;
19233
+ if (this.ttftSeconds === null) this.ttftSeconds = ttft;
19234
+ this.llm.attrs["bandit.llm.ttft_seconds"] = ttft;
19235
+ }
19236
+ this.turnChunkChars += chunk.length;
19237
+ this.turnTokens = Math.floor(this.turnChunkChars / 4);
19238
+ break;
19239
+ }
19240
+ case "tool_loop:llm_response":
19241
+ if (this.llm) {
19242
+ this.llm.endMs = this.now();
19243
+ if (typeof p.responseLength === "number") this.llm.attrs["bandit.llm.response_chars"] = p.responseLength;
19244
+ this.completedSpans.push(this.llm);
19245
+ this.llm = null;
19246
+ }
19247
+ break;
19248
+ case "tool_loop:tool_execute": {
19249
+ const name = typeof p.name === "string" ? p.name : "tool";
19250
+ const params = p.params ?? {};
19251
+ const primary = params.query ?? params.url ?? params.prompt ?? params.topic ?? "";
19252
+ this.openTools.push({
19253
+ spanId: hex(8),
19254
+ parentSpanId: this.turn.spanId,
19255
+ name: `tool.${name}`,
19256
+ startMs: this.now(),
19257
+ attrs: { "bandit.tool.name": name, "bandit.tool.primary": primary ? clip(primary) : void 0 }
19258
+ });
19259
+ break;
19260
+ }
19261
+ case "tool_loop:tool_result":
19262
+ case "tool_loop:tool_error": {
19263
+ const name = typeof p.name === "string" ? p.name : void 0;
19264
+ const span = this.takeOpenTool(name);
19265
+ if (span) {
19266
+ span.endMs = this.now();
19267
+ if (type === "tool_loop:tool_error" || p.isError === true) span.error = "tool error";
19268
+ this.completedSpans.push(span);
19269
+ }
19270
+ break;
19271
+ }
19272
+ }
19273
+ } catch {
19274
+ }
19275
+ }
19276
+ takeOpenTool(name) {
19277
+ if (name) {
19278
+ for (let i = this.openTools.length - 1; i >= 0; i -= 1) {
19279
+ if (this.openTools[i].name === `tool.${name}`) return this.openTools.splice(i, 1)[0];
19280
+ }
19281
+ }
19282
+ return this.openTools.shift();
19283
+ }
19284
+ /** Close the turn, build OTLP traces + metrics, and flush. Never rejects. */
19285
+ async endTurn(outcome) {
19286
+ const turn = this.turn;
19287
+ if (!turn) return;
19288
+ this.turn = null;
19289
+ const end = this.now();
19290
+ if (this.llm && !this.llm.endMs) {
19291
+ this.llm.endMs = end;
19292
+ this.completedSpans.push(this.llm);
19293
+ this.llm = null;
19294
+ }
19295
+ for (const t of this.openTools.splice(0)) {
19296
+ t.endMs = end;
19297
+ t.error = t.error ?? "incomplete";
19298
+ this.completedSpans.push(t);
19299
+ }
19300
+ turn.endMs = end;
19301
+ if (outcome?.error) turn.error = outcome.error;
19302
+ const traceId = this.traceId;
19303
+ const spans = [turn, ...this.completedSpans];
19304
+ const jobs = [];
19305
+ if (this.cfg.mode === "metrics+traces") jobs.push(this.post("/v1/traces", this.buildTraces(traceId, spans)));
19306
+ jobs.push(this.post("/v1/metrics", this.buildMetrics(turn)));
19307
+ try {
19308
+ await Promise.all(jobs);
19309
+ } catch {
19310
+ }
19311
+ }
19312
+ buildTraces(traceId, spans) {
19313
+ return {
19314
+ resourceSpans: [
19315
+ {
19316
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
19317
+ scopeSpans: [
19318
+ {
19319
+ scope: { name: this.cfg.serviceName },
19320
+ spans: spans.map((s) => ({
19321
+ traceId,
19322
+ spanId: s.spanId,
19323
+ parentSpanId: s.parentSpanId,
19324
+ name: s.name,
19325
+ kind: 1,
19326
+ startTimeUnixNano: nano(s.startMs),
19327
+ endTimeUnixNano: nano(s.endMs ?? s.startMs),
19328
+ attributes: toAttrs(s.attrs),
19329
+ status: s.error ? { code: 2, message: s.error.slice(0, 200) } : { code: 1 }
19330
+ }))
19331
+ }
19332
+ ]
19333
+ }
19334
+ ]
19335
+ };
19336
+ }
19337
+ buildMetrics(turn) {
19338
+ const start = turn.startMs;
19339
+ const end = turn.endMs ?? this.now();
19340
+ const metrics = [];
19341
+ if (this.turnTokens > 0) {
19342
+ metrics.push({
19343
+ name: "bandit.llm.tokens",
19344
+ sum: {
19345
+ aggregationTemporality: 1,
19346
+ isMonotonic: true,
19347
+ dataPoints: [sumPoint(this.turnTokens, { type: "output", "gen_ai.request.model": this.model }, start, end)]
19348
+ }
19349
+ });
19350
+ }
19351
+ if (this.ttftSeconds !== null) {
19352
+ metrics.push({
19353
+ name: "bandit.llm.ttft",
19354
+ unit: "s",
19355
+ histogram: {
19356
+ aggregationTemporality: 1,
19357
+ dataPoints: [histogramPoint(this.ttftSeconds, TTFT_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
19358
+ }
19359
+ });
19360
+ }
19361
+ metrics.push({
19362
+ name: "bandit.turn.duration",
19363
+ unit: "s",
19364
+ histogram: {
19365
+ aggregationTemporality: 1,
19366
+ dataPoints: [histogramPoint((end - start) / 1e3, DURATION_BUCKETS, { "gen_ai.request.model": this.model }, start, end)]
19367
+ }
19368
+ });
19369
+ return {
19370
+ resourceMetrics: [
19371
+ {
19372
+ resource: { attributes: toAttrs({ "service.name": this.cfg.serviceName }) },
19373
+ scopeMetrics: [{ scope: { name: this.cfg.serviceName }, metrics }]
19374
+ }
19375
+ ]
19376
+ };
19377
+ }
19378
+ async post(path, body) {
19379
+ const doFetch = globalThis.fetch;
19380
+ if (!doFetch) return;
19381
+ const ctrl = new AbortController();
19382
+ const timer = setTimeout(() => ctrl.abort(), 4e3);
19383
+ try {
19384
+ await doFetch(`${this.cfg.endpoint}${path}`, {
19385
+ method: "POST",
19386
+ headers: { "Content-Type": "application/json", ...this.cfg.headers },
19387
+ body: JSON.stringify(body),
19388
+ signal: ctrl.signal
19389
+ });
19390
+ } catch (e) {
19391
+ debugLogger.debug("[telemetry] OTLP post failed", {
19392
+ path,
19393
+ error: e instanceof Error ? e.message : String(e)
19394
+ });
19395
+ } finally {
19396
+ clearTimeout(timer);
19397
+ }
19398
+ }
19399
+ };
19400
+ }
19401
+ });
19402
+
19403
+ // src/services/telemetry/index.ts
19404
+ function syncTelemetry() {
19405
+ try {
19406
+ const settings = usePackageSettingsStore.getState().settings;
19407
+ const cfg = resolveTelemetryConfig({
19408
+ telemetry: settings?.telemetry,
19409
+ banditApiKey: authenticationService.getToken() ?? void 0
19410
+ });
19411
+ active = cfg ? new TelemetryExporter(cfg) : null;
19412
+ } catch {
19413
+ active = null;
19414
+ }
19415
+ return active !== null;
19416
+ }
19417
+ function telemetryStartTurn(goal, model) {
19418
+ active?.startTurn(goal, model);
19419
+ }
19420
+ function telemetryEvent(type, payload) {
19421
+ active?.onEvent(type, payload);
19422
+ }
19423
+ function telemetryEndTurn(outcome) {
19424
+ void active?.endTurn(outcome);
19425
+ }
19426
+ var active;
19427
+ var init_telemetry = __esm({
19428
+ "src/services/telemetry/index.ts"() {
19429
+ "use strict";
19430
+ init_otlpExporter();
19431
+ init_packageSettingsStore();
19432
+ init_authenticationService();
19433
+ init_otlpExporter();
19434
+ active = null;
19435
+ }
19436
+ });
19437
+
19110
19438
  // src/chat/hooks/useMemoryEnhancer.tsx
19111
19439
  var import_rxjs20, 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;
19112
19440
  var init_useMemoryEnhancer = __esm({
@@ -19770,7 +20098,14 @@ var init_mcpService = __esm({
19770
20098
  requestOptions.body = JSON.stringify(toolCall.parameters);
19771
20099
  }
19772
20100
  }
19773
- const response = await fetch(url, requestOptions);
20101
+ const controller = new AbortController();
20102
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
20103
+ let response;
20104
+ try {
20105
+ response = await fetch(url, { ...requestOptions, signal: controller.signal });
20106
+ } finally {
20107
+ clearTimeout(timeoutId);
20108
+ }
19774
20109
  const data = await response.json();
19775
20110
  if (!response.ok) {
19776
20111
  debugLogger.error("MCP tool execution failed", {
@@ -19841,6 +20176,7 @@ var init_useAIProvider = __esm({
19841
20176
  import_react22 = require("react");
19842
20177
  init_knowledgeStore();
19843
20178
  init_aiProviderStore();
20179
+ init_telemetry();
19844
20180
  init_conversationStore();
19845
20181
  init_useMemoryEnhancer();
19846
20182
  init_useVectorStore();
@@ -20605,6 +20941,9 @@ ${protocol}`;
20605
20941
  setStreamBuffer(latestDisplayMessage);
20606
20942
  }, delay);
20607
20943
  };
20944
+ syncTelemetry();
20945
+ telemetryStartTurn(question, modelName);
20946
+ telemetryEvent("tool_loop:llm_start");
20608
20947
  const stream = provider.chat(request);
20609
20948
  const initialPlaceholderQuestion = lastEntry?.question;
20610
20949
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -20618,7 +20957,10 @@ ${protocol}`;
20618
20957
  const sub = stream.subscribe({
20619
20958
  next: (data) => {
20620
20959
  if (!data?.message?.content && !data?.message?.tool_calls) return;
20621
- if (data.message.content) fullMessage += data.message.content;
20960
+ if (data.message.content) {
20961
+ fullMessage += data.message.content;
20962
+ telemetryEvent("tool_loop:llm_chunk", { chunk: data.message.content });
20963
+ }
20622
20964
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
20623
20965
  setIsThinking?.(inThinkBlock);
20624
20966
  const visibleMessage = stripThinking(fullMessage);
@@ -20650,6 +20992,7 @@ ${protocol}`;
20650
20992
  setIsThinking?.(false);
20651
20993
  setPendingMessage(null);
20652
20994
  setLogoVisible(false);
20995
+ telemetryEndTurn({ error: err?.message || "stream error" });
20653
20996
  if (onError) {
20654
20997
  onError(err);
20655
20998
  }
@@ -20657,6 +21000,7 @@ ${protocol}`;
20657
21000
  complete: async () => {
20658
21001
  try {
20659
21002
  setIsThinking?.(false);
21003
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
20660
21004
  latestDisplayMessage = stripThinking(fullMessage);
20661
21005
  if (!sawToolBlock) {
20662
21006
  flushNow();
@@ -20665,6 +21009,7 @@ ${protocol}`;
20665
21009
  let enhancedMessage = fullMessage;
20666
21010
  const summarizableResults = [];
20667
21011
  const inlineImageBlocks = [];
21012
+ const collectedSources = [];
20668
21013
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
20669
21014
  debugLogger.info("Detected tool calls in AI response", {
20670
21015
  toolCallCount: toolCallMatches.length,
@@ -20699,10 +21044,21 @@ ${protocol}`;
20699
21044
  });
20700
21045
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
20701
21046
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
21047
+ clearFlushTimer();
21048
+ 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";
21049
+ const toolPreamble = stripToolBlocks(fullMessage).trim();
21050
+ setStreamBuffer(toolPreamble ? `${toolPreamble}
21051
+
21052
+ _${toolStatus}_` : `_${toolStatus}_`);
21053
+ telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
20702
21054
  const result = await executeMCPTool({
20703
21055
  toolName: functionName,
20704
21056
  parameters: parsedParams
20705
21057
  });
21058
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
21059
+ name: functionName,
21060
+ isError: !result.success
21061
+ });
20706
21062
  let resultText = "";
20707
21063
  if (result.success) {
20708
21064
  if (functionName === "web_search" || functionName === "web-search") {
@@ -20716,18 +21072,16 @@ ${protocol}`;
20716
21072
  blocks.push(
20717
21073
  items.slice(0, 6).map((item, index) => {
20718
21074
  const title = item.title?.trim() || "Untitled";
20719
- const url = item.url?.trim();
21075
+ const url = item.url?.trim() || "";
20720
21076
  const snippet = item.content?.trim();
20721
- let line = `${index + 1}. **${title}**`;
20722
- if (url) line += `
20723
- ${url}`;
21077
+ if (url) collectedSources.push({ title, url });
21078
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
20724
21079
  if (snippet) {
20725
21080
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
20726
- line += `
20727
- ${truncated}`;
21081
+ line += ` \u2014 ${truncated}`;
20728
21082
  }
20729
21083
  return line;
20730
- }).join("\n\n")
21084
+ }).join("\n")
20731
21085
  );
20732
21086
  }
20733
21087
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -20809,7 +21163,7 @@ ${r.output}`).join("\n\n");
20809
21163
 
20810
21164
  ${toolResultsText}
20811
21165
 
20812
- 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.`
21166
+ 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.`
20813
21167
  }
20814
21168
  ];
20815
21169
  const summaryRequest = {
@@ -20819,9 +21173,22 @@ Using these results together with your own knowledge, answer my original questio
20819
21173
  options: { num_predict: tokenLimit + 250 }
20820
21174
  };
20821
21175
  clearFlushTimer();
20822
- setStreamBuffer("");
21176
+ const summaryPreamble = stripToolBlocks(fullMessage).trim();
21177
+ setStreamBuffer(
21178
+ summaryPreamble ? `${summaryPreamble}
21179
+
21180
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
21181
+ );
20823
21182
  const summaryText = await new Promise((resolve) => {
20824
21183
  let acc = "";
21184
+ let settled = false;
21185
+ let timer;
21186
+ const done = (value) => {
21187
+ if (settled) return;
21188
+ settled = true;
21189
+ if (timer) clearTimeout(timer);
21190
+ resolve(value);
21191
+ };
20825
21192
  const summarySub = provider.chat(summaryRequest).subscribe({
20826
21193
  next: (data) => {
20827
21194
  if (data?.message?.content) {
@@ -20836,14 +21203,33 @@ Using these results together with your own knowledge, answer my original questio
20836
21203
  debugLogger.error("Summarization pass failed", {
20837
21204
  error: summaryErr instanceof Error ? summaryErr.message : String(summaryErr)
20838
21205
  });
20839
- resolve("");
21206
+ done("");
20840
21207
  },
20841
- complete: () => resolve(stripThinking(acc).trim())
21208
+ complete: () => done(stripThinking(acc).trim())
20842
21209
  });
20843
21210
  currentSubRef.current = summarySub;
21211
+ timer = setTimeout(() => {
21212
+ debugLogger.warn("Summarization pass timed out; using inline tool output");
21213
+ try {
21214
+ summarySub.unsubscribe();
21215
+ } catch {
21216
+ }
21217
+ done("");
21218
+ }, 3e4);
20844
21219
  });
20845
21220
  if (summaryText.trim()) {
20846
- enhancedMessage = summaryText + (inlineImageBlocks.length ? `
21221
+ const sourcesMd = collectedSources.length ? `
21222
+
21223
+ **Sources**
21224
+ ${collectedSources.slice(0, 6).map((s) => {
21225
+ let domain = s.url;
21226
+ try {
21227
+ domain = new URL(s.url).hostname.replace(/^www\./, "");
21228
+ } catch {
21229
+ }
21230
+ return `- [${s.title || domain}](${s.url}) \u2014 ${domain}`;
21231
+ }).join("\n")}` : "";
21232
+ enhancedMessage = summaryText + sourcesMd + (inlineImageBlocks.length ? `
20847
21233
 
20848
21234
  ${inlineImageBlocks.join("\n\n")}` : "");
20849
21235
  }
@@ -20892,6 +21278,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
20892
21278
  }
20893
21279
  setInputValue("");
20894
21280
  setPastedImages([]);
21281
+ telemetryEndTurn();
20895
21282
  setTimeout(() => {
20896
21283
  clearFlushTimer();
20897
21284
  setPendingMessage(null);
@@ -20907,6 +21294,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
20907
21294
  overrideComponentStatus("Idle");
20908
21295
  setIsSubmitting(false);
20909
21296
  setIsStreaming(false);
21297
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
20910
21298
  }
20911
21299
  }
20912
21300
  });
@@ -23765,10 +24153,10 @@ var init_enhanced_mobile_conversations_modal = __esm({
23765
24153
  (0, import_react30.useEffect)(() => {
23766
24154
  setDeletedConversationIds((prev) => {
23767
24155
  let changed = false;
23768
- const active = new Set(conversations.map((conv) => conv.id));
24156
+ const active2 = new Set(conversations.map((conv) => conv.id));
23769
24157
  const next = /* @__PURE__ */ new Set();
23770
24158
  prev.forEach((id) => {
23771
- if (active.has(id)) {
24159
+ if (active2.has(id)) {
23772
24160
  next.add(id);
23773
24161
  } else {
23774
24162
  changed = true;