@glassflow-ai/rius 0.6.1 → 0.8.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/README.md CHANGED
@@ -118,8 +118,30 @@ automatically and recording any thrown exception. The options argument is
118
118
  optional in both, so `startAsCurrentSpan("step", async (span) => { ... })`
119
119
  works when you have nothing to configure.
120
120
 
121
+ `kind` sets the span's taxonomy (`openinference.span.kind`, our `SpanKind`
122
+ enum: CHAIN by default, or TOOL, RETRIEVER, EMBEDDING, AGENT, LLM). A TOOL
123
+ span also carries `gen_ai.tool.name`, set to the span name. Each kind also
124
+ decides the OpenTelemetry `SpanKind` field the conventions expect: LLM,
125
+ EMBEDDING and RETRIEVER spans are CLIENT, everything else INTERNAL. Pass
126
+ `otelKind` to override it, for example CLIENT for a call to a hosted agent.
127
+ Note the name collision: `SpanKind` exported by this package is the taxonomy
128
+ enum; the value for `otelKind` is `SpanKind` from `@opentelemetry/api`, so
129
+ import that one under an alias.
130
+
131
+ ```ts
132
+ import { SpanKind as OtelSpanKind } from "@opentelemetry/api";
133
+ import { SpanKind, startAsCurrentSpan } from "@glassflow-ai/rius";
134
+
135
+ await startAsCurrentSpan(
136
+ "delegate-to-planner",
137
+ { kind: SpanKind.AGENT, otelKind: OtelSpanKind.CLIENT, input: task },
138
+ async (span) => { ... },
139
+ );
140
+ ```
141
+
121
142
  On the manual path, `recordException()` does what the scoped form does for
122
- you: it records the error and sets ERROR status.
143
+ you: it records the error, sets ERROR status and sets `error.type` to the
144
+ error's name.
123
145
 
124
146
  ```ts
125
147
  const span = startSpan("fetch-documents");
@@ -182,7 +204,7 @@ Install only the ones you use:
182
204
  - **`anthropic`** wraps the Anthropic SDK, via `@arizeai/openinference-instrumentation-anthropic`, turning Messages calls into LLM spans with model, messages and token counts.
183
205
  - **`langchain`** traces LangChain.js chains, models, tools and retrievers, via `@arizeai/openinference-instrumentation-langchain`. It patches the callback manager in `@langchain/core`, which every LangChain.js application already has, and the SDK resolves that module for you so nothing has to be passed in at `init()`.
184
206
  - **`vercel-ai`** attaches to spans the Vercel AI SDK's own OpenTelemetry integration produces, via `@arizeai/openinference-vercel`, adding OpenInference attributes to them. This package requires Node 22 or newer.
185
- - **`mcp`** patches `@modelcontextprotocol/sdk`'s `Client.callTool` so every MCP tool call becomes a TOOL span, carrying the tool name, arguments, result, latency, and error status.
207
+ - **`mcp`** patches `@modelcontextprotocol/sdk`'s `Client.callTool` so every MCP tool call becomes a TOOL span named `execute_tool <tool>`, carrying the tool name, arguments, result, latency, and error status. The span follows the OpenTelemetry MCP conventions: OTel `SpanKind` CLIENT, `mcp.method.name` set to `tools/call`, `mcp.protocol.version` set to the negotiated version when the transport exposes it (Streamable HTTP does; stdio and SSE do not), and `error.type` set to `tool_error` when the server returns an `isError` result.
186
208
 
187
209
  There is no selection option: `init()` always attempts every bundled
188
210
  integration, so which ones actually attach is determined entirely by
package/dist/index.cjs CHANGED
@@ -367,17 +367,22 @@ var init_heartbeat = __esm({
367
367
  });
368
368
 
369
369
  // src/semconv.ts
370
- function kindAttributes(kind) {
370
+ function otelSpanKind(kind) {
371
+ return OTEL_KIND_BY_KIND[kind];
372
+ }
373
+ function kindAttributes(kind, name) {
371
374
  const attributes = { [OPENINFERENCE_SPAN_KIND]: kind };
372
375
  const operation = OPERATION_BY_KIND[kind];
373
376
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
377
+ if (kind === "TOOL" /* TOOL */ && name !== void 0) attributes[GEN_AI_TOOL_NAME] = name;
374
378
  return attributes;
375
379
  }
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;
380
+ var import_api, 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_REQUEST_STREAM, GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, 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_METHOD_NAME, MCP_METHOD_TOOLS_CALL, MCP_PROTOCOL_VERSION, ERROR_TYPE, ERROR_TYPE_TOOL_ERROR, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, OTEL_KIND_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;
377
381
  var init_semconv = __esm({
378
382
  "src/semconv.ts"() {
379
383
  "use strict";
380
384
  init_cjs_shims();
385
+ import_api = require("@opentelemetry/api");
381
386
  TRACER_NAME = "glassflow";
382
387
  SERVICE_INSTANCE_ID = "service.instance.id";
383
388
  OPENINFERENCE_SPAN_KIND = "openinference.span.kind";
@@ -391,6 +396,8 @@ var init_semconv = __esm({
391
396
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
392
397
  GEN_AI_REQUEST_REASONING_LEVEL = "gen_ai.request.reasoning.level";
393
398
  GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
399
+ GEN_AI_REQUEST_STREAM = "gen_ai.request.stream";
400
+ GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK = "gen_ai.response.time_to_first_chunk";
394
401
  GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
395
402
  GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
396
403
  GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens";
@@ -402,6 +409,11 @@ var init_semconv = __esm({
402
409
  GEN_AI_TOOL_NAME = "gen_ai.tool.name";
403
410
  GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions";
404
411
  GEN_AI_REQUEST_PREFIX = "gen_ai.request.";
412
+ MCP_METHOD_NAME = "mcp.method.name";
413
+ MCP_METHOD_TOOLS_CALL = "tools/call";
414
+ MCP_PROTOCOL_VERSION = "mcp.protocol.version";
415
+ ERROR_TYPE = "error.type";
416
+ ERROR_TYPE_TOOL_ERROR = "tool_error";
405
417
  MCP_RESULT_TYPE = "mcp.result_type";
406
418
  GEN_AI_FIRST_TOKEN_EVENT = "gen_ai.first_token";
407
419
  SpanKind = /* @__PURE__ */ ((SpanKind2) => {
@@ -419,6 +431,14 @@ var init_semconv = __esm({
419
431
  ["EMBEDDING" /* EMBEDDING */]: "embeddings",
420
432
  ["AGENT" /* AGENT */]: "invoke_agent"
421
433
  };
434
+ OTEL_KIND_BY_KIND = {
435
+ ["LLM" /* LLM */]: import_api.SpanKind.CLIENT,
436
+ ["EMBEDDING" /* EMBEDDING */]: import_api.SpanKind.CLIENT,
437
+ ["RETRIEVER" /* RETRIEVER */]: import_api.SpanKind.CLIENT,
438
+ ["TOOL" /* TOOL */]: import_api.SpanKind.INTERNAL,
439
+ ["AGENT" /* AGENT */]: import_api.SpanKind.INTERNAL,
440
+ ["CHAIN" /* CHAIN */]: import_api.SpanKind.INTERNAL
441
+ };
422
442
  CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
423
443
  INPUT_VALUE,
424
444
  OUTPUT_VALUE,
@@ -503,6 +523,11 @@ var init_semconv = __esm({
503
523
  GEN_AI_OPERATION_NAME,
504
524
  GEN_AI_PROVIDER_NAME,
505
525
  GEN_AI_TOOL_NAME,
526
+ // Protocol identity, not content: a still-running MCP call must be
527
+ // distinguishable from a local tool in the live view — the one place
528
+ // setting the marker at creation pays off.
529
+ MCP_METHOD_NAME,
530
+ MCP_PROTOCOL_VERSION,
506
531
  // Identity, not content: a pending span must be groupable into its
507
532
  // session while still running, that is the live view's whole point.
508
533
  SESSION_ID,
@@ -531,6 +556,9 @@ function renderable(raw, value) {
531
556
  if (raw instanceof ArrayBuffer) return `<ArrayBuffer ${raw.byteLength} bytes>`;
532
557
  return value;
533
558
  }
559
+ function asRecord(value) {
560
+ return typeof value === "object" && value !== null ? value : void 0;
561
+ }
534
562
  function toAttributeValue(value) {
535
563
  if (typeof value === "string") return truncate(value);
536
564
  if (typeof value === "number" || typeof value === "boolean") return value;
@@ -555,6 +583,9 @@ function toAttributeValue(value) {
555
583
  return "[unserializable]";
556
584
  }
557
585
  }
586
+ function errorType(error) {
587
+ return error instanceof Error ? error.name : typeof error;
588
+ }
558
589
  var MAX_ATTR_CHARS, TRUNCATION_MARKER;
559
590
  var init_serde = __esm({
560
591
  "src/serde.ts"() {
@@ -567,16 +598,16 @@ var init_serde = __esm({
567
598
 
568
599
  // src/user.ts
569
600
  function withUser(userId, fn) {
570
- return import_api.context.with(import_api.context.active().setValue(USER_KEY, userId), () => fn(userId));
601
+ return import_api2.context.with(import_api2.context.active().setValue(USER_KEY, userId), () => fn(userId));
571
602
  }
572
- var import_api, USER_KEY, UserSpanProcessor;
603
+ var import_api2, USER_KEY, UserSpanProcessor;
573
604
  var init_user = __esm({
574
605
  "src/user.ts"() {
575
606
  "use strict";
576
607
  init_cjs_shims();
577
- import_api = require("@opentelemetry/api");
608
+ import_api2 = require("@opentelemetry/api");
578
609
  init_semconv();
579
- USER_KEY = (0, import_api.createContextKey)("rius-user-id");
610
+ USER_KEY = (0, import_api2.createContextKey)("rius-user-id");
580
611
  UserSpanProcessor = class {
581
612
  onStart(span, parentContext) {
582
613
  const value = parentContext.getValue(USER_KEY);
@@ -597,27 +628,34 @@ function configure(observation, options) {
597
628
  if (options.input !== void 0) observation.setInput(options.input);
598
629
  return observation;
599
630
  }
600
- function creationAttributes(options) {
601
- const attributes = { ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */), ...options.attributes };
631
+ function creationAttributes(name, options) {
632
+ const attributes = {
633
+ ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */, name),
634
+ ...options.attributes
635
+ };
602
636
  if (options.userId !== void 0) attributes[USER_ID] = options.userId;
603
637
  return attributes;
604
638
  }
605
639
  function startSpan(name, options = {}) {
606
- const span = getTracer().startSpan(name, { attributes: creationAttributes(options) });
640
+ const span = getTracer().startSpan(name, {
641
+ kind: options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */),
642
+ attributes: creationAttributes(name, options)
643
+ });
607
644
  return configure(new Observation(span), options);
608
645
  }
609
646
  function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
610
647
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
611
648
  return runActive(
612
649
  name,
613
- creationAttributes(options),
650
+ creationAttributes(name, options),
614
651
  options.userId,
615
652
  (span) => configure(new Observation(span), options),
616
- fn
653
+ fn,
654
+ options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */)
617
655
  );
618
656
  }
619
- function runActive(name, attributes, userId, makeHandle, fn) {
620
- const run = () => getTracer().startActiveSpan(name, { attributes }, async (span) => {
657
+ function runActive(name, attributes, userId, makeHandle, fn, otelKind) {
658
+ const run = () => getTracer().startActiveSpan(name, { kind: otelKind, attributes }, async (span) => {
621
659
  const handle = makeHandle(span);
622
660
  try {
623
661
  return await fn(handle);
@@ -630,12 +668,12 @@ function runActive(name, attributes, userId, makeHandle, fn) {
630
668
  });
631
669
  return userId !== void 0 ? withUser(userId, run) : run();
632
670
  }
633
- var import_api2, Observation;
671
+ var import_api3, Observation;
634
672
  var init_spans = __esm({
635
673
  "src/spans.ts"() {
636
674
  "use strict";
637
675
  init_cjs_shims();
638
- import_api2 = require("@opentelemetry/api");
676
+ import_api3 = require("@opentelemetry/api");
639
677
  init_client();
640
678
  init_semconv();
641
679
  init_serde();
@@ -679,9 +717,15 @@ var init_spans = __esm({
679
717
  return this;
680
718
  }
681
719
  /**
682
- * Record an error on the span and set ERROR status. This is exactly what the
683
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
684
- * `start*` path does not have to reach through `.span` to match it.
720
+ * Record an error on the span, set ERROR status and `error.type`. This is
721
+ * exactly what the `startAsCurrent*` helpers do on a thrown error, exposed
722
+ * so the manual `start*` path does not have to reach through `.span` to
723
+ * match it.
724
+ *
725
+ * `error.type` is Conditionally Required by the GenAI conventions on every
726
+ * span that ends in an error, and every helper's throw path funnels through
727
+ * here, so this is the one place that sets it. The error's name only, never
728
+ * the message: it must stay low-cardinality and free of echoed content.
685
729
  *
686
730
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
687
731
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -689,7 +733,8 @@ var init_spans = __esm({
689
733
  recordException(error) {
690
734
  const wrapped = error instanceof Error ? error : new Error(String(error));
691
735
  this.span.recordException(wrapped);
692
- this.span.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: wrapped.message });
736
+ this.span.setStatus({ code: import_api3.SpanStatusCode.ERROR, message: wrapped.message });
737
+ this.span.setAttribute(ERROR_TYPE, errorType(error));
693
738
  return this;
694
739
  }
695
740
  end() {
@@ -711,27 +756,25 @@ __export(instrumentationMcp_exports, {
711
756
  instrumentMcpClient: () => instrumentMcpClient
712
757
  });
713
758
  function serializeResult(result) {
714
- if (typeof result !== "object" || result === null) return toAttributeValue(result);
715
- const record = result;
759
+ const record = asRecord(result);
760
+ if (record === void 0) return toAttributeValue(result);
716
761
  const structured = record.structured_content ?? record.structuredContent;
717
762
  if (structured !== void 0 && structured !== null) return toAttributeValue(structured);
718
763
  const content = record.content;
719
764
  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");
765
+ const texts = content.map((block) => asRecord(block)?.text).filter((text) => typeof text === "string");
723
766
  if (texts.length === 1) return truncate(texts[0]);
724
767
  if (texts.length > 1) return toAttributeValue(texts);
725
768
  }
726
769
  return toAttributeValue(result);
727
770
  }
728
771
  function resultIsError(result) {
729
- if (typeof result !== "object" || result === null) return false;
730
- const record = result;
731
- return Boolean(record.is_error ?? record.isError);
772
+ const record = asRecord(result);
773
+ return Boolean(record?.is_error ?? record?.isError);
732
774
  }
733
775
  function recordResult(observation, result) {
734
- const resultType = typeof result === "object" && result !== null ? result.result_type ?? result.resultType : void 0;
776
+ const record = asRecord(result);
777
+ const resultType = record?.result_type ?? record?.resultType;
735
778
  if (resultType === INPUT_REQUIRED) {
736
779
  observation.setAttribute(MCP_RESULT_TYPE, INPUT_REQUIRED);
737
780
  return;
@@ -739,11 +782,24 @@ function recordResult(observation, result) {
739
782
  observation.setAttribute(OUTPUT_VALUE, serializeResult(result));
740
783
  if (resultIsError(result)) {
741
784
  observation.span.setStatus({
742
- code: import_api3.SpanStatusCode.ERROR,
785
+ code: import_api4.SpanStatusCode.ERROR,
743
786
  message: "tool returned an error result"
744
787
  });
788
+ observation.setAttribute(ERROR_TYPE, ERROR_TYPE_TOOL_ERROR);
745
789
  }
746
790
  }
791
+ function negotiatedProtocolVersion(client) {
792
+ const version = asRecord(asRecord(client)?.transport)?.protocolVersion;
793
+ return typeof version === "string" ? version : void 0;
794
+ }
795
+ function callAttributes(toolName, protocolVersion) {
796
+ const attributes = {
797
+ [GEN_AI_TOOL_NAME]: toolName,
798
+ [MCP_METHOD_NAME]: MCP_METHOD_TOOLS_CALL
799
+ };
800
+ if (protocolVersion !== void 0) attributes[MCP_PROTOCOL_VERSION] = protocolVersion;
801
+ return attributes;
802
+ }
747
803
  function instrumentMcpClient(ClientClass) {
748
804
  const current = ClientClass.prototype.callTool;
749
805
  const alreadyWrapped = current.riusOriginal;
@@ -756,12 +812,14 @@ function instrumentMcpClient(ClientClass) {
756
812
  const instrumented = function instrumentedCallTool(params, ...rest) {
757
813
  return startAsCurrentSpan(
758
814
  `execute_tool ${params.name}`,
759
- // Tool name at CREATION: pending snapshots are built at start, so an
760
- // attribute set inside the callback never reaches them.
761
815
  {
762
816
  kind: "TOOL" /* TOOL */,
817
+ // The OTel SpanKind FIELD (orthogonal to our taxonomy attribute above):
818
+ // a remote tool call is CLIENT under both the MCP and the GenAI
819
+ // execute-tool conventions.
820
+ otelKind: import_api4.SpanKind.CLIENT,
763
821
  input: params.arguments,
764
- attributes: { [GEN_AI_TOOL_NAME]: params.name }
822
+ attributes: callAttributes(params.name, negotiatedProtocolVersion(this))
765
823
  },
766
824
  async (observation) => {
767
825
  const result = await original.call(this, params, ...rest);
@@ -776,12 +834,12 @@ function instrumentMcpClient(ClientClass) {
776
834
  ClientClass.prototype.callTool = original;
777
835
  };
778
836
  }
779
- var import_api3, INPUT_REQUIRED;
837
+ var import_api4, INPUT_REQUIRED;
780
838
  var init_instrumentationMcp = __esm({
781
839
  "src/instrumentationMcp.ts"() {
782
840
  "use strict";
783
841
  init_cjs_shims();
784
- import_api3 = require("@opentelemetry/api");
842
+ import_api4 = require("@opentelemetry/api");
785
843
  init_semconv();
786
844
  init_serde();
787
845
  init_spans();
@@ -1055,12 +1113,12 @@ function redactInvocationParameters(value) {
1055
1113
  for (const member of INVOCATION_PARAMETERS_CONTENT_MEMBERS) delete bag[member];
1056
1114
  return JSON.stringify(bag);
1057
1115
  }
1058
- var import_api4, METADATA_PREFIX, EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, STATUS_DESCRIPTION_KEY, MaskingSpanExporter;
1116
+ var import_api5, METADATA_PREFIX, EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, STATUS_DESCRIPTION_KEY, MaskingSpanExporter;
1059
1117
  var init_masking = __esm({
1060
1118
  "src/masking.ts"() {
1061
1119
  "use strict";
1062
1120
  init_cjs_shims();
1063
- import_api4 = require("@opentelemetry/api");
1121
+ import_api5 = require("@opentelemetry/api");
1064
1122
  init_semconv();
1065
1123
  init_serde();
1066
1124
  METADATA_PREFIX = "metadata.";
@@ -1104,7 +1162,7 @@ var init_masking = __esm({
1104
1162
  */
1105
1163
  sanitizeStatus(span) {
1106
1164
  const status = span.status;
1107
- if (status?.code !== import_api4.SpanStatusCode.ERROR || !status.message) return;
1165
+ if (status?.code !== import_api5.SpanStatusCode.ERROR || !status.message) return;
1108
1166
  if (!this.opts.captureContent) {
1109
1167
  status.message = void 0;
1110
1168
  return;
@@ -1174,7 +1232,7 @@ function buildSnapshot(span) {
1174
1232
  startTime: span.startTime,
1175
1233
  endTime: span.startTime,
1176
1234
  duration: [0, 0],
1177
- status: { code: import_api5.SpanStatusCode.UNSET },
1235
+ status: { code: import_api6.SpanStatusCode.UNSET },
1178
1236
  attributes,
1179
1237
  links: [],
1180
1238
  events: [],
@@ -1189,12 +1247,12 @@ function buildSnapshot(span) {
1189
1247
  function spanKey(context2) {
1190
1248
  return `${context2.traceId}:${context2.spanId}`;
1191
1249
  }
1192
- var import_api5, PendingSpanProcessor;
1250
+ var import_api6, PendingSpanProcessor;
1193
1251
  var init_pending = __esm({
1194
1252
  "src/pending.ts"() {
1195
1253
  "use strict";
1196
1254
  init_cjs_shims();
1197
- import_api5 = require("@opentelemetry/api");
1255
+ import_api6 = require("@opentelemetry/api");
1198
1256
  init_semconv();
1199
1257
  PendingSpanProcessor = class {
1200
1258
  delegate;
@@ -1242,17 +1300,17 @@ var init_pending = __esm({
1242
1300
  // src/session.ts
1243
1301
  function withSession(sessionIdOrFn, maybeFn) {
1244
1302
  const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [(0, import_node_crypto.randomUUID)(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
1245
- return import_api6.context.with(import_api6.context.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
1303
+ return import_api7.context.with(import_api7.context.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
1246
1304
  }
1247
- var import_node_crypto, import_api6, SESSION_KEY, SessionSpanProcessor;
1305
+ var import_node_crypto, import_api7, SESSION_KEY, SessionSpanProcessor;
1248
1306
  var init_session = __esm({
1249
1307
  "src/session.ts"() {
1250
1308
  "use strict";
1251
1309
  init_cjs_shims();
1252
1310
  import_node_crypto = require("crypto");
1253
- import_api6 = require("@opentelemetry/api");
1311
+ import_api7 = require("@opentelemetry/api");
1254
1312
  init_semconv();
1255
- SESSION_KEY = (0, import_api6.createContextKey)("rius-session-id");
1313
+ SESSION_KEY = (0, import_api7.createContextKey)("rius-session-id");
1256
1314
  SessionSpanProcessor = class {
1257
1315
  constructor(defaultSessionId) {
1258
1316
  this.defaultSessionId = defaultSessionId;
@@ -1260,7 +1318,7 @@ var init_session = __esm({
1260
1318
  defaultSessionId;
1261
1319
  onStart(span, parentContext) {
1262
1320
  const value = parentContext.getValue(SESSION_KEY);
1263
- const parent = import_api6.trace.getSpan(parentContext);
1321
+ const parent = import_api7.trace.getSpan(parentContext);
1264
1322
  const inherited = parent?.attributes?.[SESSION_ID];
1265
1323
  const sessionId = typeof value === "string" ? value : typeof inherited === "string" ? inherited : this.defaultSessionId;
1266
1324
  if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
@@ -1278,7 +1336,7 @@ var init_session = __esm({
1278
1336
  // src/workspace.ts
1279
1337
  function withWorkspace(alias, fn) {
1280
1338
  if (!alias) throw new Error("workspace alias must be a non-empty string");
1281
- return import_api7.context.with(import_api7.context.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1339
+ return import_api8.context.with(import_api8.context.active().setValue(WORKSPACE_KEY, alias), () => fn(alias));
1282
1340
  }
1283
1341
  function setGlobalRouting(routing) {
1284
1342
  globalRouting = routing;
@@ -1291,20 +1349,20 @@ function registerWorkspace(alias, apiKey) {
1291
1349
  }
1292
1350
  globalRouting.register(alias, apiKey);
1293
1351
  }
1294
- var import_api7, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1352
+ var import_api8, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1295
1353
  var init_workspace = __esm({
1296
1354
  "src/workspace.ts"() {
1297
1355
  "use strict";
1298
1356
  init_cjs_shims();
1299
- import_api7 = require("@opentelemetry/api");
1357
+ import_api8 = require("@opentelemetry/api");
1300
1358
  init_esm();
1301
1359
  init_semconv();
1302
- WORKSPACE_KEY = (0, import_api7.createContextKey)("rius-workspace-alias");
1360
+ WORKSPACE_KEY = (0, import_api8.createContextKey)("rius-workspace-alias");
1303
1361
  WorkspaceSpanProcessor = class {
1304
1362
  /** Straddles already warned about, as "parentAlias->alias"; one warning each, not one per span. */
1305
1363
  warnedStraddles = /* @__PURE__ */ new Set();
1306
1364
  onStart(span, parentContext) {
1307
- const parent = import_api7.trace.getSpan(parentContext);
1365
+ const parent = import_api8.trace.getSpan(parentContext);
1308
1366
  const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1309
1367
  const scoped = parentContext.getValue(WORKSPACE_KEY);
1310
1368
  const alias = typeof scoped === "string" ? scoped : parentAlias;
@@ -1493,15 +1551,15 @@ function init(options = {}) {
1493
1551
  return globalClient;
1494
1552
  }
1495
1553
  function getTracer() {
1496
- return import_api8.trace.getTracer(TRACER_NAME, SDK_VERSION);
1554
+ return import_api9.trace.getTracer(TRACER_NAME, SDK_VERSION);
1497
1555
  }
1498
- 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;
1556
+ var import_node_crypto2, import_api9, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1499
1557
  var init_client = __esm({
1500
1558
  "src/client.ts"() {
1501
1559
  "use strict";
1502
1560
  init_cjs_shims();
1503
1561
  import_node_crypto2 = require("crypto");
1504
- import_api8 = require("@opentelemetry/api");
1562
+ import_api9 = require("@opentelemetry/api");
1505
1563
  import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
1506
1564
  import_resources = require("@opentelemetry/resources");
1507
1565
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -1572,9 +1630,9 @@ var init_client = __esm({
1572
1630
  if (globalClient === this) {
1573
1631
  globalClient = void 0;
1574
1632
  setGlobalRouting(void 0);
1575
- import_api8.trace.disable();
1576
- import_api8.context.disable();
1577
- import_api8.propagation.disable();
1633
+ import_api9.trace.disable();
1634
+ import_api9.context.disable();
1635
+ import_api9.propagation.disable();
1578
1636
  }
1579
1637
  }
1580
1638
  }
@@ -1684,6 +1742,12 @@ var Generation = class extends Observation {
1684
1742
  }
1685
1743
  provider;
1686
1744
  firstTokenRecorded = false;
1745
+ /**
1746
+ * Monotonic clock at construction, which is span creation for both
1747
+ * `startGeneration` and the scoped form. The API `Span` exposes no start
1748
+ * time, so this is what `recordFirstToken` measures the first chunk against.
1749
+ */
1750
+ startedAt = performance.now();
1687
1751
  /**
1688
1752
  * Record the request messages (`gen_ai.input.messages`), normalised to the
1689
1753
  * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
@@ -1758,17 +1822,24 @@ var Generation = class extends Observation {
1758
1822
  return this;
1759
1823
  }
1760
1824
  /**
1761
- * The TTFT anchor: event time minus span start. Idempotent: only the
1762
- * first call records the event, so a streaming loop can call this
1763
- * unconditionally on every chunk without inflating the span. A no-op
1764
- * after the span has ended.
1825
+ * Mark the arrival of the first streamed token. Records the
1826
+ * `gen_ai.first_token` event (the timestamp the backend derives TTFT from)
1827
+ * and, per the GenAI conventions, the derived
1828
+ * `gen_ai.response.time_to_first_chunk` (seconds since span creation) plus
1829
+ * `gen_ai.request.stream = true`: a first chunk arriving is what tells the
1830
+ * SDK the request streamed. Idempotent: only the first call records, so a
1831
+ * streaming loop can call this unconditionally on every chunk without
1832
+ * inflating the span. A no-op after the span has ended.
1765
1833
  */
1766
1834
  recordFirstToken() {
1767
1835
  if (this.firstTokenRecorded || !this.span.isRecording()) {
1768
1836
  return this;
1769
1837
  }
1838
+ const elapsedSeconds = Math.max(performance.now() - this.startedAt, 0) / 1e3;
1770
1839
  this.span.addEvent(GEN_AI_FIRST_TOKEN_EVENT);
1771
1840
  this.firstTokenRecorded = true;
1841
+ this.span.setAttribute(GEN_AI_REQUEST_STREAM, true);
1842
+ this.span.setAttribute(GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, elapsedSeconds);
1772
1843
  return this;
1773
1844
  }
1774
1845
  };
@@ -1792,7 +1863,10 @@ function configure2(generation, options) {
1792
1863
  return generation;
1793
1864
  }
1794
1865
  function startGeneration(name, options = {}) {
1795
- const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1866
+ const span = getTracer().startSpan(name, {
1867
+ kind: otelSpanKind("LLM" /* LLM */),
1868
+ attributes: attributesFor(options)
1869
+ });
1796
1870
  return configure2(new Generation(span, options.provider), options);
1797
1871
  }
1798
1872
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
@@ -1802,7 +1876,8 @@ function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1802
1876
  attributesFor(options),
1803
1877
  options.userId,
1804
1878
  (span) => configure2(new Generation(span, options.provider), options),
1805
- fn
1879
+ fn,
1880
+ otelSpanKind("LLM" /* LLM */)
1806
1881
  );
1807
1882
  }
1808
1883
 
@@ -1835,7 +1910,7 @@ init_session();
1835
1910
  init_user();
1836
1911
  init_spans();
1837
1912
  init_workspace();
1838
- var VERSION = "0.6.1";
1913
+ var VERSION = "0.8.0";
1839
1914
  // Annotate the CommonJS export names for ESM import in node:
1840
1915
  0 && (module.exports = {
1841
1916
  Generation,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tracer, Span } from '@opentelemetry/api';
1
+ import { Tracer, Span, SpanKind as SpanKind$1 } from '@opentelemetry/api';
2
2
  import { SpanExporter } from '@opentelemetry/sdk-trace-base';
3
3
 
4
4
  /** Redacts content attribute values at export. Receives the key when it accepts one. */
@@ -171,6 +171,13 @@ declare enum SpanKind {
171
171
  /** Options for {@link startSpan} and {@link startAsCurrentSpan}. */
172
172
  interface SpanOptions {
173
173
  kind?: SpanKind;
174
+ /**
175
+ * The OpenTelemetry `SpanKind` FIELD (INTERNAL, CLIENT, …), orthogonal to
176
+ * `kind` above, which is our taxonomy attribute. Conventions set it per
177
+ * operation, so by default it is derived from `kind` (see `otelSpanKind`);
178
+ * set this to override, as the MCP wrapper does for a remote tool call.
179
+ */
180
+ otelKind?: SpanKind$1;
174
181
  input?: unknown;
175
182
  /**
176
183
  * End-user identity (`user.id`). Sugar for `withUser`: set on this span at
@@ -203,9 +210,15 @@ declare class Observation {
203
210
  */
204
211
  setAttribute(key: string, value: unknown): this;
205
212
  /**
206
- * Record an error on the span and set ERROR status. This is exactly what the
207
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
208
- * `start*` path does not have to reach through `.span` to match it.
213
+ * Record an error on the span, set ERROR status and `error.type`. This is
214
+ * exactly what the `startAsCurrent*` helpers do on a thrown error, exposed
215
+ * so the manual `start*` path does not have to reach through `.span` to
216
+ * match it.
217
+ *
218
+ * `error.type` is Conditionally Required by the GenAI conventions on every
219
+ * span that ends in an error, and every helper's throw path funnels through
220
+ * here, so this is the one place that sets it. The error's name only, never
221
+ * the message: it must stay low-cardinality and free of echoed content.
209
222
  *
210
223
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
211
224
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -275,6 +288,12 @@ interface GenerationOptions {
275
288
  declare class Generation extends Observation {
276
289
  private readonly provider?;
277
290
  private firstTokenRecorded;
291
+ /**
292
+ * Monotonic clock at construction, which is span creation for both
293
+ * `startGeneration` and the scoped form. The API `Span` exposes no start
294
+ * time, so this is what `recordFirstToken` measures the first chunk against.
295
+ */
296
+ private readonly startedAt;
278
297
  /**
279
298
  * The provider passed at creation; drives the Anthropic input-token summing
280
299
  * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
@@ -334,10 +353,14 @@ declare class Generation extends Observation {
334
353
  */
335
354
  setFinishReasons(reasons: string | string[]): this;
336
355
  /**
337
- * The TTFT anchor: event time minus span start. Idempotent: only the
338
- * first call records the event, so a streaming loop can call this
339
- * unconditionally on every chunk without inflating the span. A no-op
340
- * after the span has ended.
356
+ * Mark the arrival of the first streamed token. Records the
357
+ * `gen_ai.first_token` event (the timestamp the backend derives TTFT from)
358
+ * and, per the GenAI conventions, the derived
359
+ * `gen_ai.response.time_to_first_chunk` (seconds since span creation) plus
360
+ * `gen_ai.request.stream = true`: a first chunk arriving is what tells the
361
+ * SDK the request streamed. Idempotent: only the first call records, so a
362
+ * streaming loop can call this unconditionally on every chunk without
363
+ * inflating the span. A no-op after the span has ended.
341
364
  */
342
365
  recordFirstToken(): this;
343
366
  }
@@ -441,6 +464,6 @@ declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T)
441
464
  */
442
465
  declare function withUser<T>(userId: string, fn: (userId: string) => T): T;
443
466
 
444
- declare const VERSION = "0.6.1";
467
+ declare const VERSION = "0.8.0";
445
468
 
446
469
  export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, type WorkspaceExporterFactory, getTracer, init, observe, registerWorkspace, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession, withUser, withWorkspace };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tracer, Span } from '@opentelemetry/api';
1
+ import { Tracer, Span, SpanKind as SpanKind$1 } from '@opentelemetry/api';
2
2
  import { SpanExporter } from '@opentelemetry/sdk-trace-base';
3
3
 
4
4
  /** Redacts content attribute values at export. Receives the key when it accepts one. */
@@ -171,6 +171,13 @@ declare enum SpanKind {
171
171
  /** Options for {@link startSpan} and {@link startAsCurrentSpan}. */
172
172
  interface SpanOptions {
173
173
  kind?: SpanKind;
174
+ /**
175
+ * The OpenTelemetry `SpanKind` FIELD (INTERNAL, CLIENT, …), orthogonal to
176
+ * `kind` above, which is our taxonomy attribute. Conventions set it per
177
+ * operation, so by default it is derived from `kind` (see `otelSpanKind`);
178
+ * set this to override, as the MCP wrapper does for a remote tool call.
179
+ */
180
+ otelKind?: SpanKind$1;
174
181
  input?: unknown;
175
182
  /**
176
183
  * End-user identity (`user.id`). Sugar for `withUser`: set on this span at
@@ -203,9 +210,15 @@ declare class Observation {
203
210
  */
204
211
  setAttribute(key: string, value: unknown): this;
205
212
  /**
206
- * Record an error on the span and set ERROR status. This is exactly what the
207
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
208
- * `start*` path does not have to reach through `.span` to match it.
213
+ * Record an error on the span, set ERROR status and `error.type`. This is
214
+ * exactly what the `startAsCurrent*` helpers do on a thrown error, exposed
215
+ * so the manual `start*` path does not have to reach through `.span` to
216
+ * match it.
217
+ *
218
+ * `error.type` is Conditionally Required by the GenAI conventions on every
219
+ * span that ends in an error, and every helper's throw path funnels through
220
+ * here, so this is the one place that sets it. The error's name only, never
221
+ * the message: it must stay low-cardinality and free of echoed content.
209
222
  *
210
223
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
211
224
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -275,6 +288,12 @@ interface GenerationOptions {
275
288
  declare class Generation extends Observation {
276
289
  private readonly provider?;
277
290
  private firstTokenRecorded;
291
+ /**
292
+ * Monotonic clock at construction, which is span creation for both
293
+ * `startGeneration` and the scoped form. The API `Span` exposes no start
294
+ * time, so this is what `recordFirstToken` measures the first chunk against.
295
+ */
296
+ private readonly startedAt;
278
297
  /**
279
298
  * The provider passed at creation; drives the Anthropic input-token summing
280
299
  * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
@@ -334,10 +353,14 @@ declare class Generation extends Observation {
334
353
  */
335
354
  setFinishReasons(reasons: string | string[]): this;
336
355
  /**
337
- * The TTFT anchor: event time minus span start. Idempotent: only the
338
- * first call records the event, so a streaming loop can call this
339
- * unconditionally on every chunk without inflating the span. A no-op
340
- * after the span has ended.
356
+ * Mark the arrival of the first streamed token. Records the
357
+ * `gen_ai.first_token` event (the timestamp the backend derives TTFT from)
358
+ * and, per the GenAI conventions, the derived
359
+ * `gen_ai.response.time_to_first_chunk` (seconds since span creation) plus
360
+ * `gen_ai.request.stream = true`: a first chunk arriving is what tells the
361
+ * SDK the request streamed. Idempotent: only the first call records, so a
362
+ * streaming loop can call this unconditionally on every chunk without
363
+ * inflating the span. A no-op after the span has ended.
341
364
  */
342
365
  recordFirstToken(): this;
343
366
  }
@@ -441,6 +464,6 @@ declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T)
441
464
  */
442
465
  declare function withUser<T>(userId: string, fn: (userId: string) => T): T;
443
466
 
444
- declare const VERSION = "0.6.1";
467
+ declare const VERSION = "0.8.0";
445
468
 
446
469
  export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, type WorkspaceExporterFactory, getTracer, init, observe, registerWorkspace, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession, withUser, withWorkspace };
package/dist/index.js CHANGED
@@ -354,13 +354,18 @@ var init_heartbeat = __esm({
354
354
  });
355
355
 
356
356
  // src/semconv.ts
357
- function kindAttributes(kind) {
357
+ import { SpanKind as OtelSpanKind } from "@opentelemetry/api";
358
+ function otelSpanKind(kind) {
359
+ return OTEL_KIND_BY_KIND[kind];
360
+ }
361
+ function kindAttributes(kind, name) {
358
362
  const attributes = { [OPENINFERENCE_SPAN_KIND]: kind };
359
363
  const operation = OPERATION_BY_KIND[kind];
360
364
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
365
+ if (kind === "TOOL" /* TOOL */ && name !== void 0) attributes[GEN_AI_TOOL_NAME] = name;
361
366
  return attributes;
362
367
  }
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;
368
+ 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_REQUEST_STREAM, GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, 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_METHOD_NAME, MCP_METHOD_TOOLS_CALL, MCP_PROTOCOL_VERSION, ERROR_TYPE, ERROR_TYPE_TOOL_ERROR, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, OTEL_KIND_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;
364
369
  var init_semconv = __esm({
365
370
  "src/semconv.ts"() {
366
371
  "use strict";
@@ -378,6 +383,8 @@ var init_semconv = __esm({
378
383
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
379
384
  GEN_AI_REQUEST_REASONING_LEVEL = "gen_ai.request.reasoning.level";
380
385
  GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
386
+ GEN_AI_REQUEST_STREAM = "gen_ai.request.stream";
387
+ GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK = "gen_ai.response.time_to_first_chunk";
381
388
  GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
382
389
  GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
383
390
  GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens";
@@ -389,6 +396,11 @@ var init_semconv = __esm({
389
396
  GEN_AI_TOOL_NAME = "gen_ai.tool.name";
390
397
  GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions";
391
398
  GEN_AI_REQUEST_PREFIX = "gen_ai.request.";
399
+ MCP_METHOD_NAME = "mcp.method.name";
400
+ MCP_METHOD_TOOLS_CALL = "tools/call";
401
+ MCP_PROTOCOL_VERSION = "mcp.protocol.version";
402
+ ERROR_TYPE = "error.type";
403
+ ERROR_TYPE_TOOL_ERROR = "tool_error";
392
404
  MCP_RESULT_TYPE = "mcp.result_type";
393
405
  GEN_AI_FIRST_TOKEN_EVENT = "gen_ai.first_token";
394
406
  SpanKind = /* @__PURE__ */ ((SpanKind2) => {
@@ -406,6 +418,14 @@ var init_semconv = __esm({
406
418
  ["EMBEDDING" /* EMBEDDING */]: "embeddings",
407
419
  ["AGENT" /* AGENT */]: "invoke_agent"
408
420
  };
421
+ OTEL_KIND_BY_KIND = {
422
+ ["LLM" /* LLM */]: OtelSpanKind.CLIENT,
423
+ ["EMBEDDING" /* EMBEDDING */]: OtelSpanKind.CLIENT,
424
+ ["RETRIEVER" /* RETRIEVER */]: OtelSpanKind.CLIENT,
425
+ ["TOOL" /* TOOL */]: OtelSpanKind.INTERNAL,
426
+ ["AGENT" /* AGENT */]: OtelSpanKind.INTERNAL,
427
+ ["CHAIN" /* CHAIN */]: OtelSpanKind.INTERNAL
428
+ };
409
429
  CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
410
430
  INPUT_VALUE,
411
431
  OUTPUT_VALUE,
@@ -490,6 +510,11 @@ var init_semconv = __esm({
490
510
  GEN_AI_OPERATION_NAME,
491
511
  GEN_AI_PROVIDER_NAME,
492
512
  GEN_AI_TOOL_NAME,
513
+ // Protocol identity, not content: a still-running MCP call must be
514
+ // distinguishable from a local tool in the live view — the one place
515
+ // setting the marker at creation pays off.
516
+ MCP_METHOD_NAME,
517
+ MCP_PROTOCOL_VERSION,
493
518
  // Identity, not content: a pending span must be groupable into its
494
519
  // session while still running, that is the live view's whole point.
495
520
  SESSION_ID,
@@ -518,6 +543,9 @@ function renderable(raw, value) {
518
543
  if (raw instanceof ArrayBuffer) return `<ArrayBuffer ${raw.byteLength} bytes>`;
519
544
  return value;
520
545
  }
546
+ function asRecord(value) {
547
+ return typeof value === "object" && value !== null ? value : void 0;
548
+ }
521
549
  function toAttributeValue(value) {
522
550
  if (typeof value === "string") return truncate(value);
523
551
  if (typeof value === "number" || typeof value === "boolean") return value;
@@ -542,6 +570,9 @@ function toAttributeValue(value) {
542
570
  return "[unserializable]";
543
571
  }
544
572
  }
573
+ function errorType(error) {
574
+ return error instanceof Error ? error.name : typeof error;
575
+ }
545
576
  var MAX_ATTR_CHARS, TRUNCATION_MARKER;
546
577
  var init_serde = __esm({
547
578
  "src/serde.ts"() {
@@ -585,27 +616,34 @@ function configure(observation, options) {
585
616
  if (options.input !== void 0) observation.setInput(options.input);
586
617
  return observation;
587
618
  }
588
- function creationAttributes(options) {
589
- const attributes = { ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */), ...options.attributes };
619
+ function creationAttributes(name, options) {
620
+ const attributes = {
621
+ ...kindAttributes(options.kind ?? "CHAIN" /* CHAIN */, name),
622
+ ...options.attributes
623
+ };
590
624
  if (options.userId !== void 0) attributes[USER_ID] = options.userId;
591
625
  return attributes;
592
626
  }
593
627
  function startSpan(name, options = {}) {
594
- const span = getTracer().startSpan(name, { attributes: creationAttributes(options) });
628
+ const span = getTracer().startSpan(name, {
629
+ kind: options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */),
630
+ attributes: creationAttributes(name, options)
631
+ });
595
632
  return configure(new Observation(span), options);
596
633
  }
597
634
  function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
598
635
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
599
636
  return runActive(
600
637
  name,
601
- creationAttributes(options),
638
+ creationAttributes(name, options),
602
639
  options.userId,
603
640
  (span) => configure(new Observation(span), options),
604
- fn
641
+ fn,
642
+ options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */)
605
643
  );
606
644
  }
607
- function runActive(name, attributes, userId, makeHandle, fn) {
608
- const run = () => getTracer().startActiveSpan(name, { attributes }, async (span) => {
645
+ function runActive(name, attributes, userId, makeHandle, fn, otelKind) {
646
+ const run = () => getTracer().startActiveSpan(name, { kind: otelKind, attributes }, async (span) => {
609
647
  const handle = makeHandle(span);
610
648
  try {
611
649
  return await fn(handle);
@@ -666,9 +704,15 @@ var init_spans = __esm({
666
704
  return this;
667
705
  }
668
706
  /**
669
- * Record an error on the span and set ERROR status. This is exactly what the
670
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
671
- * `start*` path does not have to reach through `.span` to match it.
707
+ * Record an error on the span, set ERROR status and `error.type`. This is
708
+ * exactly what the `startAsCurrent*` helpers do on a thrown error, exposed
709
+ * so the manual `start*` path does not have to reach through `.span` to
710
+ * match it.
711
+ *
712
+ * `error.type` is Conditionally Required by the GenAI conventions on every
713
+ * span that ends in an error, and every helper's throw path funnels through
714
+ * here, so this is the one place that sets it. The error's name only, never
715
+ * the message: it must stay low-cardinality and free of echoed content.
672
716
  *
673
717
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
674
718
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -677,6 +721,7 @@ var init_spans = __esm({
677
721
  const wrapped = error instanceof Error ? error : new Error(String(error));
678
722
  this.span.recordException(wrapped);
679
723
  this.span.setStatus({ code: SpanStatusCode.ERROR, message: wrapped.message });
724
+ this.span.setAttribute(ERROR_TYPE, errorType(error));
680
725
  return this;
681
726
  }
682
727
  end() {
@@ -697,29 +742,27 @@ var instrumentationMcp_exports = {};
697
742
  __export(instrumentationMcp_exports, {
698
743
  instrumentMcpClient: () => instrumentMcpClient
699
744
  });
700
- import { SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
745
+ import { SpanKind as OtelSpanKind2, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
701
746
  function serializeResult(result) {
702
- if (typeof result !== "object" || result === null) return toAttributeValue(result);
703
- const record = result;
747
+ const record = asRecord(result);
748
+ if (record === void 0) return toAttributeValue(result);
704
749
  const structured = record.structured_content ?? record.structuredContent;
705
750
  if (structured !== void 0 && structured !== null) return toAttributeValue(structured);
706
751
  const content = record.content;
707
752
  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");
753
+ const texts = content.map((block) => asRecord(block)?.text).filter((text) => typeof text === "string");
711
754
  if (texts.length === 1) return truncate(texts[0]);
712
755
  if (texts.length > 1) return toAttributeValue(texts);
713
756
  }
714
757
  return toAttributeValue(result);
715
758
  }
716
759
  function resultIsError(result) {
717
- if (typeof result !== "object" || result === null) return false;
718
- const record = result;
719
- return Boolean(record.is_error ?? record.isError);
760
+ const record = asRecord(result);
761
+ return Boolean(record?.is_error ?? record?.isError);
720
762
  }
721
763
  function recordResult(observation, result) {
722
- const resultType = typeof result === "object" && result !== null ? result.result_type ?? result.resultType : void 0;
764
+ const record = asRecord(result);
765
+ const resultType = record?.result_type ?? record?.resultType;
723
766
  if (resultType === INPUT_REQUIRED) {
724
767
  observation.setAttribute(MCP_RESULT_TYPE, INPUT_REQUIRED);
725
768
  return;
@@ -730,8 +773,21 @@ function recordResult(observation, result) {
730
773
  code: SpanStatusCode2.ERROR,
731
774
  message: "tool returned an error result"
732
775
  });
776
+ observation.setAttribute(ERROR_TYPE, ERROR_TYPE_TOOL_ERROR);
733
777
  }
734
778
  }
779
+ function negotiatedProtocolVersion(client) {
780
+ const version = asRecord(asRecord(client)?.transport)?.protocolVersion;
781
+ return typeof version === "string" ? version : void 0;
782
+ }
783
+ function callAttributes(toolName, protocolVersion) {
784
+ const attributes = {
785
+ [GEN_AI_TOOL_NAME]: toolName,
786
+ [MCP_METHOD_NAME]: MCP_METHOD_TOOLS_CALL
787
+ };
788
+ if (protocolVersion !== void 0) attributes[MCP_PROTOCOL_VERSION] = protocolVersion;
789
+ return attributes;
790
+ }
735
791
  function instrumentMcpClient(ClientClass) {
736
792
  const current = ClientClass.prototype.callTool;
737
793
  const alreadyWrapped = current.riusOriginal;
@@ -744,12 +800,14 @@ function instrumentMcpClient(ClientClass) {
744
800
  const instrumented = function instrumentedCallTool(params, ...rest) {
745
801
  return startAsCurrentSpan(
746
802
  `execute_tool ${params.name}`,
747
- // Tool name at CREATION: pending snapshots are built at start, so an
748
- // attribute set inside the callback never reaches them.
749
803
  {
750
804
  kind: "TOOL" /* TOOL */,
805
+ // The OTel SpanKind FIELD (orthogonal to our taxonomy attribute above):
806
+ // a remote tool call is CLIENT under both the MCP and the GenAI
807
+ // execute-tool conventions.
808
+ otelKind: OtelSpanKind2.CLIENT,
751
809
  input: params.arguments,
752
- attributes: { [GEN_AI_TOOL_NAME]: params.name }
810
+ attributes: callAttributes(params.name, negotiatedProtocolVersion(this))
753
811
  },
754
812
  async (observation) => {
755
813
  const result = await original.call(this, params, ...rest);
@@ -1655,6 +1713,12 @@ var Generation = class extends Observation {
1655
1713
  }
1656
1714
  provider;
1657
1715
  firstTokenRecorded = false;
1716
+ /**
1717
+ * Monotonic clock at construction, which is span creation for both
1718
+ * `startGeneration` and the scoped form. The API `Span` exposes no start
1719
+ * time, so this is what `recordFirstToken` measures the first chunk against.
1720
+ */
1721
+ startedAt = performance.now();
1658
1722
  /**
1659
1723
  * Record the request messages (`gen_ai.input.messages`), normalised to the
1660
1724
  * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
@@ -1729,17 +1793,24 @@ var Generation = class extends Observation {
1729
1793
  return this;
1730
1794
  }
1731
1795
  /**
1732
- * The TTFT anchor: event time minus span start. Idempotent: only the
1733
- * first call records the event, so a streaming loop can call this
1734
- * unconditionally on every chunk without inflating the span. A no-op
1735
- * after the span has ended.
1796
+ * Mark the arrival of the first streamed token. Records the
1797
+ * `gen_ai.first_token` event (the timestamp the backend derives TTFT from)
1798
+ * and, per the GenAI conventions, the derived
1799
+ * `gen_ai.response.time_to_first_chunk` (seconds since span creation) plus
1800
+ * `gen_ai.request.stream = true`: a first chunk arriving is what tells the
1801
+ * SDK the request streamed. Idempotent: only the first call records, so a
1802
+ * streaming loop can call this unconditionally on every chunk without
1803
+ * inflating the span. A no-op after the span has ended.
1736
1804
  */
1737
1805
  recordFirstToken() {
1738
1806
  if (this.firstTokenRecorded || !this.span.isRecording()) {
1739
1807
  return this;
1740
1808
  }
1809
+ const elapsedSeconds = Math.max(performance.now() - this.startedAt, 0) / 1e3;
1741
1810
  this.span.addEvent(GEN_AI_FIRST_TOKEN_EVENT);
1742
1811
  this.firstTokenRecorded = true;
1812
+ this.span.setAttribute(GEN_AI_REQUEST_STREAM, true);
1813
+ this.span.setAttribute(GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, elapsedSeconds);
1743
1814
  return this;
1744
1815
  }
1745
1816
  };
@@ -1763,7 +1834,10 @@ function configure2(generation, options) {
1763
1834
  return generation;
1764
1835
  }
1765
1836
  function startGeneration(name, options = {}) {
1766
- const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1837
+ const span = getTracer().startSpan(name, {
1838
+ kind: otelSpanKind("LLM" /* LLM */),
1839
+ attributes: attributesFor(options)
1840
+ });
1767
1841
  return configure2(new Generation(span, options.provider), options);
1768
1842
  }
1769
1843
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
@@ -1773,7 +1847,8 @@ function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1773
1847
  attributesFor(options),
1774
1848
  options.userId,
1775
1849
  (span) => configure2(new Generation(span, options.provider), options),
1776
- fn
1850
+ fn,
1851
+ otelSpanKind("LLM" /* LLM */)
1777
1852
  );
1778
1853
  }
1779
1854
 
@@ -1806,7 +1881,7 @@ init_session();
1806
1881
  init_user();
1807
1882
  init_spans();
1808
1883
  init_workspace();
1809
- var VERSION = "0.6.1";
1884
+ var VERSION = "0.8.0";
1810
1885
  export {
1811
1886
  Generation,
1812
1887
  Observation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassflow-ai/rius",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "OpenTelemetry-native tracing for AI agents and LLM applications",
5
5
  "keywords": [
6
6
  "opentelemetry",