@glassflow-ai/rius 0.5.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_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_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,6 +384,7 @@ 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";
@@ -382,6 +400,7 @@ var init_semconv = __esm({
382
400
  GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages";
383
401
  GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons";
384
402
  GEN_AI_TOOL_NAME = "gen_ai.tool.name";
403
+ GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions";
385
404
  GEN_AI_REQUEST_PREFIX = "gen_ai.request.";
386
405
  MCP_RESULT_TYPE = "mcp.result_type";
387
406
  GEN_AI_FIRST_TOKEN_EVENT = "gen_ai.first_token";
@@ -410,7 +429,18 @@ var init_semconv = __esm({
410
429
  // or internal URLs in parameter defaults. gen_ai.tool.name stays: it is
411
430
  // identity, not content.
412
431
  "gen_ai.tool.description",
413
- "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",
414
444
  "gen_ai.prompt",
415
445
  "gen_ai.completion",
416
446
  "llm.input_messages",
@@ -424,15 +454,44 @@ var init_semconv = __esm({
424
454
  "mlflow.spanInputs",
425
455
  "mlflow.spanOutputs",
426
456
  "traceloop.entity.input",
427
- "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"
428
483
  ]);
484
+ LLM_INVOCATION_PARAMETERS = "llm.invocation_parameters";
485
+ INVOCATION_PARAMETERS_CONTENT_MEMBERS = ["tools", "functions"];
429
486
  CONTENT_ATTRIBUTE_PREFIXES = [
430
487
  "llm.input_messages.",
431
488
  "llm.output_messages.",
432
489
  "gen_ai.prompt.",
433
490
  "gen_ai.completion.",
434
491
  "llm.prompts.",
435
- "llm.prompt_template."
492
+ "llm.prompt_template.",
493
+ "llm.tools.",
494
+ "ai.prompt."
436
495
  ];
437
496
  CONTENT_ATTRIBUTE_SUFFIXES = [
438
497
  ".document.content",
@@ -447,6 +506,9 @@ var init_semconv = __esm({
447
506
  // Identity, not content: a pending span must be groupable into its
448
507
  // session while still running, that is the live view's whole point.
449
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,
450
512
  // Routing, not content: a crashed run's snapshot must land in the same
451
513
  // workspace its final span would have. Stripped at export either way.
452
514
  WORKSPACE_ROUTE
@@ -456,31 +518,77 @@ var init_semconv = __esm({
456
518
  });
457
519
 
458
520
  // src/serde.ts
459
- function toAttributeValue(value) {
460
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
461
- 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>`;
462
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;
463
537
  if (value === null || value === void 0) return "";
538
+ if (typeof value === "bigint") return String(value);
464
539
  const ancestors = [];
465
540
  try {
466
- return JSON.stringify(value, function(_key, v) {
467
- 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) {
468
545
  while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
469
546
  ancestors.pop();
470
547
  }
471
- if (ancestors.includes(v)) return "[Circular]";
472
- ancestors.push(v);
548
+ if (ancestors.includes(rendered)) return "[Circular]";
549
+ ancestors.push(rendered);
473
550
  }
474
- return v;
551
+ return rendered;
475
552
  }) ?? String(value);
553
+ return truncate(text);
476
554
  } catch {
477
555
  return "[unserializable]";
478
556
  }
479
557
  }
558
+ var MAX_ATTR_CHARS, TRUNCATION_MARKER;
480
559
  var init_serde = __esm({
481
560
  "src/serde.ts"() {
482
561
  "use strict";
483
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
+ };
484
592
  }
485
593
  });
486
594
 
@@ -489,39 +597,50 @@ function configure(observation, options) {
489
597
  if (options.input !== void 0) observation.setInput(options.input);
490
598
  return observation;
491
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
+ }
492
605
  function startSpan(name, options = {}) {
493
- const span = getTracer().startSpan(name, {
494
- attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */)
495
- });
606
+ const span = getTracer().startSpan(name, { attributes: creationAttributes(options) });
496
607
  return configure(new Observation(span), options);
497
608
  }
498
609
  function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
499
610
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
500
- return getTracer().startActiveSpan(
611
+ return runActive(
501
612
  name,
502
- { attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */) },
503
- async (span) => {
504
- const observation = configure(new Observation(span), options);
505
- try {
506
- return await fn(observation);
507
- } catch (error) {
508
- observation.recordException(error);
509
- throw error;
510
- } finally {
511
- observation.end();
512
- }
513
- }
613
+ creationAttributes(options),
614
+ options.userId,
615
+ (span) => configure(new Observation(span), options),
616
+ fn
514
617
  );
515
618
  }
516
- 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;
517
634
  var init_spans = __esm({
518
635
  "src/spans.ts"() {
519
636
  "use strict";
520
637
  init_cjs_shims();
521
- import_api = require("@opentelemetry/api");
638
+ import_api2 = require("@opentelemetry/api");
522
639
  init_client();
523
640
  init_semconv();
524
641
  init_serde();
642
+ init_user();
643
+ Symbol.dispose ??= /* @__PURE__ */ Symbol.for("Symbol.dispose");
525
644
  Observation = class {
526
645
  constructor(span) {
527
646
  this.span = span;
@@ -536,7 +655,26 @@ var init_spans = __esm({
536
655
  this.span.setAttribute(OUTPUT_VALUE, toAttributeValue(value));
537
656
  return this;
538
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
+ */
539
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
+ }
540
678
  this.span.setAttribute(key, toAttributeValue(value));
541
679
  return this;
542
680
  }
@@ -551,7 +689,7 @@ var init_spans = __esm({
551
689
  recordException(error) {
552
690
  const wrapped = error instanceof Error ? error : new Error(String(error));
553
691
  this.span.recordException(wrapped);
554
- this.span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: wrapped.message });
692
+ this.span.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: wrapped.message });
555
693
  return this;
556
694
  }
557
695
  end() {
@@ -572,6 +710,21 @@ var instrumentationMcp_exports = {};
572
710
  __export(instrumentationMcp_exports, {
573
711
  instrumentMcpClient: () => instrumentMcpClient
574
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
+ }
575
728
  function resultIsError(result) {
576
729
  if (typeof result !== "object" || result === null) return false;
577
730
  const record = result;
@@ -583,10 +736,10 @@ function recordResult(observation, result) {
583
736
  observation.setAttribute(MCP_RESULT_TYPE, INPUT_REQUIRED);
584
737
  return;
585
738
  }
586
- observation.setOutput(result);
739
+ observation.setAttribute(OUTPUT_VALUE, serializeResult(result));
587
740
  if (resultIsError(result)) {
588
741
  observation.span.setStatus({
589
- code: import_api2.SpanStatusCode.ERROR,
742
+ code: import_api3.SpanStatusCode.ERROR,
590
743
  message: "tool returned an error result"
591
744
  });
592
745
  }
@@ -603,9 +756,14 @@ function instrumentMcpClient(ClientClass) {
603
756
  const instrumented = function instrumentedCallTool(params, ...rest) {
604
757
  return startAsCurrentSpan(
605
758
  `execute_tool ${params.name}`,
606
- { 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
+ },
607
766
  async (observation) => {
608
- observation.setAttribute(GEN_AI_TOOL_NAME, params.name);
609
767
  const result = await original.call(this, params, ...rest);
610
768
  recordResult(observation, result);
611
769
  return result;
@@ -618,13 +776,14 @@ function instrumentMcpClient(ClientClass) {
618
776
  ClientClass.prototype.callTool = original;
619
777
  };
620
778
  }
621
- var import_api2, INPUT_REQUIRED;
779
+ var import_api3, INPUT_REQUIRED;
622
780
  var init_instrumentationMcp = __esm({
623
781
  "src/instrumentationMcp.ts"() {
624
782
  "use strict";
625
783
  init_cjs_shims();
626
- import_api2 = require("@opentelemetry/api");
784
+ import_api3 = require("@opentelemetry/api");
627
785
  init_semconv();
786
+ init_serde();
628
787
  init_spans();
629
788
  INPUT_REQUIRED = "input_required";
630
789
  }
@@ -716,21 +875,55 @@ function mcpClientPatchable(exports2) {
716
875
  const cls = exports2.Client;
717
876
  return typeof cls?.prototype?.callTool === "function" ? { Client: cls } : void 0;
718
877
  }
719
- 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) {
720
912
  const wanted = names ? REGISTRY.filter((e) => names.includes(e.name)) : REGISTRY;
721
913
  const enabled = [];
722
914
  for (const entry of wanted) {
723
915
  try {
724
- const loaded = await entry.load();
916
+ const loaded = await entry.load(tracerProvider, teardown);
725
917
  if (loaded === void 0) continue;
726
918
  if (entry.kind === "processor") {
727
919
  if (entry.insert === "first") sink.addFirst(loaded);
728
920
  else sink.add(loaded);
729
921
  } else if (entry.kind === "instrumentation") {
730
- (0, import_instrumentation.registerInstrumentations)({
922
+ const disable = (0, import_instrumentation.registerInstrumentations)({
731
923
  instrumentations: [loaded],
732
924
  tracerProvider
733
925
  });
926
+ teardown?.push(disable);
734
927
  }
735
928
  enabled.push(entry.name);
736
929
  } catch (error) {
@@ -739,7 +932,7 @@ async function enableInstrumentations(sink, tracerProvider, names) {
739
932
  }
740
933
  return enabled;
741
934
  }
742
- var import_node_module2, import_node_path, import_instrumentation, dynamicImport, REGISTRY;
935
+ var import_node_module2, import_node_path, import_instrumentation, dynamicImport, vercelTelemetryIntegration, REGISTRY;
743
936
  var init_instrumentation = __esm({
744
937
  "src/instrumentation.ts"() {
745
938
  "use strict";
@@ -747,6 +940,8 @@ var init_instrumentation = __esm({
747
940
  import_node_module2 = require("module");
748
941
  import_node_path = require("path");
749
942
  import_instrumentation = require("@opentelemetry/instrumentation");
943
+ init_semconv();
944
+ init_version();
750
945
  dynamicImport = new Function("s", "return import(s)");
751
946
  REGISTRY = [
752
947
  {
@@ -757,15 +952,16 @@ var init_instrumentation = __esm({
757
952
  // transform has to run before the exporting processor sees the span or the
758
953
  // attributes it added would never be sanitised.
759
954
  insert: "first",
760
- async load() {
955
+ async load(tracerProvider, teardown) {
956
+ const registered = tracerProvider === void 0 ? false : await registerVercelTelemetry(tracerProvider, teardown);
761
957
  const utils = await optional("@arizeai/openinference-vercel/utils");
762
958
  const add = utils?.addOpenInferenceAttributesToSpan;
763
- if (add === void 0) return void 0;
959
+ if (add === void 0 && !registered) return void 0;
764
960
  return {
765
961
  onStart() {
766
962
  },
767
963
  onEnd(span) {
768
- add(span);
964
+ add?.(span);
769
965
  },
770
966
  async forceFlush() {
771
967
  },
@@ -815,14 +1011,18 @@ var init_instrumentation = __esm({
815
1011
  {
816
1012
  name: "mcp",
817
1013
  kind: "self-applying",
818
- async load() {
1014
+ async load(_tracerProvider, teardown) {
819
1015
  const mod = await optional("@modelcontextprotocol/sdk/client/index.js");
820
1016
  const ClientClass = mod?.Client;
821
1017
  if (ClientClass === void 0) return void 0;
822
1018
  const { instrumentMcpClient: instrumentMcpClient2 } = await Promise.resolve().then(() => (init_instrumentationMcp(), instrumentationMcp_exports));
823
- instrumentMcpClient2(ClientClass);
1019
+ const uninstallEsm = instrumentMcpClient2(ClientClass);
1020
+ teardown?.push(uninstallEsm);
824
1021
  const cjs = cachedCjsExports("@modelcontextprotocol/sdk", mcpClientPatchable);
825
- if (cjs !== void 0) instrumentMcpClient2(cjs.Client);
1022
+ if (cjs !== void 0) {
1023
+ const uninstallCjs = instrumentMcpClient2(cjs.Client);
1024
+ teardown?.push(uninstallCjs);
1025
+ }
826
1026
  return true;
827
1027
  }
828
1028
  }
@@ -836,16 +1036,35 @@ function isContentKey(key) {
836
1036
  if (CONTENT_ATTRIBUTE_PREFIXES.some((p) => key.startsWith(p))) return true;
837
1037
  return CONTENT_ATTRIBUTE_SUFFIXES.some((s) => key.endsWith(s));
838
1038
  }
839
- 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;
840
1058
  var init_masking = __esm({
841
1059
  "src/masking.ts"() {
842
1060
  "use strict";
843
1061
  init_cjs_shims();
844
- import_api3 = require("@opentelemetry/api");
1062
+ import_api4 = require("@opentelemetry/api");
845
1063
  init_semconv();
846
1064
  init_serde();
847
1065
  EXCEPTION_EVENT_NAME = "exception";
848
1066
  EXCEPTION_CONTENT_KEYS = ["exception.message", "exception.stacktrace"];
1067
+ STATUS_DESCRIPTION_KEY = "status.description";
849
1068
  MaskingSpanExporter = class {
850
1069
  constructor(inner, opts) {
851
1070
  this.inner = inner;
@@ -871,14 +1090,42 @@ var init_masking = __esm({
871
1090
  for (const link of span.links ?? []) {
872
1091
  this.sanitizeAttributes(link.attributes);
873
1092
  }
874
- if (!this.opts.captureContent && span.status?.code === import_api3.SpanStatusCode.ERROR) {
875
- span.status.message = void 0;
876
- }
1093
+ this.sanitizeStatus(span);
877
1094
  return span;
878
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
+ }
879
1120
  /** Strips or masks the content keys of one attribute bag, in place. */
880
1121
  sanitizeAttributes(attributes) {
881
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
+ }
882
1129
  for (const key of Object.keys(attributes)) {
883
1130
  if (!isContentKey(key)) continue;
884
1131
  if (!this.opts.captureContent) {
@@ -925,7 +1172,7 @@ function buildSnapshot(span) {
925
1172
  startTime: span.startTime,
926
1173
  endTime: span.startTime,
927
1174
  duration: [0, 0],
928
- status: { code: import_api4.SpanStatusCode.UNSET },
1175
+ status: { code: import_api5.SpanStatusCode.UNSET },
929
1176
  attributes,
930
1177
  links: [],
931
1178
  events: [],
@@ -937,15 +1184,15 @@ function buildSnapshot(span) {
937
1184
  ended: true
938
1185
  };
939
1186
  }
940
- function spanKey(context) {
941
- return `${context.traceId}:${context.spanId}`;
1187
+ function spanKey(context2) {
1188
+ return `${context2.traceId}:${context2.spanId}`;
942
1189
  }
943
- var import_api4, PendingSpanProcessor;
1190
+ var import_api5, PendingSpanProcessor;
944
1191
  var init_pending = __esm({
945
1192
  "src/pending.ts"() {
946
1193
  "use strict";
947
1194
  init_cjs_shims();
948
- import_api4 = require("@opentelemetry/api");
1195
+ import_api5 = require("@opentelemetry/api");
949
1196
  init_semconv();
950
1197
  PendingSpanProcessor = class {
951
1198
  delegate;
@@ -993,17 +1240,17 @@ var init_pending = __esm({
993
1240
  // src/session.ts
994
1241
  function withSession(sessionIdOrFn, maybeFn) {
995
1242
  const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [(0, import_node_crypto.randomUUID)(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
996
- 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));
997
1244
  }
998
- var import_node_crypto, import_api5, SESSION_KEY, SessionSpanProcessor;
1245
+ var import_node_crypto, import_api6, SESSION_KEY, SessionSpanProcessor;
999
1246
  var init_session = __esm({
1000
1247
  "src/session.ts"() {
1001
1248
  "use strict";
1002
1249
  init_cjs_shims();
1003
1250
  import_node_crypto = require("crypto");
1004
- import_api5 = require("@opentelemetry/api");
1251
+ import_api6 = require("@opentelemetry/api");
1005
1252
  init_semconv();
1006
- SESSION_KEY = (0, import_api5.createContextKey)("rius-session-id");
1253
+ SESSION_KEY = (0, import_api6.createContextKey)("rius-session-id");
1007
1254
  SessionSpanProcessor = class {
1008
1255
  constructor(defaultSessionId) {
1009
1256
  this.defaultSessionId = defaultSessionId;
@@ -1011,7 +1258,9 @@ var init_session = __esm({
1011
1258
  defaultSessionId;
1012
1259
  onStart(span, parentContext) {
1013
1260
  const value = parentContext.getValue(SESSION_KEY);
1014
- 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;
1015
1264
  if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
1016
1265
  }
1017
1266
  onEnd(_span) {
@@ -1027,7 +1276,7 @@ var init_session = __esm({
1027
1276
  // src/workspace.ts
1028
1277
  function withWorkspace(alias, fn) {
1029
1278
  if (!alias) throw new Error("workspace alias must be a non-empty string");
1030
- 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));
1031
1280
  }
1032
1281
  function setGlobalRouting(routing) {
1033
1282
  globalRouting = routing;
@@ -1040,25 +1289,32 @@ function registerWorkspace(alias, apiKey) {
1040
1289
  }
1041
1290
  globalRouting.register(alias, apiKey);
1042
1291
  }
1043
- var import_api6, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1292
+ var import_api7, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1044
1293
  var init_workspace = __esm({
1045
1294
  "src/workspace.ts"() {
1046
1295
  "use strict";
1047
1296
  init_cjs_shims();
1048
- import_api6 = require("@opentelemetry/api");
1297
+ import_api7 = require("@opentelemetry/api");
1049
1298
  init_esm();
1050
1299
  init_semconv();
1051
- WORKSPACE_KEY = (0, import_api6.createContextKey)("rius-workspace-alias");
1300
+ WORKSPACE_KEY = (0, import_api7.createContextKey)("rius-workspace-alias");
1052
1301
  WorkspaceSpanProcessor = class {
1302
+ /** Straddles already warned about, as "parentAlias->alias"; one warning each, not one per span. */
1303
+ warnedStraddles = /* @__PURE__ */ new Set();
1053
1304
  onStart(span, parentContext) {
1054
- const alias = parentContext.getValue(WORKSPACE_KEY);
1055
- if (typeof alias !== "string") return;
1056
- const parent = import_api6.trace.getSpan(parentContext);
1305
+ const parent = import_api7.trace.getSpan(parentContext);
1057
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;
1058
1310
  if (parentAlias !== void 0 && parentAlias !== alias) {
1059
- console.warn(
1060
- `[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.`
1061
- );
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
+ }
1062
1318
  }
1063
1319
  span.setAttribute(WORKSPACE_ROUTE, alias);
1064
1320
  }
@@ -1179,6 +1435,7 @@ function init(options = {}) {
1179
1435
  }) : health;
1180
1436
  const batch = new import_sdk_trace_base.BatchSpanProcessor(exporter);
1181
1437
  processors.add(new SessionSpanProcessor(config.sessionId));
1438
+ processors.add(new UserSpanProcessor());
1182
1439
  if (routing !== void 0) {
1183
1440
  processors.add(new WorkspaceSpanProcessor());
1184
1441
  }
@@ -1189,9 +1446,13 @@ function init(options = {}) {
1189
1446
  }
1190
1447
  const instanceId = (0, import_node_crypto2.randomUUID)();
1191
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.
1192
1451
  resource: (0, import_resources.resourceFromAttributes)({
1193
1452
  [import_semantic_conventions.ATTR_SERVICE_NAME]: config.serviceName,
1194
- [SERVICE_INSTANCE_ID]: instanceId
1453
+ [SERVICE_INSTANCE_ID]: instanceId,
1454
+ "telemetry.distro.name": "glassflow-rius",
1455
+ "telemetry.distro.version": SDK_VERSION
1195
1456
  }),
1196
1457
  // Always ParentBased, with no AlwaysOn shortcut at rate 1. They are not
1197
1458
  // equivalent: ParentBased honours a remote UNSAMPLED parent and drops,
@@ -1201,10 +1462,12 @@ function init(options = {}) {
1201
1462
  sampler: new import_sdk_trace_base.ParentBasedSampler({ root: new import_sdk_trace_base.TraceIdRatioBasedSampler(config.sampleRate) }),
1202
1463
  spanProcessors: [processors]
1203
1464
  });
1204
- 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);
1205
1467
  provider.register();
1206
1468
  let heartbeat;
1207
- if (config.heartbeat && !config.disabled) {
1469
+ const heartbeatCanAuthenticate = config.apiKey !== void 0 || options.heartbeatTransport !== void 0;
1470
+ if (config.heartbeat && !config.disabled && heartbeatCanAuthenticate) {
1208
1471
  const tracker = new OpenRootSpanTracker();
1209
1472
  processors.add(tracker);
1210
1473
  const sender = new HeartbeatSender({
@@ -1223,20 +1486,20 @@ function init(options = {}) {
1223
1486
  process.once("beforeExit", beforeExitHandler);
1224
1487
  heartbeat = { sender, beforeExitHandler };
1225
1488
  }
1226
- globalClient = createClient({ provider, processors, health, ready, heartbeat });
1489
+ globalClient = createClient({ provider, processors, health, ready, teardown, heartbeat });
1227
1490
  setGlobalRouting(routing);
1228
1491
  return globalClient;
1229
1492
  }
1230
1493
  function getTracer() {
1231
- return import_api7.trace.getTracer(TRACER_NAME);
1494
+ return import_api8.trace.getTracer(TRACER_NAME, SDK_VERSION);
1232
1495
  }
1233
- 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;
1234
1497
  var init_client = __esm({
1235
1498
  "src/client.ts"() {
1236
1499
  "use strict";
1237
1500
  init_cjs_shims();
1238
1501
  import_node_crypto2 = require("crypto");
1239
- import_api7 = require("@opentelemetry/api");
1502
+ import_api8 = require("@opentelemetry/api");
1240
1503
  import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
1241
1504
  import_resources = require("@opentelemetry/resources");
1242
1505
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -1251,18 +1514,22 @@ var init_client = __esm({
1251
1514
  init_pending();
1252
1515
  init_semconv();
1253
1516
  init_session();
1517
+ init_user();
1518
+ init_version();
1254
1519
  init_workspace();
1255
1520
  sinks = /* @__PURE__ */ new WeakMap();
1256
1521
  heartbeats = /* @__PURE__ */ new WeakMap();
1257
1522
  RiusClient = class _RiusClient {
1258
1523
  provider;
1259
1524
  health;
1525
+ teardown;
1260
1526
  /** Resolves with the names of the auto-instrumentations that attached. */
1261
1527
  ready;
1262
1528
  constructor(parts) {
1263
1529
  this.provider = parts.provider;
1264
1530
  this.health = parts.health;
1265
1531
  this.ready = parts.ready;
1532
+ this.teardown = parts.teardown;
1266
1533
  sinks.set(this, parts.processors);
1267
1534
  if (parts.heartbeat) heartbeats.set(this, parts.heartbeat);
1268
1535
  }
@@ -1290,13 +1557,22 @@ var init_client = __esm({
1290
1557
  process.removeListener("beforeExit", heartbeat.beforeExitHandler);
1291
1558
  await heartbeat.sender.stop();
1292
1559
  }
1560
+ await this.ready;
1561
+ for (const disable of this.teardown.splice(0)) {
1562
+ try {
1563
+ disable();
1564
+ } catch {
1565
+ }
1566
+ }
1293
1567
  try {
1294
1568
  await this.provider.shutdown();
1295
1569
  } finally {
1296
1570
  if (globalClient === this) {
1297
1571
  globalClient = void 0;
1298
1572
  setGlobalRouting(void 0);
1299
- import_api7.trace.disable();
1573
+ import_api8.trace.disable();
1574
+ import_api8.context.disable();
1575
+ import_api8.propagation.disable();
1300
1576
  }
1301
1577
  }
1302
1578
  }
@@ -1321,6 +1597,7 @@ __export(index_exports, {
1321
1597
  startGeneration: () => startGeneration,
1322
1598
  startSpan: () => startSpan,
1323
1599
  withSession: () => withSession,
1600
+ withUser: () => withUser,
1324
1601
  withWorkspace: () => withWorkspace
1325
1602
  });
1326
1603
  module.exports = __toCommonJS(index_exports);
@@ -1330,6 +1607,67 @@ init_client();
1330
1607
  // src/generation.ts
1331
1608
  init_cjs_shims();
1332
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
1333
1671
  init_semconv();
1334
1672
  init_serde();
1335
1673
  init_spans();
@@ -1344,12 +1682,31 @@ var Generation = class extends Observation {
1344
1682
  }
1345
1683
  provider;
1346
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
+ */
1347
1691
  setInput(value) {
1348
- this.span.setAttribute(GEN_AI_INPUT_MESSAGES, toAttributeValue(value));
1692
+ this.span.setAttribute(GEN_AI_INPUT_MESSAGES, serializeMessages(value, "user"));
1349
1693
  return this;
1350
1694
  }
1695
+ /** Record the response messages (`gen_ai.output.messages`); bare strings default to `assistant`. */
1351
1696
  setOutput(value) {
1352
- 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));
1353
1710
  return this;
1354
1711
  }
1355
1712
  setModel(model) {
@@ -1415,8 +1772,10 @@ var Generation = class extends Observation {
1415
1772
  };
1416
1773
  function attributesFor(options) {
1417
1774
  const attributes = { ...kindAttributes("LLM" /* LLM */) };
1775
+ if (options.operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = options.operation;
1418
1776
  if (options.model !== void 0) attributes[GEN_AI_REQUEST_MODEL] = options.model;
1419
1777
  if (options.provider !== void 0) attributes[GEN_AI_PROVIDER_NAME] = options.provider;
1778
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
1420
1779
  return attributes;
1421
1780
  }
1422
1781
  function configure2(generation, options) {
@@ -1426,6 +1785,7 @@ function configure2(generation, options) {
1426
1785
  if (options.reasoningLevel !== void 0) {
1427
1786
  generation.setAttribute(GEN_AI_REQUEST_REASONING_LEVEL, options.reasoningLevel);
1428
1787
  }
1788
+ if (options.tools !== void 0) generation.setToolDefinitions(options.tools);
1429
1789
  if (options.input !== void 0) generation.setInput(options.input);
1430
1790
  return generation;
1431
1791
  }
@@ -1435,17 +1795,13 @@ function startGeneration(name, options = {}) {
1435
1795
  }
1436
1796
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1437
1797
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1438
- return getTracer().startActiveSpan(name, { attributes: attributesFor(options) }, async (span) => {
1439
- const generation = configure2(new Generation(span, options.provider), options);
1440
- try {
1441
- return await fn(generation);
1442
- } catch (error) {
1443
- generation.recordException(error);
1444
- throw error;
1445
- } finally {
1446
- generation.end();
1447
- }
1448
- });
1798
+ return runActive(
1799
+ name,
1800
+ attributesFor(options),
1801
+ options.userId,
1802
+ (span) => configure2(new Generation(span, options.provider), options),
1803
+ fn
1804
+ );
1449
1805
  }
1450
1806
 
1451
1807
  // src/observe.ts
@@ -1457,7 +1813,10 @@ function observe(fn, options = {}) {
1457
1813
  const captureOutput = options.captureOutput ?? true;
1458
1814
  const wrapped = (...args) => startAsCurrentSpan(
1459
1815
  name,
1460
- { 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 },
1461
1820
  async (observation) => {
1462
1821
  const result = await fn(...args);
1463
1822
  if (captureOutput && result !== void 0) observation.setOutput(result);
@@ -1471,9 +1830,10 @@ function observe(fn, options = {}) {
1471
1830
  // src/index.ts
1472
1831
  init_semconv();
1473
1832
  init_session();
1833
+ init_user();
1474
1834
  init_spans();
1475
1835
  init_workspace();
1476
- var VERSION = "0.5.0";
1836
+ var VERSION = "0.6.0";
1477
1837
  // Annotate the CommonJS export names for ESM import in node:
1478
1838
  0 && (module.exports = {
1479
1839
  Generation,
@@ -1490,5 +1850,6 @@ var VERSION = "0.5.0";
1490
1850
  startGeneration,
1491
1851
  startSpan,
1492
1852
  withSession,
1853
+ withUser,
1493
1854
  withWorkspace
1494
1855
  });