< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Workflows.Sequential.IterativeLoopStageExecutor
Assembly: NexusLabs.Needlr.AgentFramework.Workflows
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Sequential/IterativeLoopStageExecutor.cs
Line coverage
100%
Covered lines: 37
Uncovered lines: 0
Coverable lines: 37
Total lines: 172
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()100%1616100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Sequential/IterativeLoopStageExecutor.cs

#LineLine coverage
 1using NexusLabs.Needlr.AgentFramework.Iterative;
 2
 3namespace NexusLabs.Needlr.AgentFramework.Workflows.Sequential;
 4
 5/// <summary>
 6/// Executes a pipeline stage by running an <see cref="IIterativeAgentLoop"/> with
 7/// dynamically constructed options and context.
 8/// </summary>
 9/// <remarks>
 10/// <para>
 11/// This executor bridges the workspace-driven iterative loop pattern into the
 12/// <see cref="IStageExecutor"/> contract used by <see cref="SequentialPipelineRunner"/>.
 13/// Unlike <see cref="AgentStageExecutor"/> (which wraps a single-pass
 14/// <c>AIAgent.RunAsync</c> call), this executor runs a multi-iteration loop where each
 15/// iteration builds a fresh prompt from workspace state, maintaining O(n) token cost.
 16/// </para>
 17/// <para>
 18/// All termination paths use result-based signaling — the executor never throws
 19/// exceptions for loop-level failures. This means exception-driven decorators like
 20/// <see cref="ContinueOnFailureExecutor"/> and <see cref="FallbackExecutor"/> do not
 21/// intercept loop termination results. For advisory behavior, set
 22/// <c>failureDisposition</c> to <see cref="FailureDisposition.ContinueAdvisory"/>.
 23/// For timeout enforcement, <see cref="TimeoutExecutor"/> still works because the loop
 24/// observes the linked <see cref="CancellationToken"/> and terminates cooperatively.
 25/// </para>
 26/// <para>
 27/// The <c>onLoopCompleted</c> callback fires immediately after the loop returns,
 28/// before result mapping. Use it to capture loop-specific metadata (termination reason,
 29/// per-iteration diagnostics, tool call counts) that is not surfaced on
 30/// <see cref="StageExecutionResult"/>. This is the only point where the raw
 31/// <see cref="IterativeLoopResult"/> is accessible.
 32/// </para>
 33/// </remarks>
 34/// <example>
 35/// <code>
 36/// // Basic usage
 37/// var executor = new IterativeLoopStageExecutor(
 38///     iterativeLoop,
 39///     ctx =&gt; new IterativeLoopOptions
 40///     {
 41///         Instructions = "Write an article.",
 42///         Tools = tools,
 43///         PromptFactory = iterCtx =&gt; BuildPrompt(iterCtx.Workspace),
 44///         MaxIterations = 15,
 45///         LoopName = ctx.StageName,
 46///     });
 47///
 48/// // With onLoopCompleted to capture termination metadata
 49/// var executor = new IterativeLoopStageExecutor(
 50///     iterativeLoop,
 51///     ctx =&gt; buildOptions(ctx),
 52///     onLoopCompleted: (loopResult, ctx) =&gt;
 53///     {
 54///         accessor.LastDiagnostics = loopResult.Diagnostics;
 55///         accessor.LastTerminationReason = loopResult.Termination.ToString();
 56///     });
 57///
 58/// // With shouldTreatAsSuccess for acceptable non-success terminations
 59/// var executor = new IterativeLoopStageExecutor(
 60///     iterativeLoop,
 61///     ctx =&gt; buildOptions(ctx),
 62///     shouldTreatAsSuccess: r =&gt;
 63///         r.Termination is TerminationReason.MaxIterationsReached
 64///                       or TerminationReason.MaxToolCallsReached);
 65///
 66/// // Composing with decorators
 67/// var timedExecutor = new TimeoutExecutor(
 68///     new IterativeLoopStageExecutor(loop, optionsFactory),
 69///     TimeSpan.FromMinutes(10));
 70/// </code>
 71/// </example>
 72[DoNotAutoRegister]
 73public sealed class IterativeLoopStageExecutor : IStageExecutor
 74{
 75    private readonly IIterativeAgentLoop _loop;
 76    private readonly Func<StageExecutionContext, IterativeLoopOptions> _optionsFactory;
 77    private readonly Func<StageExecutionContext, IterativeContext>? _contextFactory;
 78    private readonly Action<IterativeLoopResult, StageExecutionContext>? _onLoopCompleted;
 79    private readonly Func<IterativeLoopResult, bool>? _shouldTreatAsSuccess;
 80    private readonly FailureDisposition _failureDisposition;
 81
 82    /// <summary>
 83    /// Initializes a new <see cref="IterativeLoopStageExecutor"/>.
 84    /// </summary>
 85    /// <param name="loop">The iterative agent loop to execute.</param>
 86    /// <param name="optionsFactory">
 87    /// Factory that produces the <see cref="IterativeLoopOptions"/> from the current stage
 88    /// context. Called once per execution — callers configure instructions, tools, prompt
 89    /// factory, iteration limits, and all other loop settings here.
 90    /// </param>
 91    /// <param name="contextFactory">
 92    /// Optional factory that produces the <see cref="IterativeContext"/> from the current stage
 93    /// context. When <see langword="null"/> (the default), the executor creates an
 94    /// <see cref="IterativeContext"/> using <see cref="StageExecutionContext.Workspace"/>.
 95    /// Provide a factory to pre-populate <see cref="IterativeContext.State"/> or use a
 96    /// different workspace.
 97    /// </param>
 98    /// <param name="onLoopCompleted">
 99    /// Optional callback invoked immediately after the loop completes, before result mapping.
 100    /// Receives the raw <see cref="IterativeLoopResult"/> and the <see cref="StageExecutionContext"/>.
 101    /// Use this to capture loop-specific metadata (termination reason, per-iteration
 102    /// diagnostics) that is not surfaced on <see cref="StageExecutionResult"/>.
 103    /// Called on both success and failure paths. Not called if the loop throws an exception.
 104    /// </param>
 105    /// <param name="shouldTreatAsSuccess">
 106    /// Optional predicate evaluated when the loop result has
 107    /// <see cref="IterativeLoopResult.Succeeded"/> = <see langword="false"/>. When the
 108    /// predicate returns <see langword="true"/>, the executor treats the result as a success.
 109    /// Use this for termination reasons like <see cref="TerminationReason.MaxIterationsReached"/>
 110    /// that are acceptable in the caller's domain.
 111    /// Not called when the loop already succeeded.
 112    /// </param>
 113    /// <param name="failureDisposition">
 114    /// The <see cref="FailureDisposition"/> applied to failed results. Defaults to
 115    /// <see cref="FailureDisposition.AbortPipeline"/>. Set to
 116    /// <see cref="FailureDisposition.ContinueAdvisory"/> for stages whose failure should
 117    /// not halt the pipeline.
 118    /// </param>
 56119    public IterativeLoopStageExecutor(
 56120        IIterativeAgentLoop loop,
 56121        Func<StageExecutionContext, IterativeLoopOptions> optionsFactory,
 56122        Func<StageExecutionContext, IterativeContext>? contextFactory = null,
 56123        Action<IterativeLoopResult, StageExecutionContext>? onLoopCompleted = null,
 56124        Func<IterativeLoopResult, bool>? shouldTreatAsSuccess = null,
 56125        FailureDisposition failureDisposition = FailureDisposition.AbortPipeline)
 126    {
 56127        _loop = loop;
 56128        _optionsFactory = optionsFactory;
 56129        _contextFactory = contextFactory;
 56130        _onLoopCompleted = onLoopCompleted;
 56131        _shouldTreatAsSuccess = shouldTreatAsSuccess;
 56132        _failureDisposition = failureDisposition;
 56133    }
 134
 135    /// <inheritdoc />
 136    public async Task<StageExecutionResult> ExecuteAsync(
 137        StageExecutionContext context,
 138        CancellationToken cancellationToken)
 139    {
 56140        var options = _optionsFactory(context);
 55141        var iterativeContext = _contextFactory?.Invoke(context)
 55142            ?? new IterativeContext { Workspace = context.Workspace };
 143
 54144        using (context.DiagnosticsAccessor.BeginCapture())
 145        {
 54146            var loopResult = await _loop.RunAsync(options, iterativeContext, cancellationToken);
 51147            var diagnostics = loopResult.Diagnostics
 51148                ?? context.DiagnosticsAccessor.LastRunDiagnostics;
 149
 51150            _onLoopCompleted?.Invoke(loopResult, context);
 151
 51152            var succeeded = loopResult.Succeeded
 51153                || (_shouldTreatAsSuccess?.Invoke(loopResult) == true);
 154
 51155            if (succeeded)
 156            {
 24157                return StageExecutionResult.Success(
 24158                    context.StageName,
 24159                    diagnostics,
 24160                    loopResult.FinalResponse?.Text);
 161            }
 162
 27163            return StageExecutionResult.Failed(
 27164                context.StageName,
 27165                new InvalidOperationException(
 27166                    $"{context.StageName} terminated [{loopResult.Termination}] after " +
 27167                    $"{loopResult.Iterations.Count} iteration(s): {loopResult.ErrorMessage}"),
 27168                diagnostics,
 27169                _failureDisposition);
 170        }
 51171    }
 172}