@mastra/observability 1.22.0 → 1.23.0-alpha.1

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.js CHANGED
@@ -2292,9 +2292,10 @@ function addUsageStats(a, b) {
2292
2292
  * Provides span tracking for Model generations, including:
2293
2293
  * - MODEL_STEP spans (one per Model API call - includes processors and tool executions)
2294
2294
  * - MODEL_INFERENCE spans (the provider call itself - model latency only)
2295
- * - MODEL_CHUNK spans (individual streaming chunks within an inference)
2295
+ * - MODEL_CHUNK spans (processed streaming content within a logical step)
2296
2296
  *
2297
- * Hierarchy: MODEL_GENERATION -> MODEL_STEP -> MODEL_INFERENCE -> MODEL_CHUNK
2297
+ * Hierarchy: MODEL_GENERATION -> MODEL_STEP -> MODEL_INFERENCE (provider timing)
2298
+ * -> MODEL_CHUNK (processed content)
2298
2299
  *
2299
2300
  * Processors and tool executions remain children of MODEL_STEP (siblings of
2300
2301
  * MODEL_INFERENCE), so MODEL_INFERENCE measures pure model time.
@@ -2311,6 +2312,12 @@ function addUsageStats(a, b) {
2311
2312
  function supportsModelInference() {
2312
2313
  return coreFeatures.has("model-inference-span");
2313
2314
  }
2315
+ function interruptedByRun(termination) {
2316
+ return termination.outcome === "abort" || termination.stopReason === "timeout" && termination.stopReasonContext && "timeoutType" in termination.stopReasonContext && termination.stopReasonContext.timeoutType === "total" ? {
2317
+ ...termination,
2318
+ outcome: "interrupted"
2319
+ } : termination;
2320
+ }
2314
2321
  function parseGatewayCost(providerMetadata) {
2315
2322
  const rawCost = providerMetadata?.gateway?.cost;
2316
2323
  const cost = typeof rawCost === "number" ? rawCost : typeof rawCost === "string" && rawCost.trim().length > 0 ? Number(rawCost) : void 0;
@@ -2501,13 +2508,17 @@ var ModelSpanTracker = class {
2501
2508
  #modelSpan;
2502
2509
  #currentStepSpan;
2503
2510
  #currentInferenceSpan;
2511
+ #stepHasInference = false;
2512
+ #tracksProviderStream = false;
2504
2513
  #currentChunkSpan;
2505
2514
  #currentChunkType;
2506
2515
  #accumulator = {};
2507
2516
  #stepIndex = 0;
2508
2517
  #chunkSequence = 0;
2509
2518
  #completionStartTime;
2519
+ #timeToFirstTokenMs;
2510
2520
  #inferenceCompletionStartTime;
2521
+ #inferenceTimeToFirstTokenMs;
2511
2522
  /**
2512
2523
  * True between a step's `step-start` and `step-finish` chunks, the window in which
2513
2524
  * the provider responds. Chunks outside it, such as a `tool-call` replayed for a
@@ -2536,22 +2547,41 @@ var ModelSpanTracker = class {
2536
2547
  this.#inferenceContext = context;
2537
2548
  }
2538
2549
  /**
2539
- * Record when the provider's first chunk arrived and copy that clock up the
2540
- * agent's own chain (inference → step → generation → agent run), first write
2541
- * wins at each level. Time to first chunk is derived later as
2542
- * `completionStartTime - startTime` on whichever span is being read.
2550
+ * Record when the provider's first chunk arrived. Time to first token is measured
2551
+ * once here, from the start of the inference that streamed the chunk; the inference
2552
+ * reports it when it ends, and the number is copied up the agent's own chain
2553
+ * (step → generation → agent run) with the chunk's clock, first write wins at each
2554
+ * level. Time to first chunk is `completionStartTime - startTime` on whichever span
2555
+ * is being read.
2543
2556
  */
2544
2557
  #captureCompletionStartTime() {
2545
2558
  this.#ensureStepAndInference();
2546
2559
  if (this.#inferenceCompletionStartTime) return;
2547
2560
  const now = /* @__PURE__ */ new Date();
2561
+ const inference = this.#currentInferenceSpan;
2562
+ const timeToFirstTokenMs = inference ? now.getTime() - inference.startTime.getTime() : void 0;
2548
2563
  this.#inferenceCompletionStartTime = now;
2564
+ this.#inferenceTimeToFirstTokenMs = timeToFirstTokenMs;
2549
2565
  this.#completionStartTime ??= now;
2566
+ this.#timeToFirstTokenMs ??= timeToFirstTokenMs;
2550
2567
  for (const span of [
2551
2568
  this.#currentStepSpan,
2552
2569
  this.#modelSpan,
2553
2570
  this.#agentRunSpan
2554
- ]) if (span && !span.attributes?.completionStartTime) span.update({ attributes: { completionStartTime: now } });
2571
+ ]) {
2572
+ if (!span || span.attributes?.completionStartTime) continue;
2573
+ span.update({ attributes: {
2574
+ completionStartTime: now,
2575
+ ...timeToFirstTokenMs !== void 0 ? { timeToFirstTokenMs } : {}
2576
+ } });
2577
+ }
2578
+ }
2579
+ /** What the current inference streamed so far, for whichever way it ends. */
2580
+ #inferenceStreamAttributes() {
2581
+ return {
2582
+ ...this.#inferenceCompletionStartTime ? { completionStartTime: this.#inferenceCompletionStartTime } : {},
2583
+ ...this.#inferenceTimeToFirstTokenMs !== void 0 ? { timeToFirstTokenMs: this.#inferenceTimeToFirstTokenMs } : {}
2584
+ };
2555
2585
  }
2556
2586
  /**
2557
2587
  * Get the tracing context for creating child spans.
@@ -2566,21 +2596,33 @@ var ModelSpanTracker = class {
2566
2596
  * doesn't leave them dangling. No-op if they were already closed.
2567
2597
  */
2568
2598
  reportGenerationError(options) {
2599
+ this.#endChunkSpan();
2600
+ const termination = options.outcome != null ? {
2601
+ outcome: options.outcome,
2602
+ stopReason: options.stopReason,
2603
+ stopReasonContext: options.stopReasonContext
2604
+ } : coreObservability.executionFailureTermination?.(options.error);
2569
2605
  if (this.#currentInferenceSpan) {
2570
2606
  this.#currentInferenceSpan.error({
2571
2607
  error: options.error,
2572
- endSpan: true
2608
+ ...termination ? interruptedByRun(termination) : {},
2609
+ endSpan: true,
2610
+ attributes: this.#inferenceStreamAttributes()
2573
2611
  });
2574
2612
  this.#currentInferenceSpan = void 0;
2575
2613
  }
2576
2614
  if (this.#currentStepSpan) {
2577
2615
  this.#currentStepSpan.error({
2578
2616
  error: options.error,
2617
+ ...termination ? interruptedByRun(termination) : {},
2579
2618
  endSpan: true
2580
2619
  });
2581
2620
  this.#currentStepSpan = void 0;
2582
2621
  }
2583
- this.#modelSpan?.error(options);
2622
+ this.#modelSpan?.error({
2623
+ ...options,
2624
+ ...termination
2625
+ });
2584
2626
  }
2585
2627
  /**
2586
2628
  * End the generation span with optional raw usage data.
@@ -2595,16 +2637,37 @@ var ModelSpanTracker = class {
2595
2637
  }, model);
2596
2638
  if (providerCostContext && !spanOptions.attributes) spanOptions.attributes = {};
2597
2639
  if (spanOptions.attributes) {
2598
- spanOptions.attributes.completionStartTime = this.#completionStartTime ?? spanOptions.attributes.completionStartTime;
2640
+ if (this.#completionStartTime) {
2641
+ spanOptions.attributes.completionStartTime = this.#completionStartTime;
2642
+ spanOptions.attributes.timeToFirstTokenMs = this.#timeToFirstTokenMs;
2643
+ }
2599
2644
  spanOptions.attributes.usage = extractUsageMetrics(usage, providerMetadata);
2600
2645
  spanOptions.attributes.costContext = providerCostContext ?? spanOptions.attributes.costContext;
2601
2646
  if (!spanOptions.attributes.costContext) spanOptions.attributes.costContext = getGatewayCostContext({ stepProviderMetadata });
2602
2647
  }
2648
+ if (spanOptions.outcome === "abort" || spanOptions.stopReason === "timeout") {
2649
+ const termination = {
2650
+ outcome: spanOptions.outcome,
2651
+ stopReason: spanOptions.stopReason,
2652
+ stopReasonContext: spanOptions.stopReasonContext
2653
+ };
2654
+ this.#endChunkSpan();
2655
+ this.#currentStepSpan?.end({
2656
+ ...interruptedByRun(termination),
2657
+ endTree: true
2658
+ });
2659
+ this.#currentStepSpan = void 0;
2660
+ this.#currentInferenceSpan = void 0;
2661
+ this.#currentStepInputIsFinal = false;
2662
+ }
2603
2663
  this.#modelSpan?.end(spanOptions);
2604
2664
  }
2605
2665
  getCompletionStartTime() {
2606
2666
  return this.#completionStartTime;
2607
2667
  }
2668
+ getTimeToFirstTokenMs() {
2669
+ return this.#timeToFirstTokenMs;
2670
+ }
2608
2671
  /**
2609
2672
  * Update the generation span
2610
2673
  */
@@ -2658,10 +2721,12 @@ var ModelSpanTracker = class {
2658
2721
  */
2659
2722
  startStep(payload) {
2660
2723
  if (this.#currentStepSpan) return;
2724
+ this.#stepHasInference = false;
2661
2725
  const input = extractStepInput(payload);
2662
2726
  this.#currentStepSpan = this.#modelSpan?.createChildSpan({
2663
2727
  name: `step: ${this.#stepIndex}`,
2664
2728
  type: SpanType.MODEL_STEP,
2729
+ ...typeof coreObservability.agentLoopTermination === "function" ? { outcome: null } : {},
2665
2730
  attributes: {
2666
2731
  stepIndex: this.#stepIndex,
2667
2732
  ...payload?.messageId ? { messageId: payload.messageId } : {},
@@ -2673,6 +2738,7 @@ var ModelSpanTracker = class {
2673
2738
  this.#currentStepInputIsFinal = Array.isArray(payload?.inputMessages);
2674
2739
  this.#chunkSequence = 0;
2675
2740
  this.#inferenceCompletionStartTime = void 0;
2741
+ this.#inferenceTimeToFirstTokenMs = void 0;
2676
2742
  }
2677
2743
  /**
2678
2744
  * End the current MODEL_INFERENCE span when the provider stream finishes.
@@ -2682,22 +2748,31 @@ var ModelSpanTracker = class {
2682
2748
  *
2683
2749
  * Safe to call multiple times - no-ops if the span is already closed.
2684
2750
  */
2685
- #endInferenceSpan(payload) {
2751
+ #endInferenceSpan(payload, captureOutput = true) {
2686
2752
  if (!this.#currentInferenceSpan) return;
2687
2753
  const { usage: rawUsage, ...otherOutput } = payload.output;
2688
2754
  const usage = extractUsageMetrics(rawUsage, payload.metadata?.providerMetadata);
2689
2755
  const responseModel = typeof payload.metadata?.modelId === "string" ? payload.metadata.modelId : void 0;
2690
2756
  this.#currentInferenceSpan.end({
2691
- output: otherOutput,
2757
+ ...coreObservability.agentLoopTermination?.({
2758
+ stopCause: "model_finished",
2759
+ finishReason: payload.stepResult.reason
2760
+ }),
2761
+ ...payload.stepResult.reason === "error" ? {
2762
+ outcome: "error",
2763
+ stopReason: "model_error"
2764
+ } : {},
2765
+ ...captureOutput ? { output: otherOutput } : {},
2692
2766
  attributes: {
2693
2767
  usage,
2694
2768
  finishReason: payload.stepResult.reason,
2695
2769
  warnings: payload.stepResult.warnings,
2696
- completionStartTime: this.#inferenceCompletionStartTime,
2770
+ ...this.#inferenceStreamAttributes(),
2697
2771
  ...responseModel?.trim() ? { responseModel } : {}
2698
2772
  }
2699
2773
  });
2700
2774
  this.#currentInferenceSpan = void 0;
2775
+ this.#inferenceCompletionStartTime = void 0;
2701
2776
  }
2702
2777
  /**
2703
2778
  * Open the MODEL_INFERENCE span for the current step. Chunks (including tool-call
@@ -2715,31 +2790,56 @@ var ModelSpanTracker = class {
2715
2790
  * start time.
2716
2791
  *
2717
2792
  * `payload.modelId` / `payload.provider` name the model the caller is about
2718
- * to invoke and are recorded on MODEL_STEP, also when a fallback attempt
2719
- * reuses the step's open inference span. The safety net never records them:
2720
- * a replayed step-start chunk names the model of the original call.
2793
+ * to invoke and are recorded on MODEL_STEP and the inference. A named call
2794
+ * while an inference is still open is the next attempt of the same step (a
2795
+ * fallback model after a failure): that inference ends as failed and the
2796
+ * attempt gets its own, so its timing and model are its own. The safety net
2797
+ * never records them: a replayed step-start chunk names the model of the
2798
+ * original call.
2721
2799
  */
2722
2800
  startInference(payload) {
2723
- if (payload?.modelId && payload.provider) this.#currentStepSpan?.update({ attributes: {
2724
- model: payload.modelId,
2725
- provider: payload.provider
2726
- } });
2801
+ if (payload?.modelId && payload.provider) {
2802
+ this.#currentStepSpan?.update({ attributes: {
2803
+ model: payload.modelId,
2804
+ provider: payload.provider
2805
+ } });
2806
+ this.failInference(/* @__PURE__ */ new Error("Model call ended without a result before the next attempt started"));
2807
+ }
2727
2808
  this.#openInference(payload);
2728
2809
  }
2810
+ /** End the current attempt only; generation and agent spans belong to their owners. */
2811
+ endAttempt(termination) {
2812
+ termination = interruptedByRun(termination);
2813
+ const payload = this.#pendingStepFinishPayload;
2814
+ if (payload) this.#endStepSpan(payload, termination);
2815
+ else {
2816
+ this.#endChunkSpan();
2817
+ this.#currentInferenceSpan?.end(termination);
2818
+ this.#currentStepSpan?.end(termination);
2819
+ this.#currentStepSpan = void 0;
2820
+ this.#currentInferenceSpan = void 0;
2821
+ }
2822
+ this.#pendingStepFinishPayload = void 0;
2823
+ this.#inferenceCompletionStartTime = void 0;
2824
+ this.#currentStepInputIsFinal = false;
2825
+ }
2729
2826
  #openInference(payload) {
2730
2827
  if (!supportsModelInference()) return;
2731
2828
  if (!this.#currentStepSpan || this.#currentInferenceSpan) return;
2732
2829
  this.#inferenceCompletionStartTime = void 0;
2830
+ this.#inferenceTimeToFirstTokenMs = void 0;
2733
2831
  const input = extractStepInput(payload);
2734
2832
  const generationAttrs = this.#modelSpan?.attributes;
2735
2833
  const ctx = this.#inferenceContext;
2834
+ this.#stepHasInference = true;
2736
2835
  this.#currentInferenceSpan = this.#currentStepSpan.createChildSpan({
2737
2836
  name: `inference: ${this.#stepIndex}`,
2738
2837
  type: SpanType.MODEL_INFERENCE,
2838
+ ...typeof coreObservability.agentLoopTermination === "function" ? { outcome: null } : {},
2739
2839
  attributes: {
2740
2840
  stepIndex: this.#stepIndex,
2741
- model: generationAttrs?.model,
2742
- provider: generationAttrs?.provider,
2841
+ model: payload?.modelId ?? generationAttrs?.model,
2842
+ provider: payload?.provider ?? generationAttrs?.provider,
2743
2843
  streaming: generationAttrs?.streaming,
2744
2844
  ...ctx?.parameters !== void 0 ? { parameters: ctx.parameters } : {},
2745
2845
  ...ctx?.providerOptions !== void 0 ? { providerOptions: ctx.providerOptions } : {},
@@ -2756,11 +2856,37 @@ var ModelSpanTracker = class {
2756
2856
  * End the open MODEL_INFERENCE span as interrupted while keeping MODEL_STEP open,
2757
2857
  * so a replacement request for the same step gets its own inference span.
2758
2858
  */
2759
- interruptInference() {
2859
+ interruptInference(termination) {
2860
+ this.#endChunkSpan();
2861
+ if (!this.#currentInferenceSpan) return;
2862
+ this.#currentInferenceSpan.end({
2863
+ attributes: {
2864
+ finishReason: "interrupted",
2865
+ ...this.#inferenceStreamAttributes()
2866
+ },
2867
+ ...termination ?? (typeof coreObservability.agentLoopTermination === "function" ? {
2868
+ outcome: "interrupted",
2869
+ stopReason: null,
2870
+ stopReasonContext: null
2871
+ } : {})
2872
+ });
2873
+ this.#currentInferenceSpan = void 0;
2874
+ this.#inferenceCompletionStartTime = void 0;
2875
+ this.#inferenceTimeToFirstTokenMs = void 0;
2876
+ }
2877
+ /** End the open inference span, if any, with `error` and what it streamed. */
2878
+ failInference(error) {
2760
2879
  this.#endChunkSpan();
2761
2880
  if (!this.#currentInferenceSpan) return;
2762
- this.#currentInferenceSpan.end({ attributes: { finishReason: "interrupted" } });
2881
+ this.#currentInferenceSpan.error({
2882
+ error,
2883
+ ...coreObservability.executionFailureTermination?.(error, "model_error"),
2884
+ endSpan: true,
2885
+ attributes: this.#inferenceStreamAttributes()
2886
+ });
2887
+ this.#inferenceTimeToFirstTokenMs = void 0;
2763
2888
  this.#currentInferenceSpan = void 0;
2889
+ this.#inferenceCompletionStartTime = void 0;
2764
2890
  }
2765
2891
  /**
2766
2892
  * Update the current step span with additional payload data.
@@ -2782,10 +2908,11 @@ var ModelSpanTracker = class {
2782
2908
  /**
2783
2909
  * End the current Model execution step with token usage, finish reason, output, and metadata
2784
2910
  */
2785
- #endStepSpan(payload) {
2911
+ #endStepSpan(payload, termination) {
2786
2912
  this.#endChunkSpan();
2787
2913
  if (!this.#currentStepSpan) return;
2788
- const { usage: rawUsage, ...otherOutput } = payload.output;
2914
+ const output = payload.output;
2915
+ const { usage: rawUsage, ...otherOutput } = output;
2789
2916
  const stepResult = payload.stepResult;
2790
2917
  const metadata = payload.metadata;
2791
2918
  const usage = extractUsageMetrics(rawUsage, metadata?.providerMetadata);
@@ -2801,6 +2928,11 @@ var ModelSpanTracker = class {
2801
2928
  ]) delete cleanMetadata[key];
2802
2929
  this.#endInferenceSpan(payload);
2803
2930
  this.#currentStepSpan.end({
2931
+ ...termination ?? coreObservability.agentLoopTermination?.({
2932
+ stopCause: "model_finished",
2933
+ finishReason: stepResult.reason,
2934
+ tripwire: output.steps?.at(-1)?.tripwire
2935
+ }),
2804
2936
  output: otherOutput,
2805
2937
  attributes: {
2806
2938
  usage,
@@ -2832,26 +2964,23 @@ var ModelSpanTracker = class {
2832
2964
  warnings: []
2833
2965
  },
2834
2966
  metadata: {}
2835
- });
2967
+ }, coreObservability.agentLoopTermination?.({
2968
+ finishReason: "suspended",
2969
+ suspension: {
2970
+ approvalRequired: chunk.type === "tool-call-approval",
2971
+ toolCallId: chunk.payload.toolCallId,
2972
+ toolName: chunk.payload.toolName
2973
+ }
2974
+ }));
2836
2975
  }
2837
- /**
2838
- * Returns the parent span for chunks. Chunks parent under MODEL_INFERENCE
2839
- * (the provider call) when available, falling back to MODEL_STEP only if
2840
- * startStep() was bypassed.
2841
- */
2976
+ /** Processed content belongs to the logical step, independently of provider timing. */
2842
2977
  #chunkParent() {
2843
- return this.#currentInferenceSpan ?? this.#currentStepSpan;
2978
+ return this.#currentStepSpan?.endTime ? void 0 : this.#currentStepSpan;
2844
2979
  }
2845
- /**
2846
- * Safety-net invoked from chunk handlers: auto-create MODEL_STEP and
2847
- * MODEL_INFERENCE if a chunk arrives before the loop has explicitly opened
2848
- * them, so chunks parent under MODEL_INFERENCE rather than falling through
2849
- * to MODEL_STEP. Idempotent — each public start* method is itself a no-op
2850
- * when its span is already live.
2851
- */
2980
+ /** Ensure a logical step and provider timing span exist for legacy stream callers. */
2852
2981
  #ensureStepAndInference() {
2853
2982
  if (!this.#currentStepSpan) this.startStep();
2854
- if (!this.#currentInferenceSpan) this.#openInference();
2983
+ if (!this.#currentInferenceSpan && !this.#stepHasInference) this.#openInference();
2855
2984
  }
2856
2985
  /**
2857
2986
  * Create a new chunk span (for multi-part chunks like text-start/delta/end)
@@ -3012,20 +3141,39 @@ var ModelSpanTracker = class {
3012
3141
  tracingPolicy: this.#modelSpan?.tracingPolicy
3013
3142
  })) this.#chunkSequence++;
3014
3143
  }
3144
+ /** Track provider timing and completion without recording unprocessed content. */
3145
+ wrapInferenceStream(stream) {
3146
+ this.#tracksProviderStream = true;
3147
+ return this.wrapStream(stream, true);
3148
+ }
3015
3149
  /**
3016
3150
  * Wraps a stream with model tracing transform to track MODEL_STEP and MODEL_CHUNK spans.
3017
3151
  *
3018
3152
  * This should be added to the stream pipeline to automatically
3019
3153
  * create MODEL_STEP and MODEL_CHUNK spans for each semantic unit in the stream.
3020
3154
  */
3021
- wrapStream(stream) {
3155
+ wrapStream(stream, inferenceOnly = false) {
3022
3156
  return stream.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
3023
- if ((this.#inModelResponse || this.#deferStepClose) && MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3157
+ if (inferenceOnly) {
3158
+ try {
3159
+ if (MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3160
+ if (chunk.type === "finish") this.#endInferenceSpan(chunk.payload, false);
3161
+ } catch {}
3162
+ controller.enqueue(chunk);
3163
+ return;
3164
+ }
3165
+ this.recordChunk(chunk);
3166
+ controller.enqueue(chunk);
3167
+ } }));
3168
+ }
3169
+ /** @internal Record a chunk only after output processors accept it. */
3170
+ recordChunk(chunk) {
3171
+ try {
3172
+ if (!this.#tracksProviderStream && (this.#inModelResponse || this.#deferStepClose) && MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3024
3173
  if (chunk.type === "tool-call-suspended" || chunk.type === "tool-call-approval") {
3025
3174
  if (chunk.type === "tool-call-approval") this.#handleToolApprovalChunk(chunk);
3026
3175
  this.#endSuspendedStep(chunk);
3027
3176
  }
3028
- controller.enqueue(chunk);
3029
3177
  switch (chunk.type) {
3030
3178
  case "text-start":
3031
3179
  case "text-delta":
@@ -3051,7 +3199,7 @@ var ModelSpanTracker = class {
3051
3199
  this.#inModelResponse = true;
3052
3200
  if (this.#currentStepSpan) this.updateStep(chunk.payload);
3053
3201
  else this.startStep(chunk.payload);
3054
- if (!this.#currentInferenceSpan) this.#openInference(chunk.payload);
3202
+ if (!this.#currentInferenceSpan && !this.#stepHasInference) this.#openInference(chunk.payload);
3055
3203
  break;
3056
3204
  case "step-finish":
3057
3205
  this.#inModelResponse = false;
@@ -3061,15 +3209,20 @@ var ModelSpanTracker = class {
3061
3209
  this.#endInferenceSpan(chunk.payload);
3062
3210
  } else this.#endStepSpan(chunk.payload);
3063
3211
  break;
3212
+ case "tripwire": {
3213
+ const termination = coreObservability.agentLoopTermination?.({ tripwire: chunk.payload });
3214
+ if (!termination) break;
3215
+ this.endAttempt(termination);
3216
+ break;
3217
+ }
3218
+ case "finish": break;
3064
3219
  case "raw":
3065
3220
  case "start":
3066
- case "finish":
3067
3221
  case "response-metadata":
3068
3222
  case "source":
3069
3223
  case "file":
3070
3224
  case "error":
3071
3225
  case "abort":
3072
- case "tripwire":
3073
3226
  case "watch":
3074
3227
  case "tool-error":
3075
3228
  case "reasoning-signature":
@@ -3094,7 +3247,7 @@ var ModelSpanTracker = class {
3094
3247
  break;
3095
3248
  }
3096
3249
  }
3097
- } }));
3250
+ } catch {}
3098
3251
  }
3099
3252
  };
3100
3253
  // src/spans/metadata.ts
@@ -3187,6 +3340,13 @@ function definedLineage(source) {
3187
3340
  }
3188
3341
  return lineage;
3189
3342
  }
3343
+ function modelPair(attributes) {
3344
+ const { model, provider } = attributes ?? {};
3345
+ return typeof model === "string" && model && typeof provider === "string" && provider ? {
3346
+ model,
3347
+ provider
3348
+ } : {};
3349
+ }
3190
3350
  /**
3191
3351
  * Determines if a span type should be considered internal based on flags.
3192
3352
  * Returns false if flags are undefined.
@@ -3283,6 +3443,9 @@ var BaseSpan = class BaseSpan {
3283
3443
  observabilityInstance;
3284
3444
  input;
3285
3445
  output;
3446
+ outcome;
3447
+ stopReason;
3448
+ stopReasonContext;
3286
3449
  errorInfo;
3287
3450
  metadata;
3288
3451
  requestContext;
@@ -3299,11 +3462,7 @@ var BaseSpan = class BaseSpan {
3299
3462
  parentSpanId;
3300
3463
  /** Parent from an external tracing system (ambient OTel / dd-trace span) that Mastra did not create; carried for external correlation, not part of Mastra's own parent/child linkage */
3301
3464
  externalParentSpanId;
3302
- /** Native span fields (see `SpanData`); carried as supplied and exported only when set */
3303
3465
  resumedFromSpanId;
3304
- outcome;
3305
- stopReason;
3306
- stopReasonContext;
3307
3466
  /** Deep clean options for serialization */
3308
3467
  deepCleanOptions;
3309
3468
  /**
@@ -3331,6 +3490,7 @@ var BaseSpan = class BaseSpan {
3331
3490
  * live `parent`, so its original ancestors are only known from the cached export.
3332
3491
  */
3333
3492
  restoredLineage;
3493
+ restoredIsRootSpan;
3334
3494
  /**
3335
3495
  * Version declared for this span's own entity, or inherited from a parent span
3336
3496
  * of the same entity. Unlike `metadata.entityVersionId`, never an ancestor's
@@ -3351,6 +3511,7 @@ var BaseSpan = class BaseSpan {
3351
3511
  constructor(options, observabilityInstance) {
3352
3512
  const observabilityConfig = observabilityInstance.getConfig();
3353
3513
  this.deepCleanOptions = mergeSerializationOptions(observabilityConfig.serializationOptions);
3514
+ this.applyOutcome(options);
3354
3515
  this.name = options.name;
3355
3516
  this.type = options.type;
3356
3517
  this.isInternal = isSpanInternal(this.type, options.tracingPolicy?.internal);
@@ -3371,9 +3532,6 @@ var BaseSpan = class BaseSpan {
3371
3532
  this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
3372
3533
  this.links = validLinks(options.links);
3373
3534
  this.resumedFromSpanId = options.resumedFromSpanId;
3374
- this.outcome = options.outcome;
3375
- this.stopReason = options.stopReason;
3376
- this.stopReasonContext = deepClean(options.stopReasonContext, this.deepCleanOptions);
3377
3535
  const entityParent = this.getParentSpan(false);
3378
3536
  this.entityType = options.entityType ?? entityParent?.entityType;
3379
3537
  this.entityId = options.entityId ?? entityParent?.entityId;
@@ -3415,14 +3573,49 @@ var BaseSpan = class BaseSpan {
3415
3573
  return value;
3416
3574
  }
3417
3575
  }
3418
- /** End the span and any descendant spans that are still open, applying `options` to each */
3576
+ /** Preserve explicitly supplied native fields; never derive results from payloads/errors. */
3577
+ applyOutcome(options) {
3578
+ if (this.endTime) return;
3579
+ if (options.outcome !== void 0) {
3580
+ this.outcome = options.outcome;
3581
+ this.stopReason = options.stopReason ?? null;
3582
+ this.stopReasonContext = options.stopReasonContext == null ? null : deepClean(options.stopReasonContext, this.deepCleanOptions);
3583
+ return;
3584
+ }
3585
+ if (options.stopReason !== void 0 && options.stopReason !== this.stopReason) this.stopReasonContext = null;
3586
+ if (options.stopReason !== void 0) this.stopReason = options.stopReason;
3587
+ if (options.stopReasonContext !== void 0) this.stopReasonContext = options.stopReasonContext == null ? null : deepClean(options.stopReasonContext, this.deepCleanOptions);
3588
+ }
3589
+ /** Forward options to open descendants, with interruption outcomes for cascading termination. */
3419
3590
  endTree(options) {
3420
- if (this.#openChildren) for (const child of [...this.#openChildren]) child.endTree(options);
3591
+ this.endOpenDescendants(options, true);
3421
3592
  this.end(options);
3422
3593
  }
3423
- /** Close any still-open descendant spans without applying options to them */
3424
- endOpenDescendants() {
3425
- if (this.#openChildren) for (const child of [...this.#openChildren]) child.endTree();
3594
+ /** Preserve completed children; only known cascading termination interrupts open work. */
3595
+ endOpenDescendants(options, forwardOptions = false) {
3596
+ const outcome = options?.outcome !== void 0 ? options.outcome : this.outcome;
3597
+ const stopReason = options?.stopReason !== void 0 ? options.stopReason : this.stopReason;
3598
+ const stopReasonContext = options?.stopReasonContext !== void 0 ? options.stopReasonContext : this.stopReasonContext;
3599
+ const interrupts = outcome === "abort" || outcome === "interrupted" || outcome === "limit" && stopReason === "timeout";
3600
+ const forwarded = forwardOptions ? { ...options } : {};
3601
+ delete forwarded.outcome;
3602
+ delete forwarded.stopReason;
3603
+ delete forwarded.stopReasonContext;
3604
+ if (this.#openChildren) for (const child of [...this.#openChildren]) {
3605
+ const childOptions = {
3606
+ ...forwarded,
3607
+ ...interrupts ? {
3608
+ outcome: "interrupted",
3609
+ stopReason,
3610
+ stopReasonContext
3611
+ } : {}
3612
+ };
3613
+ if (forwardOptions) child.endTree(childOptions);
3614
+ else child.end({
3615
+ ...childOptions,
3616
+ endTree: true
3617
+ });
3618
+ }
3426
3619
  }
3427
3620
  /**
3428
3621
  * Release this span from its parent's open-child set once it has ended.
@@ -3473,7 +3666,7 @@ var BaseSpan = class BaseSpan {
3473
3666
  }
3474
3667
  /** Returns `TRUE` if the span is the root span of a trace */
3475
3668
  get isRootSpan() {
3476
- return !this.parent;
3669
+ return this.restoredIsRootSpan ?? !this.parent;
3477
3670
  }
3478
3671
  /**
3479
3672
  * Get the closest parent span.
@@ -3537,6 +3730,7 @@ var BaseSpan = class BaseSpan {
3537
3730
  */
3538
3731
  restoreEntityLineage(cached) {
3539
3732
  this.ownEntityVersionId = cached.ownEntityVersionId ?? void 0;
3733
+ this.restoredIsRootSpan = cached.isRootSpan;
3540
3734
  const lineage = definedLineage(cached);
3541
3735
  if (Object.keys(lineage).length > 0) {
3542
3736
  this.restoredLineage = lineage;
@@ -3619,6 +3813,7 @@ var BaseSpan = class BaseSpan {
3619
3813
  const nestedUnderParent = this.traceState?.nestedUnderParent ?? false;
3620
3814
  const links = validLinks(this.links);
3621
3815
  const correlation = this.getCorrelationContext();
3816
+ const timeToFirstTokenMs = this.attributes?.timeToFirstTokenMs;
3622
3817
  return {
3623
3818
  id: this.id,
3624
3819
  traceId: this.traceId,
@@ -3634,6 +3829,9 @@ var BaseSpan = class BaseSpan {
3634
3829
  input: hideInput ? void 0 : this.input,
3635
3830
  output: hideOutput ? void 0 : this.output,
3636
3831
  errorInfo: this.errorInfo,
3832
+ ...this.outcome !== void 0 ? { outcome: this.outcome } : {},
3833
+ ...this.stopReason !== void 0 ? { stopReason: this.stopReason } : {},
3834
+ ...this.stopReasonContext !== void 0 ? { stopReasonContext: this.stopReasonContext } : {},
3637
3835
  requestContext: this.requestContext,
3638
3836
  isEvent: this.isEvent,
3639
3837
  isRootSpan: this.isRootSpan,
@@ -3643,9 +3841,8 @@ var BaseSpan = class BaseSpan {
3643
3841
  parentSpanId: this.getParentSpanId(includeInternalSpans),
3644
3842
  externalParentSpanId: this.getExportedExternalParentSpanId(includeInternalSpans),
3645
3843
  ...this.resumedFromSpanId !== void 0 ? { resumedFromSpanId: this.resumedFromSpanId } : {},
3646
- ...this.outcome !== void 0 ? { outcome: this.outcome } : {},
3647
- ...this.stopReason !== void 0 ? { stopReason: this.stopReason } : {},
3648
- ...this.stopReasonContext !== void 0 ? { stopReasonContext: this.stopReasonContext } : {},
3844
+ ...modelPair(this.attributes),
3845
+ ...typeof timeToFirstTokenMs === "number" ? { timeToFirstTokenMs } : {},
3649
3846
  ...nestedUnderParent ? { nestedUnderParent: true } : {},
3650
3847
  ...links ? { links } : {},
3651
3848
  ...this.isRootSpan && !nestedUnderParent && this.tags?.length ? { tags: this.tags } : {}
@@ -3654,7 +3851,7 @@ var BaseSpan = class BaseSpan {
3654
3851
  /** Returns the span saved for a later `rebuildSpan()`, with its trace state */
3655
3852
  snapshot() {
3656
3853
  return {
3657
- ...this.exportSpan(),
3854
+ ...this.exportSpan(this.observabilityInstance.getConfig().includeInternalSpans),
3658
3855
  ownEntityVersionId: this.ownEntityVersionId ?? null,
3659
3856
  ...this.traceState ? { traceState: this.traceState } : {}
3660
3857
  };
@@ -3755,7 +3952,9 @@ var DefaultSpan = class extends BaseSpan {
3755
3952
  end(options) {
3756
3953
  if (this.isEvent) return;
3757
3954
  if (this.endTime) return;
3758
- if (options?.endTree) this.endOpenDescendants();
3955
+ if (options?.endTree) this.endOpenDescendants(options);
3956
+ if (options) this.applyOutcome(options);
3957
+ if (this.outcome === null) this.outcome = "unknown";
3759
3958
  this.endTime = /* @__PURE__ */ new Date();
3760
3959
  this.detachFromParent();
3761
3960
  if (options?.metadata) this.metadata = {
@@ -3770,8 +3969,9 @@ var DefaultSpan = class extends BaseSpan {
3770
3969
  };
3771
3970
  }
3772
3971
  error(options) {
3773
- if (this.isEvent) return;
3972
+ if (this.isEvent || this.endTime) return;
3774
3973
  const { error, endSpan = true, endTree, attributes, metadata } = options;
3974
+ this.applyOutcome(options);
3775
3975
  if (metadata) this.metadata = {
3776
3976
  ...this.metadata,
3777
3977
  ...deepClean(this.prepareSpanMetadata(metadata), this.deepCleanOptions)
@@ -3806,6 +4006,7 @@ var DefaultSpan = class extends BaseSpan {
3806
4006
  }
3807
4007
  update(options) {
3808
4008
  if (this.isEvent) return;
4009
+ this.applyOutcome(options);
3809
4010
  if (options.name !== void 0) this.name = options.name;
3810
4011
  if (options.metadata) this.metadata = {
3811
4012
  ...this.metadata,
@@ -5275,13 +5476,13 @@ var BaseObservabilityInstance = class extends MastraBase {
5275
5476
  ...rest,
5276
5477
  metadata
5277
5478
  }, this);
5278
- } else if (!this.shouldSample(customSamplerOptions)) return new NoOpSpan({
5479
+ } else if (!(options.samplingDecision ?? this.shouldSample(customSamplerOptions))) return new NoOpSpan({
5279
5480
  ...rest,
5280
5481
  metadata
5281
5482
  }, this);
5282
5483
  let traceState;
5283
5484
  if (options.parent) traceState = options.parent.traceState;
5284
- else traceState = this.computeTraceState(tracingOptions);
5485
+ else traceState = options.traceState ?? this.computeTraceState(tracingOptions);
5285
5486
  const tracingMetadata = !options.parent ? tracingOptions?.metadata : void 0;
5286
5487
  const mergedMetadata = mergeMetadata$1(stripUndefined(metadata), stripUndefined(tracingMetadata));
5287
5488
  const enrichedMetadata = this.extractMetadataFromRequestContext(requestContext, mergedMetadata, traceState);
@@ -5336,6 +5537,11 @@ var BaseObservabilityInstance = class extends MastraBase {
5336
5537
  * @returns A span that can have lifecycle methods called on it
5337
5538
  */
5338
5539
  rebuildSpan(cached) {
5540
+ if (cached.id === "no-op" && cached.traceId === "no-op-trace") return new NoOpSpan({
5541
+ name: cached.name,
5542
+ type: cached.type,
5543
+ traceState: cached.traceState
5544
+ }, this);
5339
5545
  const span = this.createSpan({
5340
5546
  name: cached.name,
5341
5547
  type: cached.type,
@@ -5359,6 +5565,11 @@ var BaseObservabilityInstance = class extends MastraBase {
5359
5565
  traceState: this.rebuildTraceState(cached)
5360
5566
  });
5361
5567
  if (span instanceof BaseSpan) span.restoreEntityLineage(cached);
5568
+ if (cached.endTime) {
5569
+ span.endTime = cached.endTime instanceof Date ? cached.endTime : new Date(cached.endTime);
5570
+ span.output = cached.output;
5571
+ span.errorInfo = cached.errorInfo;
5572
+ }
5362
5573
  this.wireSpanLifecycle(span);
5363
5574
  return span;
5364
5575
  }
@@ -9123,7 +9334,11 @@ var TestExporter = class extends BaseExporter {
9123
9334
  if (span.parentSpanId && spanIdMap.has(span.parentSpanId)) normalizedSpan.parentId = spanIdMap.get(span.parentSpanId);
9124
9335
  if (span.entityType) normalizedSpan.entityType = span.entityType;
9125
9336
  if (span.entityId) normalizedSpan.entityId = normalizeValue(span.entityId, "entityId");
9126
- if (span.attributes && Object.keys(span.attributes).length > 0) normalizedSpan.attributes = normalizeValue(span.attributes);
9337
+ if (span.attributes && Object.keys(span.attributes).length > 0) {
9338
+ const attributes = normalizeValue(span.attributes);
9339
+ if (typeof attributes.timeToFirstTokenMs === "number") attributes.timeToFirstTokenMs = "<ms>";
9340
+ normalizedSpan.attributes = attributes;
9341
+ }
9127
9342
  if (span.metadata && Object.keys(span.metadata).length > 0) normalizedSpan.metadata = normalizeValue(span.metadata);
9128
9343
  if (span.input !== void 0) normalizedSpan.input = normalizeValue(span.input);
9129
9344
  if (span.output !== void 0) normalizedSpan.output = normalizeValue(span.output);
@@ -10015,6 +10230,7 @@ var SensitiveDataFilter = class {
10015
10230
  span.input = this.tryFilter(span.input, indexedState);
10016
10231
  span.output = this.tryFilter(span.output, indexedState);
10017
10232
  span.errorInfo = this.tryFilter(span.errorInfo, indexedState);
10233
+ span.stopReasonContext = this.tryFilter(span.stopReasonContext, indexedState);
10018
10234
  span.requestContext = this.tryFilter(span.requestContext, indexedState);
10019
10235
  return span;
10020
10236
  }