autotel-devtools 23.0.0 → 24.0.0

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 (33) hide show
  1. package/dist/cli.cjs +2 -2
  2. package/dist/cli.js +2 -2
  3. package/dist/{error-aggregator-DSwUvgFF.d.cts → error-aggregator-C0at0wzF.d.cts} +1 -1
  4. package/dist/{error-aggregator-B52JI8jL.d.ts → error-aggregator-C25_JiVI.d.ts} +1 -1
  5. package/dist/{exporter-C4uKgo7l.d.cts → exporter-AkgWRDpc.d.cts} +55 -2
  6. package/dist/{exporter-BrEcnzoT.d.ts → exporter-yo7lLDIs.d.ts} +55 -2
  7. package/dist/fullpage.global.js +37 -30
  8. package/dist/genai/index.d.cts +1 -1
  9. package/dist/genai/index.d.ts +1 -1
  10. package/dist/{grpc-CSWjMRiB.cjs → grpc-CSW1Evnt.cjs} +1 -1
  11. package/dist/{grpc-CZEDUYCM.js → grpc-D1tt8UPi.js} +1 -1
  12. package/dist/{http-Cqm_MAtc.cjs → http-CahjEnpE.cjs} +291 -5
  13. package/dist/{http-Dbd9TIYU.js → http-z8UbJsqk.js} +292 -6
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.d.cts +4 -4
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/server/exporter.d.cts +1 -1
  19. package/dist/server/exporter.d.ts +1 -1
  20. package/dist/server/index.cjs +2 -2
  21. package/dist/server/index.d.cts +4 -4
  22. package/dist/server/index.d.ts +4 -4
  23. package/dist/server/index.js +2 -2
  24. package/dist/types-B86Qkqg5.d.cts +120 -0
  25. package/dist/types-B86Qkqg5.d.ts +120 -0
  26. package/dist/{types-DM8y4A9Z.d.ts → types-BhbHVeiy.d.ts} +1 -1
  27. package/dist/{types-B0tjwFqj.d.cts → types-C-ORUrT2.d.cts} +1 -1
  28. package/dist/widget.global.js +14 -14
  29. package/dist/wire/index.d.cts +1 -1
  30. package/dist/wire/index.d.ts +1 -1
  31. package/package.json +2 -2
  32. package/dist/types-DHDvZnnn.d.cts +0 -47
  33. package/dist/types-DHDvZnnn.d.ts +0 -47
@@ -1,4 +1,4 @@
1
- import { r as SpanData } from "../types-DHDvZnnn.cjs";
1
+ import { r as SpanData } from "../types-B86Qkqg5.cjs";
2
2
  //#region src/widget/genai/detect.d.ts
3
3
  declare function isGenAiSpan(span: SpanData): boolean;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { r as SpanData } from "../types-DHDvZnnn.js";
1
+ import { r as SpanData } from "../types-B86Qkqg5.js";
2
2
  //#region src/widget/genai/detect.d.ts
3
3
  declare function isGenAiSpan(span: SpanData): boolean;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- const require_http = require('./http-Cqm_MAtc.cjs');
1
+ const require_http = require('./http-CahjEnpE.cjs');
2
2
  let _grpc_grpc_js = require("@grpc/grpc-js");
3
3
 
4
4
  //#region src/server/grpc.ts
@@ -1,4 +1,4 @@
1
- import { c as decodeOtlpTraceRequest, o as decodeOtlpLogsRequest, s as decodeOtlpMetricsRequest } from "./http-Dbd9TIYU.js";
1
+ import { c as decodeOtlpTraceRequest, o as decodeOtlpLogsRequest, s as decodeOtlpMetricsRequest } from "./http-z8UbJsqk.js";
2
2
  import { Server, ServerCredentials, status } from "@grpc/grpc-js";
3
3
 
4
4
  //#region src/server/grpc.ts
@@ -316,6 +316,181 @@ var ErrorAggregator = class {
316
316
  }
317
317
  };
318
318
 
319
+ //#endregion
320
+ //#region src/server/webmcp-aggregator.ts
321
+ /** Span names this fold consumes. Everything else in the trace is ignored. */
322
+ const INSTALL = "webmcp.install";
323
+ const REGISTER = "webmcp.tool.register";
324
+ const EXECUTE = "webmcp.tool.execute";
325
+ const WITHDRAW = "webmcp.tool.withdraw";
326
+ /** How many recent executions a tool carries. Enough to see a pattern, not a log. */
327
+ const RECENT_CALLS = 5;
328
+ /**
329
+ * `{"content":[{"type":"text","text":` + `}]}` and the quoting around it.
330
+ *
331
+ * Chrome does not unwrap the MCP envelope, so an enveloped result spends these
332
+ * bytes of the agent's context on structure carrying no information. Measured
333
+ * rather than derived: 45 bytes wrapped versus 13 plain for the same text.
334
+ */
335
+ const ENVELOPE_OVERHEAD_BYTES = 32;
336
+ const median = (values) => {
337
+ if (values.length === 0) return 0;
338
+ const sorted = [...values].sort((a, b) => a - b);
339
+ const mid = Math.floor(sorted.length / 2);
340
+ return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
341
+ };
342
+ const csv = (value) => {
343
+ const raw = require_json_fields.asString(value);
344
+ if (!raw) return [];
345
+ return raw.split(",").map((part) => part.trim()).filter(Boolean);
346
+ };
347
+ /**
348
+ * Spans with no installation id — emitted by an `autotel-webmcp` older than the
349
+ * lifecycle release. Grouping them under one key per service keeps them
350
+ * readable as a single legacy installation rather than scattering one
351
+ * installation per tool.
352
+ */
353
+ const UNKNOWN_INSTALLATION = "unknown";
354
+ const isWebMcpSpan = (span) => span.name === INSTALL || span.name === REGISTER || span.name === EXECUTE || span.name === WITHDRAW;
355
+ function foldWebMcpTools(traces, activityWindow) {
356
+ const tools = /* @__PURE__ */ new Map();
357
+ /** Installations seen, and whether each one ever registered anything. */
358
+ const installations = /* @__PURE__ */ new Map();
359
+ const latestInstallation = /* @__PURE__ */ new Map();
360
+ const spans = traces.flatMap((trace) => trace.spans.map((span) => ({
361
+ span,
362
+ service: trace.service
363
+ }))).sort((a, b) => a.span.startTime - b.span.startTime);
364
+ for (const { span, service } of spans) {
365
+ if (!isWebMcpSpan(span)) continue;
366
+ if (activityWindow && span.startTime > activityWindow.end) continue;
367
+ const attrs = span.attributes ?? {};
368
+ const installationId = require_json_fields.asString(attrs["webmcp.installation.id"]) ?? UNKNOWN_INSTALLATION;
369
+ const installationKey = `${service}${installationId}`;
370
+ if (!installations.has(installationKey)) installations.set(installationKey, false);
371
+ if (span.name === INSTALL) {
372
+ latestInstallation.set(service, installationId);
373
+ continue;
374
+ }
375
+ const name = require_json_fields.asString(attrs["webmcp.tool.name"]) ?? require_json_fields.asString(attrs["gen_ai.tool.name"]);
376
+ if (!name) continue;
377
+ const key = `${service}${installationId}${name}`;
378
+ let tool = tools.get(key);
379
+ if (!tool) {
380
+ tool = {
381
+ name,
382
+ installationId,
383
+ service,
384
+ sessionId: require_json_fields.asString(attrs["session.id"]),
385
+ observedAtRegistration: false,
386
+ offered: false,
387
+ firstSeen: span.startTime,
388
+ lastSeen: span.startTime,
389
+ annotationsSent: [],
390
+ annotationsDropped: [],
391
+ calls: 0,
392
+ errors: 0,
393
+ envelopeCalls: 0,
394
+ envelopeBytes: 0,
395
+ substitutedCalls: 0,
396
+ resultBytes: 0,
397
+ medianResultBytes: 0,
398
+ resultSizes: [],
399
+ recentCalls: [],
400
+ traceId: span.traceId,
401
+ spanId: span.spanId
402
+ };
403
+ tools.set(key, tool);
404
+ }
405
+ tool.firstSeen = Math.min(tool.firstSeen, span.startTime);
406
+ tool.lastSeen = Math.max(tool.lastSeen, span.startTime);
407
+ tool.sessionId ??= require_json_fields.asString(attrs["session.id"]);
408
+ switch (span.name) {
409
+ case REGISTER:
410
+ installations.set(installationKey, true);
411
+ tool.observedAtRegistration = true;
412
+ tool.offered = true;
413
+ tool.descriptionLength = require_json_fields.asNumber(attrs["webmcp.tool.description.length"]);
414
+ tool.hasInputSchema = require_json_fields.asBoolean(attrs["webmcp.tool.has_input_schema"]);
415
+ tool.annotationsSent = csv(attrs["webmcp.annotations.sent"]);
416
+ tool.annotationsDropped = csv(attrs["webmcp.annotations.dropped"]);
417
+ tool.traceId = span.traceId;
418
+ tool.spanId = span.spanId;
419
+ break;
420
+ case WITHDRAW:
421
+ tool.offered = false;
422
+ break;
423
+ case EXECUTE: {
424
+ if (activityWindow && span.startTime < activityWindow.start) break;
425
+ tool.calls += 1;
426
+ const bytes = require_json_fields.asNumber(attrs["webmcp.result.bytes"]) ?? 0;
427
+ tool.resultBytes += bytes;
428
+ tool.resultSizes.push(bytes);
429
+ if (require_json_fields.asBoolean(attrs["webmcp.result.envelope"])) {
430
+ tool.envelopeCalls += 1;
431
+ tool.envelopeBytes += ENVELOPE_OVERHEAD_BYTES;
432
+ }
433
+ if (require_json_fields.asBoolean(attrs["webmcp.result.substituted"])) tool.substitutedCalls += 1;
434
+ const failed = span.status.code === "ERROR" || (require_json_fields.asBoolean(attrs["webmcp.result.error"]) ?? false) || require_json_fields.asString(attrs["error.type"]) !== void 0;
435
+ if (failed) tool.errors += 1;
436
+ tool.recentCalls.push({
437
+ timestamp: span.startTime,
438
+ durationMs: span.duration,
439
+ resultBytes: bytes,
440
+ resultType: require_json_fields.asString(attrs["webmcp.result.type"]),
441
+ envelope: require_json_fields.asBoolean(attrs["webmcp.result.envelope"]) ?? false,
442
+ substituted: require_json_fields.asBoolean(attrs["webmcp.result.substituted"]) ?? false,
443
+ error: failed,
444
+ input: require_json_fields.asString(attrs["webmcp.input"]) ?? require_json_fields.asString(attrs["gen_ai.tool.call.arguments"]),
445
+ result: require_json_fields.asString(attrs["webmcp.result"]) ?? require_json_fields.asString(attrs["gen_ai.tool.call.result"]),
446
+ traceId: span.traceId,
447
+ spanId: span.spanId
448
+ });
449
+ break;
450
+ }
451
+ }
452
+ }
453
+ const list = [...tools.values()].map((tool) => {
454
+ const { resultSizes, ...rest } = tool;
455
+ return {
456
+ ...rest,
457
+ offered: rest.offered && (latestInstallation.get(rest.service) ?? rest.installationId) === rest.installationId,
458
+ medianResultBytes: median(resultSizes),
459
+ recentCalls: [...tool.recentCalls].sort((a, b) => b.timestamp - a.timestamp).slice(0, RECENT_CALLS)
460
+ };
461
+ });
462
+ list.sort((a, b) => Number(b.offered) - Number(a.offered) || b.calls - a.calls || a.name.localeCompare(b.name));
463
+ return {
464
+ tools: list,
465
+ summary: summarize(list, installations)
466
+ };
467
+ }
468
+ function summarize(tools, installations) {
469
+ const summary = {
470
+ installations: installations.size,
471
+ emptyInstallations: [...installations.values()].filter((saw) => !saw).length,
472
+ toolsOffered: 0,
473
+ toolsWithdrawn: 0,
474
+ calls: 0,
475
+ errors: 0,
476
+ resultBytes: 0,
477
+ envelopeBytes: 0,
478
+ toolsWithDroppedAnnotations: 0,
479
+ toolsWithoutInputSchema: 0
480
+ };
481
+ for (const tool of tools) {
482
+ if (tool.offered) summary.toolsOffered += 1;
483
+ else summary.toolsWithdrawn += 1;
484
+ summary.calls += tool.calls;
485
+ summary.errors += tool.errors;
486
+ summary.resultBytes += tool.resultBytes;
487
+ summary.envelopeBytes += tool.envelopeBytes;
488
+ if (tool.annotationsDropped.length > 0) summary.toolsWithDroppedAnnotations += 1;
489
+ if (tool.observedAtRegistration && tool.hasInputSchema === false) summary.toolsWithoutInputSchema += 1;
490
+ }
491
+ return summary;
492
+ }
493
+
319
494
  //#endregion
320
495
  //#region src/server/telemetry-limits.ts
321
496
  const defaultLimit = 100;
@@ -1528,6 +1703,42 @@ var DevtoolsStore = class {
1528
1703
  LIMIT ?`).all(Math.max(1, Math.min(limit, 500)));
1529
1704
  return [.../* @__PURE__ */ new Set([...Object.keys(schema.columns), ...rows.map((row) => row.key)])];
1530
1705
  }
1706
+ /**
1707
+ * Values of one attribute paired with another on the same entity.
1708
+ *
1709
+ * `searchAttributes` matches on value text, which cannot answer "what arms
1710
+ * does this experiment have". Pairing two keys across the same span can, and
1711
+ * that is what turns a pair of cohorts into something the viewer offers
1712
+ * rather than something the reader has to type.
1713
+ *
1714
+ * Rows arrive grouped by `key`, each group's values commonest first, so a
1715
+ * caller can build the groups in one pass and take the two commonest as a
1716
+ * default pair.
1717
+ *
1718
+ * The join runs over `attribute_occurrences`, not the `attribute_values`
1719
+ * dictionary: occurrences are deleted with their span, so retention prunes
1720
+ * them, and they carry the entity a value was seen on, so an arm is only
1721
+ * offered for the experiment it actually ran under. The dictionary can do
1722
+ * neither — it counts values for the lifetime of the database and forgets
1723
+ * which span each came from, which would offer arms belonging to a different
1724
+ * experiment and experiments whose spans are long gone.
1725
+ */
1726
+ pairedAttributeValues(signal, key, pairedKey, limit = 200) {
1727
+ return this.db.prepare(`
1728
+ SELECT a.value_json AS value_json, b.value_json AS paired_json, count(*) AS count
1729
+ FROM attribute_occurrences a
1730
+ JOIN attribute_occurrences b
1731
+ ON b.signal = a.signal AND b.entity_id = a.entity_id AND b.key = ?
1732
+ WHERE a.signal = ? AND a.key = ?
1733
+ GROUP BY a.value_json, b.value_json
1734
+ ORDER BY value_json ASC, count DESC, paired_json ASC
1735
+ LIMIT ?
1736
+ `).all(pairedKey, signal, key, Math.max(1, Math.min(limit, 500))).map((row) => ({
1737
+ value: JSON.parse(row.value_json),
1738
+ paired: JSON.parse(row.paired_json),
1739
+ count: Number(row.count)
1740
+ }));
1741
+ }
1531
1742
  searchAttributes(signal, value, limit = 50) {
1532
1743
  return this.db.prepare(`
1533
1744
  SELECT key, value_json, seen_count
@@ -2503,11 +2714,15 @@ var DevtoolsServer = class {
2503
2714
  this.log(`Client disconnected (${this.clients.size} total)`);
2504
2715
  });
2505
2716
  });
2506
- if (!options.server) this.httpServer.listen(this._port, () => {
2507
- const addr = this.httpServer.address();
2508
- if (addr && typeof addr === "object") this._port = addr.port;
2509
- this.log(`WebSocket server listening on port ${this._port}`);
2510
- });
2717
+ if (!options.server) {
2718
+ const listening = () => {
2719
+ const addr = this.httpServer.address();
2720
+ if (addr && typeof addr === "object") this._port = addr.port;
2721
+ this.log(`WebSocket server listening on port ${this._port}`);
2722
+ };
2723
+ if (options.host == null) this.httpServer.listen(this._port, listening);
2724
+ else this.httpServer.listen(this._port, options.host, listening);
2725
+ }
2511
2726
  }
2512
2727
  get port() {
2513
2728
  const addr = this.httpServer.address();
@@ -2714,6 +2929,44 @@ var DevtoolsServer = class {
2714
2929
  } while (cursor);
2715
2930
  return { errors: aggregator.getErrorGroups() };
2716
2931
  }
2932
+ /**
2933
+ * Fold WebMCP lifecycle history through the window end into the tool surface
2934
+ * an agent is offered, while counting executions only inside the window.
2935
+ *
2936
+ * Drains every page rather than folding the first one: an inventory built
2937
+ * from a page of results does not fail, it *under-reports* — "2 tools dropped
2938
+ * annotations" when the answer is 6 — and gets more wrong the more traffic
2939
+ * there is, which is backwards for an observability answer.
2940
+ *
2941
+ * The span-name filter is composed here rather than accepted from the client:
2942
+ * this endpoint answers one question, and a caller-supplied predicate could
2943
+ * only narrow it into a wrong answer.
2944
+ */
2945
+ queryWebMcp(args) {
2946
+ const traces = [];
2947
+ const seenCursors = /* @__PURE__ */ new Set();
2948
+ let cursor;
2949
+ do {
2950
+ const result = this.store.queryTraces({
2951
+ query: "name ^ \"webmcp.\"",
2952
+ window: args.window ? {
2953
+ start: 0,
2954
+ end: args.window.end
2955
+ } : void 0,
2956
+ limit: args.limit,
2957
+ cursor
2958
+ });
2959
+ if (result.errors) return {
2960
+ webmcp: foldWebMcpTools([]),
2961
+ errors_parse: result.errors
2962
+ };
2963
+ traces.push(...result.traces);
2964
+ cursor = result.nextCursor ?? void 0;
2965
+ if (cursor && seenCursors.has(cursor)) break;
2966
+ if (cursor) seenCursors.add(cursor);
2967
+ } while (cursor);
2968
+ return { webmcp: foldWebMcpTools(traces, args.window) };
2969
+ }
2717
2970
  /** Run a log query against the durable store. */
2718
2971
  queryLogs(args) {
2719
2972
  return this.store.queryLogs(args);
@@ -2721,6 +2974,9 @@ var DevtoolsServer = class {
2721
2974
  listQueryFields(signal) {
2722
2975
  return this.store.listQueryFields(signal);
2723
2976
  }
2977
+ pairedAttributeValues(signal, key, pairedKey, limit) {
2978
+ return this.store.pairedAttributeValues(signal, key, pairedKey, limit);
2979
+ }
2724
2980
  searchAttributes(signal, value, limit) {
2725
2981
  return this.store.searchAttributes(signal, value, limit);
2726
2982
  }
@@ -3435,6 +3691,12 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
3435
3691
  }
3436
3692
  const params = new URL(url, "http://localhost").searchParams;
3437
3693
  const signal = params.get("signal") === "logs" ? "logs" : "traces";
3694
+ const key = params.get("key");
3695
+ const pair = params.get("pair");
3696
+ if (key !== null && pair !== null) {
3697
+ sendJson(res, 200, { pairs: devtools.pairedAttributeValues(signal, key, pair) });
3698
+ return;
3699
+ }
3438
3700
  sendJson(res, 200, { attributes: devtools.searchAttributes(signal, params.get("value") ?? "") });
3439
3701
  return;
3440
3702
  }
@@ -3708,6 +3970,30 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
3708
3970
  }
3709
3971
  return;
3710
3972
  }
3973
+ if (req.method === "POST" && url === "/api/query/webmcp") {
3974
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
3975
+ sendJson(res, 403, { error: "Forbidden" });
3976
+ return;
3977
+ }
3978
+ try {
3979
+ const body = await readJsonBody(req);
3980
+ const result = devtools.queryWebMcp({
3981
+ window: body.window,
3982
+ limit: body.limit
3983
+ });
3984
+ if (result.errors_parse) {
3985
+ sendJson(res, 400, { errors: result.errors_parse });
3986
+ return;
3987
+ }
3988
+ sendJson(res, 200, { webmcp: result.webmcp });
3989
+ } catch (e) {
3990
+ sendJson(res, 400, {
3991
+ error: "Invalid query request",
3992
+ message: e instanceof Error ? e.message : String(e)
3993
+ });
3994
+ }
3995
+ return;
3996
+ }
3711
3997
  if (req.method === "GET" && url === "/api/metrics") {
3712
3998
  if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
3713
3999
  sendJson(res, 403, { error: "Forbidden" });