@inference/tracing 0.0.21 → 0.0.23

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
@@ -11,6 +11,9 @@ prompts, responses, token usage, and parent-child agent flows.
11
11
  This package is currently in beta. APIs may change before `1.0`, but the package
12
12
  name and import paths are intended to remain stable.
13
13
 
14
+ Full documentation lives at
15
+ [docs.inference.net/integrations/traces/overview](https://docs.inference.net/integrations/traces/overview).
16
+
14
17
  ## Install
15
18
 
16
19
  ```bash
@@ -192,8 +195,11 @@ await agentSpan(
192
195
  agentName: "Refund Review Agent",
193
196
  spanName: "refund-review.run",
194
197
  sessionId: "conversation-1842",
198
+ userId: "user-7723",
195
199
  role: "refunds",
196
200
  system: "internal",
201
+ metadata: { queue: "priority" },
202
+ tags: ["refunds", "beta"],
197
203
  },
198
204
  async (span) => {
199
205
  span.setInput("Review refund request #1842");
@@ -210,10 +216,12 @@ await tracing.shutdown();
210
216
  survives display-name changes so Catalyst can group executions correctly in the
211
217
  agent dashboard. `agentName` is optional and becomes `agent.name` when you want
212
218
  a human-readable display label. `sessionId` is optional and should identify one
213
- conversation, not the whole process. Any child spans created inside the callback
214
- automatically parent under the agent span and supported SDK wrappers copy the
215
- active `agent.id` and optional `agent.name` / `agent.role` onto their child
216
- spans.
219
+ conversation, not the whole process. `userId` is optional and becomes `user.id`
220
+ so Catalyst can filter and group traces by end user. `metadata` and `tags` are
221
+ also optional: `metadata` attaches a JSON bag and `tags` attaches a string
222
+ array to the span. Any child spans created inside the callback automatically
223
+ parent under the agent span and supported SDK wrappers copy the active
224
+ `agent.id` and optional `agent.name` / `agent.role` onto their child spans.
217
225
 
218
226
  ## Configuration
219
227
 
@@ -244,6 +252,7 @@ understand them without custom adapters:
244
252
  | Model metadata | `llm.model_name`, `llm.invocation_parameters` |
245
253
  | Token counts | `llm.token_count.prompt`, `llm.token_count.completion`, `llm.token_count.total` |
246
254
  | Provider/system | `gen_ai.system` |
255
+ | Agent and session | `agent.id`, `agent.name`, `agent.role`, `session.id`, `user.id`, `metadata`, `tags` |
247
256
 
248
257
  Constants are exported for custom spans:
249
258
 
@@ -56,6 +56,15 @@ export interface AgentSpanOptions {
56
56
  * so downstream viewers can group agent spans by conversation.
57
57
  */
58
58
  sessionId?: string;
59
+ /**
60
+ * End-user identifier the work runs on behalf of. Becomes this span's
61
+ * `user.id` so downstream viewers can filter and group traces by user.
62
+ */
63
+ userId?: string;
64
+ /** Free-form metadata bag — JSON-stringified onto the `metadata` attribute. */
65
+ metadata?: Record<string, unknown>;
66
+ /** Free-form string tags — set on the `tags` attribute as a string array. */
67
+ tags?: readonly string[];
59
68
  }
60
69
  /**
61
70
  * Options accepted by {@link manualSpan}.
@@ -91,6 +100,11 @@ export interface ManualSpanOptions {
91
100
  * so downstream viewers can group manual spans by conversation.
92
101
  */
93
102
  sessionId?: string;
103
+ /**
104
+ * End-user identifier the work runs on behalf of. Becomes this span's
105
+ * `user.id` so downstream viewers can filter and group traces by user.
106
+ */
107
+ userId?: string;
94
108
  /** Convenience: set `input.value` from the callback. Strings pass through; objects are JSON-stringified. */
95
109
  input?: unknown;
96
110
  /** Convenience: set `output.value` before the callback runs (rare — usually set inside the callback). */
@@ -155,6 +169,12 @@ export interface AgentSpanHandle {
155
169
  * Record the model name. Becomes `llm.model_name`.
156
170
  */
157
171
  setModel(model: string): void;
172
+ /** Record the overall agent outcome, e.g. success, failure, partial, or cancelled. */
173
+ setOutcome(outcome: string): void;
174
+ /** Record why the agent stopped, e.g. completed, error, timeout, or max_turns. */
175
+ setStopReason(reason: string): void;
176
+ /** Record whether the agent reached its expected finalization path. */
177
+ setFinalized(finalized: boolean): void;
158
178
  /**
159
179
  * Record tool identity on a TOOL-kind span.
160
180
  */
@@ -233,7 +253,7 @@ export declare function manualSpan<T>(tracer: Tracer, options: ManualSpanOptions
233
253
  * Wrap a chunk of agent work in an OpenInference-shaped AGENT span.
234
254
  *
235
255
  * Sets `openinference.span.kind=AGENT`, `agent.id`, optional
236
- * `agent.name` / `agent.role` / `session.id`, and `gen_ai.system`; the
256
+ * `agent.name` / `agent.role` / `session.id` / `user.id`, and `gen_ai.system`; the
237
257
  * callback can fill in `input`, `output`, model, and tokens via the handle.
238
258
  * The callback runs inside the span's active OTel context, so any child spans
239
259
  * created by an instrumented LLM SDK (or by `tracer.startSpan(...)` inside the
@@ -80,6 +80,9 @@ export async function manualSpan(tracer, options, fn) {
80
80
  const sessionId = options.sessionId?.trim();
81
81
  if (sessionId)
82
82
  startAttributes[Attr.SESSION_ID] = sessionId;
83
+ const userId = options.userId?.trim();
84
+ if (userId)
85
+ startAttributes[Attr.USER_ID] = userId;
83
86
  if (options.attributes)
84
87
  Object.assign(startAttributes, options.attributes);
85
88
  if (options.metadata && Object.keys(options.metadata).length > 0) {
@@ -106,11 +109,29 @@ export async function manualSpan(tracer, options, fn) {
106
109
  });
107
110
  try {
108
111
  const result = await context.with(ctx, () => fn(handle));
109
- span.setStatus({ code: SpanStatusCode.OK });
112
+ if (spanKind === SpanKindValues.AGENT) {
113
+ const finalOutcome = handle.getOutcomeSet()
114
+ ? handle.getExplicitOutcome()
115
+ : "success";
116
+ if (!handle.getOutcomeSet())
117
+ span.setAttribute(Attr.AGENT_OUTCOME, finalOutcome);
118
+ if (!handle.getStopReasonSet()) {
119
+ span.setAttribute(Attr.AGENT_STOP_REASON, defaultStopReason(finalOutcome));
120
+ }
121
+ if (!handle.getFinalizedSet()) {
122
+ span.setAttribute(Attr.AGENT_FINALIZED, finalOutcome === "success");
123
+ }
124
+ }
125
+ setStatusForOutcome(span, spanKind, handle.getExplicitOutcome());
110
126
  return result;
111
127
  }
112
128
  catch (err) {
113
129
  span.recordException(err);
130
+ if (spanKind === SpanKindValues.AGENT) {
131
+ span.setAttribute(Attr.AGENT_OUTCOME, "failure");
132
+ span.setAttribute(Attr.AGENT_STOP_REASON, "error");
133
+ span.setAttribute(Attr.AGENT_FINALIZED, false);
134
+ }
114
135
  span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
115
136
  throw err;
116
137
  }
@@ -122,7 +143,7 @@ export async function manualSpan(tracer, options, fn) {
122
143
  * Wrap a chunk of agent work in an OpenInference-shaped AGENT span.
123
144
  *
124
145
  * Sets `openinference.span.kind=AGENT`, `agent.id`, optional
125
- * `agent.name` / `agent.role` / `session.id`, and `gen_ai.system`; the
146
+ * `agent.name` / `agent.role` / `session.id` / `user.id`, and `gen_ai.system`; the
126
147
  * callback can fill in `input`, `output`, model, and tokens via the handle.
127
148
  * The callback runs inside the span's active OTel context, so any child spans
128
149
  * created by an instrumented LLM SDK (or by `tracer.startSpan(...)` inside the
@@ -190,9 +211,16 @@ export async function agentSpan(tracer, options, fn) {
190
211
  agentName,
191
212
  agentRole: options.role,
192
213
  sessionId: options.sessionId,
214
+ userId: options.userId,
215
+ metadata: options.metadata,
216
+ tags: options.tags,
193
217
  }, fn);
194
218
  }
195
219
  function makeHandle(span) {
220
+ let outcomeSet = false;
221
+ let explicitOutcome = "success";
222
+ let stopReasonSet = false;
223
+ let finalizedSet = false;
196
224
  return {
197
225
  setInput(value) {
198
226
  if (value === undefined)
@@ -218,6 +246,19 @@ function makeHandle(span) {
218
246
  setModel(model) {
219
247
  span.setAttribute(Attr.MODEL_NAME, model);
220
248
  },
249
+ setOutcome(outcome) {
250
+ outcomeSet = true;
251
+ explicitOutcome = outcome;
252
+ span.setAttribute(Attr.AGENT_OUTCOME, outcome);
253
+ },
254
+ setStopReason(reason) {
255
+ stopReasonSet = true;
256
+ span.setAttribute(Attr.AGENT_STOP_REASON, reason);
257
+ },
258
+ setFinalized(finalized) {
259
+ finalizedSet = true;
260
+ span.setAttribute(Attr.AGENT_FINALIZED, finalized);
261
+ },
221
262
  setTool({ name, callId }) {
222
263
  span.setAttribute(Attr.TOOL_NAME, name);
223
264
  if (callId != null)
@@ -229,9 +270,35 @@ function makeHandle(span) {
229
270
  setAttributes(attributes) {
230
271
  setSpanAttributes(span, attributes);
231
272
  },
273
+ getOutcomeSet() {
274
+ return outcomeSet;
275
+ },
276
+ getExplicitOutcome() {
277
+ return explicitOutcome;
278
+ },
279
+ getStopReasonSet() {
280
+ return stopReasonSet;
281
+ },
282
+ getFinalizedSet() {
283
+ return finalizedSet;
284
+ },
232
285
  raw: span,
233
286
  };
234
287
  }
288
+ function defaultStopReason(outcome) {
289
+ if (outcome === "success")
290
+ return "completed";
291
+ if (outcome === "failure")
292
+ return "error";
293
+ return outcome;
294
+ }
295
+ function setStatusForOutcome(span, spanKind, outcome) {
296
+ if (spanKind === SpanKindValues.AGENT && outcome === "failure") {
297
+ span.setStatus({ code: SpanStatusCode.ERROR, message: "Agent outcome failure" });
298
+ return;
299
+ }
300
+ span.setStatus({ code: SpanStatusCode.OK });
301
+ }
235
302
  function resolveAgentSpanName(options, agentId, agentName) {
236
303
  const explicitName = normalize(options.spanName);
237
304
  if (explicitName != null)
@@ -81,6 +81,10 @@ function makeState() {
81
81
  cacheReadTokens: 0,
82
82
  llmCalls: 0,
83
83
  pendingToolSpans: new Map(),
84
+ resultSubtype: undefined,
85
+ resultStopReason: undefined,
86
+ resultIsError: false,
87
+ agentClosed: false,
84
88
  };
85
89
  }
86
90
  function wrapAsyncIterable(upstream, span, tracer, ctx) {
@@ -97,6 +101,7 @@ function wrapAsyncIterable(upstream, span, tracer, ctx) {
97
101
  }
98
102
  else {
99
103
  finalize(span, state);
104
+ state.agentClosed = true;
100
105
  }
101
106
  return result;
102
107
  }
@@ -108,16 +113,20 @@ function wrapAsyncIterable(upstream, span, tracer, ctx) {
108
113
  }
109
114
  state.pendingToolSpans.clear();
110
115
  span.recordException(err);
116
+ span.setAttribute(Attr.AGENT_OUTCOME, "failure");
117
+ span.setAttribute(Attr.AGENT_STOP_REASON, "error");
118
+ span.setAttribute(Attr.AGENT_FINALIZED, false);
111
119
  span.setStatus({
112
120
  code: SpanStatusCode.ERROR,
113
121
  message: String(err),
114
122
  });
123
+ state.agentClosed = true;
115
124
  span.end();
116
125
  throw err;
117
126
  }
118
127
  },
119
128
  async return(value) {
120
- finalize(span, state);
129
+ closeEarly(span, state);
121
130
  if (it.return != null)
122
131
  return it.return(value);
123
132
  return { value, done: true };
@@ -184,6 +193,12 @@ function handleMessage(msg, state, tracer, ctx) {
184
193
  }
185
194
  }
186
195
  }
196
+ else if (msg.type === "result") {
197
+ state.resultSubtype = typeof msg.subtype === "string" ? msg.subtype : undefined;
198
+ state.resultStopReason =
199
+ typeof msg.stop_reason === "string" ? msg.stop_reason : undefined;
200
+ state.resultIsError = msg.is_error === true;
201
+ }
187
202
  }
188
203
  function addUsage(state, usage) {
189
204
  if (usage == null)
@@ -200,12 +215,51 @@ function addUsage(state, usage) {
200
215
  }
201
216
  }
202
217
  function finalize(span, state) {
203
- // Close any tool spans that never got a matching tool_result.
204
218
  for (const s of state.pendingToolSpans.values()) {
205
- s.setStatus({ code: SpanStatusCode.OK });
219
+ s.setAttribute(Attr.TOOL_RESULT_MISSING, true);
220
+ s.setStatus({ code: SpanStatusCode.ERROR, message: "Missing tool_result" });
221
+ s.end();
222
+ }
223
+ state.pendingToolSpans.clear();
224
+ setAgentSummary(span, state);
225
+ setAgentFinalState(span, state);
226
+ if (isErrorResult(state)) {
227
+ span.setStatus({ code: SpanStatusCode.ERROR, message: resultStopReason(state) });
228
+ }
229
+ else {
230
+ span.setStatus({ code: SpanStatusCode.OK });
231
+ }
232
+ span.end();
233
+ }
234
+ function closeEarly(span, state) {
235
+ if (state.agentClosed)
236
+ return;
237
+ if (state.resultSubtype != null) {
238
+ finalize(span, state);
239
+ state.agentClosed = true;
240
+ return;
241
+ }
242
+ cancelAgent(span, state);
243
+ state.agentClosed = true;
244
+ span.end();
245
+ }
246
+ function cancelAgent(span, state) {
247
+ for (const s of state.pendingToolSpans.values()) {
248
+ s.setAttribute(Attr.TOOL_RESULT_MISSING, true);
249
+ s.setStatus({
250
+ code: SpanStatusCode.ERROR,
251
+ message: "Stream closed before tool_result",
252
+ });
206
253
  s.end();
207
254
  }
208
255
  state.pendingToolSpans.clear();
256
+ setAgentSummary(span, state);
257
+ span.setAttribute(Attr.AGENT_OUTCOME, "cancelled");
258
+ span.setAttribute(Attr.AGENT_STOP_REASON, "cancelled");
259
+ span.setAttribute(Attr.AGENT_FINALIZED, false);
260
+ span.setStatus({ code: SpanStatusCode.OK });
261
+ }
262
+ function setAgentSummary(span, state) {
209
263
  span.setAttribute(Attr.OUTPUT_VALUE, state.lastAssistantText);
210
264
  if (state.inputTokens > 0) {
211
265
  span.setAttribute(Attr.TOKEN_COUNT_PROMPT, state.inputTokens);
@@ -224,8 +278,36 @@ function finalize(span, state) {
224
278
  }
225
279
  span.setAttribute(Attr.AGENT_TOOL_CALL_COUNT, state.toolCallCount);
226
280
  span.setAttribute(Attr.AGENT_LLM_CALL_COUNT, state.llmCalls);
227
- span.setStatus({ code: SpanStatusCode.OK });
228
- span.end();
281
+ }
282
+ function setAgentFinalState(span, state) {
283
+ const subtype = state.resultSubtype;
284
+ if (isErrorResult(state)) {
285
+ span.setAttribute(Attr.AGENT_OUTCOME, "failure");
286
+ span.setAttribute(Attr.AGENT_STOP_REASON, resultStopReason(state));
287
+ span.setAttribute(Attr.AGENT_FINALIZED, false);
288
+ }
289
+ else if (subtype === "success") {
290
+ span.setAttribute(Attr.AGENT_OUTCOME, "success");
291
+ span.setAttribute(Attr.AGENT_STOP_REASON, "completed");
292
+ span.setAttribute(Attr.AGENT_FINALIZED, true);
293
+ }
294
+ else if (subtype != null && subtype !== "") {
295
+ span.setAttribute(Attr.AGENT_OUTCOME, "partial");
296
+ span.setAttribute(Attr.AGENT_STOP_REASON, subtype);
297
+ span.setAttribute(Attr.AGENT_FINALIZED, false);
298
+ }
299
+ else {
300
+ span.setAttribute(Attr.AGENT_OUTCOME, "partial");
301
+ span.setAttribute(Attr.AGENT_STOP_REASON, "missing_result");
302
+ span.setAttribute(Attr.AGENT_FINALIZED, false);
303
+ }
304
+ }
305
+ function isErrorResult(state) {
306
+ const subtype = state.resultSubtype;
307
+ return (state.resultIsError || subtype === "error" || subtype?.startsWith("error_") === true);
308
+ }
309
+ function resultStopReason(state) {
310
+ return state.resultStopReason ?? state.resultSubtype ?? "error";
229
311
  }
230
312
  function stringify(value) {
231
313
  if (typeof value === "string")
package/dist/semconv.d.ts CHANGED
@@ -128,12 +128,27 @@ export declare const Attr: {
128
128
  readonly AGENT_TOOL_CALL_COUNT: "agent.tool_call_count";
129
129
  /** Number of LLM calls in an agent run. */
130
130
  readonly AGENT_LLM_CALL_COUNT: "agent.llm_call_count";
131
+ /** Overall agent run outcome, e.g. success, failure, partial, or cancelled. */
132
+ readonly AGENT_OUTCOME: "agent.outcome";
133
+ /** Why an agent run stopped, e.g. completed, error, timeout, or max_turns. */
134
+ readonly AGENT_STOP_REASON: "agent.stop_reason";
135
+ /** Whether the agent run reached its expected finalization path. */
136
+ readonly AGENT_FINALIZED: "agent.finalized";
137
+ /** Whether a tool-use span ended without a correlated tool result. */
138
+ readonly TOOL_RESULT_MISSING: "tool.result.missing";
131
139
  /**
132
140
  * Stable identifier for one conversation, used to group related spans.
133
141
  *
134
142
  * Wire key: `session.id`
135
143
  */
136
144
  readonly SESSION_ID: "session.id";
145
+ /**
146
+ * End-user identifier the work runs on behalf of, used to filter and
147
+ * group traces by user.
148
+ *
149
+ * Wire key: `user.id`
150
+ */
151
+ readonly USER_ID: "user.id";
137
152
  };
138
153
  /**
139
154
  * Canonical string values for {@link Attr.SPAN_KIND}.
package/dist/semconv.js CHANGED
@@ -128,12 +128,27 @@ export const Attr = {
128
128
  AGENT_TOOL_CALL_COUNT: "agent.tool_call_count",
129
129
  /** Number of LLM calls in an agent run. */
130
130
  AGENT_LLM_CALL_COUNT: "agent.llm_call_count",
131
+ /** Overall agent run outcome, e.g. success, failure, partial, or cancelled. */
132
+ AGENT_OUTCOME: "agent.outcome",
133
+ /** Why an agent run stopped, e.g. completed, error, timeout, or max_turns. */
134
+ AGENT_STOP_REASON: "agent.stop_reason",
135
+ /** Whether the agent run reached its expected finalization path. */
136
+ AGENT_FINALIZED: "agent.finalized",
137
+ /** Whether a tool-use span ended without a correlated tool result. */
138
+ TOOL_RESULT_MISSING: "tool.result.missing",
131
139
  /**
132
140
  * Stable identifier for one conversation, used to group related spans.
133
141
  *
134
142
  * Wire key: `session.id`
135
143
  */
136
144
  SESSION_ID: "session.id",
145
+ /**
146
+ * End-user identifier the work runs on behalf of, used to filter and
147
+ * group traces by user.
148
+ *
149
+ * Wire key: `user.id`
150
+ */
151
+ USER_ID: "user.id",
137
152
  };
138
153
  /**
139
154
  * Canonical string values for {@link Attr.SPAN_KIND}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inference/tracing",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "description": "First-party OpenInference-shaped tracing for TypeScript LLM and agent applications on Catalyst by Inference.net.",
6
6
  "homepage": "https://inference.net",