< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Iterative.IterativeLoopOptions
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Iterative/IterativeLoopOptions.cs
Line coverage
96%
Covered lines: 26
Uncovered lines: 1
Coverable lines: 27
Total lines: 358
Line coverage: 96.2%
Branch coverage
62%
Covered branches: 5
Total branches: 8
Branch coverage: 62.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_LoopName()100%11100%
get_Instructions()100%11100%
get_Tools()100%11100%
get_PromptFactory()100%11100%
get_MaxIterations()100%11100%
get_IsComplete()100%11100%
get_ToolResultMode()100%11100%
get_MaxToolRoundsPerIteration()100%11100%
get_MaxTotalToolCalls()100%11100%
get_StallDetection()100%11100%
get_OnIterationStart()100%11100%
get_OnToolCall()100%11100%
get_OnIterationEnd()100%11100%
get_ToolFilter()100%11100%
get_ExecutionContext()100%11100%
get_BudgetPressureThreshold()100%11100%
set_BudgetPressureThreshold(...)62.5%9875%
get_BudgetPressureInstruction()100%11100%
.ctor()100%11100%
get_CheckCompletionAfterToolCalls()100%11100%
get_ChatClientFactory()100%11100%
get_ChatReducer()100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Iterative/IterativeLoopOptions.cs

#LineLine coverage
 1using Microsoft.Extensions.AI;
 2
 3namespace NexusLabs.Needlr.AgentFramework.Iterative;
 4
 5/// <summary>
 6/// Configuration for a single run of an <see cref="IIterativeAgentLoop"/>.
 7/// </summary>
 8/// <remarks>
 9/// <para>
 10/// At minimum, callers must provide <see cref="Instructions"/> (the system prompt),
 11/// <see cref="Tools"/> (available tool functions), and <see cref="PromptFactory"/>
 12/// (builds the user message each iteration from workspace state).
 13/// </para>
 14/// <para>
 15/// The loop terminates when any of these conditions is met (checked in order):
 16/// </para>
 17/// <list type="number">
 18///   <item><description>The <see cref="CancellationToken"/> is cancelled.</description></item>
 19///   <item><description><see cref="MaxIterations"/> is reached.</description></item>
 20///   <item><description><see cref="IsComplete"/> returns <see langword="true"/>.</description></item>
 21///   <item><description>The model produces a text response without requesting tool calls
 22///   (natural completion).</description></item>
 23/// </list>
 24/// </remarks>
 25/// <example>
 26/// <code>
 27/// var options = new IterativeLoopOptions
 28/// {
 29///     LoopName = "article-writer",
 30///     Instructions = "You are a travel article writer...",
 31///     Tools = [searchTool, writeTool, outlineTool],
 32///     PromptFactory = ctx =>
 33///     {
 34///         var article = ctx.Workspace.FileExists("article.md")
 35///             ? ctx.Workspace.TryReadFile("article.md").Value.Content
 36///             : "(empty)";
 37///         return $"Continue writing. Current article:\n{article}";
 38///     },
 39///     MaxIterations = 20,
 40///     ToolResultMode = ToolResultMode.OneRoundTrip,
 41/// };
 42///
 43/// var result = await iterativeLoop.RunAsync(options, cancellationToken);
 44/// </code>
 45/// </example>
 46public sealed class IterativeLoopOptions
 47{
 48    /// <summary>
 49    /// Gets or sets a human-readable name for this loop run, used in diagnostics
 50    /// and progress events. Defaults to <c>"iterative-loop"</c>.
 51    /// </summary>
 61452    public string LoopName { get; set; } = "iterative-loop";
 53
 54    /// <summary>
 55    /// Gets or sets the system prompt (instructions) for the agent. Sent as the
 56    /// system message on every LLM call. This is constant across all iterations.
 57    /// </summary>
 35358    public required string Instructions { get; set; }
 59
 60    /// <summary>
 61    /// Gets or sets the tools available to the model. The loop matches tool call
 62    /// requests from the model against this list by function name.
 63    /// </summary>
 64    /// <remarks>
 65    /// Tools are typically created via <see cref="AIFunctionFactory"/> or obtained from
 66    /// the agent framework's function discovery. The same tool instances are reused
 67    /// across all iterations.
 68    /// </remarks>
 50569    public required IReadOnlyList<AITool> Tools { get; set; }
 70
 71    /// <summary>
 72    /// Gets or sets the factory that builds the user message for each iteration.
 73    /// Called once at the start of every iteration with the current
 74    /// <see cref="IterativeContext"/> (which includes workspace state and last tool results).
 75    /// </summary>
 76    /// <remarks>
 77    /// <para>
 78    /// This is the core extensibility point. The prompt factory reads workspace files
 79    /// to understand current state, optionally includes data from
 80    /// <see cref="IterativeContext.LastToolResults"/>, and returns a fresh user message.
 81    /// </para>
 82    /// <para>
 83    /// The returned string becomes the sole user message — there is no conversation
 84    /// history. The workspace IS the memory.
 85    /// </para>
 86    /// </remarks>
 35487    public required Func<IterativeContext, string> PromptFactory { get; set; }
 88
 89    /// <summary>
 90    /// Gets or sets the maximum number of iterations before the loop terminates.
 91    /// Defaults to <c>25</c>. Set to a lower value for cost-sensitive workloads.
 92    /// </summary>
 62793    public int MaxIterations { get; set; } = 25;
 94
 95    /// <summary>
 96    /// Gets or sets an optional predicate evaluated after each iteration. When it
 97    /// returns <see langword="true"/>, the loop terminates. The predicate receives the
 98    /// <see cref="IterativeContext"/> with updated workspace and tool results.
 99    /// </summary>
 100    /// <remarks>
 101    /// Use this for domain-specific termination conditions. For example, checking
 102    /// whether a required workspace file exists, or whether a word count target
 103    /// has been reached.
 104    /// </remarks>
 298105    public Func<IterativeContext, bool>? IsComplete { get; set; }
 106
 107    /// <summary>
 108    /// Gets or sets how tool call results are fed back to the model within a single
 109    /// iteration. Defaults to <see cref="Iterative.ToolResultMode.OneRoundTrip"/>.
 110    /// </summary>
 111    /// <seealso cref="Iterative.ToolResultMode"/>
 717112    public ToolResultMode ToolResultMode { get; set; } = ToolResultMode.OneRoundTrip;
 113
 114    /// <summary>
 115    /// Gets or sets the maximum number of tool-calling rounds within a single iteration
 116    /// when <see cref="ToolResultMode"/> is <see cref="Iterative.ToolResultMode.MultiRound"/>.
 117    /// Ignored for other modes. Defaults to <c>5</c>.
 118    /// </summary>
 119    /// <remarks>
 120    /// This is a safety valve to prevent unbounded within-iteration growth. After this many
 121    /// rounds of tool calls within one iteration, any remaining tool call requests are
 122    /// executed and stored in <see cref="IterativeContext.LastToolResults"/> for the next
 123    /// iteration.
 124    /// </remarks>
 400125    public int MaxToolRoundsPerIteration { get; set; } = 5;
 126
 127    /// <summary>
 128    /// Gets or sets the maximum cumulative tool call count across all iterations.
 129    /// When exceeded, the loop terminates with
 130    /// <see cref="TerminationReason.MaxToolCallsReached"/>.
 131    /// Defaults to <see langword="null"/> (unlimited).
 132    /// </summary>
 133    /// <remarks>
 134    /// This is a safety guard against runaway tool-calling loops that stay under
 135    /// <see cref="MaxIterations"/> but make an excessive number of tool calls per
 136    /// iteration. Set this when token cost is proportional to tool call volume
 137    /// (e.g., web search or fetch tools).
 138    /// </remarks>
 427139    public int? MaxTotalToolCalls { get; set; }
 140
 141    /// <summary>
 142    /// Gets or sets the stall detection configuration. When set, the loop
 143    /// compares consecutive iterations and terminates with
 144    /// <see cref="TerminationReason.StallDetected"/> if
 145    /// <see cref="StallDetectionOptions.ConsecutiveThreshold"/> iterations
 146    /// in a row have total token counts within
 147    /// <see cref="StallDetectionOptions.TolerancePercent"/> of each other.
 148    /// </summary>
 149    /// <remarks>
 150    /// <para>
 151    /// Stall detection catches loops where the LLM repeats identical work
 152    /// every iteration because it has no cross-iteration memory. Without stall
 153    /// detection, these loops burn through <see cref="MaxIterations"/> or
 154    /// <see cref="MaxTotalToolCalls"/> with zero useful output.
 155    /// </para>
 156    /// <para>
 157    /// When <see langword="null"/> (the default), no stall detection is
 158    /// performed — the loop relies on existing guards.
 159    /// </para>
 160    /// </remarks>
 159161    public StallDetectionOptions? StallDetection { get; set; }
 162
 163    /// <summary>
 164    /// Gets or sets an optional async callback invoked at the start of each iteration,
 165    /// before the prompt factory runs. Receives the zero-based iteration number and the
 166    /// current <see cref="IterativeContext"/>.
 167    /// </summary>
 168    /// <remarks>
 169    /// Use this for progress reporting (e.g., updating a SignalR client). Hook exceptions
 170    /// propagate directly to the caller — they are not caught by the loop's internal
 171    /// error handling.
 172    /// </remarks>
 178173    public Func<int, IterativeContext, Task>? OnIterationStart { get; set; }
 174
 175    /// <summary>
 176    /// Gets or sets an optional async callback invoked after each tool call completes.
 177    /// Receives the zero-based iteration number and the <see cref="ToolCallResult"/>.
 178    /// </summary>
 179    /// <remarks>
 180    /// Fired once per tool call, in execution order. Use for real-time progress updates
 181    /// such as streaming tool activity to a UI. Hook exceptions propagate to the caller.
 182    /// </remarks>
 153183    public Func<int, ToolCallResult, Task>? OnToolCall { get; set; }
 184
 185    /// <summary>
 186    /// Gets or sets an optional async callback invoked after each iteration completes.
 187    /// Receives the <see cref="IterationRecord"/> containing tool calls, tokens, and timing.
 188    /// </summary>
 189    /// <remarks>
 190    /// Fired after the <see cref="IterationRecord"/> is built and context is updated.
 191    /// Hook exceptions propagate to the caller.
 192    /// </remarks>
 172193    public Func<IterationRecord, Task>? OnIterationEnd { get; set; }
 194
 195    /// <summary>
 196    /// Gets or sets an optional filter that narrows the tool list on each iteration.
 197    /// Receives the zero-based iteration number, the current <see cref="IterativeContext"/>,
 198    /// and the full <see cref="Tools"/> list. Returns the subset of tools the model should
 199    /// see for that iteration.
 200    /// </summary>
 201    /// <remarks>
 202    /// <para>
 203    /// Use this for <strong>phase-gating</strong> — restricting which tools are available
 204    /// based on the current workspace state. For example, a trip planner might offer only
 205    /// <c>search</c> during the research phase and only <c>add_leg</c>/<c>book_hotel</c>
 206    /// during the build phase.
 207    /// </para>
 208    /// <para>
 209    /// When <see langword="null"/> (the default), all <see cref="Tools"/> are available on
 210    /// every iteration.
 211    /// </para>
 212    /// </remarks>
 213    /// <example>
 214    /// <code>
 215    /// ToolFilter = (iteration, ctx, allTools) =>
 216    /// {
 217    ///     var phase = ctx.Workspace.TryReadFile("status.json").Value.Content;
 218    ///     var allowed = phase.Contains("research")
 219    ///         ? new[] { "search" }
 220    ///         : new[] { "add_leg", "book_hotel", "validate_trip" };
 221    ///     return allTools.Where(t => allowed.Contains(t.Name)).ToList();
 222    /// };
 223    /// </code>
 224    /// </example>
 176225    public Func<int, IterativeContext, IReadOnlyList<AITool>, IReadOnlyList<AITool>>? ToolFilter { get; set; }
 226
 227    /// <summary>
 228    /// An optional <see cref="Context.IAgentExecutionContext"/> to use for
 229    /// bridging workspace state to DI-resolved tools.
 230    /// </summary>
 231    /// <remarks>
 232    /// <para>
 233    /// When set, the loop uses this context (and its workspace) for the
 234    /// <see cref="Context.IAgentExecutionContextAccessor"/> scope. When
 235    /// <see langword="null"/> (the default), the loop auto-creates a context
 236    /// from the <see cref="IterativeContext.Workspace"/> if an accessor is
 237    /// available via DI.
 238    /// </para>
 239    /// <para>
 240    /// This is the <strong>bootstrap execution context only</strong> — it is
 241    /// NOT the same as the full application execution context. It exists
 242    /// solely so that DI-resolved tool classes can call
 243    /// <c>accessor.Current.GetRequiredWorkspace()</c> during loop execution.
 244    /// </para>
 245    /// </remarks>
 11246    public Context.IAgentExecutionContext? ExecutionContext { get; set; }
 247
 248    private double? _budgetPressureThreshold;
 249
 250    /// <summary>
 251    /// Gets or sets the fraction of the token budget at which the loop injects
 252    /// a finalization instruction. When
 253    /// <see cref="Budget.ITokenBudgetTracker.CurrentTokens"/> divided by
 254    /// <see cref="Budget.ITokenBudgetTracker.MaxTokens"/> reaches this value,
 255    /// the loop prepends <see cref="BudgetPressureInstruction"/> to the next
 256    /// iteration's prompt and runs one final iteration before terminating with
 257    /// <see cref="TerminationReason.BudgetPressure"/>.
 258    /// </summary>
 259    /// <remarks>
 260    /// Defaults to <see langword="null"/> (disabled). Set to e.g. <c>0.8</c>
 261    /// (80%) to give the agent one iteration to finalize cleanly before the
 262    /// hard budget limit cancels the chat client. Must be between 0.0 and 1.0
 263    /// (exclusive) when set.
 264    /// </remarks>
 265    /// <exception cref="ArgumentOutOfRangeException">Value is not between 0 and 1.</exception>
 266    public double? BudgetPressureThreshold
 267    {
 373268        get => _budgetPressureThreshold;
 269        set
 270        {
 2271            if (value is < 0.0 or >= 1.0)
 0272                throw new ArgumentOutOfRangeException(nameof(value), "BudgetPressureThreshold must be between 0.0 (inclu
 2273            _budgetPressureThreshold = value;
 2274        }
 275    }
 276
 277    /// <summary>
 278    /// Gets or sets the instruction prepended to the user message on the
 279    /// budget-pressure finalization iteration.
 280    /// </summary>
 1281    public string BudgetPressureInstruction { get; set; } =
 180282        "⚠️ TOKEN BUDGET PRESSURE: You are approaching the token budget limit. " +
 180283        "Finalize your work NOW. Write any remaining output and stop. " +
 180284        "Do not start new research or tool-heavy operations.";
 285
 286    /// <summary>
 287    /// Gets or sets when to check <see cref="IsComplete"/> relative to tool call
 288    /// execution within an iteration. Defaults to
 289    /// <see cref="ToolCompletionCheckMode.None"/> (check only between iterations).
 290    /// </summary>
 291    /// <remarks>
 292    /// <para>
 293    /// When set to <see cref="ToolCompletionCheckMode.AfterToolRounds"/>, the loop
 294    /// checks <see cref="IsComplete"/> after each round's batch of tool calls
 295    /// completes. When set to <see cref="ToolCompletionCheckMode.AfterEachToolCall"/>,
 296    /// the check runs after each individual tool call, and remaining tool calls in
 297    /// the batch are skipped if the predicate returns <see langword="true"/>.
 298    /// </para>
 299    /// <para>
 300    /// Both modes terminate the loop with
 301    /// <see cref="TerminationReason.CompletedEarlyAfterToolCall"/> when the check
 302    /// fires, allowing callers to distinguish early completion from the standard
 303    /// between-iteration completion (<see cref="TerminationReason.Completed"/>).
 304    /// </para>
 305    /// </remarks>
 579306    public ToolCompletionCheckMode CheckCompletionAfterToolCalls { get; set; }
 307
 308    /// <summary>
 309    /// Optional factory that wraps the <see cref="Microsoft.Extensions.AI.IChatClient"/>
 310    /// used for LLM calls within this loop run. Use this to inject per-loop middleware
 311    /// such as <c>ReducingChatClient</c> to cap within-iteration conversation growth.
 312    /// When <see langword="null"/>, the global chat client from
 313    /// <see cref="IChatClientAccessor"/> is used unmodified.
 314    /// </summary>
 315    /// <remarks>
 316    /// When both <see cref="ChatReducer"/> and <see cref="ChatClientFactory"/> are set,
 317    /// the reducer is applied first (innermost) and the factory wraps the result.
 318    /// </remarks>
 319    /// <example>
 320    /// <code>
 321    /// var loopOptions = new IterativeLoopOptions
 322    /// {
 323    ///     ChatClientFactory = inner => new ChatClientBuilder(inner)
 324    ///         .UseChatReducer(new MessageCountingChatReducer(maxNonSystemMessages: 20))
 325    ///         .Build(),
 326    /// };
 327    /// </code>
 328    /// </example>
 129329    public Func<Microsoft.Extensions.AI.IChatClient, Microsoft.Extensions.AI.IChatClient>? ChatClientFactory { get; set;
 330
 331    /// <summary>
 332    /// Optional <see cref="Microsoft.Extensions.AI.IChatReducer"/> that automatically
 333    /// wraps the per-loop <see cref="Microsoft.Extensions.AI.IChatClient"/> with a
 334    /// <c>ReducingChatClient</c>. This is a convenience alternative to composing a
 335    /// <see cref="ChatClientFactory"/> manually.
 336    /// </summary>
 337    /// <remarks>
 338    /// <para>
 339    /// When set, the loop wraps the chat client with a <c>ReducingChatClient</c> using
 340    /// this reducer. If <see cref="ChatClientFactory"/> is also set, the reducer is
 341    /// applied first (innermost) and the factory wraps the result.
 342    /// </para>
 343    /// <para>
 344    /// Typical usage is to set this to a <c>MessageCountingChatReducer</c> or
 345    /// <c>SummarizingChatReducer</c> to prevent context window exhaustion during
 346    /// long-running iterative loops.
 347    /// </para>
 348    /// </remarks>
 349    /// <example>
 350    /// <code>
 351    /// var loopOptions = new IterativeLoopOptions
 352    /// {
 353    ///     ChatReducer = new MessageCountingChatReducer(maxNonSystemMessages: 30),
 354    /// };
 355    /// </code>
 356    /// </example>
 125357    public Microsoft.Extensions.AI.IChatReducer? ChatReducer { get; set; }
 358}