@glassflow-ai/rius 0.7.0 → 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_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, 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";
@@ -424,6 +431,14 @@ var init_semconv = __esm({
424
431
  ["EMBEDDING" /* EMBEDDING */]: "embeddings",
425
432
  ["AGENT" /* AGENT */]: "invoke_agent"
426
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
+ };
427
442
  CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
428
443
  INPUT_VALUE,
429
444
  OUTPUT_VALUE,
@@ -568,6 +583,9 @@ function toAttributeValue(value) {
568
583
  return "[unserializable]";
569
584
  }
570
585
  }
586
+ function errorType(error) {
587
+ return error instanceof Error ? error.name : typeof error;
588
+ }
571
589
  var MAX_ATTR_CHARS, TRUNCATION_MARKER;
572
590
  var init_serde = __esm({
573
591
  "src/serde.ts"() {
@@ -580,16 +598,16 @@ var init_serde = __esm({
580
598
 
581
599
  // src/user.ts
582
600
  function withUser(userId, fn) {
583
- 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));
584
602
  }
585
- var import_api, USER_KEY, UserSpanProcessor;
603
+ var import_api2, USER_KEY, UserSpanProcessor;
586
604
  var init_user = __esm({
587
605
  "src/user.ts"() {
588
606
  "use strict";
589
607
  init_cjs_shims();
590
- import_api = require("@opentelemetry/api");
608
+ import_api2 = require("@opentelemetry/api");
591
609
  init_semconv();
592
- USER_KEY = (0, import_api.createContextKey)("rius-user-id");
610
+ USER_KEY = (0, import_api2.createContextKey)("rius-user-id");
593
611
  UserSpanProcessor = class {
594
612
  onStart(span, parentContext) {
595
613
  const value = parentContext.getValue(USER_KEY);
@@ -610,15 +628,18 @@ function configure(observation, options) {
610
628
  if (options.input !== void 0) observation.setInput(options.input);
611
629
  return observation;
612
630
  }
613
- function creationAttributes(options) {
614
- 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
+ };
615
636
  if (options.userId !== void 0) attributes[USER_ID] = options.userId;
616
637
  return attributes;
617
638
  }
618
639
  function startSpan(name, options = {}) {
619
640
  const span = getTracer().startSpan(name, {
620
- kind: options.otelKind,
621
- attributes: creationAttributes(options)
641
+ kind: options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */),
642
+ attributes: creationAttributes(name, options)
622
643
  });
623
644
  return configure(new Observation(span), options);
624
645
  }
@@ -626,11 +647,11 @@ function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
626
647
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
627
648
  return runActive(
628
649
  name,
629
- creationAttributes(options),
650
+ creationAttributes(name, options),
630
651
  options.userId,
631
652
  (span) => configure(new Observation(span), options),
632
653
  fn,
633
- options.otelKind
654
+ options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */)
634
655
  );
635
656
  }
636
657
  function runActive(name, attributes, userId, makeHandle, fn, otelKind) {
@@ -647,12 +668,12 @@ function runActive(name, attributes, userId, makeHandle, fn, otelKind) {
647
668
  });
648
669
  return userId !== void 0 ? withUser(userId, run) : run();
649
670
  }
650
- var import_api2, Observation;
671
+ var import_api3, Observation;
651
672
  var init_spans = __esm({
652
673
  "src/spans.ts"() {
653
674
  "use strict";
654
675
  init_cjs_shims();
655
- import_api2 = require("@opentelemetry/api");
676
+ import_api3 = require("@opentelemetry/api");
656
677
  init_client();
657
678
  init_semconv();
658
679
  init_serde();
@@ -696,9 +717,15 @@ var init_spans = __esm({
696
717
  return this;
697
718
  }
698
719
  /**
699
- * Record an error on the span and set ERROR status. This is exactly what the
700
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
701
- * `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.
702
729
  *
703
730
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
704
731
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -706,7 +733,8 @@ var init_spans = __esm({
706
733
  recordException(error) {
707
734
  const wrapped = error instanceof Error ? error : new Error(String(error));
708
735
  this.span.recordException(wrapped);
709
- 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));
710
738
  return this;
711
739
  }
712
740
  end() {
@@ -754,15 +782,12 @@ function recordResult(observation, result) {
754
782
  observation.setAttribute(OUTPUT_VALUE, serializeResult(result));
755
783
  if (resultIsError(result)) {
756
784
  observation.span.setStatus({
757
- code: import_api3.SpanStatusCode.ERROR,
785
+ code: import_api4.SpanStatusCode.ERROR,
758
786
  message: "tool returned an error result"
759
787
  });
760
788
  observation.setAttribute(ERROR_TYPE, ERROR_TYPE_TOOL_ERROR);
761
789
  }
762
790
  }
763
- function errorType(error) {
764
- return error instanceof Error ? error.name : typeof error;
765
- }
766
791
  function negotiatedProtocolVersion(client) {
767
792
  const version = asRecord(asRecord(client)?.transport)?.protocolVersion;
768
793
  return typeof version === "string" ? version : void 0;
@@ -792,18 +817,12 @@ function instrumentMcpClient(ClientClass) {
792
817
  // The OTel SpanKind FIELD (orthogonal to our taxonomy attribute above):
793
818
  // a remote tool call is CLIENT under both the MCP and the GenAI
794
819
  // execute-tool conventions.
795
- otelKind: import_api3.SpanKind.CLIENT,
820
+ otelKind: import_api4.SpanKind.CLIENT,
796
821
  input: params.arguments,
797
822
  attributes: callAttributes(params.name, negotiatedProtocolVersion(this))
798
823
  },
799
824
  async (observation) => {
800
- let result;
801
- try {
802
- result = await original.call(this, params, ...rest);
803
- } catch (error) {
804
- observation.setAttribute(ERROR_TYPE, errorType(error));
805
- throw error;
806
- }
825
+ const result = await original.call(this, params, ...rest);
807
826
  recordResult(observation, result);
808
827
  return result;
809
828
  }
@@ -815,12 +834,12 @@ function instrumentMcpClient(ClientClass) {
815
834
  ClientClass.prototype.callTool = original;
816
835
  };
817
836
  }
818
- var import_api3, INPUT_REQUIRED;
837
+ var import_api4, INPUT_REQUIRED;
819
838
  var init_instrumentationMcp = __esm({
820
839
  "src/instrumentationMcp.ts"() {
821
840
  "use strict";
822
841
  init_cjs_shims();
823
- import_api3 = require("@opentelemetry/api");
842
+ import_api4 = require("@opentelemetry/api");
824
843
  init_semconv();
825
844
  init_serde();
826
845
  init_spans();
@@ -1094,12 +1113,12 @@ function redactInvocationParameters(value) {
1094
1113
  for (const member of INVOCATION_PARAMETERS_CONTENT_MEMBERS) delete bag[member];
1095
1114
  return JSON.stringify(bag);
1096
1115
  }
1097
- 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;
1098
1117
  var init_masking = __esm({
1099
1118
  "src/masking.ts"() {
1100
1119
  "use strict";
1101
1120
  init_cjs_shims();
1102
- import_api4 = require("@opentelemetry/api");
1121
+ import_api5 = require("@opentelemetry/api");
1103
1122
  init_semconv();
1104
1123
  init_serde();
1105
1124
  METADATA_PREFIX = "metadata.";
@@ -1143,7 +1162,7 @@ var init_masking = __esm({
1143
1162
  */
1144
1163
  sanitizeStatus(span) {
1145
1164
  const status = span.status;
1146
- if (status?.code !== import_api4.SpanStatusCode.ERROR || !status.message) return;
1165
+ if (status?.code !== import_api5.SpanStatusCode.ERROR || !status.message) return;
1147
1166
  if (!this.opts.captureContent) {
1148
1167
  status.message = void 0;
1149
1168
  return;
@@ -1213,7 +1232,7 @@ function buildSnapshot(span) {
1213
1232
  startTime: span.startTime,
1214
1233
  endTime: span.startTime,
1215
1234
  duration: [0, 0],
1216
- status: { code: import_api5.SpanStatusCode.UNSET },
1235
+ status: { code: import_api6.SpanStatusCode.UNSET },
1217
1236
  attributes,
1218
1237
  links: [],
1219
1238
  events: [],
@@ -1228,12 +1247,12 @@ function buildSnapshot(span) {
1228
1247
  function spanKey(context2) {
1229
1248
  return `${context2.traceId}:${context2.spanId}`;
1230
1249
  }
1231
- var import_api5, PendingSpanProcessor;
1250
+ var import_api6, PendingSpanProcessor;
1232
1251
  var init_pending = __esm({
1233
1252
  "src/pending.ts"() {
1234
1253
  "use strict";
1235
1254
  init_cjs_shims();
1236
- import_api5 = require("@opentelemetry/api");
1255
+ import_api6 = require("@opentelemetry/api");
1237
1256
  init_semconv();
1238
1257
  PendingSpanProcessor = class {
1239
1258
  delegate;
@@ -1281,17 +1300,17 @@ var init_pending = __esm({
1281
1300
  // src/session.ts
1282
1301
  function withSession(sessionIdOrFn, maybeFn) {
1283
1302
  const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [(0, import_node_crypto.randomUUID)(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
1284
- 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));
1285
1304
  }
1286
- var import_node_crypto, import_api6, SESSION_KEY, SessionSpanProcessor;
1305
+ var import_node_crypto, import_api7, SESSION_KEY, SessionSpanProcessor;
1287
1306
  var init_session = __esm({
1288
1307
  "src/session.ts"() {
1289
1308
  "use strict";
1290
1309
  init_cjs_shims();
1291
1310
  import_node_crypto = require("crypto");
1292
- import_api6 = require("@opentelemetry/api");
1311
+ import_api7 = require("@opentelemetry/api");
1293
1312
  init_semconv();
1294
- SESSION_KEY = (0, import_api6.createContextKey)("rius-session-id");
1313
+ SESSION_KEY = (0, import_api7.createContextKey)("rius-session-id");
1295
1314
  SessionSpanProcessor = class {
1296
1315
  constructor(defaultSessionId) {
1297
1316
  this.defaultSessionId = defaultSessionId;
@@ -1299,7 +1318,7 @@ var init_session = __esm({
1299
1318
  defaultSessionId;
1300
1319
  onStart(span, parentContext) {
1301
1320
  const value = parentContext.getValue(SESSION_KEY);
1302
- const parent = import_api6.trace.getSpan(parentContext);
1321
+ const parent = import_api7.trace.getSpan(parentContext);
1303
1322
  const inherited = parent?.attributes?.[SESSION_ID];
1304
1323
  const sessionId = typeof value === "string" ? value : typeof inherited === "string" ? inherited : this.defaultSessionId;
1305
1324
  if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
@@ -1317,7 +1336,7 @@ var init_session = __esm({
1317
1336
  // src/workspace.ts
1318
1337
  function withWorkspace(alias, fn) {
1319
1338
  if (!alias) throw new Error("workspace alias must be a non-empty string");
1320
- 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));
1321
1340
  }
1322
1341
  function setGlobalRouting(routing) {
1323
1342
  globalRouting = routing;
@@ -1330,20 +1349,20 @@ function registerWorkspace(alias, apiKey) {
1330
1349
  }
1331
1350
  globalRouting.register(alias, apiKey);
1332
1351
  }
1333
- var import_api7, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1352
+ var import_api8, WORKSPACE_KEY, WorkspaceSpanProcessor, RoutingSpanExporter, globalRouting;
1334
1353
  var init_workspace = __esm({
1335
1354
  "src/workspace.ts"() {
1336
1355
  "use strict";
1337
1356
  init_cjs_shims();
1338
- import_api7 = require("@opentelemetry/api");
1357
+ import_api8 = require("@opentelemetry/api");
1339
1358
  init_esm();
1340
1359
  init_semconv();
1341
- WORKSPACE_KEY = (0, import_api7.createContextKey)("rius-workspace-alias");
1360
+ WORKSPACE_KEY = (0, import_api8.createContextKey)("rius-workspace-alias");
1342
1361
  WorkspaceSpanProcessor = class {
1343
1362
  /** Straddles already warned about, as "parentAlias->alias"; one warning each, not one per span. */
1344
1363
  warnedStraddles = /* @__PURE__ */ new Set();
1345
1364
  onStart(span, parentContext) {
1346
- const parent = import_api7.trace.getSpan(parentContext);
1365
+ const parent = import_api8.trace.getSpan(parentContext);
1347
1366
  const parentAlias = parent?.attributes?.[WORKSPACE_ROUTE];
1348
1367
  const scoped = parentContext.getValue(WORKSPACE_KEY);
1349
1368
  const alias = typeof scoped === "string" ? scoped : parentAlias;
@@ -1532,15 +1551,15 @@ function init(options = {}) {
1532
1551
  return globalClient;
1533
1552
  }
1534
1553
  function getTracer() {
1535
- return import_api8.trace.getTracer(TRACER_NAME, SDK_VERSION);
1554
+ return import_api9.trace.getTracer(TRACER_NAME, SDK_VERSION);
1536
1555
  }
1537
- 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;
1538
1557
  var init_client = __esm({
1539
1558
  "src/client.ts"() {
1540
1559
  "use strict";
1541
1560
  init_cjs_shims();
1542
1561
  import_node_crypto2 = require("crypto");
1543
- import_api8 = require("@opentelemetry/api");
1562
+ import_api9 = require("@opentelemetry/api");
1544
1563
  import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
1545
1564
  import_resources = require("@opentelemetry/resources");
1546
1565
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -1611,9 +1630,9 @@ var init_client = __esm({
1611
1630
  if (globalClient === this) {
1612
1631
  globalClient = void 0;
1613
1632
  setGlobalRouting(void 0);
1614
- import_api8.trace.disable();
1615
- import_api8.context.disable();
1616
- import_api8.propagation.disable();
1633
+ import_api9.trace.disable();
1634
+ import_api9.context.disable();
1635
+ import_api9.propagation.disable();
1617
1636
  }
1618
1637
  }
1619
1638
  }
@@ -1723,6 +1742,12 @@ var Generation = class extends Observation {
1723
1742
  }
1724
1743
  provider;
1725
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();
1726
1751
  /**
1727
1752
  * Record the request messages (`gen_ai.input.messages`), normalised to the
1728
1753
  * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
@@ -1797,17 +1822,24 @@ var Generation = class extends Observation {
1797
1822
  return this;
1798
1823
  }
1799
1824
  /**
1800
- * The TTFT anchor: event time minus span start. Idempotent: only the
1801
- * first call records the event, so a streaming loop can call this
1802
- * unconditionally on every chunk without inflating the span. A no-op
1803
- * 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.
1804
1833
  */
1805
1834
  recordFirstToken() {
1806
1835
  if (this.firstTokenRecorded || !this.span.isRecording()) {
1807
1836
  return this;
1808
1837
  }
1838
+ const elapsedSeconds = Math.max(performance.now() - this.startedAt, 0) / 1e3;
1809
1839
  this.span.addEvent(GEN_AI_FIRST_TOKEN_EVENT);
1810
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);
1811
1843
  return this;
1812
1844
  }
1813
1845
  };
@@ -1831,7 +1863,10 @@ function configure2(generation, options) {
1831
1863
  return generation;
1832
1864
  }
1833
1865
  function startGeneration(name, options = {}) {
1834
- const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1866
+ const span = getTracer().startSpan(name, {
1867
+ kind: otelSpanKind("LLM" /* LLM */),
1868
+ attributes: attributesFor(options)
1869
+ });
1835
1870
  return configure2(new Generation(span, options.provider), options);
1836
1871
  }
1837
1872
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
@@ -1841,7 +1876,8 @@ function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1841
1876
  attributesFor(options),
1842
1877
  options.userId,
1843
1878
  (span) => configure2(new Generation(span, options.provider), options),
1844
- fn
1879
+ fn,
1880
+ otelSpanKind("LLM" /* LLM */)
1845
1881
  );
1846
1882
  }
1847
1883
 
@@ -1874,7 +1910,7 @@ init_session();
1874
1910
  init_user();
1875
1911
  init_spans();
1876
1912
  init_workspace();
1877
- var VERSION = "0.7.0";
1913
+ var VERSION = "0.8.0";
1878
1914
  // Annotate the CommonJS export names for ESM import in node:
1879
1915
  0 && (module.exports = {
1880
1916
  Generation,
package/dist/index.d.cts CHANGED
@@ -174,8 +174,8 @@ interface SpanOptions {
174
174
  /**
175
175
  * The OpenTelemetry `SpanKind` FIELD (INTERNAL, CLIENT, …), orthogonal to
176
176
  * `kind` above, which is our taxonomy attribute. Conventions set it per
177
- * operation — a remote tool call is CLIENT — so it is opt-in here and left
178
- * at the tracer's default (INTERNAL) otherwise.
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
179
  */
180
180
  otelKind?: SpanKind$1;
181
181
  input?: unknown;
@@ -210,9 +210,15 @@ declare class Observation {
210
210
  */
211
211
  setAttribute(key: string, value: unknown): this;
212
212
  /**
213
- * Record an error on the span and set ERROR status. This is exactly what the
214
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
215
- * `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.
216
222
  *
217
223
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
218
224
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -282,6 +288,12 @@ interface GenerationOptions {
282
288
  declare class Generation extends Observation {
283
289
  private readonly provider?;
284
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;
285
297
  /**
286
298
  * The provider passed at creation; drives the Anthropic input-token summing
287
299
  * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
@@ -341,10 +353,14 @@ declare class Generation extends Observation {
341
353
  */
342
354
  setFinishReasons(reasons: string | string[]): this;
343
355
  /**
344
- * The TTFT anchor: event time minus span start. Idempotent: only the
345
- * first call records the event, so a streaming loop can call this
346
- * unconditionally on every chunk without inflating the span. A no-op
347
- * 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.
348
364
  */
349
365
  recordFirstToken(): this;
350
366
  }
@@ -448,6 +464,6 @@ declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T)
448
464
  */
449
465
  declare function withUser<T>(userId: string, fn: (userId: string) => T): T;
450
466
 
451
- declare const VERSION = "0.7.0";
467
+ declare const VERSION = "0.8.0";
452
468
 
453
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
@@ -174,8 +174,8 @@ interface SpanOptions {
174
174
  /**
175
175
  * The OpenTelemetry `SpanKind` FIELD (INTERNAL, CLIENT, …), orthogonal to
176
176
  * `kind` above, which is our taxonomy attribute. Conventions set it per
177
- * operation — a remote tool call is CLIENT — so it is opt-in here and left
178
- * at the tracer's default (INTERNAL) otherwise.
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
179
  */
180
180
  otelKind?: SpanKind$1;
181
181
  input?: unknown;
@@ -210,9 +210,15 @@ declare class Observation {
210
210
  */
211
211
  setAttribute(key: string, value: unknown): this;
212
212
  /**
213
- * Record an error on the span and set ERROR status. This is exactly what the
214
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
215
- * `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.
216
222
  *
217
223
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
218
224
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -282,6 +288,12 @@ interface GenerationOptions {
282
288
  declare class Generation extends Observation {
283
289
  private readonly provider?;
284
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;
285
297
  /**
286
298
  * The provider passed at creation; drives the Anthropic input-token summing
287
299
  * in {@link setUsage}. A bare `new Generation(span)` has none and never sums.
@@ -341,10 +353,14 @@ declare class Generation extends Observation {
341
353
  */
342
354
  setFinishReasons(reasons: string | string[]): this;
343
355
  /**
344
- * The TTFT anchor: event time minus span start. Idempotent: only the
345
- * first call records the event, so a streaming loop can call this
346
- * unconditionally on every chunk without inflating the span. A no-op
347
- * 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.
348
364
  */
349
365
  recordFirstToken(): this;
350
366
  }
@@ -448,6 +464,6 @@ declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T)
448
464
  */
449
465
  declare function withUser<T>(userId: string, fn: (userId: string) => T): T;
450
466
 
451
- declare const VERSION = "0.7.0";
467
+ declare const VERSION = "0.8.0";
452
468
 
453
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_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, 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";
@@ -411,6 +418,14 @@ var init_semconv = __esm({
411
418
  ["EMBEDDING" /* EMBEDDING */]: "embeddings",
412
419
  ["AGENT" /* AGENT */]: "invoke_agent"
413
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
+ };
414
429
  CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
415
430
  INPUT_VALUE,
416
431
  OUTPUT_VALUE,
@@ -555,6 +570,9 @@ function toAttributeValue(value) {
555
570
  return "[unserializable]";
556
571
  }
557
572
  }
573
+ function errorType(error) {
574
+ return error instanceof Error ? error.name : typeof error;
575
+ }
558
576
  var MAX_ATTR_CHARS, TRUNCATION_MARKER;
559
577
  var init_serde = __esm({
560
578
  "src/serde.ts"() {
@@ -598,15 +616,18 @@ function configure(observation, options) {
598
616
  if (options.input !== void 0) observation.setInput(options.input);
599
617
  return observation;
600
618
  }
601
- function creationAttributes(options) {
602
- 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
+ };
603
624
  if (options.userId !== void 0) attributes[USER_ID] = options.userId;
604
625
  return attributes;
605
626
  }
606
627
  function startSpan(name, options = {}) {
607
628
  const span = getTracer().startSpan(name, {
608
- kind: options.otelKind,
609
- attributes: creationAttributes(options)
629
+ kind: options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */),
630
+ attributes: creationAttributes(name, options)
610
631
  });
611
632
  return configure(new Observation(span), options);
612
633
  }
@@ -614,11 +635,11 @@ function startAsCurrentSpan(name, optionsOrFn, maybeFn) {
614
635
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
615
636
  return runActive(
616
637
  name,
617
- creationAttributes(options),
638
+ creationAttributes(name, options),
618
639
  options.userId,
619
640
  (span) => configure(new Observation(span), options),
620
641
  fn,
621
- options.otelKind
642
+ options.otelKind ?? otelSpanKind(options.kind ?? "CHAIN" /* CHAIN */)
622
643
  );
623
644
  }
624
645
  function runActive(name, attributes, userId, makeHandle, fn, otelKind) {
@@ -683,9 +704,15 @@ var init_spans = __esm({
683
704
  return this;
684
705
  }
685
706
  /**
686
- * Record an error on the span and set ERROR status. This is exactly what the
687
- * `startAsCurrent*` helpers do on a thrown error, exposed so the manual
688
- * `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.
689
716
  *
690
717
  * Accepts `unknown` because that is what a `catch` binding is; a non-Error
691
718
  * throwable is wrapped so `recordException` still gets a real Error.
@@ -694,6 +721,7 @@ var init_spans = __esm({
694
721
  const wrapped = error instanceof Error ? error : new Error(String(error));
695
722
  this.span.recordException(wrapped);
696
723
  this.span.setStatus({ code: SpanStatusCode.ERROR, message: wrapped.message });
724
+ this.span.setAttribute(ERROR_TYPE, errorType(error));
697
725
  return this;
698
726
  }
699
727
  end() {
@@ -714,7 +742,7 @@ var instrumentationMcp_exports = {};
714
742
  __export(instrumentationMcp_exports, {
715
743
  instrumentMcpClient: () => instrumentMcpClient
716
744
  });
717
- import { SpanKind as OtelSpanKind, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
745
+ import { SpanKind as OtelSpanKind2, SpanStatusCode as SpanStatusCode2 } from "@opentelemetry/api";
718
746
  function serializeResult(result) {
719
747
  const record = asRecord(result);
720
748
  if (record === void 0) return toAttributeValue(result);
@@ -748,9 +776,6 @@ function recordResult(observation, result) {
748
776
  observation.setAttribute(ERROR_TYPE, ERROR_TYPE_TOOL_ERROR);
749
777
  }
750
778
  }
751
- function errorType(error) {
752
- return error instanceof Error ? error.name : typeof error;
753
- }
754
779
  function negotiatedProtocolVersion(client) {
755
780
  const version = asRecord(asRecord(client)?.transport)?.protocolVersion;
756
781
  return typeof version === "string" ? version : void 0;
@@ -780,18 +805,12 @@ function instrumentMcpClient(ClientClass) {
780
805
  // The OTel SpanKind FIELD (orthogonal to our taxonomy attribute above):
781
806
  // a remote tool call is CLIENT under both the MCP and the GenAI
782
807
  // execute-tool conventions.
783
- otelKind: OtelSpanKind.CLIENT,
808
+ otelKind: OtelSpanKind2.CLIENT,
784
809
  input: params.arguments,
785
810
  attributes: callAttributes(params.name, negotiatedProtocolVersion(this))
786
811
  },
787
812
  async (observation) => {
788
- let result;
789
- try {
790
- result = await original.call(this, params, ...rest);
791
- } catch (error) {
792
- observation.setAttribute(ERROR_TYPE, errorType(error));
793
- throw error;
794
- }
813
+ const result = await original.call(this, params, ...rest);
795
814
  recordResult(observation, result);
796
815
  return result;
797
816
  }
@@ -1694,6 +1713,12 @@ var Generation = class extends Observation {
1694
1713
  }
1695
1714
  provider;
1696
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();
1697
1722
  /**
1698
1723
  * Record the request messages (`gen_ai.input.messages`), normalised to the
1699
1724
  * GenAI `{role, parts}` shape like the Python SDK does: bare strings, OpenAI
@@ -1768,17 +1793,24 @@ var Generation = class extends Observation {
1768
1793
  return this;
1769
1794
  }
1770
1795
  /**
1771
- * The TTFT anchor: event time minus span start. Idempotent: only the
1772
- * first call records the event, so a streaming loop can call this
1773
- * unconditionally on every chunk without inflating the span. A no-op
1774
- * 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.
1775
1804
  */
1776
1805
  recordFirstToken() {
1777
1806
  if (this.firstTokenRecorded || !this.span.isRecording()) {
1778
1807
  return this;
1779
1808
  }
1809
+ const elapsedSeconds = Math.max(performance.now() - this.startedAt, 0) / 1e3;
1780
1810
  this.span.addEvent(GEN_AI_FIRST_TOKEN_EVENT);
1781
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);
1782
1814
  return this;
1783
1815
  }
1784
1816
  };
@@ -1802,7 +1834,10 @@ function configure2(generation, options) {
1802
1834
  return generation;
1803
1835
  }
1804
1836
  function startGeneration(name, options = {}) {
1805
- const span = getTracer().startSpan(name, { attributes: attributesFor(options) });
1837
+ const span = getTracer().startSpan(name, {
1838
+ kind: otelSpanKind("LLM" /* LLM */),
1839
+ attributes: attributesFor(options)
1840
+ });
1806
1841
  return configure2(new Generation(span, options.provider), options);
1807
1842
  }
1808
1843
  function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
@@ -1812,7 +1847,8 @@ function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
1812
1847
  attributesFor(options),
1813
1848
  options.userId,
1814
1849
  (span) => configure2(new Generation(span, options.provider), options),
1815
- fn
1850
+ fn,
1851
+ otelSpanKind("LLM" /* LLM */)
1816
1852
  );
1817
1853
  }
1818
1854
 
@@ -1845,7 +1881,7 @@ init_session();
1845
1881
  init_user();
1846
1882
  init_spans();
1847
1883
  init_workspace();
1848
- var VERSION = "0.7.0";
1884
+ var VERSION = "0.8.0";
1849
1885
  export {
1850
1886
  Generation,
1851
1887
  Observation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassflow-ai/rius",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "OpenTelemetry-native tracing for AI agents and LLM applications",
5
5
  "keywords": [
6
6
  "opentelemetry",