< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Workflows.Middleware.ToolResultFunctionMiddleware
Assembly: NexusLabs.Needlr.AgentFramework.Workflows
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Middleware/ToolResultFunctionMiddleware.cs
Line coverage
95%
Covered lines: 19
Uncovered lines: 1
Coverable lines: 20
Total lines: 99
Line coverage: 95%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Configure(...)100%1187.5%
HandleInvocationAsync()100%44100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Middleware/ToolResultFunctionMiddleware.cs

#LineLine coverage
 1using Microsoft.Agents.AI;
 2using Microsoft.Extensions.AI;
 3
 4using NexusLabs.Needlr.AgentFramework;
 5using NexusLabs.Needlr.AgentFramework.Tools;
 6
 7namespace NexusLabs.Needlr.AgentFramework.Workflows.Middleware;
 8
 9/// <summary>
 10/// MAF function-invocation middleware that intercepts <c>[AgentFunction]</c> return values and
 11/// exceptions, ensuring the LLM always receives a structured JSON response instead of a raw
 12/// stack trace.
 13/// </summary>
 14/// <remarks>
 15/// <para>
 16/// When an <c>[AgentFunction]</c> method returns a <see cref="IToolResult"/>:
 17/// <list type="bullet">
 18/// <item>
 19/// <description>Success — the LLM receives the <see cref="IToolResult.BoxedValue"/> directly.</description>
 20/// </item>
 21/// <item>
 22/// <description>
 23/// Failure — the LLM receives <c>{ "error": { … } }</c> (the <see cref="IToolResult.BoxedError"/>
 24/// wrapped), and the original <see cref="Exception"/> is preserved on <see cref="IToolResult.Exception"/>
 25/// for diagnostics.
 26/// </description>
 27/// </item>
 28/// </list>
 29/// </para>
 30/// <para>
 31/// When an <c>[AgentFunction]</c> throws an <em>unhandled</em> exception, the middleware catches it,
 32/// wraps it in an <see cref="ToolResult.UnhandledFailure"/> result, and returns a safe generic error
 33/// message to the LLM. <see cref="IToolResult.IsTransient"/> is <see langword="null"/> in this case.
 34/// </para>
 35/// <para>
 36/// <see cref="OperationCanceledException"/> is intentionally <em>not</em> caught — it propagates so
 37/// cooperative cancellation (parent timeouts, user cancels, structured-concurrency aborts) continues
 38/// to function correctly. Tools that legitimately catch and translate cancellation should do so
 39/// inside the tool body, not rely on this middleware.
 40/// </para>
 41/// <para>
 42/// Non-<see cref="IToolResult"/> return values pass through unchanged.
 43/// </para>
 44/// </remarks>
 45public sealed class ToolResultFunctionMiddleware : IAIAgentBuilderPlugin
 46{
 47    /// <inheritdoc />
 48    public void Configure(AIAgentBuilderPluginOptions options)
 49    {
 750        ArgumentNullException.ThrowIfNull(options);
 51
 652        FunctionInvocationDelegatingAgentBuilderExtensions.Use(
 653            options.AgentBuilder,
 654            async (agent, context, next, cancellationToken) =>
 655                await HandleInvocationAsync(
 056                    invokeNext: ct => next(context, ct),
 657                    cancellationToken: cancellationToken).ConfigureAwait(false));
 658    }
 59
 60    /// <summary>
 61    /// Core middleware logic: invoke <paramref name="invokeNext"/>, translate exceptions into
 62    /// <see cref="IToolResult"/> failures, and unwrap <see cref="IToolResult"/> returns into
 63    /// LLM-facing <see cref="IToolResult.BoxedValue"/> or <c>{ error: BoxedError }</c>.
 64    /// Cooperative <see cref="OperationCanceledException"/> propagates unchanged so cancellation
 65    /// signals are not swallowed.
 66    /// </summary>
 67    /// <remarks>
 68    /// Internal-but-exposed-via-<c>InternalsVisibleTo</c> for direct unit testing — exercising the
 69    /// translation logic without standing up a full agent pipeline.
 70    /// </remarks>
 71    internal static async ValueTask<object?> HandleInvocationAsync(
 72        Func<CancellationToken, ValueTask<object?>> invokeNext,
 73        CancellationToken cancellationToken)
 74    {
 75        object? raw;
 76
 77        try
 78        {
 679            raw = await invokeNext(cancellationToken).ConfigureAwait(false);
 380        }
 281        catch (OperationCanceledException)
 82        {
 283            throw;
 84        }
 85        catch (Exception ex)
 86        {
 187            raw = ToolResult.UnhandledFailure(ex);
 188        }
 89
 490        if (raw is IToolResult result)
 91        {
 392            return result.IsSuccess
 393                ? result.BoxedValue
 394                : new { error = result.BoxedError };
 95        }
 96
 197        return raw;
 498    }
 99}