@glassflow-ai/rius 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -89,7 +89,10 @@ function resolveConfig(options = {}, env = process.env) {
89
89
  // on every heartbeat payload, so `??` would be wrong here.
90
90
  agentName: options.agentName || env.RIUS_AGENT_NAME || serviceName,
91
91
  partialSpans: options.partialSpans ?? bool("RIUS_PARTIAL_SPANS", env.RIUS_PARTIAL_SPANS, false),
92
- partialSpansDelayMs: partialSpansDelaySeconds * 1e3
92
+ partialSpansDelayMs: partialSpansDelaySeconds * 1e3,
93
+ // Empty is unset, like agentName: a blank session id would group every
94
+ // span under the meaningless session "".
95
+ sessionId: options.sessionId || env.RIUS_SESSION_ID || void 0
93
96
  };
94
97
  }
95
98
  var DEFAULT_ENDPOINT, DEFAULT_SERVICE_NAME, HEARTBEAT_INTERVAL_MIN, HEARTBEAT_INTERVAL_MAX, DEFAULT_HEARTBEAT_INTERVAL, PARTIAL_SPANS_DELAY_MIN, PARTIAL_SPANS_DELAY_MAX, DEFAULT_PARTIAL_SPANS_DELAY, AFFIRMATIVE, NEGATIVE;
@@ -234,12 +237,11 @@ function httpTransport(url, headers) {
234
237
  }
235
238
  };
236
239
  }
237
- var import_node_crypto, import_node_module, PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, SDK_VERSION, OpenRootSpanTracker, HeartbeatSender;
240
+ var import_node_module, PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, SDK_VERSION, OpenRootSpanTracker, HeartbeatSender;
238
241
  var init_heartbeat = __esm({
239
242
  "src/heartbeat.ts"() {
240
243
  "use strict";
241
244
  init_cjs_shims();
242
- import_node_crypto = require("crypto");
243
245
  import_node_module = require("module");
244
246
  PAYLOAD_VERSION = 1;
245
247
  OPEN_TRACES_CAP = 32;
@@ -282,8 +284,8 @@ var init_heartbeat = __esm({
282
284
  }
283
285
  };
284
286
  HeartbeatSender = class {
285
- /** Identity of one process lifetime, fresh per sender. */
286
- instanceId = (0, import_node_crypto.randomUUID)();
287
+ /** Identity of one process lifetime; injected, shared with span resources. */
288
+ instanceId;
287
289
  agentName;
288
290
  tracker;
289
291
  intervalMs;
@@ -294,6 +296,7 @@ var init_heartbeat = __esm({
294
296
  stopped = false;
295
297
  deliveryWarned = false;
296
298
  constructor(opts) {
299
+ this.instanceId = opts.instanceId;
297
300
  this.agentName = opts.agentName;
298
301
  this.tracker = opts.tracker;
299
302
  this.intervalMs = opts.intervalMs;
@@ -353,15 +356,17 @@ function kindAttributes(kind) {
353
356
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
354
357
  return attributes;
355
358
  }
356
- var TRACER_NAME, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
359
+ var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
357
360
  var init_semconv = __esm({
358
361
  "src/semconv.ts"() {
359
362
  "use strict";
360
363
  init_cjs_shims();
361
364
  TRACER_NAME = "glassflow";
365
+ SERVICE_INSTANCE_ID = "service.instance.id";
362
366
  OPENINFERENCE_SPAN_KIND = "openinference.span.kind";
363
367
  INPUT_VALUE = "input.value";
364
368
  OUTPUT_VALUE = "output.value";
369
+ SESSION_ID = "session.id";
365
370
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
366
371
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
367
372
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
@@ -427,7 +432,10 @@ var init_semconv = __esm({
427
432
  OPENINFERENCE_SPAN_KIND,
428
433
  GEN_AI_OPERATION_NAME,
429
434
  GEN_AI_PROVIDER_NAME,
430
- GEN_AI_TOOL_NAME
435
+ GEN_AI_TOOL_NAME,
436
+ // Identity, not content: a pending span must be groupable into its
437
+ // session while still running, that is the live view's whole point.
438
+ SESSION_ID
431
439
  ]);
432
440
  PENDING_IDENTITY_PREFIXES = [GEN_AI_REQUEST_PREFIX];
433
441
  }
@@ -968,6 +976,40 @@ var init_pending = __esm({
968
976
  }
969
977
  });
970
978
 
979
+ // src/session.ts
980
+ function withSession(sessionIdOrFn, maybeFn) {
981
+ const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [(0, import_node_crypto.randomUUID)(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
982
+ return import_api5.context.with(import_api5.context.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
983
+ }
984
+ var import_node_crypto, import_api5, SESSION_KEY, SessionSpanProcessor;
985
+ var init_session = __esm({
986
+ "src/session.ts"() {
987
+ "use strict";
988
+ init_cjs_shims();
989
+ import_node_crypto = require("crypto");
990
+ import_api5 = require("@opentelemetry/api");
991
+ init_semconv();
992
+ SESSION_KEY = (0, import_api5.createContextKey)("rius-session-id");
993
+ SessionSpanProcessor = class {
994
+ constructor(defaultSessionId) {
995
+ this.defaultSessionId = defaultSessionId;
996
+ }
997
+ defaultSessionId;
998
+ onStart(span, parentContext) {
999
+ const value = parentContext.getValue(SESSION_KEY);
1000
+ const sessionId = typeof value === "string" ? value : this.defaultSessionId;
1001
+ if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
1002
+ }
1003
+ onEnd(_span) {
1004
+ }
1005
+ async forceFlush() {
1006
+ }
1007
+ async shutdown() {
1008
+ }
1009
+ };
1010
+ }
1011
+ });
1012
+
971
1013
  // src/client.ts
972
1014
  function init(options = {}) {
973
1015
  if (globalClient !== void 0) {
@@ -991,13 +1033,18 @@ function init(options = {}) {
991
1033
  mask: config.mask
992
1034
  }) : health;
993
1035
  const batch = new import_sdk_trace_base.BatchSpanProcessor(exporter);
1036
+ processors.add(new SessionSpanProcessor(config.sessionId));
994
1037
  if (config.partialSpans) {
995
1038
  processors.add(new PendingSpanProcessor(batch, { delayMs: config.partialSpansDelayMs }));
996
1039
  }
997
1040
  processors.add(batch);
998
1041
  }
1042
+ const instanceId = (0, import_node_crypto2.randomUUID)();
999
1043
  const provider = new import_sdk_trace_node.NodeTracerProvider({
1000
- resource: (0, import_resources.resourceFromAttributes)({ [import_semantic_conventions.ATTR_SERVICE_NAME]: config.serviceName }),
1044
+ resource: (0, import_resources.resourceFromAttributes)({
1045
+ [import_semantic_conventions.ATTR_SERVICE_NAME]: config.serviceName,
1046
+ [SERVICE_INSTANCE_ID]: instanceId
1047
+ }),
1001
1048
  // Always ParentBased, with no AlwaysOn shortcut at rate 1. They are not
1002
1049
  // equivalent: ParentBased honours a remote UNSAMPLED parent and drops,
1003
1050
  // while AlwaysOn records regardless, producing children of a span the
@@ -1017,6 +1064,7 @@ function init(options = {}) {
1017
1064
  headers: authHeaders,
1018
1065
  intervalMs: config.heartbeatIntervalMs,
1019
1066
  agentName: config.agentName,
1067
+ instanceId,
1020
1068
  tracker,
1021
1069
  transport: options.heartbeatTransport
1022
1070
  });
@@ -1031,14 +1079,15 @@ function init(options = {}) {
1031
1079
  return globalClient;
1032
1080
  }
1033
1081
  function getTracer() {
1034
- return import_api5.trace.getTracer(TRACER_NAME);
1082
+ return import_api6.trace.getTracer(TRACER_NAME);
1035
1083
  }
1036
- var import_api5, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1084
+ var import_node_crypto2, import_api6, import_exporter_trace_otlp_proto, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
1037
1085
  var init_client = __esm({
1038
1086
  "src/client.ts"() {
1039
1087
  "use strict";
1040
1088
  init_cjs_shims();
1041
- import_api5 = require("@opentelemetry/api");
1089
+ import_node_crypto2 = require("crypto");
1090
+ import_api6 = require("@opentelemetry/api");
1042
1091
  import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
1043
1092
  import_resources = require("@opentelemetry/resources");
1044
1093
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -1052,6 +1101,7 @@ var init_client = __esm({
1052
1101
  init_masking();
1053
1102
  init_pending();
1054
1103
  init_semconv();
1104
+ init_session();
1055
1105
  sinks = /* @__PURE__ */ new WeakMap();
1056
1106
  heartbeats = /* @__PURE__ */ new WeakMap();
1057
1107
  RiusClient = class _RiusClient {
@@ -1095,7 +1145,7 @@ var init_client = __esm({
1095
1145
  } finally {
1096
1146
  if (globalClient === this) {
1097
1147
  globalClient = void 0;
1098
- import_api5.trace.disable();
1148
+ import_api6.trace.disable();
1099
1149
  }
1100
1150
  }
1101
1151
  }
@@ -1117,7 +1167,8 @@ __export(index_exports, {
1117
1167
  startAsCurrentGeneration: () => startAsCurrentGeneration,
1118
1168
  startAsCurrentSpan: () => startAsCurrentSpan,
1119
1169
  startGeneration: () => startGeneration,
1120
- startSpan: () => startSpan
1170
+ startSpan: () => startSpan,
1171
+ withSession: () => withSession
1121
1172
  });
1122
1173
  module.exports = __toCommonJS(index_exports);
1123
1174
  init_cjs_shims();
@@ -1233,8 +1284,9 @@ function observe(fn, options = {}) {
1233
1284
 
1234
1285
  // src/index.ts
1235
1286
  init_semconv();
1287
+ init_session();
1236
1288
  init_spans();
1237
- var VERSION = "0.2.4";
1289
+ var VERSION = "0.3.0";
1238
1290
  // Annotate the CommonJS export names for ESM import in node:
1239
1291
  0 && (module.exports = {
1240
1292
  Generation,
@@ -1248,5 +1300,6 @@ var VERSION = "0.2.4";
1248
1300
  startAsCurrentGeneration,
1249
1301
  startAsCurrentSpan,
1250
1302
  startGeneration,
1251
- startSpan
1303
+ startSpan,
1304
+ withSession
1252
1305
  });
package/dist/index.d.cts CHANGED
@@ -25,6 +25,12 @@ interface RiusOptions {
25
25
  partialSpans?: boolean;
26
26
  /** Seconds to debounce a pending-span snapshot after span start. */
27
27
  partialSpansDelay?: number;
28
+ /**
29
+ * Process-wide session id, stamped as `session.id` on every span. For
30
+ * one-run-per-process agents; a server handling many sessions scopes each
31
+ * one with `withSession()` instead, which overrides this default.
32
+ */
33
+ sessionId?: string;
28
34
  }
29
35
 
30
36
  type HeartbeatTransport = (payload: Record<string, unknown>, timeoutMs: number) => Promise<void>;
@@ -203,6 +209,30 @@ interface ObserveOptions {
203
209
  */
204
210
  declare function observe<F extends (...args: never[]) => unknown>(fn: F, options?: ObserveOptions): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
205
211
 
206
- declare const VERSION = "0.2.4";
212
+ /**
213
+ * Scope every span started inside `fn` to one session id.
214
+ *
215
+ * Pass the application's own id (a conversation id, a job id) to correlate
216
+ * with it; omit it to mint a fresh UUID for the scope. Either way the scope's
217
+ * id is passed to the callback, so it can be logged or handed to the next
218
+ * turn. Nested scopes override outer ones, and an active scope overrides the
219
+ * `init({ sessionId })` default.
220
+ *
221
+ * There is deliberately no process-wide auto-generation: without an id the
222
+ * backend groups each trace as its own session, which stays true in a server
223
+ * handling many users, while an auto-minted global id would merge every user
224
+ * into one. A generated id is only ever scoped to an explicit callback, where
225
+ * "this is one session" is the caller's own claim.
226
+ *
227
+ * ```typescript
228
+ * await withSession(conversationId, async () => {
229
+ * await handleTurn(message) // every span of the turn carries session.id
230
+ * })
231
+ * ```
232
+ */
233
+ declare function withSession<T>(fn: (sessionId: string) => T): T;
234
+ declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T): T;
235
+
236
+ declare const VERSION = "0.3.0";
207
237
 
208
- export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, getTracer, init, observe, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan };
238
+ export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, getTracer, init, observe, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession };
package/dist/index.d.ts CHANGED
@@ -25,6 +25,12 @@ interface RiusOptions {
25
25
  partialSpans?: boolean;
26
26
  /** Seconds to debounce a pending-span snapshot after span start. */
27
27
  partialSpansDelay?: number;
28
+ /**
29
+ * Process-wide session id, stamped as `session.id` on every span. For
30
+ * one-run-per-process agents; a server handling many sessions scopes each
31
+ * one with `withSession()` instead, which overrides this default.
32
+ */
33
+ sessionId?: string;
28
34
  }
29
35
 
30
36
  type HeartbeatTransport = (payload: Record<string, unknown>, timeoutMs: number) => Promise<void>;
@@ -203,6 +209,30 @@ interface ObserveOptions {
203
209
  */
204
210
  declare function observe<F extends (...args: never[]) => unknown>(fn: F, options?: ObserveOptions): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
205
211
 
206
- declare const VERSION = "0.2.4";
212
+ /**
213
+ * Scope every span started inside `fn` to one session id.
214
+ *
215
+ * Pass the application's own id (a conversation id, a job id) to correlate
216
+ * with it; omit it to mint a fresh UUID for the scope. Either way the scope's
217
+ * id is passed to the callback, so it can be logged or handed to the next
218
+ * turn. Nested scopes override outer ones, and an active scope overrides the
219
+ * `init({ sessionId })` default.
220
+ *
221
+ * There is deliberately no process-wide auto-generation: without an id the
222
+ * backend groups each trace as its own session, which stays true in a server
223
+ * handling many users, while an auto-minted global id would merge every user
224
+ * into one. A generated id is only ever scoped to an explicit callback, where
225
+ * "this is one session" is the caller's own claim.
226
+ *
227
+ * ```typescript
228
+ * await withSession(conversationId, async () => {
229
+ * await handleTurn(message) // every span of the turn carries session.id
230
+ * })
231
+ * ```
232
+ */
233
+ declare function withSession<T>(fn: (sessionId: string) => T): T;
234
+ declare function withSession<T>(sessionId: string, fn: (sessionId: string) => T): T;
235
+
236
+ declare const VERSION = "0.3.0";
207
237
 
208
- export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, getTracer, init, observe, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan };
238
+ export { Generation, type GenerationBody, type GenerationOptions, type InitOptions, type Mask, Observation, type ObserveOptions, RiusClient, type RiusOptions, type SpanBody, SpanKind, type SpanOptions, VERSION, getTracer, init, observe, startAsCurrentGeneration, startAsCurrentSpan, startGeneration, startSpan, withSession };
package/dist/index.js CHANGED
@@ -76,7 +76,10 @@ function resolveConfig(options = {}, env = process.env) {
76
76
  // on every heartbeat payload, so `??` would be wrong here.
77
77
  agentName: options.agentName || env.RIUS_AGENT_NAME || serviceName,
78
78
  partialSpans: options.partialSpans ?? bool("RIUS_PARTIAL_SPANS", env.RIUS_PARTIAL_SPANS, false),
79
- partialSpansDelayMs: partialSpansDelaySeconds * 1e3
79
+ partialSpansDelayMs: partialSpansDelaySeconds * 1e3,
80
+ // Empty is unset, like agentName: a blank session id would group every
81
+ // span under the meaningless session "".
82
+ sessionId: options.sessionId || env.RIUS_SESSION_ID || void 0
80
83
  };
81
84
  }
82
85
  var DEFAULT_ENDPOINT, DEFAULT_SERVICE_NAME, HEARTBEAT_INTERVAL_MIN, HEARTBEAT_INTERVAL_MAX, DEFAULT_HEARTBEAT_INTERVAL, PARTIAL_SPANS_DELAY_MIN, PARTIAL_SPANS_DELAY_MAX, DEFAULT_PARTIAL_SPANS_DELAY, AFFIRMATIVE, NEGATIVE;
@@ -206,7 +209,6 @@ var init_exportHealth = __esm({
206
209
  });
207
210
 
208
211
  // src/heartbeat.ts
209
- import { randomUUID } from "crypto";
210
212
  import { createRequire } from "module";
211
213
  function httpTransport(url, headers) {
212
214
  return async (payload, timeoutMs) => {
@@ -269,8 +271,8 @@ var init_heartbeat = __esm({
269
271
  }
270
272
  };
271
273
  HeartbeatSender = class {
272
- /** Identity of one process lifetime, fresh per sender. */
273
- instanceId = randomUUID();
274
+ /** Identity of one process lifetime; injected, shared with span resources. */
275
+ instanceId;
274
276
  agentName;
275
277
  tracker;
276
278
  intervalMs;
@@ -281,6 +283,7 @@ var init_heartbeat = __esm({
281
283
  stopped = false;
282
284
  deliveryWarned = false;
283
285
  constructor(opts) {
286
+ this.instanceId = opts.instanceId;
284
287
  this.agentName = opts.agentName;
285
288
  this.tracker = opts.tracker;
286
289
  this.intervalMs = opts.intervalMs;
@@ -340,15 +343,17 @@ function kindAttributes(kind) {
340
343
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
341
344
  return attributes;
342
345
  }
343
- var TRACER_NAME, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
346
+ var TRACER_NAME, SERVICE_INSTANCE_ID, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, SESSION_ID, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
344
347
  var init_semconv = __esm({
345
348
  "src/semconv.ts"() {
346
349
  "use strict";
347
350
  init_esm_shims();
348
351
  TRACER_NAME = "glassflow";
352
+ SERVICE_INSTANCE_ID = "service.instance.id";
349
353
  OPENINFERENCE_SPAN_KIND = "openinference.span.kind";
350
354
  INPUT_VALUE = "input.value";
351
355
  OUTPUT_VALUE = "output.value";
356
+ SESSION_ID = "session.id";
352
357
  GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
353
358
  GEN_AI_PROVIDER_NAME = "gen_ai.provider.name";
354
359
  GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
@@ -414,7 +419,10 @@ var init_semconv = __esm({
414
419
  OPENINFERENCE_SPAN_KIND,
415
420
  GEN_AI_OPERATION_NAME,
416
421
  GEN_AI_PROVIDER_NAME,
417
- GEN_AI_TOOL_NAME
422
+ GEN_AI_TOOL_NAME,
423
+ // Identity, not content: a pending span must be groupable into its
424
+ // session while still running, that is the live view's whole point.
425
+ SESSION_ID
418
426
  ]);
419
427
  PENDING_IDENTITY_PREFIXES = [GEN_AI_REQUEST_PREFIX];
420
428
  }
@@ -955,7 +963,42 @@ var init_pending = __esm({
955
963
  }
956
964
  });
957
965
 
966
+ // src/session.ts
967
+ import { randomUUID } from "crypto";
968
+ import { context as apiContext, createContextKey } from "@opentelemetry/api";
969
+ function withSession(sessionIdOrFn, maybeFn) {
970
+ const [sessionId, fn] = typeof sessionIdOrFn === "function" ? [randomUUID(), sessionIdOrFn] : [sessionIdOrFn, maybeFn];
971
+ return apiContext.with(apiContext.active().setValue(SESSION_KEY, sessionId), () => fn(sessionId));
972
+ }
973
+ var SESSION_KEY, SessionSpanProcessor;
974
+ var init_session = __esm({
975
+ "src/session.ts"() {
976
+ "use strict";
977
+ init_esm_shims();
978
+ init_semconv();
979
+ SESSION_KEY = createContextKey("rius-session-id");
980
+ SessionSpanProcessor = class {
981
+ constructor(defaultSessionId) {
982
+ this.defaultSessionId = defaultSessionId;
983
+ }
984
+ defaultSessionId;
985
+ onStart(span, parentContext) {
986
+ const value = parentContext.getValue(SESSION_KEY);
987
+ const sessionId = typeof value === "string" ? value : this.defaultSessionId;
988
+ if (sessionId !== void 0) span.setAttribute(SESSION_ID, sessionId);
989
+ }
990
+ onEnd(_span) {
991
+ }
992
+ async forceFlush() {
993
+ }
994
+ async shutdown() {
995
+ }
996
+ };
997
+ }
998
+ });
999
+
958
1000
  // src/client.ts
1001
+ import { randomUUID as randomUUID2 } from "crypto";
959
1002
  import { trace } from "@opentelemetry/api";
960
1003
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
961
1004
  import { resourceFromAttributes } from "@opentelemetry/resources";
@@ -988,13 +1031,18 @@ function init(options = {}) {
988
1031
  mask: config.mask
989
1032
  }) : health;
990
1033
  const batch = new BatchSpanProcessor(exporter);
1034
+ processors.add(new SessionSpanProcessor(config.sessionId));
991
1035
  if (config.partialSpans) {
992
1036
  processors.add(new PendingSpanProcessor(batch, { delayMs: config.partialSpansDelayMs }));
993
1037
  }
994
1038
  processors.add(batch);
995
1039
  }
1040
+ const instanceId = randomUUID2();
996
1041
  const provider = new NodeTracerProvider({
997
- resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: config.serviceName }),
1042
+ resource: resourceFromAttributes({
1043
+ [ATTR_SERVICE_NAME]: config.serviceName,
1044
+ [SERVICE_INSTANCE_ID]: instanceId
1045
+ }),
998
1046
  // Always ParentBased, with no AlwaysOn shortcut at rate 1. They are not
999
1047
  // equivalent: ParentBased honours a remote UNSAMPLED parent and drops,
1000
1048
  // while AlwaysOn records regardless, producing children of a span the
@@ -1014,6 +1062,7 @@ function init(options = {}) {
1014
1062
  headers: authHeaders,
1015
1063
  intervalMs: config.heartbeatIntervalMs,
1016
1064
  agentName: config.agentName,
1065
+ instanceId,
1017
1066
  tracker,
1018
1067
  transport: options.heartbeatTransport
1019
1068
  });
@@ -1043,6 +1092,7 @@ var init_client = __esm({
1043
1092
  init_masking();
1044
1093
  init_pending();
1045
1094
  init_semconv();
1095
+ init_session();
1046
1096
  sinks = /* @__PURE__ */ new WeakMap();
1047
1097
  heartbeats = /* @__PURE__ */ new WeakMap();
1048
1098
  RiusClient = class _RiusClient {
@@ -1208,8 +1258,9 @@ function observe(fn, options = {}) {
1208
1258
 
1209
1259
  // src/index.ts
1210
1260
  init_semconv();
1261
+ init_session();
1211
1262
  init_spans();
1212
- var VERSION = "0.2.4";
1263
+ var VERSION = "0.3.0";
1213
1264
  export {
1214
1265
  Generation,
1215
1266
  Observation,
@@ -1222,5 +1273,6 @@ export {
1222
1273
  startAsCurrentGeneration,
1223
1274
  startAsCurrentSpan,
1224
1275
  startGeneration,
1225
- startSpan
1276
+ startSpan,
1277
+ withSession
1226
1278
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassflow-ai/rius",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "OpenTelemetry-native tracing for AI agents and LLM applications",
5
5
  "keywords": [
6
6
  "opentelemetry",