< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Workflows.Sequential.SequentialPipelineRunner
Assembly: NexusLabs.Needlr.AgentFramework.Workflows
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Sequential/SequentialPipelineRunner.cs
Line coverage
85%
Covered lines: 381
Uncovered lines: 65
Coverable lines: 446
Total lines: 780
Line coverage: 85.4%
Branch coverage
80%
Covered branches: 117
Total branches: 146
Branch coverage: 80.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
RunAsync(...)100%11100%
RunAsync(...)100%11100%
RunCoreAsync()87.5%646498.9%
RunPhasedAsync(...)100%11100%
RunPhasedAsync(...)100%11100%
RunPhasedCoreAsync()74.39%2048273.75%
ReportCompleted(...)100%11100%

File(s)

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

#LineLine coverage
 1using System.Diagnostics;
 2
 3using Microsoft.Extensions.AI;
 4
 5using NexusLabs.Needlr.AgentFramework.Budget;
 6using NexusLabs.Needlr.AgentFramework.Diagnostics;
 7using NexusLabs.Needlr.AgentFramework.Progress;
 8using NexusLabs.Needlr.AgentFramework.Workspace;
 9
 10namespace NexusLabs.Needlr.AgentFramework.Workflows.Sequential;
 11
 12/// <summary>
 13/// Executes a linear sequence of <see cref="PipelineStage"/> instances,
 14/// evaluating policies (skip, retry, budget) and producing an
 15/// <see cref="IPipelineRunResult"/> with per-stage diagnostics.
 16/// </summary>
 17/// <remarks>
 18/// <para>
 19/// This runner is a peer of <see cref="GraphWorkflowRunner"/> for linear pipelines.
 20/// It supports hybrid agent/programmatic stages via <see cref="IStageExecutor"/>,
 21/// conditional skipping, post-validation with retries, per-stage and overall token
 22/// budgets, and structured progress reporting.
 23/// </para>
 24/// </remarks>
 25/// <example>
 26/// <code>
 27/// var runner = new SequentialPipelineRunner(diagnosticsAccessor, budgetTracker, progressFactory);
 28/// var stages = new[]
 29/// {
 30///     new PipelineStage("Writer", new AgentStageExecutor(writerAgent, ctx =&gt; "Write a draft.")),
 31///     new PipelineStage("Editor", new AgentStageExecutor(editorAgent, ctx =&gt; "Edit the draft.")),
 32/// };
 33/// var result = await runner.RunAsync(workspace, stages, options: null, cancellationToken);
 34/// </code>
 35/// </example>
 36[DoNotAutoRegister]
 37public sealed class SequentialPipelineRunner
 38{
 39    private readonly IAgentDiagnosticsAccessor _diagnosticsAccessor;
 40    private readonly ITokenBudgetTracker _budgetTracker;
 41    private readonly IProgressReporterFactory _progressReporterFactory;
 42
 43    /// <summary>
 44    /// Initializes a new <see cref="SequentialPipelineRunner"/>.
 45    /// </summary>
 46    /// <param name="diagnosticsAccessor">Accessor for capturing per-stage agent diagnostics.</param>
 47    /// <param name="budgetTracker">Token budget tracker for scoping per-stage and pipeline-level budgets.</param>
 48    /// <param name="progressReporterFactory">Factory for creating progress reporters.</param>
 6449    public SequentialPipelineRunner(
 6450        IAgentDiagnosticsAccessor diagnosticsAccessor,
 6451        ITokenBudgetTracker budgetTracker,
 6452        IProgressReporterFactory progressReporterFactory)
 53    {
 6454        _diagnosticsAccessor = diagnosticsAccessor;
 6455        _budgetTracker = budgetTracker;
 6456        _progressReporterFactory = progressReporterFactory;
 6457    }
 58
 59    /// <summary>
 60    /// Runs all pipeline stages sequentially, applying policies and collecting results.
 61    /// </summary>
 62    /// <param name="workspace">The shared workspace for file I/O across stages.</param>
 63    /// <param name="stages">The ordered list of stages to execute.</param>
 64    /// <param name="options">Optional pipeline-level configuration.</param>
 65    /// <param name="cancellationToken">Token to observe for cancellation.</param>
 66    /// <returns>An <see cref="IPipelineRunResult"/> describing the pipeline outcome.</returns>
 67    public Task<IPipelineRunResult> RunAsync(
 68        IWorkspace workspace,
 69        IReadOnlyList<PipelineStage> stages,
 70        SequentialPipelineOptions? options,
 71        CancellationToken cancellationToken) =>
 3272        RunCoreAsync(workspace, stages, pipelineState: null, options, cancellationToken);
 73
 74    /// <summary>
 75    /// Runs all pipeline stages sequentially with a shared typed state object,
 76    /// applying policies and collecting results.
 77    /// </summary>
 78    /// <typeparam name="TState">The type of the shared pipeline state.</typeparam>
 79    /// <param name="workspace">The shared workspace for file I/O across stages.</param>
 80    /// <param name="stages">The ordered list of stages to execute.</param>
 81    /// <param name="state">A shared state object accessible to all stages via
 82    /// <see cref="StageExecutionContext.GetRequiredState{T}"/>.</param>
 83    /// <param name="options">Optional pipeline-level configuration.</param>
 84    /// <param name="cancellationToken">Token to observe for cancellation.</param>
 85    /// <returns>An <see cref="IPipelineRunResult"/> describing the pipeline outcome.</returns>
 86    public Task<IPipelineRunResult> RunAsync<TState>(
 87        IWorkspace workspace,
 88        IReadOnlyList<PipelineStage> stages,
 89        TState state,
 90        SequentialPipelineOptions? options,
 91        CancellationToken cancellationToken) where TState : class =>
 292        RunCoreAsync(workspace, stages, state, options, cancellationToken);
 93
 94    private async Task<IPipelineRunResult> RunCoreAsync(
 95        IWorkspace workspace,
 96        IReadOnlyList<PipelineStage> stages,
 97        object? pipelineState,
 98        SequentialPipelineOptions? options,
 99        CancellationToken cancellationToken)
 100    {
 34101        var stopwatch = Stopwatch.StartNew();
 34102        var reporter = _progressReporterFactory.Create(Guid.NewGuid().ToString("N"));
 34103        var stageResults = new List<IAgentStageResult>();
 104
 34105        reporter.Report(new WorkflowStartedEvent(
 34106            DateTimeOffset.UtcNow,
 34107            reporter.WorkflowId,
 34108            reporter.AgentId,
 34109            ParentAgentId: null,
 34110            reporter.Depth,
 34111            reporter.NextSequence()));
 112
 34113        IDisposable? pipelineBudgetScope = null;
 114        try
 115        {
 34116            if (options?.TotalTokenBudget is { } totalBudget)
 117            {
 0118                pipelineBudgetScope = _budgetTracker.BeginScope(totalBudget);
 119            }
 120
 146121            for (var i = 0; i < stages.Count; i++)
 122            {
 48123                cancellationToken.ThrowIfCancellationRequested();
 124
 47125                var stage = stages[i];
 47126                var policy = stage.Policy;
 127
 47128                var context = new StageExecutionContext(
 47129                    workspace,
 47130                    _diagnosticsAccessor,
 47131                    reporter,
 47132                    StageIndex: i,
 47133                    TotalStages: stages.Count,
 47134                    StageName: stage.Name,
 47135                    CallerCancellationToken: cancellationToken,
 47136                    PipelineState: pipelineState);
 137
 138                // Evaluate ShouldSkip
 47139                if (policy?.ShouldSkip?.Invoke(context) == true)
 140                {
 3141                    stageResults.Add(new AgentStageResult(
 3142                        stage.Name,
 3143                        FinalResponse: null,
 3144                        Diagnostics: null,
 3145                        Outcome: StageOutcome.Skipped));
 3146                    continue;
 147                }
 148
 44149                reporter.Report(new AgentInvokedEvent(
 44150                    DateTimeOffset.UtcNow,
 44151                    reporter.WorkflowId,
 44152                    stage.Name,
 44153                    ParentAgentId: null,
 44154                    reporter.Depth,
 44155                    reporter.NextSequence(),
 44156                    stage.Name));
 157
 44158                var maxAttempts = policy?.MaxAttempts ?? 1;
 44159                StageExecutionResult? stageResult = null;
 44160                string? validationError = null;
 161
 44162                IDisposable? stageBudgetScope = null;
 163                try
 164                {
 44165                    if (policy?.TokenBudget is { } stageBudget)
 166                    {
 0167                        stageBudgetScope = _budgetTracker.BeginChildScope(stage.Name, stageBudget);
 168                    }
 169
 96170                    for (var attempt = 0; attempt < maxAttempts; attempt++)
 171                    {
 47172                        cancellationToken.ThrowIfCancellationRequested();
 47173                        stageResult = await stage.Executor.ExecuteAsync(context, cancellationToken);
 174
 43175                        if (policy?.PostValidation is { } validate)
 176                        {
 5177                            validationError = validate(stageResult);
 5178                            if (validationError is null)
 179                            {
 180                                break;
 181                            }
 182
 183                            // Last attempt failed â€” will throw after loop
 4184                            if (attempt < maxAttempts - 1)
 185                            {
 3186                                validationError = null;
 187                            }
 188                        }
 189                        else
 190                        {
 191                            break;
 192                        }
 193                    }
 40194                }
 4195                catch (Exception ex)
 196                {
 197                    // Always record the failed stage so it appears in diagnostics.
 198                    // Capture any partial diagnostics the stage may have produced.
 4199                    var partialDiag = _diagnosticsAccessor.LastRunDiagnostics;
 4200                    stageResults.Add(new AgentStageResult(
 4201                        stage.Name,
 4202                        FinalResponse: null,
 4203                        Diagnostics: partialDiag,
 4204                        Outcome: StageOutcome.Failed));
 205
 4206                    reporter.Report(new AgentFailedEvent(
 4207                        DateTimeOffset.UtcNow,
 4208                        reporter.WorkflowId,
 4209                        stage.Name,
 4210                        ParentAgentId: null,
 4211                        reporter.Depth,
 4212                        reporter.NextSequence(),
 4213                        AgentName: stage.Name,
 4214                        ErrorMessage: ex.Message));
 215
 4216                    throw;
 217                }
 218                finally
 219                {
 44220                    stageBudgetScope?.Dispose();
 221                }
 222
 40223                if (stageResult is not null && policy?.AfterExecution is { } afterExec)
 224                {
 3225                    await afterExec(stageResult, context);
 226                }
 227
 40228                if (validationError is not null)
 229                {
 1230                    throw new StageValidationException(stage.Name, validationError);
 231                }
 232
 233                // Handle explicit failure results from the stage executor.
 39234                if (!stageResult!.Succeeded)
 235                {
 7236                    stageResults.Add(new AgentStageResult(
 7237                        stage.Name,
 7238                        FinalResponse: null,
 7239                        Diagnostics: stageResult.Diagnostics,
 7240                        Outcome: StageOutcome.Failed));
 241
 7242                    reporter.Report(new AgentFailedEvent(
 7243                        DateTimeOffset.UtcNow,
 7244                        reporter.WorkflowId,
 7245                        stage.Name,
 7246                        ParentAgentId: null,
 7247                        reporter.Depth,
 7248                        reporter.NextSequence(),
 7249                        AgentName: stage.Name,
 7250                        ErrorMessage: stageResult.Exception?.Message ?? "Stage failed"));
 251
 7252                    if (stageResult.FailureDisposition == FailureDisposition.AbortPipeline)
 253                    {
 3254                        stopwatch.Stop();
 3255                        var errorMsg = stageResult.Exception?.Message
 3256                            ?? $"Stage '{stage.Name}' failed";
 3257                        ReportCompleted(reporter, stopwatch.Elapsed, succeeded: false, errorMsg);
 3258                        return new PipelineRunResult(
 3259                            stageResults,
 3260                            stopwatch.Elapsed,
 3261                            succeeded: false,
 3262                            errorMessage: errorMsg,
 3263                            exception: stageResult.Exception,
 3264                            plannedStageCount: stages.Count);
 265                    }
 266
 267                    // ContinueAdvisory â€” proceed to the next stage.
 268                    continue;
 269                }
 270
 32271                ChatResponse? chatResponse = stageResult!.ResponseText is not null
 32272                    ? new ChatResponse(new ChatMessage(ChatRole.Assistant, stageResult.ResponseText))
 32273                    : null;
 274
 32275                stageResults.Add(new AgentStageResult(
 32276                    stage.Name,
 32277                    chatResponse,
 32278                    stageResult.Diagnostics));
 279
 32280                reporter.Report(new AgentCompletedEvent(
 32281                    DateTimeOffset.UtcNow,
 32282                    reporter.WorkflowId,
 32283                    stage.Name,
 32284                    ParentAgentId: null,
 32285                    reporter.Depth,
 32286                    reporter.NextSequence(),
 32287                    stage.Name,
 32288                    Duration: stopwatch.Elapsed,
 32289                    TotalTokens: stageResult.Diagnostics?.AggregateTokenUsage.TotalTokens ?? 0));
 32290            }
 291
 25292            stopwatch.Stop();
 293
 25294            var pipelineResult = new PipelineRunResult(
 25295                stageResults,
 25296                stopwatch.Elapsed,
 25297                succeeded: true,
 25298                errorMessage: null,
 25299                plannedStageCount: stages.Count);
 300
 25301            if (options?.CompletionGate is { } gate)
 302            {
 1303                var gateError = gate(pipelineResult);
 1304                if (gateError is not null)
 305                {
 1306                    var failedResult = new PipelineRunResult(
 1307                        stageResults,
 1308                        stopwatch.Elapsed,
 1309                        succeeded: false,
 1310                        errorMessage: gateError,
 1311                        plannedStageCount: stages.Count);
 312
 1313                    ReportCompleted(reporter, stopwatch.Elapsed, succeeded: false, gateError);
 1314                    return failedResult;
 315                }
 316            }
 317
 24318            ReportCompleted(reporter, stopwatch.Elapsed, succeeded: true, errorMessage: null);
 24319            return pipelineResult;
 320        }
 4321        catch (OperationCanceledException ex) when (ex.InnerException is TokenBudgetExceededException budgetEx)
 322        {
 1323            stopwatch.Stop();
 1324            ReportCompleted(reporter, stopwatch.Elapsed, succeeded: false, budgetEx.Message);
 1325            return new PipelineRunResult(
 1326                stageResults,
 1327                stopwatch.Elapsed,
 1328                succeeded: false,
 1329                errorMessage: budgetEx.Message,
 1330                exception: budgetEx,
 1331                plannedStageCount: stages.Count);
 332        }
 3333        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 334        {
 1335            stopwatch.Stop();
 1336            ReportCompleted(reporter, stopwatch.Elapsed, succeeded: false, "Cancelled");
 1337            throw;
 338        }
 2339        catch (OperationCanceledException ex)
 340        {
 341            // HTTP timeouts and other non-user cancellations â€” treat as stage failure,
 342            // not as user cancellation. HttpClient.Timeout throws TaskCanceledException
 343            // which is OperationCanceledException, but the caller's token is NOT cancelled.
 2344            stopwatch.Stop();
 2345            var message = ex.InnerException is TimeoutException
 2346                ? $"Stage timed out: {ex.InnerException.Message}"
 2347                : $"Operation cancelled (not by caller): {ex.Message}";
 2348            ReportCompleted(reporter, stopwatch.Elapsed, succeeded: false, message);
 2349            return new PipelineRunResult(
 2350                stageResults,
 2351                stopwatch.Elapsed,
 2352                succeeded: false,
 2353                errorMessage: message,
 2354                exception: ex,
 2355                plannedStageCount: stages.Count);
 356        }
 2357        catch (Exception ex)
 358        {
 2359            stopwatch.Stop();
 2360            ReportCompleted(reporter, stopwatch.Elapsed, succeeded: false, ex.Message);
 2361            return new PipelineRunResult(
 2362                stageResults,
 2363                stopwatch.Elapsed,
 2364                succeeded: false,
 2365                errorMessage: ex.Message,
 2366                exception: ex,
 2367                plannedStageCount: stages.Count);
 368        }
 369        finally
 370        {
 34371            pipelineBudgetScope?.Dispose();
 372        }
 33373    }
 374
 375    /// <summary>
 376    /// Runs a phased pipeline where stages are grouped into named phases with
 377    /// lifecycle hooks and optional phase-level token budgets.
 378    /// </summary>
 379    /// <param name="workspace">The shared workspace for file I/O across stages.</param>
 380    /// <param name="phases">The ordered list of phases, each containing stages.</param>
 381    /// <param name="options">Optional pipeline-level configuration.</param>
 382    /// <param name="cancellationToken">Token to observe for cancellation.</param>
 383    /// <returns>An <see cref="IPipelineRunResult"/> describing the pipeline outcome.</returns>
 384    public Task<IPipelineRunResult> RunPhasedAsync(
 385        IWorkspace workspace,
 386        IReadOnlyList<PipelinePhase> phases,
 387        SequentialPipelineOptions? options,
 388        CancellationToken cancellationToken) =>
 27389        RunPhasedCoreAsync(workspace, phases, pipelineState: null, options, cancellationToken);
 390
 391    /// <summary>
 392    /// Runs a phased pipeline with a shared typed state object accessible to both
 393    /// phase lifecycle hooks and stage executors.
 394    /// </summary>
 395    /// <typeparam name="TState">The type of the shared pipeline state.</typeparam>
 396    /// <param name="workspace">The shared workspace for file I/O across stages.</param>
 397    /// <param name="phases">The ordered list of phases, each containing stages.</param>
 398    /// <param name="state">A shared state object accessible via
 399    /// <see cref="PhaseContext.GetRequiredState{T}"/> and
 400    /// <see cref="StageExecutionContext.GetRequiredState{T}"/>.</param>
 401    /// <param name="options">Optional pipeline-level configuration.</param>
 402    /// <param name="cancellationToken">Token to observe for cancellation.</param>
 403    /// <returns>An <see cref="IPipelineRunResult"/> describing the pipeline outcome.</returns>
 404    public Task<IPipelineRunResult> RunPhasedAsync<TState>(
 405        IWorkspace workspace,
 406        IReadOnlyList<PipelinePhase> phases,
 407        TState state,
 408        SequentialPipelineOptions? options,
 409        CancellationToken cancellationToken) where TState : class =>
 1410        RunPhasedCoreAsync(workspace, phases, state, options, cancellationToken);
 411
 412    private async Task<IPipelineRunResult> RunPhasedCoreAsync(
 413        IWorkspace workspace,
 414        IReadOnlyList<PipelinePhase> phases,
 415        object? pipelineState,
 416        SequentialPipelineOptions? options,
 417        CancellationToken cancellationToken)
 418    {
 28419        var pipelineStopwatch = Stopwatch.StartNew();
 28420        var reporter = _progressReporterFactory.Create(Guid.NewGuid().ToString("N"));
 28421        var allStageResults = new List<IAgentStageResult>();
 71422        var totalStages = phases.Sum(p => p.Stages.Count);
 28423        var globalStageIndex = 0;
 424
 28425        reporter.Report(new WorkflowStartedEvent(
 28426            DateTimeOffset.UtcNow,
 28427            reporter.WorkflowId,
 28428            reporter.AgentId,
 28429            ParentAgentId: null,
 28430            reporter.Depth,
 28431            reporter.NextSequence()));
 432
 28433        IDisposable? pipelineBudgetScope = null;
 434        try
 435        {
 28436            if (options?.TotalTokenBudget is { } totalBudget)
 437            {
 1438                pipelineBudgetScope = _budgetTracker.BeginScope(totalBudget);
 439            }
 440
 136441            for (var phaseIndex = 0; phaseIndex < phases.Count; phaseIndex++)
 442            {
 43443                cancellationToken.ThrowIfCancellationRequested();
 444
 43445                var phase = phases[phaseIndex];
 43446                var phasePolicy = phase.Policy;
 43447                var phaseStopwatch = Stopwatch.StartNew();
 43448                var phaseSucceeded = true;
 449
 43450                reporter.Report(new PhaseStartedEvent(
 43451                    DateTimeOffset.UtcNow,
 43452                    reporter.WorkflowId,
 43453                    reporter.AgentId,
 43454                    ParentAgentId: null,
 43455                    reporter.Depth,
 43456                    reporter.NextSequence(),
 43457                    phase.Name,
 43458                    phaseIndex,
 43459                    phases.Count,
 43460                    phase.Stages.Count));
 461
 43462                var phaseContext = new PhaseContext(
 43463                    phase.Name,
 43464                    phaseIndex,
 43465                    phases.Count,
 43466                    workspace,
 43467                    pipelineState);
 468
 43469                IDisposable? phaseBudgetScope = null;
 470                try
 471                {
 43472                    if (phasePolicy?.TokenBudget is { } phaseBudget)
 473                    {
 3474                        phaseBudgetScope = _budgetTracker.BeginScope(phaseBudget);
 475                    }
 476
 43477                    if (phasePolicy?.OnEnterAsync is { } onEnter)
 478                    {
 10479                        await onEnter(phaseContext, cancellationToken);
 480                    }
 481
 182482                    for (var stageInPhase = 0; stageInPhase < phase.Stages.Count; stageInPhase++)
 483                    {
 51484                        cancellationToken.ThrowIfCancellationRequested();
 485
 51486                        var stage = phase.Stages[stageInPhase];
 51487                        var policy = stage.Policy;
 488
 51489                        var context = new StageExecutionContext(
 51490                            workspace,
 51491                            _diagnosticsAccessor,
 51492                            reporter,
 51493                            StageIndex: globalStageIndex,
 51494                            TotalStages: totalStages,
 51495                            StageName: stage.Name,
 51496                            CallerCancellationToken: cancellationToken,
 51497                            PipelineState: pipelineState,
 51498                            PhaseName: phase.Name,
 51499                            PhaseIndex: phaseIndex,
 51500                            StageIndexInPhase: stageInPhase,
 51501                            TotalStagesInPhase: phase.Stages.Count);
 502
 51503                        if (policy?.ShouldSkip?.Invoke(context) == true)
 504                        {
 2505                            allStageResults.Add(new AgentStageResult(
 2506                                stage.Name,
 2507                                FinalResponse: null,
 2508                                Diagnostics: null,
 2509                                Outcome: StageOutcome.Skipped,
 2510                                PhaseName: phase.Name));
 2511                            globalStageIndex++;
 2512                            continue;
 513                        }
 514
 49515                        reporter.Report(new AgentInvokedEvent(
 49516                            DateTimeOffset.UtcNow,
 49517                            reporter.WorkflowId,
 49518                            stage.Name,
 49519                            ParentAgentId: null,
 49520                            reporter.Depth,
 49521                            reporter.NextSequence(),
 49522                            stage.Name));
 523
 49524                        var maxAttempts = policy?.MaxAttempts ?? 1;
 49525                        StageExecutionResult? stageResult = null;
 49526                        string? validationError = null;
 527
 49528                        IDisposable? stageBudgetScope = null;
 529                        try
 530                        {
 49531                            if (policy?.TokenBudget is { } stageBudget)
 532                            {
 1533                                stageBudgetScope = _budgetTracker.BeginChildScope(stage.Name, stageBudget);
 534                            }
 535
 98536                            for (var attempt = 0; attempt < maxAttempts; attempt++)
 537                            {
 49538                                cancellationToken.ThrowIfCancellationRequested();
 49539                                stageResult = await stage.Executor.ExecuteAsync(context, cancellationToken);
 540
 47541                                if (policy?.PostValidation is { } validate)
 542                                {
 0543                                    validationError = validate(stageResult);
 0544                                    if (validationError is null)
 545                                    {
 546                                        break;
 547                                    }
 548
 0549                                    if (attempt < maxAttempts - 1)
 550                                    {
 0551                                        validationError = null;
 552                                    }
 553                                }
 554                                else
 555                                {
 556                                    break;
 557                                }
 558                            }
 47559                        }
 2560                        catch (Exception ex)
 561                        {
 2562                            var partialDiag = _diagnosticsAccessor.LastRunDiagnostics;
 2563                            allStageResults.Add(new AgentStageResult(
 2564                                stage.Name,
 2565                                FinalResponse: null,
 2566                                Diagnostics: partialDiag,
 2567                                Outcome: StageOutcome.Failed,
 2568                                PhaseName: phase.Name));
 569
 2570                            reporter.Report(new AgentFailedEvent(
 2571                                DateTimeOffset.UtcNow,
 2572                                reporter.WorkflowId,
 2573                                stage.Name,
 2574                                ParentAgentId: null,
 2575                                reporter.Depth,
 2576                                reporter.NextSequence(),
 2577                                AgentName: stage.Name,
 2578                                ErrorMessage: ex.Message));
 579
 2580                            throw;
 581                        }
 582                        finally
 583                        {
 49584                            stageBudgetScope?.Dispose();
 585                        }
 586
 47587                        if (stageResult is not null && policy?.AfterExecution is { } afterExec)
 588                        {
 0589                            await afterExec(stageResult, context);
 590                        }
 591
 47592                        if (validationError is not null)
 593                        {
 0594                            throw new StageValidationException(stage.Name, validationError);
 595                        }
 596
 47597                        if (!stageResult!.Succeeded)
 598                        {
 0599                            allStageResults.Add(new AgentStageResult(
 0600                                stage.Name,
 0601                                FinalResponse: null,
 0602                                Diagnostics: stageResult.Diagnostics,
 0603                                Outcome: StageOutcome.Failed,
 0604                                PhaseName: phase.Name));
 605
 0606                            reporter.Report(new AgentFailedEvent(
 0607                                DateTimeOffset.UtcNow,
 0608                                reporter.WorkflowId,
 0609                                stage.Name,
 0610                                ParentAgentId: null,
 0611                                reporter.Depth,
 0612                                reporter.NextSequence(),
 0613                                AgentName: stage.Name,
 0614                                ErrorMessage: stageResult.Exception?.Message ?? "Stage failed"));
 615
 0616                            if (stageResult.FailureDisposition == FailureDisposition.AbortPipeline)
 617                            {
 0618                                phaseSucceeded = false;
 0619                                pipelineStopwatch.Stop();
 0620                                var errorMsg = stageResult.Exception?.Message
 0621                                    ?? $"Stage '{stage.Name}' failed";
 0622                                ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: false, errorMsg);
 0623                                return new PipelineRunResult(
 0624                                    allStageResults,
 0625                                    pipelineStopwatch.Elapsed,
 0626                                    succeeded: false,
 0627                                    errorMessage: errorMsg,
 0628                                    exception: stageResult.Exception,
 0629                                    plannedStageCount: totalStages);
 630                            }
 631
 0632                            globalStageIndex++;
 0633                            continue;
 634                        }
 635
 47636                        ChatResponse? chatResponse = stageResult.ResponseText is not null
 47637                            ? new ChatResponse(new ChatMessage(ChatRole.Assistant, stageResult.ResponseText))
 47638                            : null;
 639
 47640                        allStageResults.Add(new AgentStageResult(
 47641                            stage.Name,
 47642                            chatResponse,
 47643                            stageResult.Diagnostics,
 47644                            PhaseName: phase.Name));
 645
 47646                        reporter.Report(new AgentCompletedEvent(
 47647                            DateTimeOffset.UtcNow,
 47648                            reporter.WorkflowId,
 47649                            stage.Name,
 47650                            ParentAgentId: null,
 47651                            reporter.Depth,
 47652                            reporter.NextSequence(),
 47653                            stage.Name,
 47654                            Duration: pipelineStopwatch.Elapsed,
 47655                            TotalTokens: stageResult.Diagnostics?.AggregateTokenUsage.TotalTokens ?? 0));
 656
 47657                        globalStageIndex++;
 47658                    }
 659                }
 660                finally
 661                {
 43662                    if (phasePolicy?.OnExitAsync is { } onExit)
 663                    {
 5664                        await onExit(phaseContext, cancellationToken);
 665                    }
 666
 43667                    phaseBudgetScope?.Dispose();
 668
 43669                    phaseStopwatch.Stop();
 43670                    reporter.Report(new PhaseCompletedEvent(
 43671                        DateTimeOffset.UtcNow,
 43672                        reporter.WorkflowId,
 43673                        reporter.AgentId,
 43674                        ParentAgentId: null,
 43675                        reporter.Depth,
 43676                        reporter.NextSequence(),
 43677                        phase.Name,
 43678                        phaseIndex,
 43679                        phases.Count,
 43680                        phaseSucceeded,
 43681                        phaseStopwatch.Elapsed));
 682                }
 40683            }
 684
 25685            pipelineStopwatch.Stop();
 686
 25687            var pipelineResult = new PipelineRunResult(
 25688                allStageResults,
 25689                pipelineStopwatch.Elapsed,
 25690                succeeded: true,
 25691                errorMessage: null,
 25692                plannedStageCount: totalStages);
 693
 25694            if (options?.CompletionGate is { } gate)
 695            {
 1696                var gateError = gate(pipelineResult);
 1697                if (gateError is not null)
 698                {
 1699                    ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: false, gateError);
 1700                    return new PipelineRunResult(
 1701                        allStageResults,
 1702                        pipelineStopwatch.Elapsed,
 1703                        succeeded: false,
 1704                        errorMessage: gateError,
 1705                        plannedStageCount: totalStages);
 706                }
 707            }
 708
 24709            ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: true, errorMessage: null);
 24710            return pipelineResult;
 711        }
 0712        catch (OperationCanceledException ex) when (ex.InnerException is TokenBudgetExceededException budgetEx)
 713        {
 0714            pipelineStopwatch.Stop();
 0715            ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: false, budgetEx.Message);
 0716            return new PipelineRunResult(
 0717                allStageResults,
 0718                pipelineStopwatch.Elapsed,
 0719                succeeded: false,
 0720                errorMessage: budgetEx.Message,
 0721                exception: budgetEx,
 0722                plannedStageCount: totalStages);
 723        }
 0724        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 725        {
 0726            pipelineStopwatch.Stop();
 0727            ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: false, "Cancelled");
 0728            throw;
 729        }
 0730        catch (OperationCanceledException ex)
 731        {
 0732            pipelineStopwatch.Stop();
 0733            var message = ex.InnerException is TimeoutException
 0734                ? $"Stage timed out: {ex.InnerException.Message}"
 0735                : $"Operation cancelled (not by caller): {ex.Message}";
 0736            ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: false, message);
 0737            return new PipelineRunResult(
 0738                allStageResults,
 0739                pipelineStopwatch.Elapsed,
 0740                succeeded: false,
 0741                errorMessage: message,
 0742                exception: ex,
 0743                plannedStageCount: totalStages);
 744        }
 3745        catch (Exception ex)
 746        {
 3747            pipelineStopwatch.Stop();
 3748            ReportCompleted(reporter, pipelineStopwatch.Elapsed, succeeded: false, ex.Message);
 3749            return new PipelineRunResult(
 3750                allStageResults,
 3751                pipelineStopwatch.Elapsed,
 3752                succeeded: false,
 3753                errorMessage: ex.Message,
 3754                exception: ex,
 3755                plannedStageCount: totalStages);
 756        }
 757        finally
 758        {
 28759            pipelineBudgetScope?.Dispose();
 760        }
 28761    }
 762
 763    private static void ReportCompleted(
 764        IProgressReporter reporter,
 765        TimeSpan duration,
 766        bool succeeded,
 767        string? errorMessage)
 768    {
 62769        reporter.Report(new WorkflowCompletedEvent(
 62770            DateTimeOffset.UtcNow,
 62771            reporter.WorkflowId,
 62772            reporter.AgentId,
 62773            ParentAgentId: null,
 62774            reporter.Depth,
 62775            reporter.NextSequence(),
 62776            succeeded,
 62777            errorMessage,
 62778            duration));
 62779    }
 780}

Methods/Properties

.ctor(NexusLabs.Needlr.AgentFramework.Diagnostics.IAgentDiagnosticsAccessor,NexusLabs.Needlr.AgentFramework.Budget.ITokenBudgetTracker,NexusLabs.Needlr.AgentFramework.Progress.IProgressReporterFactory)
RunAsync(NexusLabs.Needlr.AgentFramework.Workspace.IWorkspace,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.Workflows.Sequential.PipelineStage>,NexusLabs.Needlr.AgentFramework.Workflows.Sequential.SequentialPipelineOptions,System.Threading.CancellationToken)
RunAsync(NexusLabs.Needlr.AgentFramework.Workspace.IWorkspace,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.Workflows.Sequential.PipelineStage>,TState,NexusLabs.Needlr.AgentFramework.Workflows.Sequential.SequentialPipelineOptions,System.Threading.CancellationToken)
RunCoreAsync()
RunPhasedAsync(NexusLabs.Needlr.AgentFramework.Workspace.IWorkspace,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.Workflows.Sequential.PipelinePhase>,NexusLabs.Needlr.AgentFramework.Workflows.Sequential.SequentialPipelineOptions,System.Threading.CancellationToken)
RunPhasedAsync(NexusLabs.Needlr.AgentFramework.Workspace.IWorkspace,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.Workflows.Sequential.PipelinePhase>,TState,NexusLabs.Needlr.AgentFramework.Workflows.Sequential.SequentialPipelineOptions,System.Threading.CancellationToken)
RunPhasedCoreAsync()
ReportCompleted(NexusLabs.Needlr.AgentFramework.Progress.IProgressReporter,System.TimeSpan,System.Boolean,System.String)