< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Diagnostics.StageTermination
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/StageTermination.cs
Line coverage
100%
Covered lines: 9
Uncovered lines: 0
Coverable lines: 9
Total lines: 179
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_Limit()100%11100%
get_Limit()100%11100%
get_Threshold()100%11100%
get_ConsecutiveThreshold()100%11100%
get_Exception()100%11100%
get_Reason()100%11100%
get_Reason()100%11100%
ToTagValue()100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/StageTermination.cs

#LineLine coverage
 1using System.Text.Json.Serialization;
 2
 3namespace NexusLabs.Needlr.AgentFramework.Diagnostics;
 4
 5/// <summary>
 6/// Describes why a pipeline stage terminated. Framework cases carry structured
 7/// metadata (e.g. <see cref="MaxIterationsReached"/> knows the limit and the iterations
 8/// used); application narrative goes through <see cref="Custom"/>.
 9/// </summary>
 10/// <remarks>
 11/// <para>
 12/// Use pattern matching to inspect the termination at a stage:
 13/// </para>
 14/// <code>
 15/// if (stage.Termination is StageTermination.MaxToolCallsReached { Limit: var limit })
 16/// {
 17///     _logger.LogWarning("Stage exceeded {Limit} tool calls", limit);
 18/// }
 19/// </code>
 20/// <para>
 21/// For OpenTelemetry / Prometheus tag values, use <see cref="ToTagValue"/> — it
 22/// returns a stable, low-cardinality string suitable for use as a metric dimension.
 23/// Every case — including <see cref="Custom"/> — returns its case name (e.g.
 24/// <c>"MaxIterationsReached"</c>, <c>"Custom"</c>), so the tag stays bounded by the
 25/// case enumeration no matter what free-form text a <see cref="Custom.Reason"/>
 26/// carries. The reason is preserved on the record and in JSON for diagnostics — it
 27/// is deliberately not used as a metric/span tag.
 28/// </para>
 29/// <para>
 30/// JSON serialization is supported via <see cref="JsonPolymorphicAttribute"/> with
 31/// the <c>$kind</c> discriminator. Each derived type is registered with its case name
 32/// (e.g. <c>"MaxIterationsReached"</c>) as the discriminator value. Once shipped,
 33/// the JSON wire format is part of the API surface — renaming or removing a case
 34/// breaks the wire format.
 35/// </para>
 36/// <para>
 37/// Adding a new framework case in a future release is a soft break for consumers
 38/// using exhaustive switch <em>expressions</em> — they will need to add a default
 39/// arm. Switch statements with no return value continue to work without modification.
 40/// </para>
 41/// </remarks>
 42[JsonPolymorphic(TypeDiscriminatorPropertyName = "$kind")]
 43[JsonDerivedType(typeof(Completed), nameof(Completed))]
 44[JsonDerivedType(typeof(NaturalCompletion), nameof(NaturalCompletion))]
 45[JsonDerivedType(typeof(CompletedEarlyAfterToolCall), nameof(CompletedEarlyAfterToolCall))]
 46[JsonDerivedType(typeof(MaxIterationsReached), nameof(MaxIterationsReached))]
 47[JsonDerivedType(typeof(MaxToolCallsReached), nameof(MaxToolCallsReached))]
 48[JsonDerivedType(typeof(BudgetPressure), nameof(BudgetPressure))]
 49[JsonDerivedType(typeof(StallDetected), nameof(StallDetected))]
 50[JsonDerivedType(typeof(Cancelled), nameof(Cancelled))]
 51[JsonDerivedType(typeof(Failed), nameof(Failed))]
 52[JsonDerivedType(typeof(Skipped), nameof(Skipped))]
 53[JsonDerivedType(typeof(Custom), nameof(Custom))]
 54public abstract record StageTermination : IStageTermination
 55{
 56    /// <summary>
 57    /// Internal constructor closes the hierarchy for external derivation. Consumers
 58    /// who need a typed extension case implement <see cref="IStageTermination"/>
 59    /// directly rather than inheriting from this abstract record (see the interface's
 60    /// XML doc for the contract). The framework's nested case records still chain
 61    /// through this constructor because they live in the same assembly.
 62    /// </summary>
 42463    internal StageTermination() { }
 64
 65    /// <summary>
 66    /// The stage's <see cref="Iterative.IterativeLoopOptions.IsComplete"/> predicate
 67    /// returned <see langword="true"/> after an iteration — the stage achieved its goal.
 68    /// </summary>
 69    public sealed record Completed : StageTermination;
 70
 71    /// <summary>
 72    /// The model produced a text response without requesting tool calls, signalling
 73    /// natural completion of the task.
 74    /// </summary>
 75    public sealed record NaturalCompletion : StageTermination;
 76
 77    /// <summary>
 78    /// The <see cref="Iterative.IterativeLoopOptions.IsComplete"/> predicate returned
 79    /// <see langword="true"/> after a tool call within an iteration. This is a success
 80    /// termination — the stage achieved its goal and the loop exited early to avoid a
 81    /// wasted chat completion call.
 82    /// </summary>
 83    public sealed record CompletedEarlyAfterToolCall : StageTermination;
 84
 85    /// <summary>
 86    /// The loop exhausted its configured iteration limit without the
 87    /// <see cref="Iterative.IterativeLoopOptions.IsComplete"/> predicate returning
 88    /// <see langword="true"/>.
 89    /// </summary>
 90    /// <param name="Limit">The configured iteration limit.</param>
 91    /// <param name="IterationsUsed">How many iterations actually ran.</param>
 4292    public sealed record MaxIterationsReached(int Limit, int IterationsUsed) : StageTermination;
 93
 94    /// <summary>
 95    /// The cumulative tool-call count across all iterations exceeded the configured
 96    /// <see cref="Iterative.IterativeLoopOptions.MaxTotalToolCalls"/> limit.
 97    /// </summary>
 98    /// <param name="Limit">The configured cumulative tool-call limit.</param>
 99    /// <param name="ToolCallsUsed">How many tool calls actually ran across all iterations.</param>
 22100    public sealed record MaxToolCallsReached(int Limit, int ToolCallsUsed) : StageTermination;
 101
 102    /// <summary>
 103    /// The token budget tracker reported usage above the configured budget pressure
 104    /// threshold, and the loop ran one final finalization iteration before terminating.
 105    /// </summary>
 106    /// <param name="Threshold">
 107    /// The configured budget pressure threshold (0.0–1.0), or <see langword="null"/>
 108    /// if no threshold was configured but pressure was reported through other means.
 109    /// </param>
 22110    public sealed record BudgetPressure(double? Threshold) : StageTermination;
 111
 112    /// <summary>
 113    /// The loop detected that consecutive iterations produced nearly identical token
 114    /// usage, indicating the LLM is repeating the same work without making progress.
 115    /// </summary>
 116    /// <param name="ConsecutiveThreshold">
 117    /// The number of consecutive stalled iterations the loop required before terminating,
 118    /// or <see langword="null"/> if the configuration was not snapshot at termination time.
 119    /// </param>
 23120    public sealed record StallDetected(int? ConsecutiveThreshold) : StageTermination;
 121
 122    /// <summary>
 123    /// The loop was cancelled via <see cref="System.Threading.CancellationToken"/>.
 124    /// </summary>
 125    public sealed record Cancelled : StageTermination;
 126
 127    /// <summary>
 128    /// The stage threw an exception, OR the iterative loop reported an unrecoverable
 129    /// error (in which case the loop's error message is wrapped in an
 130    /// <see cref="System.InvalidOperationException"/> here).
 131    /// </summary>
 132    /// <param name="Exception">The exception that caused the failure.</param>
 46133    public sealed record Failed(Exception Exception) : StageTermination;
 134
 135    /// <summary>
 136    /// The stage was skipped via a <c>ShouldSkip</c> predicate on
 137    /// <c>StageExecutionPolicy</c>. <see cref="Reason"/> is optional because the
 138    /// current predicate signature does not supply one.
 139    /// </summary>
 140    /// <param name="Reason">
 141    /// Optional free-form description of why the stage was skipped. <see langword="null"/>
 142    /// when the runner has no detail to supply.
 143    /// </param>
 28144    public sealed record Skipped(string? Reason = null) : StageTermination;
 145
 146    /// <summary>
 147    /// Application-specific termination cause not covered by framework cases. Use this
 148    /// from an <c>onLoopCompleted</c> callback when a stage has app-specific semantics
 149    /// (e.g. <c>"Reconciled — 7 outstanding issues"</c>).
 150    /// </summary>
 151    /// <remarks>
 152    /// <see cref="ToTagValue"/> is intentionally not overridden: it returns the bounded
 153    /// discriminator <c>"Custom"</c> (the inherited default) so a run-specific
 154    /// <see cref="Reason"/> cannot explode the cardinality of the <c>termination_cause</c>
 155    /// metric/span tag. The full reason is preserved on the record, round-trips through
 156    /// JSON, and is reachable via <see cref="Properties"/> — read it there for
 157    /// diagnostics rather than from the tag.
 158    /// </remarks>
 159    /// <param name="Reason">
 160    /// Human-readable description of the termination, kept for diagnostics. It round-trips
 161    /// through JSON and is available via pattern matching; it is deliberately not used as
 162    /// a metric/span tag value, so it is safe to include run-specific detail here.
 163    /// </param>
 164    /// <param name="Properties">
 165    /// Optional structured metadata for richer post-mortem queries. Values typed as
 166    /// <see cref="object"/> for flexibility — note that JSON deserialization yields
 167    /// <see cref="System.Text.Json.JsonElement"/> values, not the original concrete types.
 168    /// </param>
 31169    public sealed record Custom(string Reason, IReadOnlyDictionary<string, object?>? Properties = null) : StageTerminati
 170
 171    /// <summary>
 172    /// Returns a stable, low-cardinality string suitable for OpenTelemetry tag values.
 173    /// Returns the case name (e.g. <c>"MaxIterationsReached"</c>, <c>"Custom"</c>) for
 174    /// every case, so the value is bounded by the case enumeration. Free-form detail on
 175    /// <see cref="Custom"/> lives in <see cref="Custom.Reason"/> /
 176    /// <see cref="Custom.Properties"/>, not in this tag value.
 177    /// </summary>
 26178    public virtual string ToTagValue() => GetType().Name;
 179}