@glassflow-ai/rius 0.4.0 → 0.6.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.
package/dist/index.cjs CHANGED
@@ -53,7 +53,11 @@ function seconds(raw, fallback) {
53
53
  if (Number.isNaN(parsed)) return fallback;
54
54
  return parsed;
55
55
  }
56
- function clamp(name, value, min, max) {
56
+ function clamp(name, value, min, max, fallback) {
57
+ if (!Number.isFinite(value)) {
58
+ console.warn(`[rius] ${name}=${value} is not a finite number; using ${fallback}.`);
59
+ return fallback;
60
+ }
57
61
  if (value >= min && value <= max) return value;
58
62
  const clamped = Math.min(Math.max(value, min), max);
59
63
  console.warn(`[rius] ${name}=${value} is outside [${min}, ${max}]; clamped to ${clamped}.`);
@@ -66,13 +70,15 @@ function resolveConfig(options = {}, env = process.env) {
66
70
  "heartbeatInterval",
67
71
  options.heartbeatInterval ?? seconds(env.RIUS_HEARTBEAT_INTERVAL, DEFAULT_HEARTBEAT_INTERVAL),
68
72
  HEARTBEAT_INTERVAL_MIN,
69
- HEARTBEAT_INTERVAL_MAX
73
+ HEARTBEAT_INTERVAL_MAX,
74
+ DEFAULT_HEARTBEAT_INTERVAL
70
75
  );
71
76
  const partialSpansDelaySeconds = clamp(
72
77
  "partialSpansDelay",
73
78
  options.partialSpansDelay ?? seconds(env.RIUS_PARTIAL_SPANS_DELAY, DEFAULT_PARTIAL_SPANS_DELAY),
74
79
  PARTIAL_SPANS_DELAY_MIN,
75
- PARTIAL_SPANS_DELAY_MAX
80
+ PARTIAL_SPANS_DELAY_MAX,
81
+ DEFAULT_PARTIAL_SPANS_DELAY
76
82
  );
77
83
  return {
78
84
  endpoint,
@@ -221,6 +227,24 @@ var init_exportHealth = __esm({
221
227
  }
222
228
  });
223
229
 
230
+ // src/version.ts
231
+ var import_node_module, SDK_VERSION;
232
+ var init_version = __esm({
233
+ "src/version.ts"() {
234
+ "use strict";
235
+ init_cjs_shims();
236
+ import_node_module = require("module");
237
+ SDK_VERSION = (() => {
238
+ try {
239
+ const pkg = (0, import_node_module.createRequire)(importMetaUrl)("../package.json");
240
+ return pkg.version ?? "0.0.0";
241
+ } catch {
242
+ return "0.0.0";
243
+ }
244
+ })();
245
+ }
246
+ });
247
+
224
248
  // src/heartbeat.ts
225
249
  function httpTransport(url, headers) {
226
250
  return async (payload, timeoutMs) => {
@@ -237,24 +261,16 @@ function httpTransport(url, headers) {
237
261
  }
238
262
  };
239
263
  }
240
- var import_node_module, PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, SDK_VERSION, OpenRootSpanTracker, HeartbeatSender;
264
+ var PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, OpenRootSpanTracker, HeartbeatSender;
241
265
  var init_heartbeat = __esm({
242
266
  "src/heartbeat.ts"() {
243
267
  "use strict";
244
268
  init_cjs_shims();
245
- import_node_module = require("module");
269
+ init_version();
246
270
  PAYLOAD_VERSION = 1;
247
271
  OPEN_TRACES_CAP = 32;
248
272
  DEFAULT_PING_TIMEOUT_MS = 3e3;
249
273
  DEFAULT_FINAL_PING_TIMEOUT_MS = 1e3;
250
- SDK_VERSION = (() => {
251
- try {
252
- const pkg = (0, import_node_module.createRequire)(importMetaUrl)("../package.json");
253
- return pkg.version ?? "0.0.0";
254
- } catch {
255
- return "0.0.0";
256
- }
257
- })();
258
274
  OpenRootSpanTracker = class {
259
275
  // trace id (32-hex) -> count of open root spans in that trace. A trace id
260
276
  // normally has one root, but the count guards against duplicates.
@@ -306,6 +322,7 @@ var init_heartbeat = __esm({
306
322
  }
307
323
  /** Starts pinging: an immediate first ping, then every `intervalMs`. */
308
324
  start() {
325
+ if (this.timer !== void 0) return;
309
326
  void this.ping(this.pingTimeoutMs, false);
310
327
  const timer = setInterval(() => void this.ping(this.pingTimeoutMs, false), this.intervalMs);
311
328
  if (typeof timer.unref === "function") timer.unref();
@@ -356,7 +373,7 @@ function kindAttributes(kind) {
356
373
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
357
374
  return attributes;
358
375
  }
359
- var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, WORKSPACE_ROUTE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
376
+ var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, USER_ID, WORKSPACE_ROUTE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_REQUEST_REASONING_LEVEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_TOOL_DEFINITIONS, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, LLM_INVOCATION_PARAMETERS, INVOCATION_PARAMETERS_CONTENT_MEMBERS, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
360
377
  var init_semconv = __esm({
361
378
  "src/semconv.ts"() {
362
379
  "use strict";
@@ -367,19 +384,23 @@ var init_semconv = __esm({
367
384
  INPUT_VALUE = "input.value";
368
385
  OUTPUT_VALUE = "output.value";
369
386
  SESSION_ID = "session.id";
387
+ USER_ID = "user.id";
370
388
  WORKSPACE_ROUTE = "rius.workspace";
371
389
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
372
390
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
373
391
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
392
+ GEN_AI_REQUEST_REASONING_LEVEL = "gen_ai.request.reasoning.level";
374
393
  GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
375
394
  GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
376
395
  GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
377
396
  GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens";
378
397
  GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS = "gen_ai.usage.cache_write.input_tokens";
398
+ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS = "gen_ai.usage.reasoning.output_tokens";
379
399
  GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages";
380
400
  GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages";
381
401
  GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons";
382
402
  GEN_AI_TOOL_NAME = "gen_ai.tool.name";
403
+ GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions";
383
404
  GEN_AI_REQUEST_PREFIX = "gen_ai.request.";
384
405
  MCP_RESULT_TYPE = "mcp.result_type";
385
406
  GEN_AI_FIRST_TOKEN_EVENT = "gen_ai.first_token";
@@ -408,7 +429,18 @@ var init_semconv = __esm({
408
429
  // or internal URLs in parameter defaults. gen_ai.tool.name stays: it is
409
430
  // identity, not content.
410
431
  "gen_ai.tool.description",
411
- "gen_ai.tool.definitions",
432
+ GEN_AI_TOOL_DEFINITIONS,
433
+ // GenAI semconv keys emitted by OTel-native instrumentations (@ai-sdk/otel,
434
+ // which init() registers for ai v7, among them; pinned 2026-09-14): the
435
+ // system prompt and each tool call's input and output are content in the
436
+ // same sense messages are. gen_ai.tool.call.id stays: identity.
437
+ "gen_ai.system_instructions",
438
+ "gen_ai.tool.call.arguments",
439
+ "gen_ai.tool.call.result",
440
+ // OpenInference TOOL-kind spans carry the definition under these two bare
441
+ // keys, sensitive for the reason gen_ai.tool.description is.
442
+ "tool.description",
443
+ "tool.parameters",
412
444
  "gen_ai.prompt",
413
445
  "gen_ai.completion",
414
446
  "llm.input_messages",
@@ -422,15 +454,44 @@ var init_semconv = __esm({
422
454
  "mlflow.spanInputs",
423
455
  "mlflow.spanOutputs",
424
456
  "traceloop.entity.input",
425
- "traceloop.entity.output"
457
+ "traceloop.entity.output",
458
+ // Every bundled OpenInference instrumentation emits tool definitions as
459
+ // llm.tools.{i}.tool.json_schema (pinned empirically 2026-09-14); covered
460
+ // by prefix below, bare key listed per the bare-key rule.
461
+ "llm.tools",
462
+ // The Vercel AI SDK's own telemetry keys survive the OpenInference
463
+ // transform untouched, and they carry the full prompt (messages AND tool
464
+ // definitions), response content, and tool-call I/O. Names, ids and models
465
+ // (ai.toolCall.name, ai.response.model, ...) are identity and stay.
466
+ "ai.prompt",
467
+ "ai.response.text",
468
+ "ai.response.object",
469
+ "ai.toolCall.args",
470
+ "ai.toolCall.result",
471
+ // Same family, keys enumerated from the ai v7 telemetry surface
472
+ // (2026-09-14): model reasoning, tool calls in the response, response
473
+ // files, embedding inputs, rerank documents, and the generateObject schema
474
+ // (a tool definition by another name).
475
+ "ai.response.reasoning",
476
+ "ai.response.toolCalls",
477
+ "ai.response.files",
478
+ "ai.value",
479
+ "ai.values",
480
+ "ai.documents",
481
+ "ai.schema",
482
+ "ai.schema.description"
426
483
  ]);
484
+ LLM_INVOCATION_PARAMETERS = "llm.invocation_parameters";
485
+ INVOCATION_PARAMETERS_CONTENT_MEMBERS = ["tools", "functions"];
427
486
  CONTENT_ATTRIBUTE_PREFIXES = [
428
487
  "llm.input_messages.",
429
488
  "llm.output_messages.",
430
489
  "gen_ai.prompt.",
431
490
  "gen_ai.completion.",
432
491
  "llm.prompts.",
433
- "llm.prompt_template."
492
+ "llm.prompt_template.",
493
+ "llm.tools.",
494
+ "ai.prompt."
434
495
  ];
435
496
  CONTENT_ATTRIBUTE_SUFFIXES = [
436
497
  ".document.content",
@@ -445,6 +506,9 @@ var init_semconv = __esm({
445
506
  // Identity, not content: a pending span must be groupable into its
446
507
  // session while still running, that is the live view's whole point.
447
508
  SESSION_ID,
509
+ // Same reason: a crashed run's partial spans must still be attributable
510
+ // to the user they served.
511
+ USER_ID,
448
512
  // Routing, not content: a crashed run's snapshot must land in the same
449
513
  // workspace its final span would have. Stripped at export either way.
450
514
  WORKSPACE_ROUTE
@@ -454,31 +518,77 @@ var init_semconv = __esm({
454
518
  });
455
519
 
456
520
  // src/serde.ts
457
- function toAttributeValue(value) {
458
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
459
- return value;
521
+ function truncate(text) {
522
+ return text.length > MAX_ATTR_CHARS ? text.slice(0, MAX_ATTR_CHARS) + TRUNCATION_MARKER : text;
523
+ }
524
+ function renderable(raw, value) {
525
+ if (typeof raw === "bigint") return String(raw);
526
+ if (raw instanceof Map) return Object.fromEntries(raw);
527
+ if (raw instanceof Set) return [...raw];
528
+ if (ArrayBuffer.isView(raw)) {
529
+ return `<${raw.constructor.name} ${raw.byteLength} bytes>`;
460
530
  }
531
+ if (raw instanceof ArrayBuffer) return `<ArrayBuffer ${raw.byteLength} bytes>`;
532
+ return value;
533
+ }
534
+ function toAttributeValue(value) {
535
+ if (typeof value === "string") return truncate(value);
536
+ if (typeof value === "number" || typeof value === "boolean") return value;
461
537
  if (value === null || value === void 0) return "";
538
+ if (typeof value === "bigint") return String(value);
462
539
  const ancestors = [];
463
540
  try {
464
- return JSON.stringify(value, function(_key, v) {
465
- if (typeof v === "object" && v !== null) {
541
+ const text = JSON.stringify({ "": value }[""], function(key, v) {
542
+ const raw = this?.[key];
543
+ const rendered = renderable(raw, v);
544
+ if (typeof rendered === "object" && rendered !== null) {
466
545
  while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
467
546
  ancestors.pop();
468
547
  }
469
- if (ancestors.includes(v)) return "[Circular]";
470
- ancestors.push(v);
548
+ if (ancestors.includes(rendered)) return "[Circular]";
549
+ ancestors.push(rendered);
471
550
  }
472
- return v;
551
+ return rendered;
473
552
  }) ?? String(value);
553
+ return truncate(text);
474
554
  } catch {
475
555
  return "[unserializable]";
476
556
  }
477
557
  }
558
+ var MAX_ATTR_CHARS, TRUNCATION_MARKER;
478
559
  var init_serde = __esm({
479
560
  "src/serde.ts"() {
480
561
  "use strict";
481
562
  init_cjs_shims();
563
+ MAX_ATTR_CHARS = 32 * 1024;
564
+ TRUNCATION_MARKER = "\u2026(truncated)";
565
+ }
566
+ });
567
+
568
+ // src/user.ts
569
+ function withUser(userId, fn) {
570
+ return import_api.context.with(import_api.context.active().setValue(USER_KEY, userId), () => fn(userId));
571
+ }
572
+ var import_api, USER_KEY, UserSpanProcessor;
573
+ var init_user = __esm({
574
+ "src/user.ts"() {
575
+ "use strict";
576
+ init_cjs_shims();
577
+ import_api = require("@opentelemetry/api");
578
+ init_semconv();
579
+ USER_KEY = (0, import_api.createContextKey)("rius-user-id");
580
+ UserSpanProcessor = class {
581
+ onStart(span, parentContext) {
582
+ const value = parentContext.getValue(USER_KEY);
583
+ if (typeof value === "string") span.setAttribute(USER_ID, value);
584
+ }
585
+ onEnd(_span) {
586
+ }
587
+ async forceFlush() {
588
+ }
589
+ async shutdown() {
590
+ }
591
+ };
482
592
  }
483
593
  });
484
594
 
@@ -487,39 +597,50 @@ function configure(observation, options) {
487
597
  if (options.input !== void 0) observation.setInput(options.input);
488
598
  return observation;
489
599
  }
600
+ function creationAttributes(options) {
601
+ const attributes = { ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */), ...options.attributes };
602
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
603
+ return attributes;
604
+ }
490
605
  function startSpan(name, options = {}) {
491
- const span = getTracer().startSpan(name, {
492
- attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */)
493
- });
606
+ const span = getTracer().startSpan(name, { attributes: creationAttributes(options) });
494
607
  return configure(new Observation(span), options);
495
608
  }
496
609
  function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
497
610
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
498
- return getTracer().startActiveSpan(
611
+ return runActive(
499
612
  name,
500
- { attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */) },
501
- async (span) => {
502
- const observation = configure(new Observation(span), options);
503
- try {
504
- return await fn(observation);
505
- } catch (error) {
506
- observation.recordException(error);
507
- throw error;
508
- } finally {
509
- observation.end();
510
- }
511
- }
613
+ creationAttributes(options),
614
+ options.userId,
615
+ (span) => configure(new Observation(span), options),
616
+ fn
512
617
  );
513
618
  }
514
- var import_api, Observation;
619
+ function runActive(name, attributes, userId, makeHandle, fn) {
620
+ const run = () => getTracer().startActiveSpan(name, { attributes }, async (span) => {
621
+ const handle = makeHandle(span);
622
+ try {
623
+ return await fn(handle);
624
+ } catch (error) {
625
+ handle.recordException(error);
626
+ throw error;
627
+ } finally {
628
+ handle.end();
629
+ }
630
+ });
631
+ return userId !== void 0 ? withUser(userId, run) : run();
632
+ }
633
+ var import_api2, Observation;
515
634
  var init_spans = __esm({
516
635
  "src/spans.ts"() {
517
636
  "use strict";
518
637
  init_cjs_shims();
519
- import_api = require("@opentelemetry/api");
638
+ import_api2 = require("@opentelemetry/api");
520
639
  init_client();
521
640
  init_semconv();
522
641
  init_serde();
642
+ init_user();
643
+ Symbol.dispose ??= /* @__PURE__ */ Symbol.for("Symbol.dispose");
523
644
  Observation = class {
524
645
  constructor(span) {
525
646
  this.span = span;
@@ -534,7 +655,26 @@ var init_spans = __esm({
534
655
  this.span.setAttribute(OUTPUT_VALUE, toAttributeValue(value));
535
656
  return this;
536
657
  }
658
+ /**
659
+ * Set an arbitrary attribute. Primitives and homogeneous primitive arrays
660
+ * are passed through as the OTel values they are; objects are JSON-encoded
661
+ * and bounded; `undefined` and `null` set nothing, since "no value" is not
662
+ * an empty string.
663
+ */
537
664
  setAttribute(key, value) {
665
+ if (value === void 0 || value === null) return this;
666
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
667
+ this.span.setAttribute(key, value);
668
+ return this;
669
+ }
670
+ if (Array.isArray(value) && value.every((v) => typeof v === "number")) {
671
+ this.span.setAttribute(key, value);
672
+ return this;
673
+ }
674
+ if (Array.isArray(value) && value.every((v) => typeof v === "boolean")) {
675
+ this.span.setAttribute(key, value);
676
+ return this;
677
+ }
538
678
  this.span.setAttribute(key, toAttributeValue(value));
539
679
  return this;
540
680
  }
@@ -549,7 +689,7 @@ var init_spans = __esm({
549
689
  recordException(error) {
550
690
  const wrapped = error instanceof Error ? error : new Error(String(error));
551
691
  this.span.recordException(wrapped);
552
- this.span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: wrapped.message });
692
+ this.span.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: wrapped.message });
553
693
  return this;
554
694
  }
555
695
  end() {
@@ -570,6 +710,21 @@ var instrumentationMcp_exports = {};
570
710
  __export(instrumentationMcp_exports, {
571
711
  instrumentMcpClient: () => instrumentMcpClient
572
712
  });
713
+ function serializeResult(result) {
714
+ if (typeof result !== "object" || result === null) return toAttributeValue(result);
715
+ const record = result;
716
+ const structured = record.structured_content ?? record.structuredContent;
717
+ if (structured !== void 0 && structured !== null) return toAttributeValue(structured);
718
+ const content = record.content;
719
+ if (Array.isArray(content)) {
720
+ const texts = content.map(
721
+ (block) => typeof block === "object" && block !== null ? block.text : void 0
722
+ ).filter((text) => typeof text === "string");
723
+ if (texts.length === 1) return truncate(texts[0]);
724
+ if (texts.length > 1) return toAttributeValue(texts);
725
+ }
726
+ return toAttributeValue(result);
727
+ }
573
728
  function resultIsError(result) {
574
729
  if (typeof result !== "object" || result === null) return false;
575
730
  const record = result;
@@ -581,10 +736,10 @@ function recordResult(observation, result) {
581
736
  observation.setAttribute(MCP_RESULT_TYPE, INPUT_REQUIRED);
582
737
  return;
583
738
  }
584
- observation.setOutput(result);
739
+ observation.setAttribute(OUTPUT_VALUE, serializeResult(result));
585
740
  if (resultIsError(result)) {
586
741
  observation.span.setStatus({
587
- code: import_api2.SpanStatusCode.ERROR,
742
+ code: import_api3.SpanStatusCode.ERROR,
588
743
  message: "tool returned an error result"
589
744
  });
590
745
  }
@@ -601,9 +756,14 @@ function instrumentMcpClient(ClientClass) {
601
756
  const instrumented = function instrumentedCallTool(params, ...rest) {
602
757
  return startAsCurrentSpan(
603
758
  `execute_tool ${params.name}`,
604
- { kind: "TOOL" /* TOOL */, input: params.arguments },
759
+ // Tool name at CREATION: pending snapshots are built at start, so an
760
+ // attribute set inside the callback never reaches them.
761
+ {
762
+ kind: "TOOL" /* TOOL */,
763
+ input: params.arguments,
764
+ attributes: { [GEN_AI_TOOL_NAME]: params.name }
765
+ },
605
766
  async (observation) => {
606
- observation.setAttribute(GEN_AI_TOOL_NAME, params.name);
607
767
  const result = await original.call(this, params, ...rest);
608
768
  recordResult(observation, result);
609
769
  return result;
@@ -616,13 +776,14 @@ function instrumentMcpClient(ClientClass) {
616
776
  ClientClass.prototype.callTool = original;
617
777
  };
618
778
  }
619
- var import_api2, INPUT_REQUIRED;
779
+ var import_api3, INPUT_REQUIRED;
620
780
  var init_instrumentationMcp = __esm({
621
781
  "src/instrumentationMcp.ts"() {
622
782
  "use strict";
623
783
  init_cjs_shims();
624
- import_api2 = require("@opentelemetry/api");
784
+ import_api3 = require("@opentelemetry/api");
625
785
  init_semconv();
786
+ init_serde();
626
787
  init_spans();
627
788
  INPUT_REQUIRED = "input_required";
628
789
  }
@@ -714,21 +875,55 @@ function mcpClientPatchable(exports2) {
714
875
  const cls = exports2.Client;
715
876
  return typeof cls?.prototype?.callTool === "function" ? { Client: cls } : void 0;
716
877
  }
717
- async function enableInstrumentations(sink, tracerProvider, names) {
878
+ async function registerVercelTelemetry(tracerProvider, teardown) {
879
+ const ai = await optional("ai");
880
+ const register = ai?.registerTelemetry;
881
+ if (register === void 0) return false;
882
+ const otel = await optional("@ai-sdk/otel");
883
+ const OpenTelemetryIntegration = otel?.OpenTelemetry;
884
+ if (OpenTelemetryIntegration === void 0) {
885
+ console.warn(
886
+ "[rius] Vercel AI SDK v7+ found, but its spans require the @ai-sdk/otel package, which is not installed \u2014 `ai` calls will not be traced. Install it (npm i @ai-sdk/otel) and rius registers it automatically."
887
+ );
888
+ return false;
889
+ }
890
+ const bag = globalThis;
891
+ if (vercelTelemetryIntegration !== void 0 && bag.AI_SDK_TELEMETRY_INTEGRATIONS) {
892
+ bag.AI_SDK_TELEMETRY_INTEGRATIONS = bag.AI_SDK_TELEMETRY_INTEGRATIONS.filter(
893
+ (integration2) => integration2 !== vercelTelemetryIntegration
894
+ );
895
+ }
896
+ const integration = new OpenTelemetryIntegration({
897
+ tracer: tracerProvider.getTracer(TRACER_NAME, SDK_VERSION)
898
+ });
899
+ vercelTelemetryIntegration = integration;
900
+ register(integration);
901
+ teardown?.push(() => {
902
+ if (bag.AI_SDK_TELEMETRY_INTEGRATIONS) {
903
+ bag.AI_SDK_TELEMETRY_INTEGRATIONS = bag.AI_SDK_TELEMETRY_INTEGRATIONS.filter(
904
+ (candidate) => candidate !== integration
905
+ );
906
+ }
907
+ if (vercelTelemetryIntegration === integration) vercelTelemetryIntegration = void 0;
908
+ });
909
+ return true;
910
+ }
911
+ async function enableInstrumentations(sink, tracerProvider, names, teardown) {
718
912
  const wanted = names ? REGISTRY.filter((e) => names.includes(e.name)) : REGISTRY;
719
913
  const enabled = [];
720
914
  for (const entry of wanted) {
721
915
  try {
722
- const loaded = await entry.load();
916
+ const loaded = await entry.load(tracerProvider, teardown);
723
917
  if (loaded === void 0) continue;
724
918
  if (entry.kind === "processor") {
725
919
  if (entry.insert === "first") sink.addFirst(loaded);
726
920
  else sink.add(loaded);
727
921
  } else if (entry.kind === "instrumentation") {
728
- (0, import_instrumentation.registerInstrumentations)({
922
+ const disable = (0, import_instrumentation.registerInstrumentations)({
729
923
  instrumentations: [loaded],
730
924
  tracerProvider
731
925
  });
926
+ teardown?.push(disable);
732
927
  }
733
928
  enabled.push(entry.name);
734
929
  } catch (error) {
@@ -737,7 +932,7 @@ async function enableInstrumentations(sink, tracerProvider, names) {
737
932
  }
738
933
  return enabled;
739
934
  }
740
- var import_node_module2, import_node_path, import_instrumentation, dynamicImport, REGISTRY;
935
+ var import_node_module2, import_node_path, import_instrumentation, dynamicImport, vercelTelemetryIntegration, REGISTRY;
741
936
  var init_instrumentation = __esm({
742
937
  "src/instrumentation.ts"() {
743
938
  "use strict";
@@ -745,6 +940,8 @@ var init_instrumentation = __esm({
745
940
  import_node_module2 = require("module");
746
941
  import_node_path = require("path");
747
942
  import_instrumentation = require("@opentelemetry/instrumentation");
943
+ init_semconv();
944
+ init_version();
748
945
  dynamicImport = new Function("s", "return import(s)");
749
946
  REGISTRY = [
750
947
  {
@@ -755,15 +952,16 @@ var init_instrumentation = __esm({
755
952
  // transform has to run before the exporting processor sees the span or the
756
953
  // attributes it added would never be sanitised.
757
954
  insert: "first",
758
- async load() {
955
+ async load(tracerProvider, teardown) {
956
+ const registered = tracerProvider === void 0 ? false : await registerVercelTelemetry(tracerProvider, teardown);
759
957
  const utils = await optional("@arizeai/openinference-vercel/utils");
760
958
  const add = utils?.addOpenInferenceAttributesToSpan;
761
- if (add === void 0) return void 0;
959
+ if (add === void 0 && !registered) return void 0;
762
960
  return {
763
961
  onStart() {
764
962
  },
765
963
  onEnd(span) {
766
- add(span);
964
+ add?.(span);
767
965
  },
768
966
  async forceFlush() {
769
967
  },
@@ -813,14 +1011,18 @@ var init_instrumentation = __esm({
813
1011
  {
814
1012
  name: "mcp",
815
1013
  kind: "self-applying",
816
- async load() {
1014
+ async load(_tracerProvider, teardown) {
817
1015
  const mod = await optional("@modelcontextprotocol/sdk/client/index.js");
818
1016
  const ClientClass = mod?.Client;
819
1017
  if (ClientClass === void 0) return void 0;
820
1018
  const { instrumentMcpClient: instrumentMcpClient2 } = await Promise.resolve().then(() => (init_instrumentationMcp(), instrumentationMcp_exports));
821
- instrumentMcpClient2(ClientClass);
1019
+ const uninstallEsm = instrumentMcpClient2(ClientClass);
1020
+ teardown?.push(uninstallEsm);
822
1021
  const cjs = cachedCjsExports("@modelcontextprotocol/sdk", mcpClientPatchable);
823
- if (cjs !== void 0) instrumentMcpClient2(cjs.Client);
1022
+ if (cjs !== void 0) {
1023
+ const uninstallCjs = instrumentMcpClient2(cjs.Client);
1024
+ teardown?.push(uninstallCjs);
1025
+ }
824
1026
  return true;
825
1027
  }
826
1028
  }
@@ -834,16 +1036,35 @@ function isContentKey(key) {
834
1036
  if (CONTENT_ATTRIBUTE_PREFIXES.some((p) => key.startsWith(p))) return true;
835
1037
  return CONTENT_ATTRIBUTE_SUFFIXES.some((s) => key.endsWith(s));
836
1038
  }
837
- var import_api3, EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, MaskingSpanExporter;
1039
+ function redactInvocationParameters(value) {
1040
+ if (typeof value !== "string") return void 0;
1041
+ let parameters;
1042
+ try {
1043
+ parameters = JSON.parse(value);
1044
+ } catch {
1045
+ return void 0;
1046
+ }
1047
+ if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) {
1048
+ return void 0;
1049
+ }
1050
+ const bag = parameters;
1051
+ if (!INVOCATION_PARAMETERS_CONTENT_MEMBERS.some((member) => member in bag)) {
1052
+ return value;
1053
+ }
1054
+ for (const member of INVOCATION_PARAMETERS_CONTENT_MEMBERS) delete bag[member];
1055
+ return JSON.stringify(bag);
1056
+ }
1057
+ var import_api4, EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, STATUS_DESCRIPTION_KEY, MaskingSpanExporter;
838
1058
  var init_masking = __esm({
839
1059
  "src/masking.ts"() {
840
1060
  "use strict";
841
1061
  init_cjs_shims();
842
- import_api3 = require("@opentelemetry/api");
1062
+ import_api4 = require("@opentelemetry/api");
843
1063
  init_semconv();
844
1064
  init_serde();
845
1065
  EXCEPTION_EVENT_NAME = "exception";
846
1066
  EXCEPTION_CONTENT_KEYS = ["exception.message", "exception.stacktrace"];
1067
+ STATUS_DESCRIPTION_KEY = "status.description";
847
1068
  MaskingSpanExporter = class {
848
1069
  constructor(inner, opts) {
849
1070
  this.inner = inner;
@@ -869,14 +1090,42 @@ var init_masking = __esm({
869
1090
  for (const link of span.links ?? []) {
870
1091
  this.sanitizeAttributes(link.attributes);
871
1092
  }
872
- if (!this.opts.captureContent && span.status?.code === import_api3.SpanStatusCode.ERROR) {
873
- span.status.message = void 0;
874
- }
1093
+ this.sanitizeStatus(span);
875
1094
  return span;
876
1095
  }
1096
+ /**
1097
+ * Drops or masks the status description, in place. `status` is mutable and
1098
+ * exposed by reference on the SDK span, same as `attributes`, so mutating it
1099
+ * here reaches the span about to be handed to the inner exporter. The ERROR
1100
+ * code itself is left alone so the failure stays visible and classifiable,
1101
+ * the same policy as `exception.type`.
1102
+ */
1103
+ sanitizeStatus(span) {
1104
+ const status = span.status;
1105
+ if (status?.code !== import_api4.SpanStatusCode.ERROR || !status.message) return;
1106
+ if (!this.opts.captureContent) {
1107
+ status.message = void 0;
1108
+ return;
1109
+ }
1110
+ if (this.opts.mask === void 0) return;
1111
+ try {
1112
+ const masked = toAttributeValue(
1113
+ this.opts.mask(status.message, { key: STATUS_DESCRIPTION_KEY })
1114
+ );
1115
+ status.message = typeof masked === "string" ? masked : String(masked);
1116
+ } catch {
1117
+ status.message = "[mask error]";
1118
+ }
1119
+ }
877
1120
  /** Strips or masks the content keys of one attribute bag, in place. */
878
1121
  sanitizeAttributes(attributes) {
879
1122
  if (attributes === void 0) return;
1123
+ const sanitizing = !this.opts.captureContent || this.opts.mask !== void 0;
1124
+ if (sanitizing && LLM_INVOCATION_PARAMETERS in attributes) {
1125
+ const redacted = redactInvocationParameters(attributes[LLM_INVOCATION_PARAMETERS]);
1126
+ if (redacted === void 0) delete attributes[LLM_INVOCATION_PARAMETERS];
1127
+ else attributes[LLM_INVOCATION_PARAMETERS] = redacted;
1128
+ }
880
1129
  for (const key of Object.keys(attributes)) {
881
1130
  if (!isContentKey(key)) continue;
882
1131
  if (!this.opts.captureContent) {
@@ -923,7 +1172,7 @@ function buildSnapshot(span) {
923
1172
  startTime: span.startTime,
924
1173
  endTime: span.startTime,
925
1174
  duration: [0, 0],
926
- status: { code: import_api4.SpanStatusCode.UNSET },
1175
+ status: { code: import_api5.SpanStatusCode.UNSET },
927
1176
  attributes,
928
1177
  links: [],
929
1178
  events: [],
@@ -935,15 +1184,15 @@ function buildSnapshot(span) {
935
1184
  ended: true
936
1185
  };
937
1186
  }
938
- function spanKey(context) {
939
- return `${context.traceId}:${context.spanId}`;
1187
+ function spanKey(context2) {
1188
+ return `${context2.traceId}:${context2.spanId}`;
940
1189
  }
941
- var import_api4, PendingSpanProcessor;
1190
+ var import_api5, PendingSpanProcessor;
942
1191
  var init_pending = __esm({
943
1192
  "src/pending.ts"() {
944
1193
  "use strict";
945
1194
  init_cjs_shims();
946
- import_api4 = require("@opentelemetry/api");
1195
+ import_api5 = require("@opentelemetry/api");
947
1196
  init_semconv();
948
1197
  PendingSpanProcessor = class {
949
1198
  delegate;
@@ -991,17 +1240,17 @@ var init_pending = __esm({
991
1240
  // src/session.ts
992
1241
  function withSession(sessionIdOrFn, maybeFn) {
993
1242
  const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [(0, import_node_crypto.randomUUID)(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
994
- return import_api5.context.with(import_api5.context.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
1243
+ return import_api6.context.with(import_api6.context.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
995
1244
  }
996
- var import_node_crypto, import_api5, SESSION_KEY, SessionSpanProcessor;
1245
+ var import_node_crypto, import_api6, SESSION_KEY, SessionSpanProcessor;
997
1246
  var init_session = __esm({
998
1247
  "src/session.ts"() {
999
1248
  "use strict";
1000
1249
  init_cjs_shims();
1001
1250
  import_node_crypto = require("crypto");
1002
- import_api5 = require("@opentelemetry/api");
1251
+ import_api6 = require("@opentelemetry/api");
1003
1252
  init_semconv();
1004
- SESSION_KEY = (0, import_api5.createContextKey)("rius-session-id");
1253
+ SESSION_KEY = (0, import_api6.createContextKey)("rius-session-id");
1005
1254
  SessionSpanProcessor = class {
1006
1255
  constructor(defaultSessionId) {
1007
1256
  this.defaultSessionId = defaultSessionId;
@@ -1009,7 +1258,9 @@ var init_session = __esm({
1009
1258
  defaultSessionId;
1010
1259
  onStart(span, parentContext) {
1011
1260
  const value = parentContext.getValue(SESSION_KEY);
1012
- const sessionId = typeof value === "string" ? value : this.defaultSessionId;
1261
+ const parent = import_api6.trace.getSpan(parentContext);
1262
+ const inherited = parent?.attributes?.[SESSION_ID];
1263
+ const sessionId = typeof value === "string" ? value : typeof inherited === "string" ? inherited : this.defaultSessionId;
1013
1264
  if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
1014
1265
  }
1015
1266
  onEnd(_span) {
@@ -1025,7 +1276,7 @@ var init_session = __esm({
1025
1276
  // src/workspace.ts
1026
1277
  function withWorkspace(alias, fn) {
1027
1278
  if (!alias) throw new Error("workspace alias must be a non-empty string");
1028
- return import_api6.context.with(import_api6.context.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1279
+ return import_api7.context.with(import_api7.context.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1029
1280
  }
1030
1281
  function setGlobalRouting(routing) {
1031
1282
  globalRouting = routing;
@@ -1038,25 +1289,32 @@ function registerWorkspace(alias, apiKey) {
1038
1289
  }
1039
1290
  globalRouting.register(alias, apiKey);
1040
1291
  }
1041
- var import_api6, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1292
+ var import_api7, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1042
1293
  var init_workspace = __esm({
1043
1294
  "src/workspace.ts"() {
1044
1295
  "use strict";
1045
1296
  init_cjs_shims();
1046
- import_api6 = require("@opentelemetry/api");
1297
+ import_api7 = require("@opentelemetry/api");
1047
1298
  init_esm();
1048
1299
  init_semconv();
1049
- WORKSPACE_KEY = (0, import_api6.createContextKey)("rius-workspace-alias");
1300
+ WORKSPACE_KEY = (0, import_api7.createContextKey)("rius-workspace-alias");
1050
1301
  WorkspaceSpanProcessor = class {
1302
+ /** Straddles already warned about, as "parentAlias->alias"; one warning each, not one per span. */
1303
+ warnedStraddles = /* @__PURE__ */ new Set();
1051
1304
  onStart(span, parentContext) {
1052
- const alias = parentContext.getValue(WORKSPACE_KEY);
1053
- if (typeof alias !== "string") return;
1054
- const parent = import_api6.trace.getSpan(parentContext);
1305
+ const parent = import_api7.trace.getSpan(parentContext);
1055
1306
  const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1307
+ const scoped = parentContext.getValue(WORKSPACE_KEY);
1308
+ const alias = typeof scoped === "string" ? scoped : parentAlias;
1309
+ if (typeof alias !== "string") return;
1056
1310
  if (parentAlias !== void 0 && parentAlias !== alias) {
1057
- console.warn(
1058
- `[rius] span "${span.name}" starts under workspace "${alias}" but its parent is stamped "${String(parentAlias)}"; a trace cannot straddle two workspaces (the backend derives the workspace from the API key). Switch workspaces at request boundaries, before the root span starts.`
1059
- );
1311
+ const straddle = `${String(parentAlias)}->${alias}`;
1312
+ if (!this.warnedStraddles.has(straddle)) {
1313
+ this.warnedStraddles.add(straddle);
1314
+ console.warn(
1315
+ `[rius] span "${span.name}" starts under workspace "${alias}" but its parent is stamped "${String(parentAlias)}"; a trace cannot straddle two workspaces (the backend derives the workspace from the API key). Switch workspaces at request boundaries, before the root span starts. Further spans of this straddle are not reported.`
1316
+ );
1317
+ }
1060
1318
  }
1061
1319
  span.setAttribute(WORKSPACE_ROUTE, alias);
1062
1320
  }
@@ -1177,6 +1435,7 @@ function init(options = {}) {
1177
1435
  }) : health;
1178
1436
  const batch = new import_sdk_trace_base.BatchSpanProcessor(exporter);
1179
1437
  processors.add(new SessionSpanProcessor(config.sessionId));
1438
+ processors.add(new UserSpanProcessor());
1180
1439
  if (routing !== void 0) {
1181
1440
  processors.add(new WorkspaceSpanProcessor());
1182
1441
  }
@@ -1187,9 +1446,13 @@ function init(options = {}) {
1187
1446
  }
1188
1447
  const instanceId = (0, import_node_crypto2.randomUUID)();
1189
1448
  const provider = new import_sdk_trace_node.NodeTracerProvider({
1449
+ // telemetry.sdk.* is reserved for the OTel SDK itself; we identify as a
1450
+ // distribution via telemetry.distro.*, the same two keys Python stamps.
1190
1451
  resource: (0, import_resources.resourceFromAttributes)({
1191
1452
  [import_semantic_conventions.ATTR_SERVICE_NAME]: config.serviceName,
1192
- [SERVICE_INSTANCE_ID]: instanceId
1453
+ [SERVICE_INSTANCE_ID]: instanceId,
1454
+ "telemetry.distro.name": "glassflow-rius",
1455
+ "telemetry.distro.version": SDK_VERSION
1193
1456
  }),
1194
1457
  // Always ParentBased, with no AlwaysOn shortcut at rate 1. They are not
1195
1458
  // equivalent: ParentBased honours a remote UNSAMPLED parent and drops,
@@ -1199,10 +1462,12 @@ function init(options = {}) {
1199
1462
  sampler: new import_sdk_trace_base.ParentBasedSampler({ root: new import_sdk_trace_base.TraceIdRatioBasedSampler(config.sampleRate) }),
1200
1463
  spanProcessors: [processors]
1201
1464
  });
1202
- const ready = config.disabled ? Promise.resolve([]) : enableInstrumentations(processors, provider).catch(() => []);
1465
+ const teardown = [];
1466
+ const ready = config.disabled ? Promise.resolve([]) : enableInstrumentations(processors, provider, void 0, teardown);
1203
1467
  provider.register();
1204
1468
  let heartbeat;
1205
- if (config.heartbeat && !config.disabled) {
1469
+ const heartbeatCanAuthenticate = config.apiKey !== void 0 || options.heartbeatTransport !== void 0;
1470
+ if (config.heartbeat && !config.disabled && heartbeatCanAuthenticate) {
1206
1471
  const tracker = new OpenRootSpanTracker();
1207
1472
  processors.add(tracker);
1208
1473
  const sender = new HeartbeatSender({
@@ -1221,20 +1486,20 @@ function init(options = {}) {
1221
1486
  process.once("beforeExit", beforeExitHandler);
1222
1487
  heartbeat = { sender, beforeExitHandler };
1223
1488
  }
1224
- globalClient = createClient({ provider, processors, health, ready, heartbeat });
1489
+ globalClient = createClient({ provider, processors, health, ready, teardown, heartbeat });
1225
1490
  setGlobalRouting(routing);
1226
1491
  return globalClient;
1227
1492
  }
1228
1493
  function getTracer() {
1229
- return import_api7.trace.getTracer(TRACER_NAME);
1494
+ return import_api8.trace.getTracer(TRACER_NAME, SDK_VERSION);
1230
1495
  }
1231
- var import_node_crypto2, import_api7, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1496
+ var import_node_crypto2, import_api8, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1232
1497
  var init_client = __esm({
1233
1498
  "src/client.ts"() {
1234
1499
  "use strict";
1235
1500
  init_cjs_shims();
1236
1501
  import_node_crypto2 = require("crypto");
1237
- import_api7 = require("@opentelemetry/api");
1502
+ import_api8 = require("@opentelemetry/api");
1238
1503
  import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
1239
1504
  import_resources = require("@opentelemetry/resources");
1240
1505
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -1249,18 +1514,22 @@ var init_client = __esm({
1249
1514
  init_pending();
1250
1515
  init_semconv();
1251
1516
  init_session();
1517
+ init_user();
1518
+ init_version();
1252
1519
  init_workspace();
1253
1520
  sinks = /* @__PURE__ */ new WeakMap();
1254
1521
  heartbeats = /* @__PURE__ */ new WeakMap();
1255
1522
  RiusClient = class _RiusClient {
1256
1523
  provider;
1257
1524
  health;
1525
+ teardown;
1258
1526
  /** Resolves with the names of the auto-instrumentations that attached. */
1259
1527
  ready;
1260
1528
  constructor(parts) {
1261
1529
  this.provider = parts.provider;
1262
1530
  this.health = parts.health;
1263
1531
  this.ready = parts.ready;
1532
+ this.teardown = parts.teardown;
1264
1533
  sinks.set(this, parts.processors);
1265
1534
  if (parts.heartbeat) heartbeats.set(this, parts.heartbeat);
1266
1535
  }
@@ -1288,13 +1557,22 @@ var init_client = __esm({
1288
1557
  process.removeListener("beforeExit", heartbeat.beforeExitHandler);
1289
1558
  await heartbeat.sender.stop();
1290
1559
  }
1560
+ await this.ready;
1561
+ for (const disable of this.teardown.splice(0)) {
1562
+ try {
1563
+ disable();
1564
+ } catch {
1565
+ }
1566
+ }
1291
1567
  try {
1292
1568
  await this.provider.shutdown();
1293
1569
  } finally {
1294
1570
  if (globalClient === this) {
1295
1571
  globalClient = void 0;
1296
1572
  setGlobalRouting(void 0);
1297
- import_api7.trace.disable();
1573
+ import_api8.trace.disable();
1574
+ import_api8.context.disable();
1575
+ import_api8.propagation.disable();
1298
1576
  }
1299
1577
  }
1300
1578
  }
@@ -1319,6 +1597,7 @@ __export(index_exports, {
1319
1597
  startGeneration: () => startGeneration,
1320
1598
  startSpan: () => startSpan,
1321
1599
  withSession: () => withSession,
1600
+ withUser: () => withUser,
1322
1601
  withWorkspace: () => withWorkspace
1323
1602
  });
1324
1603
  module.exports = __toCommonJS(index_exports);
@@ -1328,17 +1607,106 @@ init_client();
1328
1607
  // src/generation.ts
1329
1608
  init_cjs_shims();
1330
1609
  init_client();
1610
+
1611
+ // src/messages.ts
1612
+ init_cjs_shims();
1613
+ init_serde();
1614
+ function isRecord(value) {
1615
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1616
+ }
1617
+ function asText(value) {
1618
+ if (typeof value === "string") return value;
1619
+ const text = toAttributeValue(value);
1620
+ return typeof text === "string" ? text : String(text);
1621
+ }
1622
+ function normalizePart(item) {
1623
+ if (isRecord(item)) {
1624
+ if (item.type === "text" && "text" in item) return { type: "text", content: item.text };
1625
+ if ("type" in item) return item;
1626
+ }
1627
+ return { type: "text", content: asText(item) };
1628
+ }
1629
+ function normalizeMessage(message, defaultRole) {
1630
+ if (typeof message === "string") {
1631
+ return { role: defaultRole, parts: [{ type: "text", content: message }] };
1632
+ }
1633
+ if (!isRecord(message)) {
1634
+ return { role: defaultRole, parts: [{ type: "text", content: asText(message) }] };
1635
+ }
1636
+ if ("parts" in message) {
1637
+ return { ...message, role: message.role ?? defaultRole };
1638
+ }
1639
+ const role = message.role ?? defaultRole;
1640
+ if (role === "tool" && "tool_call_id" in message) {
1641
+ return {
1642
+ role: "tool",
1643
+ parts: [{ type: "tool_call_response", id: message.tool_call_id, response: message.content }]
1644
+ };
1645
+ }
1646
+ const parts = [];
1647
+ const content = message.content;
1648
+ if (typeof content === "string") {
1649
+ parts.push({ type: "text", content });
1650
+ } else if (Array.isArray(content)) {
1651
+ for (const item of content) parts.push(normalizePart(item));
1652
+ } else if (content !== null && content !== void 0) {
1653
+ parts.push({ type: "text", content: asText(content) });
1654
+ }
1655
+ const toolCalls = message.tool_calls;
1656
+ if (Array.isArray(toolCalls)) {
1657
+ for (const call of toolCalls) {
1658
+ if (!isRecord(call)) continue;
1659
+ const fn = isRecord(call.function) ? call.function : {};
1660
+ parts.push({ type: "tool_call", id: call.id, name: fn.name, arguments: fn.arguments });
1661
+ }
1662
+ }
1663
+ return { role, parts };
1664
+ }
1665
+ function serializeMessages(messages, defaultRole) {
1666
+ const list = Array.isArray(messages) ? messages : [messages];
1667
+ return toAttributeValue(list.map((message) => normalizeMessage(message, defaultRole)));
1668
+ }
1669
+
1670
+ // src/generation.ts
1331
1671
  init_semconv();
1332
1672
  init_serde();
1333
1673
  init_spans();
1334
1674
  var Generation = class extends Observation {
1675
+ /**
1676
+ * The provider passed at creation; drives the Anthropic input-token summing
1677
+ * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
1678
+ */
1679
+ constructor(span, provider) {
1680
+ super(span);
1681
+ this.provider = provider;
1682
+ }
1683
+ provider;
1335
1684
  firstTokenRecorded = false;
1685
+ /**
1686
+ * Record the request messages (`gen_ai.input.messages`), normalised to the
1687
+ * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
1688
+ * dicts (including `tool_calls` and tool responses) and multimodal content
1689
+ * lists are all accepted. Bare strings default to the `user` role.
1690
+ */
1336
1691
  setInput(value) {
1337
- this.span.setAttribute(GEN_AI_INPUT_MESSAGES, toAttributeValue(value));
1692
+ this.span.setAttribute(GEN_AI_INPUT_MESSAGES, serializeMessages(value, "user"));
1338
1693
  return this;
1339
1694
  }
1695
+ /** Record the response messages (`gen_ai.output.messages`); bare strings default to `assistant`. */
1340
1696
  setOutput(value) {
1341
- this.span.setAttribute(GEN_AI_OUTPUT_MESSAGES, toAttributeValue(value));
1697
+ this.span.setAttribute(GEN_AI_OUTPUT_MESSAGES, serializeMessages(value, "assistant"));
1698
+ return this;
1699
+ }
1700
+ /**
1701
+ * The request's tool/function definitions (`gen_ai.tool.definitions`).
1702
+ * Serialized verbatim, in whatever shape the provider request used (OpenAI
1703
+ * nests each tool under `function`, Anthropic uses top-level
1704
+ * `name`/`input_schema`) — no normalization, so what is recorded is exactly
1705
+ * what the model was shown. Definitions are content, not identity: they are
1706
+ * masked/stripped under `captureContent: false` like messages are.
1707
+ */
1708
+ setToolDefinitions(tools) {
1709
+ this.span.setAttribute(GEN_AI_TOOL_DEFINITIONS, toAttributeValue(tools));
1342
1710
  return this;
1343
1711
  }
1344
1712
  setModel(model) {
@@ -1346,15 +1714,20 @@ var Generation = class extends Observation {
1346
1714
  return this;
1347
1715
  }
1348
1716
  /**
1349
- * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is.
1350
- * Per the GenAI conventions, `inputTokens` is the total including cached
1351
- * tokens (the cache counts are subsets of it); providers that report an
1352
- * exclusive `inputTokens` (e.g. Anthropic) are detected and normalized by
1353
- * the backend, so no client-side arithmetic is needed.
1717
+ * Token usage (`gen_ai.usage.*`). Pass provider-reported values as-is;
1718
+ * never pre-add anything. Per the GenAI conventions, `inputTokens` is the
1719
+ * total including cached tokens (the cache counts are subsets of it).
1720
+ * Anthropic's API reports `input_tokens` excluding the cache counts, and
1721
+ * the conventions require the instrumentation to do the summing, so when
1722
+ * the generation's provider is `"anthropic"` the emitted total is
1723
+ * `inputTokens` plus both cache counts. Every other provider is recorded
1724
+ * verbatim.
1354
1725
  */
1355
1726
  setUsage(usage) {
1356
1727
  if (usage.inputTokens !== void 0) {
1357
- this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, usage.inputTokens);
1728
+ const sums = this.provider?.toLowerCase() === "anthropic";
1729
+ const total = sums ? usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) : usage.inputTokens;
1730
+ this.span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, total);
1358
1731
  }
1359
1732
  if (usage.outputTokens !== void 0) {
1360
1733
  this.span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage.outputTokens);
@@ -1365,6 +1738,9 @@ var Generation = class extends Observation {
1365
1738
  if (usage.cacheWriteInputTokens !== void 0) {
1366
1739
  this.span.setAttribute(GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS, usage.cacheWriteInputTokens);
1367
1740
  }
1741
+ if (usage.reasoningOutputTokens !== void 0) {
1742
+ this.span.setAttribute(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.reasoningOutputTokens);
1743
+ }
1368
1744
  return this;
1369
1745
  }
1370
1746
  /**
@@ -1396,34 +1772,36 @@ var Generation = class extends Observation {
1396
1772
  };
1397
1773
  function attributesFor(options) {
1398
1774
  const attributes = { ...kindAttributes("LLM" /* LLM */) };
1775
+ if (options.operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = options.operation;
1399
1776
  if (options.model !== void 0) attributes[GEN_AI_REQUEST_MODEL] = options.model;
1400
1777
  if (options.provider !== void 0) attributes[GEN_AI_PROVIDER_NAME] = options.provider;
1778
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
1401
1779
  return attributes;
1402
1780
  }
1403
1781
  function configure2(generation, options) {
1404
1782
  for (const [key, value] of Object.entries(options.modelParameters ?? {})) {
1405
1783
  generation.setAttribute(`${GEN_AI_REQUEST_PREFIX}${key}`, value);
1406
1784
  }
1785
+ if (options.reasoningLevel !== void 0) {
1786
+ generation.setAttribute(GEN_AI_REQUEST_REASONING_LEVEL, options.reasoningLevel);
1787
+ }
1788
+ if (options.tools !== void 0) generation.setToolDefinitions(options.tools);
1407
1789
  if (options.input !== void 0) generation.setInput(options.input);
1408
1790
  return generation;
1409
1791
  }
1410
1792
  function startGeneration(name, options = {}) {
1411
1793
  const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1412
- return configure2(new Generation(span), options);
1794
+ return configure2(new Generation(span, options.provider), options);
1413
1795
  }
1414
1796
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1415
1797
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1416
- return getTracer().startActiveSpan(name, { attributes: attributesFor(options) }, async (span) => {
1417
- const generation = configure2(new Generation(span), options);
1418
- try {
1419
- return await fn(generation);
1420
- } catch (error) {
1421
- generation.recordException(error);
1422
- throw error;
1423
- } finally {
1424
- generation.end();
1425
- }
1426
- });
1798
+ return runActive(
1799
+ name,
1800
+ attributesFor(options),
1801
+ options.userId,
1802
+ (span) => configure2(new Generation(span, options.provider), options),
1803
+ fn
1804
+ );
1427
1805
  }
1428
1806
 
1429
1807
  // src/observe.ts
@@ -1435,7 +1813,10 @@ function observe(fn, options = {}) {
1435
1813
  const captureOutput = options.captureOutput ?? true;
1436
1814
  const wrapped = (...args) => startAsCurrentSpan(
1437
1815
  name,
1438
- { kind: options.kind, input: captureInput ? args : void 0 },
1816
+ // {args, kwargs} is the Python SDK's shape; JavaScript has no keyword
1817
+ // arguments, so kwargs is always empty, but the key stays so a saved
1818
+ // search or a console view reads both SDKs' input.value the same way.
1819
+ { kind: options.kind, input: captureInput ? { args, kwargs: {} } : void 0 },
1439
1820
  async (observation) => {
1440
1821
  const result = await fn(...args);
1441
1822
  if (captureOutput && result !== void 0) observation.setOutput(result);
@@ -1449,9 +1830,10 @@ function observe(fn, options = {}) {
1449
1830
  // src/index.ts
1450
1831
  init_semconv();
1451
1832
  init_session();
1833
+ init_user();
1452
1834
  init_spans();
1453
1835
  init_workspace();
1454
- var VERSION = "0.4.0";
1836
+ var VERSION = "0.6.0";
1455
1837
  // Annotate the CommonJS export names for ESM import in node:
1456
1838
  0 && (module.exports = {
1457
1839
  Generation,
@@ -1468,5 +1850,6 @@ var VERSION = "0.4.0";
1468
1850
  startGeneration,
1469
1851
  startSpan,
1470
1852
  withSession,
1853
+ withUser,
1471
1854
  withWorkspace
1472
1855
  });