@glassflow-ai/rius 0.5.0 → 0.6.1

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.js CHANGED
@@ -40,7 +40,11 @@ function seconds(raw, fallback) {
40
40
  if (Number.isNaN(parsed)) return fallback;
41
41
  return parsed;
42
42
  }
43
- function clamp(name, value, min, max) {
43
+ function clamp(name, value, min, max, fallback) {
44
+ if (!Number.isFinite(value)) {
45
+ console.warn(`[rius] ${name}=${value} is not a finite number; using ${fallback}.`);
46
+ return fallback;
47
+ }
44
48
  if (value >= min && value <= max) return value;
45
49
  const clamped = Math.min(Math.max(value, min), max);
46
50
  console.warn(`[rius] ${name}=${value} is outside [${min}, ${max}]; clamped to ${clamped}.`);
@@ -53,13 +57,15 @@ function resolveConfig(options = {}, env = process.env) {
53
57
  "heartbeatInterval",
54
58
  options.heartbeatInterval ?? seconds(env.RIUS_HEARTBEAT_INTERVAL, DEFAULT_HEARTBEAT_INTERVAL),
55
59
  HEARTBEAT_INTERVAL_MIN,
56
- HEARTBEAT_INTERVAL_MAX
60
+ HEARTBEAT_INTERVAL_MAX,
61
+ DEFAULT_HEARTBEAT_INTERVAL
57
62
  );
58
63
  const partialSpansDelaySeconds = clamp(
59
64
  "partialSpansDelay",
60
65
  options.partialSpansDelay ?? seconds(env.RIUS_PARTIAL_SPANS_DELAY, DEFAULT_PARTIAL_SPANS_DELAY),
61
66
  PARTIAL_SPANS_DELAY_MIN,
62
- PARTIAL_SPANS_DELAY_MAX
67
+ PARTIAL_SPANS_DELAY_MAX,
68
+ DEFAULT_PARTIAL_SPANS_DELAY
63
69
  );
64
70
  return {
65
71
  endpoint,
@@ -208,8 +214,25 @@ var init_exportHealth = __esm({
208
214
  }
209
215
  });
210
216
 
211
- // src/heartbeat.ts
217
+ // src/version.ts
212
218
  import { createRequire } from "module";
219
+ var SDK_VERSION;
220
+ var init_version = __esm({
221
+ "src/version.ts"() {
222
+ "use strict";
223
+ init_esm_shims();
224
+ SDK_VERSION = (() => {
225
+ try {
226
+ const pkg = createRequire(import.meta.url)("../package.json");
227
+ return pkg.version ?? "0.0.0";
228
+ } catch {
229
+ return "0.0.0";
230
+ }
231
+ })();
232
+ }
233
+ });
234
+
235
+ // src/heartbeat.ts
213
236
  function httpTransport(url, headers) {
214
237
  return async (payload, timeoutMs) => {
215
238
  const response = await fetch(url, {
@@ -225,23 +248,16 @@ function httpTransport(url, headers) {
225
248
  }
226
249
  };
227
250
  }
228
- var PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, SDK_VERSION, OpenRootSpanTracker, HeartbeatSender;
251
+ var PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, OpenRootSpanTracker, HeartbeatSender;
229
252
  var init_heartbeat = __esm({
230
253
  "src/heartbeat.ts"() {
231
254
  "use strict";
232
255
  init_esm_shims();
256
+ init_version();
233
257
  PAYLOAD_VERSION = 1;
234
258
  OPEN_TRACES_CAP = 32;
235
259
  DEFAULT_PING_TIMEOUT_MS = 3e3;
236
260
  DEFAULT_FINAL_PING_TIMEOUT_MS = 1e3;
237
- SDK_VERSION = (() => {
238
- try {
239
- const pkg = createRequire(import.meta.url)("../package.json");
240
- return pkg.version ?? "0.0.0";
241
- } catch {
242
- return "0.0.0";
243
- }
244
- })();
245
261
  OpenRootSpanTracker = class {
246
262
  // trace id (32-hex) -> count of open root spans in that trace. A trace id
247
263
  // normally has one root, but the count guards against duplicates.
@@ -293,6 +309,7 @@ var init_heartbeat = __esm({
293
309
  }
294
310
  /** Starts pinging: an immediate first ping, then every `intervalMs`. */
295
311
  start() {
312
+ if (this.timer !== void 0) return;
296
313
  void this.ping(this.pingTimeoutMs, false);
297
314
  const timer = setInterval(() => void this.ping(this.pingTimeoutMs, false), this.intervalMs);
298
315
  if (typeof timer.unref === "function") timer.unref();
@@ -343,7 +360,7 @@ function kindAttributes(kind) {
343
360
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
344
361
  return attributes;
345
362
  }
346
- 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;
363
+ 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;
347
364
  var init_semconv = __esm({
348
365
  "src/semconv.ts"() {
349
366
  "use strict";
@@ -354,6 +371,7 @@ var init_semconv = __esm({
354
371
  INPUT_VALUE = "input.value";
355
372
  OUTPUT_VALUE = "output.value";
356
373
  SESSION_ID = "session.id";
374
+ USER_ID = "user.id";
357
375
  WORKSPACE_ROUTE = "rius.workspace";
358
376
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
359
377
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
@@ -369,6 +387,7 @@ var init_semconv = __esm({
369
387
  GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages";
370
388
  GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons";
371
389
  GEN_AI_TOOL_NAME = "gen_ai.tool.name";
390
+ GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions";
372
391
  GEN_AI_REQUEST_PREFIX = "gen_ai.request.";
373
392
  MCP_RESULT_TYPE = "mcp.result_type";
374
393
  GEN_AI_FIRST_TOKEN_EVENT = "gen_ai.first_token";
@@ -397,7 +416,18 @@ var init_semconv = __esm({
397
416
  // or internal URLs in parameter defaults. gen_ai.tool.name stays: it is
398
417
  // identity, not content.
399
418
  "gen_ai.tool.description",
400
- "gen_ai.tool.definitions",
419
+ GEN_AI_TOOL_DEFINITIONS,
420
+ // GenAI semconv keys emitted by OTel-native instrumentations (@ai-sdk/otel,
421
+ // which init() registers for ai v7, among them; pinned 2026-09-14): the
422
+ // system prompt and each tool call's input and output are content in the
423
+ // same sense messages are. gen_ai.tool.call.id stays: identity.
424
+ "gen_ai.system_instructions",
425
+ "gen_ai.tool.call.arguments",
426
+ "gen_ai.tool.call.result",
427
+ // OpenInference TOOL-kind spans carry the definition under these two bare
428
+ // keys, sensitive for the reason gen_ai.tool.description is.
429
+ "tool.description",
430
+ "tool.parameters",
401
431
  "gen_ai.prompt",
402
432
  "gen_ai.completion",
403
433
  "llm.input_messages",
@@ -411,15 +441,44 @@ var init_semconv = __esm({
411
441
  "mlflow.spanInputs",
412
442
  "mlflow.spanOutputs",
413
443
  "traceloop.entity.input",
414
- "traceloop.entity.output"
444
+ "traceloop.entity.output",
445
+ // Every bundled OpenInference instrumentation emits tool definitions as
446
+ // llm.tools.{i}.tool.json_schema (pinned empirically 2026-09-14); covered
447
+ // by prefix below, bare key listed per the bare-key rule.
448
+ "llm.tools",
449
+ // The Vercel AI SDK's own telemetry keys survive the OpenInference
450
+ // transform untouched, and they carry the full prompt (messages AND tool
451
+ // definitions), response content, and tool-call I/O. Names, ids and models
452
+ // (ai.toolCall.name, ai.response.model, ...) are identity and stay.
453
+ "ai.prompt",
454
+ "ai.response.text",
455
+ "ai.response.object",
456
+ "ai.toolCall.args",
457
+ "ai.toolCall.result",
458
+ // Same family, keys enumerated from the ai v7 telemetry surface
459
+ // (2026-09-14): model reasoning, tool calls in the response, response
460
+ // files, embedding inputs, rerank documents, and the generateObject schema
461
+ // (a tool definition by another name).
462
+ "ai.response.reasoning",
463
+ "ai.response.toolCalls",
464
+ "ai.response.files",
465
+ "ai.value",
466
+ "ai.values",
467
+ "ai.documents",
468
+ "ai.schema",
469
+ "ai.schema.description"
415
470
  ]);
471
+ LLM_INVOCATION_PARAMETERS = "llm.invocation_parameters";
472
+ INVOCATION_PARAMETERS_CONTENT_MEMBERS = ["tools", "functions"];
416
473
  CONTENT_ATTRIBUTE_PREFIXES = [
417
474
  "llm.input_messages.",
418
475
  "llm.output_messages.",
419
476
  "gen_ai.prompt.",
420
477
  "gen_ai.completion.",
421
478
  "llm.prompts.",
422
- "llm.prompt_template."
479
+ "llm.prompt_template.",
480
+ "llm.tools.",
481
+ "ai.prompt."
423
482
  ];
424
483
  CONTENT_ATTRIBUTE_SUFFIXES = [
425
484
  ".document.content",
@@ -434,6 +493,9 @@ var init_semconv = __esm({
434
493
  // Identity, not content: a pending span must be groupable into its
435
494
  // session while still running, that is the live view's whole point.
436
495
  SESSION_ID,
496
+ // Same reason: a crashed run's partial spans must still be attributable
497
+ // to the user they served.
498
+ USER_ID,
437
499
  // Routing, not content: a crashed run's snapshot must land in the same
438
500
  // workspace its final span would have. Stripped at export either way.
439
501
  WORKSPACE_ROUTE
@@ -443,31 +505,77 @@ var init_semconv = __esm({
443
505
  });
444
506
 
445
507
  // src/serde.ts
446
- function toAttributeValue(value) {
447
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
448
- return value;
508
+ function truncate(text) {
509
+ return text.length > MAX_ATTR_CHARS ? text.slice(0, MAX_ATTR_CHARS) + TRUNCATION_MARKER : text;
510
+ }
511
+ function renderable(raw, value) {
512
+ if (typeof raw === "bigint") return String(raw);
513
+ if (raw instanceof Map) return Object.fromEntries(raw);
514
+ if (raw instanceof Set) return [...raw];
515
+ if (ArrayBuffer.isView(raw)) {
516
+ return `<${raw.constructor.name} ${raw.byteLength} bytes>`;
449
517
  }
518
+ if (raw instanceof ArrayBuffer) return `<ArrayBuffer ${raw.byteLength} bytes>`;
519
+ return value;
520
+ }
521
+ function toAttributeValue(value) {
522
+ if (typeof value === "string") return truncate(value);
523
+ if (typeof value === "number" || typeof value === "boolean") return value;
450
524
  if (value === null || value === void 0) return "";
525
+ if (typeof value === "bigint") return String(value);
451
526
  const ancestors = [];
452
527
  try {
453
- return JSON.stringify(value, function(_key, v) {
454
- if (typeof v === "object" && v !== null) {
528
+ const text = JSON.stringify({ "": value }[""], function(key, v) {
529
+ const raw = this?.[key];
530
+ const rendered = renderable(raw, v);
531
+ if (typeof rendered === "object" && rendered !== null) {
455
532
  while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
456
533
  ancestors.pop();
457
534
  }
458
- if (ancestors.includes(v)) return "[Circular]";
459
- ancestors.push(v);
535
+ if (ancestors.includes(rendered)) return "[Circular]";
536
+ ancestors.push(rendered);
460
537
  }
461
- return v;
538
+ return rendered;
462
539
  }) ?? String(value);
540
+ return truncate(text);
463
541
  } catch {
464
542
  return "[unserializable]";
465
543
  }
466
544
  }
545
+ var MAX_ATTR_CHARS, TRUNCATION_MARKER;
467
546
  var init_serde = __esm({
468
547
  "src/serde.ts"() {
469
548
  "use strict";
470
549
  init_esm_shims();
550
+ MAX_ATTR_CHARS = 32 * 1024;
551
+ TRUNCATION_MARKER = "\u2026(truncated)";
552
+ }
553
+ });
554
+
555
+ // src/user.ts
556
+ import { context as apiContext, createContextKey } from "@opentelemetry/api";
557
+ function withUser(userId, fn) {
558
+ return apiContext.with(apiContext.active().setValue(USER_KEY, userId), () => fn(userId));
559
+ }
560
+ var USER_KEY, UserSpanProcessor;
561
+ var init_user = __esm({
562
+ "src/user.ts"() {
563
+ "use strict";
564
+ init_esm_shims();
565
+ init_semconv();
566
+ USER_KEY = createContextKey("rius-user-id");
567
+ UserSpanProcessor = class {
568
+ onStart(span, parentContext) {
569
+ const value = parentContext.getValue(USER_KEY);
570
+ if (typeof value === "string") span.setAttribute(USER_ID, value);
571
+ }
572
+ onEnd(_span) {
573
+ }
574
+ async forceFlush() {
575
+ }
576
+ async shutdown() {
577
+ }
578
+ };
471
579
  }
472
580
  });
473
581
 
@@ -477,30 +585,39 @@ function configure(observation, options) {
477
585
  if (options.input !== void 0) observation.setInput(options.input);
478
586
  return observation;
479
587
  }
588
+ function creationAttributes(options) {
589
+ const attributes = { ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */), ...options.attributes };
590
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
591
+ return attributes;
592
+ }
480
593
  function startSpan(name, options = {}) {
481
- const span = getTracer().startSpan(name, {
482
- attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */)
483
- });
594
+ const span = getTracer().startSpan(name, { attributes: creationAttributes(options) });
484
595
  return configure(new Observation(span), options);
485
596
  }
486
597
  function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
487
598
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
488
- return getTracer().startActiveSpan(
599
+ return runActive(
489
600
  name,
490
- { attributes: kindAttributes(options.kind ?? "CHAIN" /* CHAIN */) },
491
- async (span) => {
492
- const observation = configure(new Observation(span), options);
493
- try {
494
- return await fn(observation);
495
- } catch (error) {
496
- observation.recordException(error);
497
- throw error;
498
- } finally {
499
- observation.end();
500
- }
501
- }
601
+ creationAttributes(options),
602
+ options.userId,
603
+ (span) => configure(new Observation(span), options),
604
+ fn
502
605
  );
503
606
  }
607
+ function runActive(name, attributes, userId, makeHandle, fn) {
608
+ const run = () => getTracer().startActiveSpan(name, { attributes }, async (span) => {
609
+ const handle = makeHandle(span);
610
+ try {
611
+ return await fn(handle);
612
+ } catch (error) {
613
+ handle.recordException(error);
614
+ throw error;
615
+ } finally {
616
+ handle.end();
617
+ }
618
+ });
619
+ return userId !== void 0 ? withUser(userId, run) : run();
620
+ }
504
621
  var Observation;
505
622
  var init_spans = __esm({
506
623
  "src/spans.ts"() {
@@ -509,6 +626,8 @@ var init_spans = __esm({
509
626
  init_client();
510
627
  init_semconv();
511
628
  init_serde();
629
+ init_user();
630
+ Symbol.dispose ??= /* @__PURE__ */ Symbol.for("Symbol.dispose");
512
631
  Observation = class {
513
632
  constructor(span) {
514
633
  this.span = span;
@@ -523,7 +642,26 @@ var init_spans = __esm({
523
642
  this.span.setAttribute(OUTPUT_VALUE, toAttributeValue(value));
524
643
  return this;
525
644
  }
645
+ /**
646
+ * Set an arbitrary attribute. Primitives and homogeneous primitive arrays
647
+ * are passed through as the OTel values they are; objects are JSON-encoded
648
+ * and bounded; `undefined` and `null` set nothing, since "no value" is not
649
+ * an empty string.
650
+ */
526
651
  setAttribute(key, value) {
652
+ if (value === void 0 || value === null) return this;
653
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
654
+ this.span.setAttribute(key, value);
655
+ return this;
656
+ }
657
+ if (Array.isArray(value) && value.every((v) => typeof v === "number")) {
658
+ this.span.setAttribute(key, value);
659
+ return this;
660
+ }
661
+ if (Array.isArray(value) && value.every((v) => typeof v === "boolean")) {
662
+ this.span.setAttribute(key, value);
663
+ return this;
664
+ }
527
665
  this.span.setAttribute(key, toAttributeValue(value));
528
666
  return this;
529
667
  }
@@ -560,6 +698,21 @@ __export(instrumentationMcp_exports, {
560
698
  instrumentMcpClient: () => instrumentMcpClient
561
699
  });
562
700
  import { SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
701
+ function serializeResult(result) {
702
+ if (typeof result !== "object" || result === null) return toAttributeValue(result);
703
+ const record = result;
704
+ const structured = record.structured_content ?? record.structuredContent;
705
+ if (structured !== void 0 && structured !== null) return toAttributeValue(structured);
706
+ const content = record.content;
707
+ if (Array.isArray(content)) {
708
+ const texts = content.map(
709
+ (block) => typeof block === "object" && block !== null ? block.text : void 0
710
+ ).filter((text) => typeof text === "string");
711
+ if (texts.length === 1) return truncate(texts[0]);
712
+ if (texts.length > 1) return toAttributeValue(texts);
713
+ }
714
+ return toAttributeValue(result);
715
+ }
563
716
  function resultIsError(result) {
564
717
  if (typeof result !== "object" || result === null) return false;
565
718
  const record = result;
@@ -571,7 +724,7 @@ function recordResult(observation, result) {
571
724
  observation.setAttribute(MCP_RESULT_TYPE, INPUT_REQUIRED);
572
725
  return;
573
726
  }
574
- observation.setOutput(result);
727
+ observation.setAttribute(OUTPUT_VALUE, serializeResult(result));
575
728
  if (resultIsError(result)) {
576
729
  observation.span.setStatus({
577
730
  code: SpanStatusCode2.ERROR,
@@ -591,9 +744,14 @@ function instrumentMcpClient(ClientClass) {
591
744
  const instrumented = function instrumentedCallTool(params, ...rest) {
592
745
  return startAsCurrentSpan(
593
746
  `execute_tool ${params.name}`,
594
- { kind: "TOOL" /* TOOL */, input: params.arguments },
747
+ // Tool name at CREATION: pending snapshots are built at start, so an
748
+ // attribute set inside the callback never reaches them.
749
+ {
750
+ kind: "TOOL" /* TOOL */,
751
+ input: params.arguments,
752
+ attributes: { [GEN_AI_TOOL_NAME]: params.name }
753
+ },
595
754
  async (observation) => {
596
- observation.setAttribute(GEN_AI_TOOL_NAME, params.name);
597
755
  const result = await original.call(this, params, ...rest);
598
756
  recordResult(observation, result);
599
757
  return result;
@@ -612,6 +770,7 @@ var init_instrumentationMcp = __esm({
612
770
  "use strict";
613
771
  init_esm_shims();
614
772
  init_semconv();
773
+ init_serde();
615
774
  init_spans();
616
775
  INPUT_REQUIRED = "input_required";
617
776
  }
@@ -706,21 +865,55 @@ function mcpClientPatchable(exports) {
706
865
  const cls = exports.Client;
707
866
  return typeof cls?.prototype?.callTool === "function" ? { Client: cls } : void 0;
708
867
  }
709
- async function enableInstrumentations(sink, tracerProvider, names) {
868
+ async function registerVercelTelemetry(tracerProvider, teardown) {
869
+ const ai = await optional("ai");
870
+ const register = ai?.registerTelemetry;
871
+ if (register === void 0) return false;
872
+ const otel = await optional("@ai-sdk/otel");
873
+ const OpenTelemetryIntegration = otel?.OpenTelemetry;
874
+ if (OpenTelemetryIntegration === void 0) {
875
+ console.warn(
876
+ "[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."
877
+ );
878
+ return false;
879
+ }
880
+ const bag = globalThis;
881
+ if (vercelTelemetryIntegration !== void 0 && bag.AI_SDK_TELEMETRY_INTEGRATIONS) {
882
+ bag.AI_SDK_TELEMETRY_INTEGRATIONS = bag.AI_SDK_TELEMETRY_INTEGRATIONS.filter(
883
+ (integration2) => integration2 !== vercelTelemetryIntegration
884
+ );
885
+ }
886
+ const integration = new OpenTelemetryIntegration({
887
+ tracer: tracerProvider.getTracer(TRACER_NAME, SDK_VERSION)
888
+ });
889
+ vercelTelemetryIntegration = integration;
890
+ register(integration);
891
+ teardown?.push(() => {
892
+ if (bag.AI_SDK_TELEMETRY_INTEGRATIONS) {
893
+ bag.AI_SDK_TELEMETRY_INTEGRATIONS = bag.AI_SDK_TELEMETRY_INTEGRATIONS.filter(
894
+ (candidate) => candidate !== integration
895
+ );
896
+ }
897
+ if (vercelTelemetryIntegration === integration) vercelTelemetryIntegration = void 0;
898
+ });
899
+ return true;
900
+ }
901
+ async function enableInstrumentations(sink, tracerProvider, names, teardown) {
710
902
  const wanted = names ? REGISTRY.filter((e) => names.includes(e.name)) : REGISTRY;
711
903
  const enabled = [];
712
904
  for (const entry of wanted) {
713
905
  try {
714
- const loaded = await entry.load();
906
+ const loaded = await entry.load(tracerProvider, teardown);
715
907
  if (loaded === void 0) continue;
716
908
  if (entry.kind === "processor") {
717
909
  if (entry.insert === "first") sink.addFirst(loaded);
718
910
  else sink.add(loaded);
719
911
  } else if (entry.kind === "instrumentation") {
720
- registerInstrumentations({
912
+ const disable = registerInstrumentations({
721
913
  instrumentations: [loaded],
722
914
  tracerProvider
723
915
  });
916
+ teardown?.push(disable);
724
917
  }
725
918
  enabled.push(entry.name);
726
919
  } catch (error) {
@@ -729,11 +922,13 @@ async function enableInstrumentations(sink, tracerProvider, names) {
729
922
  }
730
923
  return enabled;
731
924
  }
732
- var dynamicImport, REGISTRY;
925
+ var dynamicImport, vercelTelemetryIntegration, REGISTRY;
733
926
  var init_instrumentation = __esm({
734
927
  "src/instrumentation.ts"() {
735
928
  "use strict";
736
929
  init_esm_shims();
930
+ init_semconv();
931
+ init_version();
737
932
  dynamicImport = new Function("s", "return import(s)");
738
933
  REGISTRY = [
739
934
  {
@@ -744,15 +939,16 @@ var init_instrumentation = __esm({
744
939
  // transform has to run before the exporting processor sees the span or the
745
940
  // attributes it added would never be sanitised.
746
941
  insert: "first",
747
- async load() {
942
+ async load(tracerProvider, teardown) {
943
+ const registered = tracerProvider === void 0 ? false : await registerVercelTelemetry(tracerProvider, teardown);
748
944
  const utils = await optional("@arizeai/openinference-vercel/utils");
749
945
  const add = utils?.addOpenInferenceAttributesToSpan;
750
- if (add === void 0) return void 0;
946
+ if (add === void 0 && !registered) return void 0;
751
947
  return {
752
948
  onStart() {
753
949
  },
754
950
  onEnd(span) {
755
- add(span);
951
+ add?.(span);
756
952
  },
757
953
  async forceFlush() {
758
954
  },
@@ -802,14 +998,18 @@ var init_instrumentation = __esm({
802
998
  {
803
999
  name: "mcp",
804
1000
  kind: "self-applying",
805
- async load() {
1001
+ async load(_tracerProvider, teardown) {
806
1002
  const mod = await optional("@modelcontextprotocol/sdk/client/index.js");
807
1003
  const ClientClass = mod?.Client;
808
1004
  if (ClientClass === void 0) return void 0;
809
1005
  const { instrumentMcpClient: instrumentMcpClient2 } = await Promise.resolve().then(() => (init_instrumentationMcp(), instrumentationMcp_exports));
810
- instrumentMcpClient2(ClientClass);
1006
+ const uninstallEsm = instrumentMcpClient2(ClientClass);
1007
+ teardown?.push(uninstallEsm);
811
1008
  const cjs = cachedCjsExports("@modelcontextprotocol/sdk", mcpClientPatchable);
812
- if (cjs !== void 0) instrumentMcpClient2(cjs.Client);
1009
+ if (cjs !== void 0) {
1010
+ const uninstallCjs = instrumentMcpClient2(cjs.Client);
1011
+ teardown?.push(uninstallCjs);
1012
+ }
813
1013
  return true;
814
1014
  }
815
1015
  }
@@ -822,17 +1022,38 @@ import { SpanStatusCode as SpanStatusCode3 } from "@opentelemetry/api";
822
1022
  function isContentKey(key) {
823
1023
  if (CONTENT_ATTRIBUTES.has(key)) return true;
824
1024
  if (CONTENT_ATTRIBUTE_PREFIXES.some((p) => key.startsWith(p))) return true;
825
- return CONTENT_ATTRIBUTE_SUFFIXES.some((s) => key.endsWith(s));
1025
+ if (CONTENT_ATTRIBUTE_SUFFIXES.some((s) => key.endsWith(s))) return true;
1026
+ return key.startsWith(METADATA_PREFIX) && isContentKey(key.slice(METADATA_PREFIX.length));
826
1027
  }
827
- var EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, MaskingSpanExporter;
1028
+ function redactInvocationParameters(value) {
1029
+ if (typeof value !== "string") return void 0;
1030
+ let parameters;
1031
+ try {
1032
+ parameters = JSON.parse(value);
1033
+ } catch {
1034
+ return void 0;
1035
+ }
1036
+ if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) {
1037
+ return void 0;
1038
+ }
1039
+ const bag = parameters;
1040
+ if (!INVOCATION_PARAMETERS_CONTENT_MEMBERS.some((member) => member in bag)) {
1041
+ return value;
1042
+ }
1043
+ for (const member of INVOCATION_PARAMETERS_CONTENT_MEMBERS) delete bag[member];
1044
+ return JSON.stringify(bag);
1045
+ }
1046
+ var METADATA_PREFIX, EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, STATUS_DESCRIPTION_KEY, MaskingSpanExporter;
828
1047
  var init_masking = __esm({
829
1048
  "src/masking.ts"() {
830
1049
  "use strict";
831
1050
  init_esm_shims();
832
1051
  init_semconv();
833
1052
  init_serde();
1053
+ METADATA_PREFIX = "metadata.";
834
1054
  EXCEPTION_EVENT_NAME = "exception";
835
1055
  EXCEPTION_CONTENT_KEYS = ["exception.message", "exception.stacktrace"];
1056
+ STATUS_DESCRIPTION_KEY = "status.description";
836
1057
  MaskingSpanExporter = class {
837
1058
  constructor(inner, opts) {
838
1059
  this.inner = inner;
@@ -858,14 +1079,42 @@ var init_masking = __esm({
858
1079
  for (const link of span.links ?? []) {
859
1080
  this.sanitizeAttributes(link.attributes);
860
1081
  }
861
- if (!this.opts.captureContent && span.status?.code === SpanStatusCode3.ERROR) {
862
- span.status.message = void 0;
863
- }
1082
+ this.sanitizeStatus(span);
864
1083
  return span;
865
1084
  }
1085
+ /**
1086
+ * Drops or masks the status description, in place. `status` is mutable and
1087
+ * exposed by reference on the SDK span, same as `attributes`, so mutating it
1088
+ * here reaches the span about to be handed to the inner exporter. The ERROR
1089
+ * code itself is left alone so the failure stays visible and classifiable,
1090
+ * the same policy as `exception.type`.
1091
+ */
1092
+ sanitizeStatus(span) {
1093
+ const status = span.status;
1094
+ if (status?.code !== SpanStatusCode3.ERROR || !status.message) return;
1095
+ if (!this.opts.captureContent) {
1096
+ status.message = void 0;
1097
+ return;
1098
+ }
1099
+ if (this.opts.mask === void 0) return;
1100
+ try {
1101
+ const masked = toAttributeValue(
1102
+ this.opts.mask(status.message, { key: STATUS_DESCRIPTION_KEY })
1103
+ );
1104
+ status.message = typeof masked === "string" ? masked : String(masked);
1105
+ } catch {
1106
+ status.message = "[mask error]";
1107
+ }
1108
+ }
866
1109
  /** Strips or masks the content keys of one attribute bag, in place. */
867
1110
  sanitizeAttributes(attributes) {
868
1111
  if (attributes === void 0) return;
1112
+ const sanitizing = !this.opts.captureContent || this.opts.mask !== void 0;
1113
+ if (sanitizing && LLM_INVOCATION_PARAMETERS in attributes) {
1114
+ const redacted = redactInvocationParameters(attributes[LLM_INVOCATION_PARAMETERS]);
1115
+ if (redacted === void 0) delete attributes[LLM_INVOCATION_PARAMETERS];
1116
+ else attributes[LLM_INVOCATION_PARAMETERS] = redacted;
1117
+ }
869
1118
  for (const key of Object.keys(attributes)) {
870
1119
  if (!isContentKey(key)) continue;
871
1120
  if (!this.opts.captureContent) {
@@ -925,8 +1174,8 @@ function buildSnapshot(span) {
925
1174
  ended: true
926
1175
  };
927
1176
  }
928
- function spanKey(context) {
929
- return `${context.traceId}:${context.spanId}`;
1177
+ function spanKey(context2) {
1178
+ return `${context2.traceId}:${context2.spanId}`;
930
1179
  }
931
1180
  var PendingSpanProcessor;
932
1181
  var init_pending = __esm({
@@ -979,10 +1228,10 @@ var init_pending = __esm({
979
1228
 
980
1229
  // src/session.ts
981
1230
  import { randomUUID } from "crypto";
982
- import { context as apiContext, createContextKey } from "@opentelemetry/api";
1231
+ import { context as apiContext2, createContextKey as createContextKey2, trace } from "@opentelemetry/api";
983
1232
  function withSession(sessionIdOrFn, maybeFn) {
984
1233
  const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [randomUUID(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
985
- return apiContext.with(apiContext.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
1234
+ return apiContext2.with(apiContext2.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
986
1235
  }
987
1236
  var SESSION_KEY, SessionSpanProcessor;
988
1237
  var init_session = __esm({
@@ -990,7 +1239,7 @@ var init_session = __esm({
990
1239
  "use strict";
991
1240
  init_esm_shims();
992
1241
  init_semconv();
993
- SESSION_KEY = createContextKey("rius-session-id");
1242
+ SESSION_KEY = createContextKey2("rius-session-id");
994
1243
  SessionSpanProcessor = class {
995
1244
  constructor(defaultSessionId) {
996
1245
  this.defaultSessionId = defaultSessionId;
@@ -998,7 +1247,9 @@ var init_session = __esm({
998
1247
  defaultSessionId;
999
1248
  onStart(span, parentContext) {
1000
1249
  const value = parentContext.getValue(SESSION_KEY);
1001
- const sessionId = typeof value === "string" ? value : this.defaultSessionId;
1250
+ const parent = trace.getSpan(parentContext);
1251
+ const inherited = parent?.attributes?.[SESSION_ID];
1252
+ const sessionId = typeof value === "string" ? value : typeof inherited === "string" ? inherited : this.defaultSessionId;
1002
1253
  if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
1003
1254
  }
1004
1255
  onEnd(_span) {
@@ -1012,10 +1263,10 @@ var init_session = __esm({
1012
1263
  });
1013
1264
 
1014
1265
  // src/workspace.ts
1015
- import { context as apiContext2, createContextKey as createContextKey2, trace } from "@opentelemetry/api";
1266
+ import { context as apiContext3, createContextKey as createContextKey3, trace as trace2 } from "@opentelemetry/api";
1016
1267
  function withWorkspace(alias, fn) {
1017
1268
  if (!alias) throw new Error("workspace alias must be a non-empty string");
1018
- return apiContext2.with(apiContext2.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1269
+ return apiContext3.with(apiContext3.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1019
1270
  }
1020
1271
  function setGlobalRouting(routing) {
1021
1272
  globalRouting = routing;
@@ -1035,17 +1286,24 @@ var init_workspace = __esm({
1035
1286
  init_esm_shims();
1036
1287
  init_esm();
1037
1288
  init_semconv();
1038
- WORKSPACE_KEY = createContextKey2("rius-workspace-alias");
1289
+ WORKSPACE_KEY = createContextKey3("rius-workspace-alias");
1039
1290
  WorkspaceSpanProcessor = class {
1291
+ /** Straddles already warned about, as "parentAlias->alias"; one warning each, not one per span. */
1292
+ warnedStraddles = /* @__PURE__ */ new Set();
1040
1293
  onStart(span, parentContext) {
1041
- const alias = parentContext.getValue(WORKSPACE_KEY);
1042
- if (typeof alias !== "string") return;
1043
- const parent = trace.getSpan(parentContext);
1294
+ const parent = trace2.getSpan(parentContext);
1044
1295
  const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1296
+ const scoped = parentContext.getValue(WORKSPACE_KEY);
1297
+ const alias = typeof scoped === "string" ? scoped : parentAlias;
1298
+ if (typeof alias !== "string") return;
1045
1299
  if (parentAlias !== void 0 && parentAlias !== alias) {
1046
- console.warn(
1047
- `[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.`
1048
- );
1300
+ const straddle = `${String(parentAlias)}->${alias}`;
1301
+ if (!this.warnedStraddles.has(straddle)) {
1302
+ this.warnedStraddles.add(straddle);
1303
+ console.warn(
1304
+ `[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.`
1305
+ );
1306
+ }
1049
1307
  }
1050
1308
  span.setAttribute(WORKSPACE_ROUTE, alias);
1051
1309
  }
@@ -1135,7 +1393,7 @@ var init_workspace = __esm({
1135
1393
 
1136
1394
  // src/client.ts
1137
1395
  import { randomUUID as randomUUID2 } from "crypto";
1138
- import { trace as trace2 } from "@opentelemetry/api";
1396
+ import { context, propagation, trace as trace3 } from "@opentelemetry/api";
1139
1397
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
1140
1398
  import { resourceFromAttributes } from "@opentelemetry/resources";
1141
1399
  import {
@@ -1177,6 +1435,7 @@ function init(options = {}) {
1177
1435
  }) : health;
1178
1436
  const batch = new 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 = randomUUID2();
1189
1448
  const provider = new 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: resourceFromAttributes({
1191
1452
  [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 ParentBasedSampler({ root: new 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,12 +1486,12 @@ 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 trace2.getTracer(TRACER_NAME);
1494
+ return trace3.getTracer(TRACER_NAME, SDK_VERSION);
1230
1495
  }
1231
1496
  var sinks, heartbeats, createClient, RiusClient, globalClient;
1232
1497
  var init_client = __esm({
@@ -1242,18 +1507,22 @@ var init_client = __esm({
1242
1507
  init_pending();
1243
1508
  init_semconv();
1244
1509
  init_session();
1510
+ init_user();
1511
+ init_version();
1245
1512
  init_workspace();
1246
1513
  sinks = /* @__PURE__ */ new WeakMap();
1247
1514
  heartbeats = /* @__PURE__ */ new WeakMap();
1248
1515
  RiusClient = class _RiusClient {
1249
1516
  provider;
1250
1517
  health;
1518
+ teardown;
1251
1519
  /** Resolves with the names of the auto-instrumentations that attached. */
1252
1520
  ready;
1253
1521
  constructor(parts) {
1254
1522
  this.provider = parts.provider;
1255
1523
  this.health = parts.health;
1256
1524
  this.ready = parts.ready;
1525
+ this.teardown = parts.teardown;
1257
1526
  sinks.set(this, parts.processors);
1258
1527
  if (parts.heartbeat) heartbeats.set(this, parts.heartbeat);
1259
1528
  }
@@ -1281,13 +1550,22 @@ var init_client = __esm({
1281
1550
  process.removeListener("beforeExit", heartbeat.beforeExitHandler);
1282
1551
  await heartbeat.sender.stop();
1283
1552
  }
1553
+ await this.ready;
1554
+ for (const disable of this.teardown.splice(0)) {
1555
+ try {
1556
+ disable();
1557
+ } catch {
1558
+ }
1559
+ }
1284
1560
  try {
1285
1561
  await this.provider.shutdown();
1286
1562
  } finally {
1287
1563
  if (globalClient === this) {
1288
1564
  globalClient = void 0;
1289
1565
  setGlobalRouting(void 0);
1290
- trace2.disable();
1566
+ trace3.disable();
1567
+ context.disable();
1568
+ propagation.disable();
1291
1569
  }
1292
1570
  }
1293
1571
  }
@@ -1302,6 +1580,67 @@ init_client();
1302
1580
  // src/generation.ts
1303
1581
  init_esm_shims();
1304
1582
  init_client();
1583
+
1584
+ // src/messages.ts
1585
+ init_esm_shims();
1586
+ init_serde();
1587
+ function isRecord(value) {
1588
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1589
+ }
1590
+ function asText(value) {
1591
+ if (typeof value === "string") return value;
1592
+ const text = toAttributeValue(value);
1593
+ return typeof text === "string" ? text : String(text);
1594
+ }
1595
+ function normalizePart(item) {
1596
+ if (isRecord(item)) {
1597
+ if (item.type === "text" && "text" in item) return { type: "text", content: item.text };
1598
+ if ("type" in item) return item;
1599
+ }
1600
+ return { type: "text", content: asText(item) };
1601
+ }
1602
+ function normalizeMessage(message, defaultRole) {
1603
+ if (typeof message === "string") {
1604
+ return { role: defaultRole, parts: [{ type: "text", content: message }] };
1605
+ }
1606
+ if (!isRecord(message)) {
1607
+ return { role: defaultRole, parts: [{ type: "text", content: asText(message) }] };
1608
+ }
1609
+ if ("parts" in message) {
1610
+ return { ...message, role: message.role ?? defaultRole };
1611
+ }
1612
+ const role = message.role ?? defaultRole;
1613
+ if (role === "tool" && "tool_call_id" in message) {
1614
+ return {
1615
+ role: "tool",
1616
+ parts: [{ type: "tool_call_response", id: message.tool_call_id, response: message.content }]
1617
+ };
1618
+ }
1619
+ const parts = [];
1620
+ const content = message.content;
1621
+ if (typeof content === "string") {
1622
+ parts.push({ type: "text", content });
1623
+ } else if (Array.isArray(content)) {
1624
+ for (const item of content) parts.push(normalizePart(item));
1625
+ } else if (content !== null && content !== void 0) {
1626
+ parts.push({ type: "text", content: asText(content) });
1627
+ }
1628
+ const toolCalls = message.tool_calls;
1629
+ if (Array.isArray(toolCalls)) {
1630
+ for (const call of toolCalls) {
1631
+ if (!isRecord(call)) continue;
1632
+ const fn = isRecord(call.function) ? call.function : {};
1633
+ parts.push({ type: "tool_call", id: call.id, name: fn.name, arguments: fn.arguments });
1634
+ }
1635
+ }
1636
+ return { role, parts };
1637
+ }
1638
+ function serializeMessages(messages, defaultRole) {
1639
+ const list = Array.isArray(messages) ? messages : [messages];
1640
+ return toAttributeValue(list.map((message) => normalizeMessage(message, defaultRole)));
1641
+ }
1642
+
1643
+ // src/generation.ts
1305
1644
  init_semconv();
1306
1645
  init_serde();
1307
1646
  init_spans();
@@ -1316,12 +1655,31 @@ var Generation = class extends Observation {
1316
1655
  }
1317
1656
  provider;
1318
1657
  firstTokenRecorded = false;
1658
+ /**
1659
+ * Record the request messages (`gen_ai.input.messages`), normalised to the
1660
+ * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
1661
+ * dicts (including `tool_calls` and tool responses) and multimodal content
1662
+ * lists are all accepted. Bare strings default to the `user` role.
1663
+ */
1319
1664
  setInput(value) {
1320
- this.span.setAttribute(GEN_AI_INPUT_MESSAGES, toAttributeValue(value));
1665
+ this.span.setAttribute(GEN_AI_INPUT_MESSAGES, serializeMessages(value, "user"));
1321
1666
  return this;
1322
1667
  }
1668
+ /** Record the response messages (`gen_ai.output.messages`); bare strings default to `assistant`. */
1323
1669
  setOutput(value) {
1324
- this.span.setAttribute(GEN_AI_OUTPUT_MESSAGES, toAttributeValue(value));
1670
+ this.span.setAttribute(GEN_AI_OUTPUT_MESSAGES, serializeMessages(value, "assistant"));
1671
+ return this;
1672
+ }
1673
+ /**
1674
+ * The request's tool/function definitions (`gen_ai.tool.definitions`).
1675
+ * Serialized verbatim, in whatever shape the provider request used (OpenAI
1676
+ * nests each tool under `function`, Anthropic uses top-level
1677
+ * `name`/`input_schema`) — no normalization, so what is recorded is exactly
1678
+ * what the model was shown. Definitions are content, not identity: they are
1679
+ * masked/stripped under `captureContent: false` like messages are.
1680
+ */
1681
+ setToolDefinitions(tools) {
1682
+ this.span.setAttribute(GEN_AI_TOOL_DEFINITIONS, toAttributeValue(tools));
1325
1683
  return this;
1326
1684
  }
1327
1685
  setModel(model) {
@@ -1387,8 +1745,10 @@ var Generation = class extends Observation {
1387
1745
  };
1388
1746
  function attributesFor(options) {
1389
1747
  const attributes = { ...kindAttributes("LLM" /* LLM */) };
1748
+ if (options.operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = options.operation;
1390
1749
  if (options.model !== void 0) attributes[GEN_AI_REQUEST_MODEL] = options.model;
1391
1750
  if (options.provider !== void 0) attributes[GEN_AI_PROVIDER_NAME] = options.provider;
1751
+ if (options.userId !== void 0) attributes[USER_ID] = options.userId;
1392
1752
  return attributes;
1393
1753
  }
1394
1754
  function configure2(generation, options) {
@@ -1398,6 +1758,7 @@ function configure2(generation, options) {
1398
1758
  if (options.reasoningLevel !== void 0) {
1399
1759
  generation.setAttribute(GEN_AI_REQUEST_REASONING_LEVEL, options.reasoningLevel);
1400
1760
  }
1761
+ if (options.tools !== void 0) generation.setToolDefinitions(options.tools);
1401
1762
  if (options.input !== void 0) generation.setInput(options.input);
1402
1763
  return generation;
1403
1764
  }
@@ -1407,17 +1768,13 @@ function startGeneration(name, options = {}) {
1407
1768
  }
1408
1769
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1409
1770
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1410
- return getTracer().startActiveSpan(name, { attributes: attributesFor(options) }, async (span) => {
1411
- const generation = configure2(new Generation(span, options.provider), options);
1412
- try {
1413
- return await fn(generation);
1414
- } catch (error) {
1415
- generation.recordException(error);
1416
- throw error;
1417
- } finally {
1418
- generation.end();
1419
- }
1420
- });
1771
+ return runActive(
1772
+ name,
1773
+ attributesFor(options),
1774
+ options.userId,
1775
+ (span) => configure2(new Generation(span, options.provider), options),
1776
+ fn
1777
+ );
1421
1778
  }
1422
1779
 
1423
1780
  // src/observe.ts
@@ -1429,7 +1786,10 @@ function observe(fn, options = {}) {
1429
1786
  const captureOutput = options.captureOutput ?? true;
1430
1787
  const wrapped = (...args) => startAsCurrentSpan(
1431
1788
  name,
1432
- { kind: options.kind, input: captureInput ? args : void 0 },
1789
+ // {args, kwargs} is the Python SDK's shape; JavaScript has no keyword
1790
+ // arguments, so kwargs is always empty, but the key stays so a saved
1791
+ // search or a console view reads both SDKs' input.value the same way.
1792
+ { kind: options.kind, input: captureInput ? { args, kwargs: {} } : void 0 },
1433
1793
  async (observation) => {
1434
1794
  const result = await fn(...args);
1435
1795
  if (captureOutput && result !== void 0) observation.setOutput(result);
@@ -1443,9 +1803,10 @@ function observe(fn, options = {}) {
1443
1803
  // src/index.ts
1444
1804
  init_semconv();
1445
1805
  init_session();
1806
+ init_user();
1446
1807
  init_spans();
1447
1808
  init_workspace();
1448
- var VERSION = "0.5.0";
1809
+ var VERSION = "0.6.1";
1449
1810
  export {
1450
1811
  Generation,
1451
1812
  Observation,
@@ -1461,5 +1822,6 @@ export {
1461
1822
  startGeneration,
1462
1823
  startSpan,
1463
1824
  withSession,
1825
+ withUser,
1464
1826
  withWorkspace
1465
1827
  };