@mastra/observability 1.20.2-alpha.0 → 1.21.0-alpha.2

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
@@ -1969,12 +1969,20 @@ function emitDurationMetrics(span, metrics) {
1969
1969
  const durationMs = span.endTime.getTime() - span.startTime.getTime();
1970
1970
  metrics.emit(durationMetricName, durationMs, { status: span.errorInfo ? "error" : "ok" });
1971
1971
  }
1972
- /** Emit token usage metrics for a model-generation span. */
1972
+ /** Emit token usage metrics for a model-generation or classifier-evaluation span. */
1973
1973
  function emitTokenMetrics(span, metrics) {
1974
- if (span.type !== _mastra_core_observability.SpanType.MODEL_GENERATION) return;
1975
- const attrs = span.attributes;
1976
- if (!attrs?.usage) return;
1977
- emitUsageMetrics(attrs, attrs.usage, metrics);
1974
+ if (span.type === _mastra_core_observability.SpanType.MODEL_GENERATION) {
1975
+ const attrs = span.attributes;
1976
+ if (attrs?.usage) emitUsageMetrics(attrs, attrs.usage, metrics);
1977
+ return;
1978
+ }
1979
+ if (span.type === _mastra_core_observability.SpanType.CLASSIFIER_EVALUATION) {
1980
+ const attrs = span.attributes;
1981
+ if (attrs?.usage) emitUsageMetrics({
1982
+ provider: attrs.provider,
1983
+ model: attrs.modelId
1984
+ }, attrs.usage, metrics);
1985
+ }
1978
1986
  }
1979
1987
  /**
1980
1988
  * Emit token usage metrics from an explicit usage payload, using the supplied
@@ -2506,6 +2514,26 @@ function extractStepInput(payload) {
2506
2514
  }
2507
2515
  }
2508
2516
  /**
2517
+ * Chunks that come from the model provider. The first one marks the response's
2518
+ * first chunk, matching OTel's `gen_ai.response.time_to_first_chunk`. Everything
2519
+ * else, such as lifecycle markers or `data-*` parts a processor writes before the
2520
+ * model call, is Mastra's own and does not count.
2521
+ */
2522
+ const MODEL_RESPONSE_CHUNK_TYPES = /* @__PURE__ */ new Set([
2523
+ "response-metadata",
2524
+ "raw",
2525
+ "text-start",
2526
+ "text-delta",
2527
+ "reasoning-start",
2528
+ "reasoning-delta",
2529
+ "reasoning-file",
2530
+ "tool-call-input-streaming-start",
2531
+ "tool-call-delta",
2532
+ "tool-call",
2533
+ "source",
2534
+ "file"
2535
+ ]);
2536
+ /**
2509
2537
  * Manages MODEL_STEP and MODEL_CHUNK span tracking for streaming Model responses.
2510
2538
  *
2511
2539
  * Should be instantiated once per MODEL_GENERATION span and shared across
@@ -2522,6 +2550,15 @@ var ModelSpanTracker = class {
2522
2550
  #chunkSequence = 0;
2523
2551
  #completionStartTime;
2524
2552
  #inferenceCompletionStartTime;
2553
+ /**
2554
+ * True between a step's `step-start` and `step-finish` chunks, the window in which
2555
+ * the provider responds. Chunks outside it, such as a `tool-call` replayed for a
2556
+ * finished background task on resume, are Mastra's own and never start the clock.
2557
+ * Durable runs wrap one step's provider stream at a time, so every chunk there counts.
2558
+ */
2559
+ #inModelResponse = false;
2560
+ /** Set only by the agent's main loop; receives the first chunk's completionStartTime. */
2561
+ #agentRunSpan;
2525
2562
  #currentStepInputIsFinal = false;
2526
2563
  /** When true, step-finish chunks don't auto-close the step span (for durable execution) */
2527
2564
  #deferStepClose = false;
@@ -2529,8 +2566,9 @@ var ModelSpanTracker = class {
2529
2566
  #pendingStepFinishPayload;
2530
2567
  /** Static request-side context applied to every MODEL_INFERENCE span */
2531
2568
  #inferenceContext;
2532
- constructor(modelSpan) {
2569
+ constructor(modelSpan, options) {
2533
2570
  this.#modelSpan = modelSpan;
2571
+ this.#agentRunSpan = options?.agentRunSpan;
2534
2572
  }
2535
2573
  /**
2536
2574
  * Set request-side context applied to subsequent MODEL_INFERENCE spans.
@@ -2540,12 +2578,22 @@ var ModelSpanTracker = class {
2540
2578
  this.#inferenceContext = context;
2541
2579
  }
2542
2580
  /**
2543
- * Capture the completion start time (time to first token) when the first content chunk arrives.
2581
+ * Record when the provider's first chunk arrived and copy that clock up the
2582
+ * agent's own chain (inference → step → generation → agent run), first write
2583
+ * wins at each level. Time to first chunk is derived later as
2584
+ * `completionStartTime - startTime` on whichever span is being read.
2544
2585
  */
2545
2586
  #captureCompletionStartTime() {
2587
+ this.#ensureStepAndInference();
2588
+ if (this.#inferenceCompletionStartTime) return;
2546
2589
  const now = /* @__PURE__ */ new Date();
2590
+ this.#inferenceCompletionStartTime = now;
2547
2591
  this.#completionStartTime ??= now;
2548
- this.#inferenceCompletionStartTime ??= now;
2592
+ for (const span of [
2593
+ this.#currentStepSpan,
2594
+ this.#modelSpan,
2595
+ this.#agentRunSpan
2596
+ ]) if (span && !span.attributes?.completionStartTime) span.update({ attributes: { completionStartTime: now } });
2549
2597
  }
2550
2598
  /**
2551
2599
  * Get the tracing context for creating child spans.
@@ -2589,13 +2637,16 @@ var ModelSpanTracker = class {
2589
2637
  }, model);
2590
2638
  if (providerCostContext && !spanOptions.attributes) spanOptions.attributes = {};
2591
2639
  if (spanOptions.attributes) {
2592
- spanOptions.attributes.completionStartTime = this.#completionStartTime;
2640
+ spanOptions.attributes.completionStartTime = this.#completionStartTime ?? spanOptions.attributes.completionStartTime;
2593
2641
  spanOptions.attributes.usage = extractUsageMetrics(usage, providerMetadata);
2594
2642
  spanOptions.attributes.costContext = providerCostContext ?? spanOptions.attributes.costContext;
2595
2643
  if (!spanOptions.attributes.costContext) spanOptions.attributes.costContext = getGatewayCostContext({ stepProviderMetadata });
2596
2644
  }
2597
2645
  this.#modelSpan?.end(spanOptions);
2598
2646
  }
2647
+ getCompletionStartTime() {
2648
+ return this.#completionStartTime;
2649
+ }
2599
2650
  /**
2600
2651
  * Update the generation span
2601
2652
  */
@@ -2663,6 +2714,7 @@ var ModelSpanTracker = class {
2663
2714
  });
2664
2715
  this.#currentStepInputIsFinal = Array.isArray(payload?.inputMessages);
2665
2716
  this.#chunkSequence = 0;
2717
+ this.#inferenceCompletionStartTime = void 0;
2666
2718
  }
2667
2719
  /**
2668
2720
  * End the current MODEL_INFERENCE span when the provider stream finishes.
@@ -2793,6 +2845,26 @@ var ModelSpanTracker = class {
2793
2845
  this.#stepIndex++;
2794
2846
  }
2795
2847
  /**
2848
+ * A suspended step emits no step-finish; its suspension chunk carries the
2849
+ * step usage instead. Close MODEL_INFERENCE and MODEL_STEP with that usage so
2850
+ * the suspended call is not reported without tokens.
2851
+ */
2852
+ #endSuspendedStep(chunk) {
2853
+ if (chunk.type !== "tool-call-suspended" && chunk.type !== "tool-call-approval") return;
2854
+ const usage = chunk.payload.usage;
2855
+ if (!usage) return;
2856
+ this.#inModelResponse = false;
2857
+ this.#endStepSpan({
2858
+ output: { usage },
2859
+ stepResult: {
2860
+ reason: "suspended",
2861
+ isContinued: false,
2862
+ warnings: []
2863
+ },
2864
+ metadata: {}
2865
+ });
2866
+ }
2867
+ /**
2796
2868
  * Returns the parent span for chunks. Chunks parent under MODEL_INFERENCE
2797
2869
  * (the provider call) when available, falling back to MODEL_STEP only if
2798
2870
  * startStep() was bypassed.
@@ -2964,8 +3036,8 @@ var ModelSpanTracker = class {
2964
3036
  #handleToolApprovalChunk(chunk) {
2965
3037
  if (chunk.type !== "tool-call-approval") return;
2966
3038
  const payload = chunk.payload;
2967
- this.#ensureStepAndInference();
2968
- if (this.#chunkParent()?.createEventSpan({
3039
+ if (payload.usage || this.#currentStepSpan) this.#ensureStepAndInference();
3040
+ if ((this.#chunkParent() ?? this.#modelSpan)?.createEventSpan({
2969
3041
  name: `chunk: 'tool-call-approval'`,
2970
3042
  type: _mastra_core_observability.SpanType.MODEL_CHUNK,
2971
3043
  attributes: {
@@ -2984,12 +3056,10 @@ var ModelSpanTracker = class {
2984
3056
  */
2985
3057
  wrapStream(stream) {
2986
3058
  return stream.pipeThrough(new stream_web.TransformStream({ transform: (chunk, controller) => {
2987
- switch (chunk.type) {
2988
- case "text-delta":
2989
- case "tool-call-delta":
2990
- case "reasoning-delta":
2991
- this.#captureCompletionStartTime();
2992
- break;
3059
+ if ((this.#inModelResponse || this.#deferStepClose) && MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3060
+ if (chunk.type === "tool-call-suspended" || chunk.type === "tool-call-approval") {
3061
+ if (chunk.type === "tool-call-approval") this.#handleToolApprovalChunk(chunk);
3062
+ this.#endSuspendedStep(chunk);
2993
3063
  }
2994
3064
  controller.enqueue(chunk);
2995
3065
  switch (chunk.type) {
@@ -3014,11 +3084,13 @@ var ModelSpanTracker = class {
3014
3084
  this.#handleObjectChunk(chunk);
3015
3085
  break;
3016
3086
  case "step-start":
3087
+ this.#inModelResponse = true;
3017
3088
  if (this.#currentStepSpan) this.updateStep(chunk.payload);
3018
3089
  else this.startStep(chunk.payload);
3019
3090
  if (!this.#currentInferenceSpan) this.startInference(chunk.payload);
3020
3091
  break;
3021
3092
  case "step-finish":
3093
+ this.#inModelResponse = false;
3022
3094
  if (this.#deferStepClose) {
3023
3095
  this.#pendingStepFinishPayload = chunk.payload;
3024
3096
  this.#endChunkSpan();
@@ -3036,13 +3108,11 @@ var ModelSpanTracker = class {
3036
3108
  case "tripwire":
3037
3109
  case "watch":
3038
3110
  case "tool-error":
3039
- case "tool-call-suspended":
3040
3111
  case "reasoning-signature":
3041
3112
  case "redacted-reasoning":
3042
3113
  case "step-output": break;
3043
- case "tool-call-approval":
3044
- this.#handleToolApprovalChunk(chunk);
3045
- break;
3114
+ case "tool-call-suspended":
3115
+ case "tool-call-approval": break;
3046
3116
  case "tool-output": break;
3047
3117
  case "tool-result": {
3048
3118
  const { toolCallId, toolName, isError, dynamic, providerExecuted, providerMetadata, result, args: _args } = chunk.payload || {};
@@ -3376,9 +3446,9 @@ var BaseSpan = class BaseSpan {
3376
3446
  * Create a ModelSpanTracker for this span (only works if this is a MODEL_GENERATION span)
3377
3447
  * Returns undefined for non-MODEL_GENERATION spans
3378
3448
  */
3379
- createTracker() {
3449
+ createTracker(options) {
3380
3450
  if (this.type !== _mastra_core_observability.SpanType.MODEL_GENERATION) return;
3381
- return new ModelSpanTracker(this);
3451
+ return new ModelSpanTracker(this, options);
3382
3452
  }
3383
3453
  /** Returns `TRUE` if the span is the root span of a trace */
3384
3454
  get isRootSpan() {