@mastra/observability 1.22.0-alpha.3 → 1.23.0-alpha.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
@@ -2317,9 +2317,10 @@ function addUsageStats(a, b) {
2317
2317
  * Provides span tracking for Model generations, including:
2318
2318
  * - MODEL_STEP spans (one per Model API call - includes processors and tool executions)
2319
2319
  * - MODEL_INFERENCE spans (the provider call itself - model latency only)
2320
- * - MODEL_CHUNK spans (individual streaming chunks within an inference)
2320
+ * - MODEL_CHUNK spans (processed streaming content within a logical step)
2321
2321
  *
2322
- * Hierarchy: MODEL_GENERATION -> MODEL_STEP -> MODEL_INFERENCE -> MODEL_CHUNK
2322
+ * Hierarchy: MODEL_GENERATION -> MODEL_STEP -> MODEL_INFERENCE (provider timing)
2323
+ * -> MODEL_CHUNK (processed content)
2323
2324
  *
2324
2325
  * Processors and tool executions remain children of MODEL_STEP (siblings of
2325
2326
  * MODEL_INFERENCE), so MODEL_INFERENCE measures pure model time.
@@ -2336,6 +2337,12 @@ function addUsageStats(a, b) {
2336
2337
  function supportsModelInference() {
2337
2338
  return _mastra_core_features.coreFeatures.has("model-inference-span");
2338
2339
  }
2340
+ function interruptedByRun(termination) {
2341
+ return termination.outcome === "abort" || termination.stopReason === "timeout" && termination.stopReasonContext && "timeoutType" in termination.stopReasonContext && termination.stopReasonContext.timeoutType === "total" ? {
2342
+ ...termination,
2343
+ outcome: "interrupted"
2344
+ } : termination;
2345
+ }
2339
2346
  function parseGatewayCost(providerMetadata) {
2340
2347
  const rawCost = providerMetadata?.gateway?.cost;
2341
2348
  const cost = typeof rawCost === "number" ? rawCost : typeof rawCost === "string" && rawCost.trim().length > 0 ? Number(rawCost) : void 0;
@@ -2526,13 +2533,17 @@ var ModelSpanTracker = class {
2526
2533
  #modelSpan;
2527
2534
  #currentStepSpan;
2528
2535
  #currentInferenceSpan;
2536
+ #stepHasInference = false;
2537
+ #tracksProviderStream = false;
2529
2538
  #currentChunkSpan;
2530
2539
  #currentChunkType;
2531
2540
  #accumulator = {};
2532
2541
  #stepIndex = 0;
2533
2542
  #chunkSequence = 0;
2534
2543
  #completionStartTime;
2544
+ #timeToFirstTokenMs;
2535
2545
  #inferenceCompletionStartTime;
2546
+ #inferenceTimeToFirstTokenMs;
2536
2547
  /**
2537
2548
  * True between a step's `step-start` and `step-finish` chunks, the window in which
2538
2549
  * the provider responds. Chunks outside it, such as a `tool-call` replayed for a
@@ -2561,22 +2572,41 @@ var ModelSpanTracker = class {
2561
2572
  this.#inferenceContext = context;
2562
2573
  }
2563
2574
  /**
2564
- * Record when the provider's first chunk arrived and copy that clock up the
2565
- * agent's own chain (inference → step → generation → agent run), first write
2566
- * wins at each level. Time to first chunk is derived later as
2567
- * `completionStartTime - startTime` on whichever span is being read.
2575
+ * Record when the provider's first chunk arrived. Time to first token is measured
2576
+ * once here, from the start of the inference that streamed the chunk; the inference
2577
+ * reports it when it ends, and the number is copied up the agent's own chain
2578
+ * (step → generation → agent run) with the chunk's clock, first write wins at each
2579
+ * level. Time to first chunk is `completionStartTime - startTime` on whichever span
2580
+ * is being read.
2568
2581
  */
2569
2582
  #captureCompletionStartTime() {
2570
2583
  this.#ensureStepAndInference();
2571
2584
  if (this.#inferenceCompletionStartTime) return;
2572
2585
  const now = /* @__PURE__ */ new Date();
2586
+ const inference = this.#currentInferenceSpan;
2587
+ const timeToFirstTokenMs = inference ? now.getTime() - inference.startTime.getTime() : void 0;
2573
2588
  this.#inferenceCompletionStartTime = now;
2589
+ this.#inferenceTimeToFirstTokenMs = timeToFirstTokenMs;
2574
2590
  this.#completionStartTime ??= now;
2591
+ this.#timeToFirstTokenMs ??= timeToFirstTokenMs;
2575
2592
  for (const span of [
2576
2593
  this.#currentStepSpan,
2577
2594
  this.#modelSpan,
2578
2595
  this.#agentRunSpan
2579
- ]) if (span && !span.attributes?.completionStartTime) span.update({ attributes: { completionStartTime: now } });
2596
+ ]) {
2597
+ if (!span || span.attributes?.completionStartTime) continue;
2598
+ span.update({ attributes: {
2599
+ completionStartTime: now,
2600
+ ...timeToFirstTokenMs !== void 0 ? { timeToFirstTokenMs } : {}
2601
+ } });
2602
+ }
2603
+ }
2604
+ /** What the current inference streamed so far, for whichever way it ends. */
2605
+ #inferenceStreamAttributes() {
2606
+ return {
2607
+ ...this.#inferenceCompletionStartTime ? { completionStartTime: this.#inferenceCompletionStartTime } : {},
2608
+ ...this.#inferenceTimeToFirstTokenMs !== void 0 ? { timeToFirstTokenMs: this.#inferenceTimeToFirstTokenMs } : {}
2609
+ };
2580
2610
  }
2581
2611
  /**
2582
2612
  * Get the tracing context for creating child spans.
@@ -2591,21 +2621,33 @@ var ModelSpanTracker = class {
2591
2621
  * doesn't leave them dangling. No-op if they were already closed.
2592
2622
  */
2593
2623
  reportGenerationError(options) {
2624
+ this.#endChunkSpan();
2625
+ const termination = options.outcome != null ? {
2626
+ outcome: options.outcome,
2627
+ stopReason: options.stopReason,
2628
+ stopReasonContext: options.stopReasonContext
2629
+ } : _mastra_core_observability.executionFailureTermination?.(options.error);
2594
2630
  if (this.#currentInferenceSpan) {
2595
2631
  this.#currentInferenceSpan.error({
2596
2632
  error: options.error,
2597
- endSpan: true
2633
+ ...termination ? interruptedByRun(termination) : {},
2634
+ endSpan: true,
2635
+ attributes: this.#inferenceStreamAttributes()
2598
2636
  });
2599
2637
  this.#currentInferenceSpan = void 0;
2600
2638
  }
2601
2639
  if (this.#currentStepSpan) {
2602
2640
  this.#currentStepSpan.error({
2603
2641
  error: options.error,
2642
+ ...termination ? interruptedByRun(termination) : {},
2604
2643
  endSpan: true
2605
2644
  });
2606
2645
  this.#currentStepSpan = void 0;
2607
2646
  }
2608
- this.#modelSpan?.error(options);
2647
+ this.#modelSpan?.error({
2648
+ ...options,
2649
+ ...termination
2650
+ });
2609
2651
  }
2610
2652
  /**
2611
2653
  * End the generation span with optional raw usage data.
@@ -2620,16 +2662,37 @@ var ModelSpanTracker = class {
2620
2662
  }, model);
2621
2663
  if (providerCostContext && !spanOptions.attributes) spanOptions.attributes = {};
2622
2664
  if (spanOptions.attributes) {
2623
- spanOptions.attributes.completionStartTime = this.#completionStartTime ?? spanOptions.attributes.completionStartTime;
2665
+ if (this.#completionStartTime) {
2666
+ spanOptions.attributes.completionStartTime = this.#completionStartTime;
2667
+ spanOptions.attributes.timeToFirstTokenMs = this.#timeToFirstTokenMs;
2668
+ }
2624
2669
  spanOptions.attributes.usage = extractUsageMetrics(usage, providerMetadata);
2625
2670
  spanOptions.attributes.costContext = providerCostContext ?? spanOptions.attributes.costContext;
2626
2671
  if (!spanOptions.attributes.costContext) spanOptions.attributes.costContext = getGatewayCostContext({ stepProviderMetadata });
2627
2672
  }
2673
+ if (spanOptions.outcome === "abort" || spanOptions.stopReason === "timeout") {
2674
+ const termination = {
2675
+ outcome: spanOptions.outcome,
2676
+ stopReason: spanOptions.stopReason,
2677
+ stopReasonContext: spanOptions.stopReasonContext
2678
+ };
2679
+ this.#endChunkSpan();
2680
+ this.#currentStepSpan?.end({
2681
+ ...interruptedByRun(termination),
2682
+ endTree: true
2683
+ });
2684
+ this.#currentStepSpan = void 0;
2685
+ this.#currentInferenceSpan = void 0;
2686
+ this.#currentStepInputIsFinal = false;
2687
+ }
2628
2688
  this.#modelSpan?.end(spanOptions);
2629
2689
  }
2630
2690
  getCompletionStartTime() {
2631
2691
  return this.#completionStartTime;
2632
2692
  }
2693
+ getTimeToFirstTokenMs() {
2694
+ return this.#timeToFirstTokenMs;
2695
+ }
2633
2696
  /**
2634
2697
  * Update the generation span
2635
2698
  */
@@ -2683,10 +2746,12 @@ var ModelSpanTracker = class {
2683
2746
  */
2684
2747
  startStep(payload) {
2685
2748
  if (this.#currentStepSpan) return;
2749
+ this.#stepHasInference = false;
2686
2750
  const input = extractStepInput(payload);
2687
2751
  this.#currentStepSpan = this.#modelSpan?.createChildSpan({
2688
2752
  name: `step: ${this.#stepIndex}`,
2689
2753
  type: _mastra_core_observability.SpanType.MODEL_STEP,
2754
+ ...typeof _mastra_core_observability.agentLoopTermination === "function" ? { outcome: null } : {},
2690
2755
  attributes: {
2691
2756
  stepIndex: this.#stepIndex,
2692
2757
  ...payload?.messageId ? { messageId: payload.messageId } : {},
@@ -2698,6 +2763,7 @@ var ModelSpanTracker = class {
2698
2763
  this.#currentStepInputIsFinal = Array.isArray(payload?.inputMessages);
2699
2764
  this.#chunkSequence = 0;
2700
2765
  this.#inferenceCompletionStartTime = void 0;
2766
+ this.#inferenceTimeToFirstTokenMs = void 0;
2701
2767
  }
2702
2768
  /**
2703
2769
  * End the current MODEL_INFERENCE span when the provider stream finishes.
@@ -2707,22 +2773,31 @@ var ModelSpanTracker = class {
2707
2773
  *
2708
2774
  * Safe to call multiple times - no-ops if the span is already closed.
2709
2775
  */
2710
- #endInferenceSpan(payload) {
2776
+ #endInferenceSpan(payload, captureOutput = true) {
2711
2777
  if (!this.#currentInferenceSpan) return;
2712
2778
  const { usage: rawUsage, ...otherOutput } = payload.output;
2713
2779
  const usage = extractUsageMetrics(rawUsage, payload.metadata?.providerMetadata);
2714
2780
  const responseModel = typeof payload.metadata?.modelId === "string" ? payload.metadata.modelId : void 0;
2715
2781
  this.#currentInferenceSpan.end({
2716
- output: otherOutput,
2782
+ ..._mastra_core_observability.agentLoopTermination?.({
2783
+ stopCause: "model_finished",
2784
+ finishReason: payload.stepResult.reason
2785
+ }),
2786
+ ...payload.stepResult.reason === "error" ? {
2787
+ outcome: "error",
2788
+ stopReason: "model_error"
2789
+ } : {},
2790
+ ...captureOutput ? { output: otherOutput } : {},
2717
2791
  attributes: {
2718
2792
  usage,
2719
2793
  finishReason: payload.stepResult.reason,
2720
2794
  warnings: payload.stepResult.warnings,
2721
- completionStartTime: this.#inferenceCompletionStartTime,
2795
+ ...this.#inferenceStreamAttributes(),
2722
2796
  ...responseModel?.trim() ? { responseModel } : {}
2723
2797
  }
2724
2798
  });
2725
2799
  this.#currentInferenceSpan = void 0;
2800
+ this.#inferenceCompletionStartTime = void 0;
2726
2801
  }
2727
2802
  /**
2728
2803
  * Open the MODEL_INFERENCE span for the current step. Chunks (including tool-call
@@ -2740,31 +2815,56 @@ var ModelSpanTracker = class {
2740
2815
  * start time.
2741
2816
  *
2742
2817
  * `payload.modelId` / `payload.provider` name the model the caller is about
2743
- * to invoke and are recorded on MODEL_STEP, also when a fallback attempt
2744
- * reuses the step's open inference span. The safety net never records them:
2745
- * a replayed step-start chunk names the model of the original call.
2818
+ * to invoke and are recorded on MODEL_STEP and the inference. A named call
2819
+ * while an inference is still open is the next attempt of the same step (a
2820
+ * fallback model after a failure): that inference ends as failed and the
2821
+ * attempt gets its own, so its timing and model are its own. The safety net
2822
+ * never records them: a replayed step-start chunk names the model of the
2823
+ * original call.
2746
2824
  */
2747
2825
  startInference(payload) {
2748
- if (payload?.modelId && payload.provider) this.#currentStepSpan?.update({ attributes: {
2749
- model: payload.modelId,
2750
- provider: payload.provider
2751
- } });
2826
+ if (payload?.modelId && payload.provider) {
2827
+ this.#currentStepSpan?.update({ attributes: {
2828
+ model: payload.modelId,
2829
+ provider: payload.provider
2830
+ } });
2831
+ this.failInference(/* @__PURE__ */ new Error("Model call ended without a result before the next attempt started"));
2832
+ }
2752
2833
  this.#openInference(payload);
2753
2834
  }
2835
+ /** End the current attempt only; generation and agent spans belong to their owners. */
2836
+ endAttempt(termination) {
2837
+ termination = interruptedByRun(termination);
2838
+ const payload = this.#pendingStepFinishPayload;
2839
+ if (payload) this.#endStepSpan(payload, termination);
2840
+ else {
2841
+ this.#endChunkSpan();
2842
+ this.#currentInferenceSpan?.end(termination);
2843
+ this.#currentStepSpan?.end(termination);
2844
+ this.#currentStepSpan = void 0;
2845
+ this.#currentInferenceSpan = void 0;
2846
+ }
2847
+ this.#pendingStepFinishPayload = void 0;
2848
+ this.#inferenceCompletionStartTime = void 0;
2849
+ this.#currentStepInputIsFinal = false;
2850
+ }
2754
2851
  #openInference(payload) {
2755
2852
  if (!supportsModelInference()) return;
2756
2853
  if (!this.#currentStepSpan || this.#currentInferenceSpan) return;
2757
2854
  this.#inferenceCompletionStartTime = void 0;
2855
+ this.#inferenceTimeToFirstTokenMs = void 0;
2758
2856
  const input = extractStepInput(payload);
2759
2857
  const generationAttrs = this.#modelSpan?.attributes;
2760
2858
  const ctx = this.#inferenceContext;
2859
+ this.#stepHasInference = true;
2761
2860
  this.#currentInferenceSpan = this.#currentStepSpan.createChildSpan({
2762
2861
  name: `inference: ${this.#stepIndex}`,
2763
2862
  type: _mastra_core_observability.SpanType.MODEL_INFERENCE,
2863
+ ...typeof _mastra_core_observability.agentLoopTermination === "function" ? { outcome: null } : {},
2764
2864
  attributes: {
2765
2865
  stepIndex: this.#stepIndex,
2766
- model: generationAttrs?.model,
2767
- provider: generationAttrs?.provider,
2866
+ model: payload?.modelId ?? generationAttrs?.model,
2867
+ provider: payload?.provider ?? generationAttrs?.provider,
2768
2868
  streaming: generationAttrs?.streaming,
2769
2869
  ...ctx?.parameters !== void 0 ? { parameters: ctx.parameters } : {},
2770
2870
  ...ctx?.providerOptions !== void 0 ? { providerOptions: ctx.providerOptions } : {},
@@ -2781,11 +2881,37 @@ var ModelSpanTracker = class {
2781
2881
  * End the open MODEL_INFERENCE span as interrupted while keeping MODEL_STEP open,
2782
2882
  * so a replacement request for the same step gets its own inference span.
2783
2883
  */
2784
- interruptInference() {
2884
+ interruptInference(termination) {
2885
+ this.#endChunkSpan();
2886
+ if (!this.#currentInferenceSpan) return;
2887
+ this.#currentInferenceSpan.end({
2888
+ attributes: {
2889
+ finishReason: "interrupted",
2890
+ ...this.#inferenceStreamAttributes()
2891
+ },
2892
+ ...termination ?? (typeof _mastra_core_observability.agentLoopTermination === "function" ? {
2893
+ outcome: "interrupted",
2894
+ stopReason: null,
2895
+ stopReasonContext: null
2896
+ } : {})
2897
+ });
2898
+ this.#currentInferenceSpan = void 0;
2899
+ this.#inferenceCompletionStartTime = void 0;
2900
+ this.#inferenceTimeToFirstTokenMs = void 0;
2901
+ }
2902
+ /** End the open inference span, if any, with `error` and what it streamed. */
2903
+ failInference(error) {
2785
2904
  this.#endChunkSpan();
2786
2905
  if (!this.#currentInferenceSpan) return;
2787
- this.#currentInferenceSpan.end({ attributes: { finishReason: "interrupted" } });
2906
+ this.#currentInferenceSpan.error({
2907
+ error,
2908
+ ..._mastra_core_observability.executionFailureTermination?.(error, "model_error"),
2909
+ endSpan: true,
2910
+ attributes: this.#inferenceStreamAttributes()
2911
+ });
2912
+ this.#inferenceTimeToFirstTokenMs = void 0;
2788
2913
  this.#currentInferenceSpan = void 0;
2914
+ this.#inferenceCompletionStartTime = void 0;
2789
2915
  }
2790
2916
  /**
2791
2917
  * Update the current step span with additional payload data.
@@ -2807,10 +2933,11 @@ var ModelSpanTracker = class {
2807
2933
  /**
2808
2934
  * End the current Model execution step with token usage, finish reason, output, and metadata
2809
2935
  */
2810
- #endStepSpan(payload) {
2936
+ #endStepSpan(payload, termination) {
2811
2937
  this.#endChunkSpan();
2812
2938
  if (!this.#currentStepSpan) return;
2813
- const { usage: rawUsage, ...otherOutput } = payload.output;
2939
+ const output = payload.output;
2940
+ const { usage: rawUsage, ...otherOutput } = output;
2814
2941
  const stepResult = payload.stepResult;
2815
2942
  const metadata = payload.metadata;
2816
2943
  const usage = extractUsageMetrics(rawUsage, metadata?.providerMetadata);
@@ -2826,6 +2953,11 @@ var ModelSpanTracker = class {
2826
2953
  ]) delete cleanMetadata[key];
2827
2954
  this.#endInferenceSpan(payload);
2828
2955
  this.#currentStepSpan.end({
2956
+ ...termination ?? _mastra_core_observability.agentLoopTermination?.({
2957
+ stopCause: "model_finished",
2958
+ finishReason: stepResult.reason,
2959
+ tripwire: output.steps?.at(-1)?.tripwire
2960
+ }),
2829
2961
  output: otherOutput,
2830
2962
  attributes: {
2831
2963
  usage,
@@ -2857,26 +2989,23 @@ var ModelSpanTracker = class {
2857
2989
  warnings: []
2858
2990
  },
2859
2991
  metadata: {}
2860
- });
2992
+ }, _mastra_core_observability.agentLoopTermination?.({
2993
+ finishReason: "suspended",
2994
+ suspension: {
2995
+ approvalRequired: chunk.type === "tool-call-approval",
2996
+ toolCallId: chunk.payload.toolCallId,
2997
+ toolName: chunk.payload.toolName
2998
+ }
2999
+ }));
2861
3000
  }
2862
- /**
2863
- * Returns the parent span for chunks. Chunks parent under MODEL_INFERENCE
2864
- * (the provider call) when available, falling back to MODEL_STEP only if
2865
- * startStep() was bypassed.
2866
- */
3001
+ /** Processed content belongs to the logical step, independently of provider timing. */
2867
3002
  #chunkParent() {
2868
- return this.#currentInferenceSpan ?? this.#currentStepSpan;
3003
+ return this.#currentStepSpan?.endTime ? void 0 : this.#currentStepSpan;
2869
3004
  }
2870
- /**
2871
- * Safety-net invoked from chunk handlers: auto-create MODEL_STEP and
2872
- * MODEL_INFERENCE if a chunk arrives before the loop has explicitly opened
2873
- * them, so chunks parent under MODEL_INFERENCE rather than falling through
2874
- * to MODEL_STEP. Idempotent — each public start* method is itself a no-op
2875
- * when its span is already live.
2876
- */
3005
+ /** Ensure a logical step and provider timing span exist for legacy stream callers. */
2877
3006
  #ensureStepAndInference() {
2878
3007
  if (!this.#currentStepSpan) this.startStep();
2879
- if (!this.#currentInferenceSpan) this.#openInference();
3008
+ if (!this.#currentInferenceSpan && !this.#stepHasInference) this.#openInference();
2880
3009
  }
2881
3010
  /**
2882
3011
  * Create a new chunk span (for multi-part chunks like text-start/delta/end)
@@ -3037,20 +3166,39 @@ var ModelSpanTracker = class {
3037
3166
  tracingPolicy: this.#modelSpan?.tracingPolicy
3038
3167
  })) this.#chunkSequence++;
3039
3168
  }
3169
+ /** Track provider timing and completion without recording unprocessed content. */
3170
+ wrapInferenceStream(stream) {
3171
+ this.#tracksProviderStream = true;
3172
+ return this.wrapStream(stream, true);
3173
+ }
3040
3174
  /**
3041
3175
  * Wraps a stream with model tracing transform to track MODEL_STEP and MODEL_CHUNK spans.
3042
3176
  *
3043
3177
  * This should be added to the stream pipeline to automatically
3044
3178
  * create MODEL_STEP and MODEL_CHUNK spans for each semantic unit in the stream.
3045
3179
  */
3046
- wrapStream(stream) {
3180
+ wrapStream(stream, inferenceOnly = false) {
3047
3181
  return stream.pipeThrough(new stream_web.TransformStream({ transform: (chunk, controller) => {
3048
- if ((this.#inModelResponse || this.#deferStepClose) && MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3182
+ if (inferenceOnly) {
3183
+ try {
3184
+ if (MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3185
+ if (chunk.type === "finish") this.#endInferenceSpan(chunk.payload, false);
3186
+ } catch {}
3187
+ controller.enqueue(chunk);
3188
+ return;
3189
+ }
3190
+ this.recordChunk(chunk);
3191
+ controller.enqueue(chunk);
3192
+ } }));
3193
+ }
3194
+ /** @internal Record a chunk only after output processors accept it. */
3195
+ recordChunk(chunk) {
3196
+ try {
3197
+ if (!this.#tracksProviderStream && (this.#inModelResponse || this.#deferStepClose) && MODEL_RESPONSE_CHUNK_TYPES.has(chunk.type)) this.#captureCompletionStartTime();
3049
3198
  if (chunk.type === "tool-call-suspended" || chunk.type === "tool-call-approval") {
3050
3199
  if (chunk.type === "tool-call-approval") this.#handleToolApprovalChunk(chunk);
3051
3200
  this.#endSuspendedStep(chunk);
3052
3201
  }
3053
- controller.enqueue(chunk);
3054
3202
  switch (chunk.type) {
3055
3203
  case "text-start":
3056
3204
  case "text-delta":
@@ -3076,7 +3224,7 @@ var ModelSpanTracker = class {
3076
3224
  this.#inModelResponse = true;
3077
3225
  if (this.#currentStepSpan) this.updateStep(chunk.payload);
3078
3226
  else this.startStep(chunk.payload);
3079
- if (!this.#currentInferenceSpan) this.#openInference(chunk.payload);
3227
+ if (!this.#currentInferenceSpan && !this.#stepHasInference) this.#openInference(chunk.payload);
3080
3228
  break;
3081
3229
  case "step-finish":
3082
3230
  this.#inModelResponse = false;
@@ -3086,15 +3234,20 @@ var ModelSpanTracker = class {
3086
3234
  this.#endInferenceSpan(chunk.payload);
3087
3235
  } else this.#endStepSpan(chunk.payload);
3088
3236
  break;
3237
+ case "tripwire": {
3238
+ const termination = _mastra_core_observability.agentLoopTermination?.({ tripwire: chunk.payload });
3239
+ if (!termination) break;
3240
+ this.endAttempt(termination);
3241
+ break;
3242
+ }
3243
+ case "finish": break;
3089
3244
  case "raw":
3090
3245
  case "start":
3091
- case "finish":
3092
3246
  case "response-metadata":
3093
3247
  case "source":
3094
3248
  case "file":
3095
3249
  case "error":
3096
3250
  case "abort":
3097
- case "tripwire":
3098
3251
  case "watch":
3099
3252
  case "tool-error":
3100
3253
  case "reasoning-signature":
@@ -3119,7 +3272,7 @@ var ModelSpanTracker = class {
3119
3272
  break;
3120
3273
  }
3121
3274
  }
3122
- } }));
3275
+ } catch {}
3123
3276
  }
3124
3277
  };
3125
3278
  // src/spans/metadata.ts
@@ -3308,6 +3461,9 @@ var BaseSpan = class BaseSpan {
3308
3461
  observabilityInstance;
3309
3462
  input;
3310
3463
  output;
3464
+ outcome;
3465
+ stopReason;
3466
+ stopReasonContext;
3311
3467
  errorInfo;
3312
3468
  metadata;
3313
3469
  requestContext;
@@ -3324,6 +3480,7 @@ var BaseSpan = class BaseSpan {
3324
3480
  parentSpanId;
3325
3481
  /** 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 */
3326
3482
  externalParentSpanId;
3483
+ resumedFromSpanId;
3327
3484
  /** Deep clean options for serialization */
3328
3485
  deepCleanOptions;
3329
3486
  /**
@@ -3351,6 +3508,7 @@ var BaseSpan = class BaseSpan {
3351
3508
  * live `parent`, so its original ancestors are only known from the cached export.
3352
3509
  */
3353
3510
  restoredLineage;
3511
+ restoredIsRootSpan;
3354
3512
  /**
3355
3513
  * Version declared for this span's own entity, or inherited from a parent span
3356
3514
  * of the same entity. Unlike `metadata.entityVersionId`, never an ancestor's
@@ -3371,6 +3529,7 @@ var BaseSpan = class BaseSpan {
3371
3529
  constructor(options, observabilityInstance) {
3372
3530
  const observabilityConfig = observabilityInstance.getConfig();
3373
3531
  this.deepCleanOptions = mergeSerializationOptions(observabilityConfig.serializationOptions);
3532
+ this.applyOutcome(options);
3374
3533
  this.name = options.name;
3375
3534
  this.type = options.type;
3376
3535
  this.isInternal = isSpanInternal(this.type, options.tracingPolicy?.internal);
@@ -3390,6 +3549,7 @@ var BaseSpan = class BaseSpan {
3390
3549
  }
3391
3550
  this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
3392
3551
  this.links = validLinks(options.links);
3552
+ this.resumedFromSpanId = options.resumedFromSpanId;
3393
3553
  const entityParent = this.getParentSpan(false);
3394
3554
  this.entityType = options.entityType ?? entityParent?.entityType;
3395
3555
  this.entityId = options.entityId ?? entityParent?.entityId;
@@ -3431,14 +3591,49 @@ var BaseSpan = class BaseSpan {
3431
3591
  return value;
3432
3592
  }
3433
3593
  }
3434
- /** End the span and any descendant spans that are still open, applying `options` to each */
3594
+ /** Preserve explicitly supplied native fields; never derive results from payloads/errors. */
3595
+ applyOutcome(options) {
3596
+ if (this.endTime) return;
3597
+ if (options.outcome !== void 0) {
3598
+ this.outcome = options.outcome;
3599
+ this.stopReason = options.stopReason ?? null;
3600
+ this.stopReasonContext = options.stopReasonContext == null ? null : deepClean(options.stopReasonContext, this.deepCleanOptions);
3601
+ return;
3602
+ }
3603
+ if (options.stopReason !== void 0 && options.stopReason !== this.stopReason) this.stopReasonContext = null;
3604
+ if (options.stopReason !== void 0) this.stopReason = options.stopReason;
3605
+ if (options.stopReasonContext !== void 0) this.stopReasonContext = options.stopReasonContext == null ? null : deepClean(options.stopReasonContext, this.deepCleanOptions);
3606
+ }
3607
+ /** Forward options to open descendants, with interruption outcomes for cascading termination. */
3435
3608
  endTree(options) {
3436
- if (this.#openChildren) for (const child of [...this.#openChildren]) child.endTree(options);
3609
+ this.endOpenDescendants(options, true);
3437
3610
  this.end(options);
3438
3611
  }
3439
- /** Close any still-open descendant spans without applying options to them */
3440
- endOpenDescendants() {
3441
- if (this.#openChildren) for (const child of [...this.#openChildren]) child.endTree();
3612
+ /** Preserve completed children; only known cascading termination interrupts open work. */
3613
+ endOpenDescendants(options, forwardOptions = false) {
3614
+ const outcome = options?.outcome !== void 0 ? options.outcome : this.outcome;
3615
+ const stopReason = options?.stopReason !== void 0 ? options.stopReason : this.stopReason;
3616
+ const stopReasonContext = options?.stopReasonContext !== void 0 ? options.stopReasonContext : this.stopReasonContext;
3617
+ const interrupts = outcome === "abort" || outcome === "interrupted" || outcome === "limit" && stopReason === "timeout";
3618
+ const forwarded = forwardOptions ? { ...options } : {};
3619
+ delete forwarded.outcome;
3620
+ delete forwarded.stopReason;
3621
+ delete forwarded.stopReasonContext;
3622
+ if (this.#openChildren) for (const child of [...this.#openChildren]) {
3623
+ const childOptions = {
3624
+ ...forwarded,
3625
+ ...interrupts ? {
3626
+ outcome: "interrupted",
3627
+ stopReason,
3628
+ stopReasonContext
3629
+ } : {}
3630
+ };
3631
+ if (forwardOptions) child.endTree(childOptions);
3632
+ else child.end({
3633
+ ...childOptions,
3634
+ endTree: true
3635
+ });
3636
+ }
3442
3637
  }
3443
3638
  /**
3444
3639
  * Release this span from its parent's open-child set once it has ended.
@@ -3489,7 +3684,7 @@ var BaseSpan = class BaseSpan {
3489
3684
  }
3490
3685
  /** Returns `TRUE` if the span is the root span of a trace */
3491
3686
  get isRootSpan() {
3492
- return !this.parent;
3687
+ return this.restoredIsRootSpan ?? !this.parent;
3493
3688
  }
3494
3689
  /**
3495
3690
  * Get the closest parent span.
@@ -3553,6 +3748,7 @@ var BaseSpan = class BaseSpan {
3553
3748
  */
3554
3749
  restoreEntityLineage(cached) {
3555
3750
  this.ownEntityVersionId = cached.ownEntityVersionId ?? void 0;
3751
+ this.restoredIsRootSpan = cached.isRootSpan;
3556
3752
  const lineage = definedLineage(cached);
3557
3753
  if (Object.keys(lineage).length > 0) {
3558
3754
  this.restoredLineage = lineage;
@@ -3635,6 +3831,7 @@ var BaseSpan = class BaseSpan {
3635
3831
  const nestedUnderParent = this.traceState?.nestedUnderParent ?? false;
3636
3832
  const links = validLinks(this.links);
3637
3833
  const correlation = this.getCorrelationContext();
3834
+ const timeToFirstTokenMs = this.attributes?.timeToFirstTokenMs;
3638
3835
  return {
3639
3836
  id: this.id,
3640
3837
  traceId: this.traceId,
@@ -3650,6 +3847,9 @@ var BaseSpan = class BaseSpan {
3650
3847
  input: hideInput ? void 0 : this.input,
3651
3848
  output: hideOutput ? void 0 : this.output,
3652
3849
  errorInfo: this.errorInfo,
3850
+ ...this.outcome !== void 0 ? { outcome: this.outcome } : {},
3851
+ ...this.stopReason !== void 0 ? { stopReason: this.stopReason } : {},
3852
+ ...this.stopReasonContext !== void 0 ? { stopReasonContext: this.stopReasonContext } : {},
3653
3853
  requestContext: this.requestContext,
3654
3854
  isEvent: this.isEvent,
3655
3855
  isRootSpan: this.isRootSpan,
@@ -3658,6 +3858,8 @@ var BaseSpan = class BaseSpan {
3658
3858
  ...definedLineage(correlation),
3659
3859
  parentSpanId: this.getParentSpanId(includeInternalSpans),
3660
3860
  externalParentSpanId: this.getExportedExternalParentSpanId(includeInternalSpans),
3861
+ ...this.resumedFromSpanId !== void 0 ? { resumedFromSpanId: this.resumedFromSpanId } : {},
3862
+ ...typeof timeToFirstTokenMs === "number" ? { timeToFirstTokenMs } : {},
3661
3863
  ...nestedUnderParent ? { nestedUnderParent: true } : {},
3662
3864
  ...links ? { links } : {},
3663
3865
  ...this.isRootSpan && !nestedUnderParent && this.tags?.length ? { tags: this.tags } : {}
@@ -3767,7 +3969,9 @@ var DefaultSpan = class extends BaseSpan {
3767
3969
  end(options) {
3768
3970
  if (this.isEvent) return;
3769
3971
  if (this.endTime) return;
3770
- if (options?.endTree) this.endOpenDescendants();
3972
+ if (options?.endTree) this.endOpenDescendants(options);
3973
+ if (options) this.applyOutcome(options);
3974
+ if (this.outcome === null) this.outcome = "unknown";
3771
3975
  this.endTime = /* @__PURE__ */ new Date();
3772
3976
  this.detachFromParent();
3773
3977
  if (options?.metadata) this.metadata = {
@@ -3782,8 +3986,9 @@ var DefaultSpan = class extends BaseSpan {
3782
3986
  };
3783
3987
  }
3784
3988
  error(options) {
3785
- if (this.isEvent) return;
3989
+ if (this.isEvent || this.endTime) return;
3786
3990
  const { error, endSpan = true, endTree, attributes, metadata } = options;
3991
+ this.applyOutcome(options);
3787
3992
  if (metadata) this.metadata = {
3788
3993
  ...this.metadata,
3789
3994
  ...deepClean(this.prepareSpanMetadata(metadata), this.deepCleanOptions)
@@ -3818,6 +4023,7 @@ var DefaultSpan = class extends BaseSpan {
3818
4023
  }
3819
4024
  update(options) {
3820
4025
  if (this.isEvent) return;
4026
+ this.applyOutcome(options);
3821
4027
  if (options.name !== void 0) this.name = options.name;
3822
4028
  if (options.metadata) this.metadata = {
3823
4029
  ...this.metadata,
@@ -3886,7 +4092,7 @@ function isValidSpanId(spanId) {
3886
4092
  function getOrCreateTraceId(options) {
3887
4093
  if (options.traceId) {
3888
4094
  if (isValidTraceId(options.traceId)) return options.traceId;
3889
- else console.error(`[Mastra Tracing] Invalid traceId: must be 1-32 hexadecimal characters, got "${options.traceId}". Generating new trace ID.`);
4095
+ console.error(`[Mastra Tracing] Invalid traceId: must be 1-32 hexadecimal characters, got "${options.traceId}". Generating new trace ID.`);
3890
4096
  }
3891
4097
  return generateTraceId();
3892
4098
  }
@@ -5287,13 +5493,13 @@ var BaseObservabilityInstance = class extends _mastra_core_base.MastraBase {
5287
5493
  ...rest,
5288
5494
  metadata
5289
5495
  }, this);
5290
- } else if (!this.shouldSample(customSamplerOptions)) return new NoOpSpan({
5496
+ } else if (!(options.samplingDecision ?? this.shouldSample(customSamplerOptions))) return new NoOpSpan({
5291
5497
  ...rest,
5292
5498
  metadata
5293
5499
  }, this);
5294
5500
  let traceState;
5295
5501
  if (options.parent) traceState = options.parent.traceState;
5296
- else traceState = this.computeTraceState(tracingOptions);
5502
+ else traceState = options.traceState ?? this.computeTraceState(tracingOptions);
5297
5503
  const tracingMetadata = !options.parent ? tracingOptions?.metadata : void 0;
5298
5504
  const mergedMetadata = mergeMetadata$1(stripUndefined(metadata), stripUndefined(tracingMetadata));
5299
5505
  const enrichedMetadata = this.extractMetadataFromRequestContext(requestContext, mergedMetadata, traceState);
@@ -5348,6 +5554,11 @@ var BaseObservabilityInstance = class extends _mastra_core_base.MastraBase {
5348
5554
  * @returns A span that can have lifecycle methods called on it
5349
5555
  */
5350
5556
  rebuildSpan(cached) {
5557
+ if (cached.id === "no-op" && cached.traceId === "no-op-trace") return new NoOpSpan({
5558
+ name: cached.name,
5559
+ type: cached.type,
5560
+ traceState: cached.traceState
5561
+ }, this);
5351
5562
  const span = this.createSpan({
5352
5563
  name: cached.name,
5353
5564
  type: cached.type,
@@ -5355,6 +5566,10 @@ var BaseObservabilityInstance = class extends _mastra_core_base.MastraBase {
5355
5566
  spanId: cached.id,
5356
5567
  parentSpanId: cached.parentSpanId,
5357
5568
  externalParentSpanId: cached.externalParentSpanId,
5569
+ resumedFromSpanId: cached.resumedFromSpanId,
5570
+ outcome: cached.outcome,
5571
+ stopReason: cached.stopReason,
5572
+ stopReasonContext: cached.stopReasonContext,
5358
5573
  links: cached.links,
5359
5574
  startTime: cached.startTime instanceof Date ? cached.startTime : new Date(cached.startTime),
5360
5575
  input: cached.input,
@@ -5367,6 +5582,11 @@ var BaseObservabilityInstance = class extends _mastra_core_base.MastraBase {
5367
5582
  traceState: this.rebuildTraceState(cached)
5368
5583
  });
5369
5584
  if (span instanceof BaseSpan) span.restoreEntityLineage(cached);
5585
+ if (cached.endTime) {
5586
+ span.endTime = cached.endTime instanceof Date ? cached.endTime : new Date(cached.endTime);
5587
+ span.output = cached.output;
5588
+ span.errorInfo = cached.errorInfo;
5589
+ }
5370
5590
  this.wireSpanLifecycle(span);
5371
5591
  return span;
5372
5592
  }
@@ -9131,7 +9351,11 @@ var TestExporter = class extends BaseExporter {
9131
9351
  if (span.parentSpanId && spanIdMap.has(span.parentSpanId)) normalizedSpan.parentId = spanIdMap.get(span.parentSpanId);
9132
9352
  if (span.entityType) normalizedSpan.entityType = span.entityType;
9133
9353
  if (span.entityId) normalizedSpan.entityId = normalizeValue(span.entityId, "entityId");
9134
- if (span.attributes && Object.keys(span.attributes).length > 0) normalizedSpan.attributes = normalizeValue(span.attributes);
9354
+ if (span.attributes && Object.keys(span.attributes).length > 0) {
9355
+ const attributes = normalizeValue(span.attributes);
9356
+ if (typeof attributes.timeToFirstTokenMs === "number") attributes.timeToFirstTokenMs = "<ms>";
9357
+ normalizedSpan.attributes = attributes;
9358
+ }
9135
9359
  if (span.metadata && Object.keys(span.metadata).length > 0) normalizedSpan.metadata = normalizeValue(span.metadata);
9136
9360
  if (span.input !== void 0) normalizedSpan.input = normalizeValue(span.input);
9137
9361
  if (span.output !== void 0) normalizedSpan.output = normalizeValue(span.output);
@@ -10023,6 +10247,7 @@ var SensitiveDataFilter = class {
10023
10247
  span.input = this.tryFilter(span.input, indexedState);
10024
10248
  span.output = this.tryFilter(span.output, indexedState);
10025
10249
  span.errorInfo = this.tryFilter(span.errorInfo, indexedState);
10250
+ span.stopReasonContext = this.tryFilter(span.stopReasonContext, indexedState);
10026
10251
  span.requestContext = this.tryFilter(span.requestContext, indexedState);
10027
10252
  return span;
10028
10253
  }