< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Workflows.Diagnostics.DiagnosticsFunctionCallingMiddleware
Assembly: NexusLabs.Needlr.AgentFramework.Workflows
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Diagnostics/DiagnosticsFunctionCallingMiddleware.cs
Line coverage
26%
Covered lines: 32
Uncovered lines: 91
Coverable lines: 123
Total lines: 155
Line coverage: 26%
Branch coverage
0%
Covered branches: 0
Total branches: 4
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Wire(...)100%1126.66%
SnapshotArguments(...)0%2040%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Workflows/Diagnostics/DiagnosticsFunctionCallingMiddleware.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Diagnostics;
 3
 4using Microsoft.Agents.AI;
 5
 6using NexusLabs.Needlr.AgentFramework.Diagnostics;
 7using NexusLabs.Needlr.AgentFramework.Progress;
 8
 9namespace NexusLabs.Needlr.AgentFramework.Workflows.Diagnostics;
 10
 11/// <summary>
 12/// Innermost middleware layer: wraps each tool/function invocation to capture per-call
 13/// timing and custom metrics. Emits <see cref="ToolCallStartedEvent"/> and
 14/// <see cref="ToolCallCompletedEvent"/> to the progress reporter in real-time.
 15/// </summary>
 16internal static class DiagnosticsFunctionCallingMiddleware
 17{
 18    internal static void Wire(
 19        AIAgentBuilder builder,
 20        IAgentMetrics metrics,
 21        IProgressReporterAccessor progressAccessor,
 22        IToolCallCollector? toolCallCollector = null)
 23    {
 3924        FunctionInvocationDelegatingAgentBuilderExtensions.Use(
 3925            builder,
 3926            async (agent, context, next, cancellationToken) =>
 3927            {
 028                var diagnosticsBuilder = AgentRunDiagnosticsBuilder.GetCurrent();
 029                var sequence = diagnosticsBuilder?.NextToolCallSequence() ?? -1;
 030                var startedAt = DateTimeOffset.UtcNow;
 031                var stopwatch = Stopwatch.StartNew();
 3932
 033                var toolName = context.Function?.Name ?? "unknown";
 3934
 035                using var activity = metrics.ActivitySource.StartActivity($"agent.tool {toolName}", ActivityKind.Interna
 036                activity?.SetTag("agent.tool.name", toolName);
 037                activity?.SetTag("agent.tool.sequence", sequence);
 038                activity?.SetTag("gen_ai.agent.name", diagnosticsBuilder?.AgentName);
 3939
 040                progressAccessor.Current.Report(new ToolCallStartedEvent(
 041                    Timestamp: startedAt,
 042                    WorkflowId: progressAccessor.Current.WorkflowId,
 043                    AgentId: progressAccessor.Current.AgentId,
 044                    ParentAgentId: diagnosticsBuilder?.ParentAgentName,
 045                    Depth: progressAccessor.Current.Depth,
 046                    SequenceNumber: progressAccessor.Current.NextSequence(),
 047                    ToolName: toolName));
 3948
 049                var customMetrics = new ConcurrentDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 050                ToolMetricsAccessor.CurrentToolMetrics.Value = customMetrics;
 3951
 3952                try
 3953                {
 054                    var result = await next(context, cancellationToken).ConfigureAwait(false);
 055                    stopwatch.Stop();
 3956
 057                    activity?.SetTag("status", "success");
 3958
 059                    if (activity is not null && customMetrics.Count > 0)
 3960                    {
 061                        foreach (var (key, value) in customMetrics)
 062                            activity.SetTag($"tool.custom.{key}", value);
 3963                    }
 3964
 065                    metrics.RecordToolCall(toolName, stopwatch.Elapsed, succeeded: true, agentName: diagnosticsBuilder?.
 3966
 067                    var toolDiag = new ToolCallDiagnostics(
 068                        Sequence: sequence,
 069                        ToolName: toolName,
 070                        Duration: stopwatch.Elapsed,
 071                        Succeeded: true,
 072                        ErrorMessage: null,
 073                        StartedAt: startedAt,
 074                        CompletedAt: DateTimeOffset.UtcNow,
 075                        CustomMetrics: customMetrics.Count > 0 ? customMetrics : null)
 076                    {
 077                        AgentName = diagnosticsBuilder?.AgentName,
 078                        Arguments = SnapshotArguments(context.Arguments),
 079                        Result = result,
 080                        ArgumentsCharCount = DiagnosticsCharCounter.JsonLength(SnapshotArguments(context.Arguments)),
 081                        ResultCharCount = DiagnosticsCharCounter.JsonLength(result),
 082                    };
 083                    diagnosticsBuilder?.AddToolCall(toolDiag);
 084                    (toolCallCollector as ToolCallCollector)?.Add(toolDiag);
 3985
 086                    progressAccessor.Current.Report(new ToolCallCompletedEvent(
 087                        Timestamp: DateTimeOffset.UtcNow,
 088                        WorkflowId: progressAccessor.Current.WorkflowId,
 089                        AgentId: progressAccessor.Current.AgentId,
 090                        ParentAgentId: diagnosticsBuilder?.ParentAgentName,
 091                        Depth: progressAccessor.Current.Depth,
 092                        SequenceNumber: progressAccessor.Current.NextSequence(),
 093                        ToolName: toolName,
 094                        Duration: stopwatch.Elapsed,
 095                        CustomMetrics: customMetrics.Count > 0 ? customMetrics : null));
 3996
 097                    return result;
 3998                }
 099                catch (Exception ex)
 39100                {
 0101                    stopwatch.Stop();
 39102
 0103                    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
 0104                    activity?.SetTag("status", "failed");
 39105
 0106                    metrics.RecordToolCall(toolName, stopwatch.Elapsed, succeeded: false, agentName: diagnosticsBuilder?
 39107
 0108                    var failedToolDiag = new ToolCallDiagnostics(
 0109                        Sequence: sequence,
 0110                        ToolName: toolName,
 0111                        Duration: stopwatch.Elapsed,
 0112                        Succeeded: false,
 0113                        ErrorMessage: ex.Message,
 0114                        StartedAt: startedAt,
 0115                        CompletedAt: DateTimeOffset.UtcNow,
 0116                        CustomMetrics: customMetrics.Count > 0 ? customMetrics : null)
 0117                    {
 0118                        AgentName = diagnosticsBuilder?.AgentName,
 0119                        Arguments = SnapshotArguments(context.Arguments),
 0120                        ArgumentsCharCount = DiagnosticsCharCounter.JsonLength(SnapshotArguments(context.Arguments)),
 0121                    };
 0122                    diagnosticsBuilder?.AddToolCall(failedToolDiag);
 0123                    (toolCallCollector as ToolCallCollector)?.Add(failedToolDiag);
 39124
 0125                    progressAccessor.Current.Report(new ToolCallFailedEvent(
 0126                        Timestamp: DateTimeOffset.UtcNow,
 0127                        WorkflowId: progressAccessor.Current.WorkflowId,
 0128                        AgentId: progressAccessor.Current.AgentId,
 0129                        ParentAgentId: diagnosticsBuilder?.ParentAgentName,
 0130                        Depth: progressAccessor.Current.Depth,
 0131                        SequenceNumber: progressAccessor.Current.NextSequence(),
 0132                        ToolName: toolName,
 0133                        ErrorMessage: ex.Message,
 0134                        Duration: stopwatch.Elapsed));
 39135
 0136                    throw;
 39137                }
 39138                finally
 39139                {
 0140                    ToolMetricsAccessor.CurrentToolMetrics.Value = null;
 39141                }
 39142            });
 39143    }
 144
 145    private static IReadOnlyDictionary<string, object?>? SnapshotArguments(
 146        IDictionary<string, object?>? arguments)
 147    {
 0148        if (arguments is null || arguments.Count == 0)
 149        {
 0150            return null;
 151        }
 152
 0153        return new Dictionary<string, object?>(arguments);
 154    }
 155}