< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Workflows.Sequential.CritiqueAndReviseExecutor
Assembly: NexusLabs.Needlr.AgentFramework.Workflows
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Sequential/CritiqueAndReviseExecutor.cs
Line coverage
33%
Covered lines: 18
Uncovered lines: 36
Coverable lines: 54
Total lines: 137
Line coverage: 33.3%
Branch coverage
0%
Covered branches: 0
Total branches: 26
Branch coverage: 0%
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()0%702260%

File(s)

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

#LineLine coverage
 1using Microsoft.Agents.AI;
 2
 3using NexusLabs.Needlr.AgentFramework.Diagnostics;
 4
 5namespace NexusLabs.Needlr.AgentFramework.Workflows.Sequential;
 6
 7/// <summary>
 8/// Implements an evaluate→revise→retry loop. A critic agent evaluates the
 9/// current state, a programmatic check determines pass/fail, and if it fails
 10/// a reviser agent applies feedback. The loop repeats up to the configured
 11/// maximum number of additional attempts.
 12/// </summary>
 13/// <example>
 14/// <code>
 15/// var executor = new CritiqueAndReviseExecutor(
 16///     criticAgent,
 17///     reviserAgent,
 18///     ctx => $"Review the draft in the workspace for quality.",
 19///     (ctx, feedback) => $"Revise the draft. Feedback: {feedback}",
 20///     (diag, feedback) => feedback?.Contains("PASS") == true,
 21///     maxRetries: 2);
 22///
 23/// var result = await executor.ExecuteAsync(context, cancellationToken);
 24/// </code>
 25/// </example>
 26[DoNotAutoRegister]
 27public sealed class CritiqueAndReviseExecutor : IStageExecutor
 28{
 29    private readonly AIAgent _critic;
 30    private readonly AIAgent _reviser;
 31    private readonly Func<StageExecutionContext, string> _criticPromptFactory;
 32    private readonly Func<StageExecutionContext, string, string> _reviserPromptFactory;
 33    private readonly Func<IAgentRunDiagnostics?, string?, bool> _passCheck;
 34    private readonly int _maxRetries;
 35    private readonly Func<StageExecutionContext, string?, bool>? _postPassCheck;
 36    private readonly Func<StageExecutionContext, string, Task>? _onRevisionCompleted;
 37
 38    /// <summary>
 39    /// Initializes a new <see cref="CritiqueAndReviseExecutor"/>.
 40    /// </summary>
 41    /// <param name="critic">The AI agent that evaluates the current state.</param>
 42    /// <param name="reviser">The AI agent that applies revision feedback.</param>
 43    /// <param name="criticPromptFactory">Builds the prompt sent to the critic agent from the stage context.</param>
 44    /// <param name="reviserPromptFactory">Builds the prompt sent to the reviser agent from the stage context and critic
 45    /// <param name="passCheck">Determines whether the critic's response constitutes a pass.</param>
 46    /// <param name="maxRetries">Maximum number of revision attempts after the initial critique. Defaults to 3.</param>
 47    /// <param name="postPassCheck">Optional secondary check applied after <paramref name="passCheck"/> returns <see lan
 48    /// Returns <see langword="false"/> to override the pass and continue revision.</param>
 49    /// <param name="onRevisionCompleted">Optional callback invoked after the reviser runs, receiving the reviser's outp
 350    public CritiqueAndReviseExecutor(
 351        AIAgent critic,
 352        AIAgent reviser,
 353        Func<StageExecutionContext, string> criticPromptFactory,
 354        Func<StageExecutionContext, string, string> reviserPromptFactory,
 355        Func<IAgentRunDiagnostics?, string?, bool> passCheck,
 356        int maxRetries = 3,
 357        Func<StageExecutionContext, string?, bool>? postPassCheck = null,
 358        Func<StageExecutionContext, string, Task>? onRevisionCompleted = null)
 59    {
 360        _critic = critic;
 361        _reviser = reviser;
 362        _criticPromptFactory = criticPromptFactory;
 363        _reviserPromptFactory = reviserPromptFactory;
 364        _passCheck = passCheck;
 365        _maxRetries = maxRetries;
 366        _postPassCheck = postPassCheck;
 367        _onRevisionCompleted = onRevisionCompleted;
 368    }
 69
 70    /// <inheritdoc />
 71    public async Task<StageExecutionResult> ExecuteAsync(
 72        StageExecutionContext context,
 73        CancellationToken cancellationToken)
 74    {
 075        var allDiagnostics = new List<IAgentRunDiagnostics>();
 076        string? feedback = null;
 077        bool passed = false;
 78
 079        for (int i = 0; i <= _maxRetries; i++)
 80        {
 081            var criticPrompt = _criticPromptFactory(context);
 082            using (context.DiagnosticsAccessor.BeginCapture())
 83            {
 084                var response = await _critic.RunAsync(criticPrompt, cancellationToken: cancellationToken);
 085                feedback = response.GetText();
 086                var diag = context.DiagnosticsAccessor.LastRunDiagnostics;
 087                if (diag is not null)
 88                {
 089                    allDiagnostics.Add(diag);
 90                }
 91
 092                passed = _passCheck(diag, feedback);
 093            }
 94
 095            if (passed && _postPassCheck is not null)
 96            {
 097                passed = _postPassCheck(context, feedback);
 98            }
 99
 0100            if (passed)
 101            {
 102                break;
 103            }
 104
 105            // Run reviser only if not the last attempt
 0106            if (i < _maxRetries)
 107            {
 0108                var reviserPrompt = _reviserPromptFactory(context, feedback ?? "");
 0109                using (context.DiagnosticsAccessor.BeginCapture())
 110                {
 0111                    var reviserResponse = await _reviser.RunAsync(reviserPrompt, cancellationToken: cancellationToken);
 0112                    var diag = context.DiagnosticsAccessor.LastRunDiagnostics;
 0113                    if (diag is not null)
 114                    {
 0115                        allDiagnostics.Add(diag);
 116                    }
 117
 0118                    if (_onRevisionCompleted is not null)
 119                    {
 0120                        var reviserText = reviserResponse.GetText();
 0121                        await _onRevisionCompleted(context, reviserText ?? "");
 122                    }
 0123                }
 124            }
 125        }
 126
 0127        var lastDiag = allDiagnostics.Count > 0 ? allDiagnostics[^1] : null;
 128
 0129        return passed
 0130            ? StageExecutionResult.Success(context.StageName, lastDiag, feedback)
 0131            : StageExecutionResult.Failed(
 0132                context.StageName,
 0133                new InvalidOperationException(
 0134                    $"Critique-and-revise did not pass after {_maxRetries + 1} attempts. Last feedback: {feedback}"),
 0135                lastDiag);
 0136    }
 137}