@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.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({
@@ -19848,6 +20176,7 @@ var init_useAIProvider = __esm({
19848
20176
  import_react22 = require("react");
19849
20177
  init_knowledgeStore();
19850
20178
  init_aiProviderStore();
20179
+ init_telemetry();
19851
20180
  init_conversationStore();
19852
20181
  init_useMemoryEnhancer();
19853
20182
  init_useVectorStore();
@@ -20612,6 +20941,9 @@ ${protocol}`;
20612
20941
  setStreamBuffer(latestDisplayMessage);
20613
20942
  }, delay);
20614
20943
  };
20944
+ syncTelemetry();
20945
+ telemetryStartTurn(question, modelName);
20946
+ telemetryEvent("tool_loop:llm_start");
20615
20947
  const stream = provider.chat(request);
20616
20948
  const initialPlaceholderQuestion = lastEntry?.question;
20617
20949
  lastPartialRef.current = { text: "", images: [...imageList], usedDocs, question };
@@ -20625,7 +20957,10 @@ ${protocol}`;
20625
20957
  const sub = stream.subscribe({
20626
20958
  next: (data) => {
20627
20959
  if (!data?.message?.content && !data?.message?.tool_calls) return;
20628
- 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
+ }
20629
20964
  const inThinkBlock = /<think>/.test(fullMessage) && !/<think>[\s\S]*<\/think>/.test(fullMessage);
20630
20965
  setIsThinking?.(inThinkBlock);
20631
20966
  const visibleMessage = stripThinking(fullMessage);
@@ -20657,6 +20992,7 @@ ${protocol}`;
20657
20992
  setIsThinking?.(false);
20658
20993
  setPendingMessage(null);
20659
20994
  setLogoVisible(false);
20995
+ telemetryEndTurn({ error: err?.message || "stream error" });
20660
20996
  if (onError) {
20661
20997
  onError(err);
20662
20998
  }
@@ -20664,6 +21000,7 @@ ${protocol}`;
20664
21000
  complete: async () => {
20665
21001
  try {
20666
21002
  setIsThinking?.(false);
21003
+ telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
20667
21004
  latestDisplayMessage = stripThinking(fullMessage);
20668
21005
  if (!sawToolBlock) {
20669
21006
  flushNow();
@@ -20672,6 +21009,7 @@ ${protocol}`;
20672
21009
  let enhancedMessage = fullMessage;
20673
21010
  const summarizableResults = [];
20674
21011
  const inlineImageBlocks = [];
21012
+ const collectedSources = [];
20675
21013
  if (toolCallMatches && toolCallMatches.length > 0 && mcpToolsAvailable) {
20676
21014
  debugLogger.info("Detected tool calls in AI response", {
20677
21015
  toolCallCount: toolCallMatches.length,
@@ -20706,10 +21044,21 @@ ${protocol}`;
20706
21044
  });
20707
21045
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
20708
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 });
20709
21054
  const result = await executeMCPTool({
20710
21055
  toolName: functionName,
20711
21056
  parameters: parsedParams
20712
21057
  });
21058
+ telemetryEvent(result.success ? "tool_loop:tool_result" : "tool_loop:tool_error", {
21059
+ name: functionName,
21060
+ isError: !result.success
21061
+ });
20713
21062
  let resultText = "";
20714
21063
  if (result.success) {
20715
21064
  if (functionName === "web_search" || functionName === "web-search") {
@@ -20723,18 +21072,16 @@ ${protocol}`;
20723
21072
  blocks.push(
20724
21073
  items.slice(0, 6).map((item, index) => {
20725
21074
  const title = item.title?.trim() || "Untitled";
20726
- const url = item.url?.trim();
21075
+ const url = item.url?.trim() || "";
20727
21076
  const snippet = item.content?.trim();
20728
- let line = `${index + 1}. **${title}**`;
20729
- if (url) line += `
20730
- ${url}`;
21077
+ if (url) collectedSources.push({ title, url });
21078
+ let line = url ? `${index + 1}. [${title}](${url})` : `${index + 1}. ${title}`;
20731
21079
  if (snippet) {
20732
21080
  const truncated = snippet.length > 300 ? `${snippet.slice(0, 300)}\u2026` : snippet;
20733
- line += `
20734
- ${truncated}`;
21081
+ line += ` \u2014 ${truncated}`;
20735
21082
  }
20736
21083
  return line;
20737
- }).join("\n\n")
21084
+ }).join("\n")
20738
21085
  );
20739
21086
  }
20740
21087
  resultText = blocks.length ? blocks.join("\n\n") : `No results found${search.query ? ` for "${search.query}"` : ""}.`;
@@ -20816,7 +21163,7 @@ ${r.output}`).join("\n\n");
20816
21163
 
20817
21164
  ${toolResultsText}
20818
21165
 
20819
- 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.`
20820
21167
  }
20821
21168
  ];
20822
21169
  const summaryRequest = {
@@ -20830,7 +21177,7 @@ Using these results together with your own knowledge, answer my original questio
20830
21177
  setStreamBuffer(
20831
21178
  summaryPreamble ? `${summaryPreamble}
20832
21179
 
20833
- _Working on it\u2026_` : "_Working on it\u2026_"
21180
+ _Writing the answer\u2026_` : "_Writing the answer\u2026_"
20834
21181
  );
20835
21182
  const summaryText = await new Promise((resolve) => {
20836
21183
  let acc = "";
@@ -20871,7 +21218,18 @@ _Working on it\u2026_` : "_Working on it\u2026_"
20871
21218
  }, 3e4);
20872
21219
  });
20873
21220
  if (summaryText.trim()) {
20874
- 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 ? `
20875
21233
 
20876
21234
  ${inlineImageBlocks.join("\n\n")}` : "");
20877
21235
  }
@@ -20920,6 +21278,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
20920
21278
  }
20921
21279
  setInputValue("");
20922
21280
  setPastedImages([]);
21281
+ telemetryEndTurn();
20923
21282
  setTimeout(() => {
20924
21283
  clearFlushTimer();
20925
21284
  setPendingMessage(null);
@@ -20935,6 +21294,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
20935
21294
  overrideComponentStatus("Idle");
20936
21295
  setIsSubmitting(false);
20937
21296
  setIsStreaming(false);
21297
+ telemetryEndTurn({ error: e instanceof Error ? e.message : String(e) });
20938
21298
  }
20939
21299
  }
20940
21300
  });
@@ -23793,10 +24153,10 @@ var init_enhanced_mobile_conversations_modal = __esm({
23793
24153
  (0, import_react30.useEffect)(() => {
23794
24154
  setDeletedConversationIds((prev) => {
23795
24155
  let changed = false;
23796
- const active = new Set(conversations.map((conv) => conv.id));
24156
+ const active2 = new Set(conversations.map((conv) => conv.id));
23797
24157
  const next = /* @__PURE__ */ new Set();
23798
24158
  prev.forEach((id) => {
23799
- if (active.has(id)) {
24159
+ if (active2.has(id)) {
23800
24160
  next.add(id);
23801
24161
  } else {
23802
24162
  changed = true;