< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Diagnostics.AgentRunDiagnosticsBuilder
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/AgentRunDiagnosticsBuilder.cs
Line coverage
100%
Covered lines: 85
Uncovered lines: 0
Coverable lines: 85
Total lines: 249
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_AgentName()100%11100%
get_ParentAgentName()100%22100%
get_StartedAt()100%11100%
StartNew(...)100%11100%
StartNew(...)100%11100%
GetCurrent()100%11100%
NextToolCallSequence()100%11100%
NextChatCompletionSequence()100%11100%
AddChatCompletion(...)100%11100%
AddToolCall(...)100%11100%
RecordInputMessageCount(...)100%11100%
RecordOutputMessageCount(...)100%11100%
RecordInputMessages(...)100%11100%
RecordOutputResponse(...)100%11100%
RecordFailure(...)100%11100%
SetExecutionMode(...)100%11100%
Build()100%22100%
ClearCurrent()100%11100%
Dispose()100%11100%
ForwardToSecondarySinks(...)100%66100%
ForwardToSecondarySinks(...)100%66100%

File(s)

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

#LineLine coverage
 1using System.Collections.Concurrent;
 2
 3using Microsoft.Agents.AI;
 4using Microsoft.Extensions.AI;
 5
 6namespace NexusLabs.Needlr.AgentFramework.Diagnostics;
 7
 8/// <summary>
 9/// Thread-safe accumulator for diagnostics captured during a single agent run.
 10/// Implements <see cref="IDiagnosticsSink"/> so writers (middleware, loops) record
 11/// through a stable interface rather than coupling to the concrete builder.
 12/// </summary>
 13/// <remarks>
 14/// <para>
 15/// Stored in an <see cref="AsyncLocal{T}"/> so middleware layers access the same
 16/// builder instance within an async flow. Call <see cref="StartNew(string)"/> at the
 17/// beginning of an agent run and dispose the returned builder when the run completes.
 18/// </para>
 19/// <para>
 20/// Each event class has a single designated writer. Chat completions are written
 21/// exclusively by <see cref="DiagnosticsChatClientMiddleware"/>. Tool calls are
 22/// written exclusively by the <c>IterativeAgentLoop</c> (MEAI path) or
 23/// <c>DiagnosticsFunctionCallingMiddleware</c> (MAF path). Two writers for the same
 24/// event class is a bug.
 25/// </para>
 26/// <para>
 27/// Sequence numbers are reserved BEFORE async work begins (via
 28/// <see cref="Interlocked.Increment(ref int)"/>), ensuring parallel tool calls are
 29/// ordered by invocation time, not completion time.
 30/// </para>
 31/// </remarks>
 32public sealed class AgentRunDiagnosticsBuilder : IDiagnosticsSink, IDisposable
 33{
 134    private static readonly AsyncLocal<AgentRunDiagnosticsBuilder?> CurrentBuilder = new();
 35
 30636    private readonly ConcurrentQueue<ChatCompletionDiagnostics> _chatCompletions = new();
 30637    private readonly ConcurrentQueue<ToolCallDiagnostics> _toolCalls = new();
 38    private readonly AgentRunDiagnosticsBuilder? _previousBuilder;
 39    private readonly IReadOnlyList<IDiagnosticsSink>? _secondarySinks;
 40
 41    private int _nextChatCompletionSequence;
 42    private int _nextToolCallSequence;
 43
 44    private long _totalInputTokens;
 45    private long _totalOutputTokens;
 46    private long _totalTokens;
 47    private long _cachedInputTokens;
 48    private long _reasoningTokens;
 49
 50    private int _totalInputMessages;
 51    private int _totalOutputMessages;
 30652    private bool _succeeded = true;
 53    private string? _errorMessage;
 54    private string? _executionMode;
 55    private IReadOnlyList<ChatMessage>? _inputMessages;
 56    private AgentResponse? _outputResponse;
 57
 82458    public string AgentName { get; }
 59
 60    /// <summary>
 61    /// Gets the name of the parent (outer) agent when this builder was created
 62    /// inside a nested sub-agent run, or <see langword="null"/> if this is a
 63    /// top-level agent.
 64    /// </summary>
 10865    public string? ParentAgentName => _previousBuilder?.AgentName;
 66
 56867    public DateTimeOffset StartedAt { get; }
 68
 30669    private AgentRunDiagnosticsBuilder(
 30670        string agentName,
 30671        AgentRunDiagnosticsBuilder? previous,
 30672        IReadOnlyList<IDiagnosticsSink>? secondarySinks)
 73    {
 30674        AgentName = agentName;
 30675        StartedAt = DateTimeOffset.UtcNow;
 30676        _previousBuilder = previous;
 30677        _secondarySinks = secondarySinks;
 30678    }
 79
 80    /// <summary>
 81    /// Creates a new builder and stores it in the current async flow so middleware can access it.
 82    /// If a builder already exists (nested sub-agent scenario), the previous builder is saved
 83    /// and restored when this builder is disposed.
 84    /// </summary>
 85    public static AgentRunDiagnosticsBuilder StartNew(string agentName)
 86    {
 29887        var previous = CurrentBuilder.Value;
 29888        var builder = new AgentRunDiagnosticsBuilder(agentName, previous, secondarySinks: null);
 29889        CurrentBuilder.Value = builder;
 29890        return builder;
 91    }
 92
 93    /// <summary>
 94    /// Creates a new builder with secondary sinks that receive forwarded diagnostic
 95    /// records. The builder remains the primary accumulator; secondary sinks receive
 96    /// copies of each <see cref="AddChatCompletion"/> and <see cref="AddToolCall"/>
 97    /// call on a best-effort basis (sink failures are swallowed).
 98    /// </summary>
 99    /// <param name="agentName">The name of the agent being traced.</param>
 100    /// <param name="secondarySinks">Additional sinks to fan out to, or <see langword="null"/>.</param>
 101    public static AgentRunDiagnosticsBuilder StartNew(
 102        string agentName,
 103        IReadOnlyList<IDiagnosticsSink>? secondarySinks)
 104    {
 8105        var previous = CurrentBuilder.Value;
 8106        var builder = new AgentRunDiagnosticsBuilder(agentName, previous, secondarySinks);
 8107        CurrentBuilder.Value = builder;
 8108        return builder;
 109    }
 110
 111    /// <summary>Gets the builder for the current async flow, or <see langword="null"/> if outside a run.</summary>
 320112    public static AgentRunDiagnosticsBuilder? GetCurrent() => CurrentBuilder.Value;
 113
 114    /// <summary>Reserves a sequence number for a tool call (thread-safe).</summary>
 115    public int NextToolCallSequence() =>
 166116        Interlocked.Increment(ref _nextToolCallSequence) - 1;
 117
 118    /// <summary>Reserves a sequence number for a chat completion (thread-safe).</summary>
 119    public int NextChatCompletionSequence() =>
 295120        Interlocked.Increment(ref _nextChatCompletionSequence) - 1;
 121
 122    public void AddChatCompletion(ChatCompletionDiagnostics diagnostics)
 123    {
 312124        _chatCompletions.Enqueue(diagnostics);
 125
 312126        Interlocked.Add(ref _totalInputTokens, diagnostics.Tokens.InputTokens);
 312127        Interlocked.Add(ref _totalOutputTokens, diagnostics.Tokens.OutputTokens);
 312128        Interlocked.Add(ref _totalTokens, diagnostics.Tokens.TotalTokens);
 312129        Interlocked.Add(ref _cachedInputTokens, diagnostics.Tokens.CachedInputTokens);
 312130        Interlocked.Add(ref _reasoningTokens, diagnostics.Tokens.ReasoningTokens);
 131
 312132        ForwardToSecondarySinks(diagnostics);
 312133    }
 134
 135    public void AddToolCall(ToolCallDiagnostics diagnostics)
 136    {
 177137        _toolCalls.Enqueue(diagnostics);
 177138        ForwardToSecondarySinks(diagnostics);
 177139    }
 140
 141    public void RecordInputMessageCount(int count) =>
 295142        Interlocked.Add(ref _totalInputMessages, count);
 143
 144    public void RecordOutputMessageCount(int count) =>
 288145        Interlocked.Add(ref _totalOutputMessages, count);
 146
 147    /// <summary>
 148    /// Records the full input messages supplied to the agent for this run. Captured
 149    /// once at the agent-run boundary; calling more than once replaces the snapshot.
 150    /// </summary>
 151    public void RecordInputMessages(IReadOnlyList<ChatMessage> messages) =>
 46152        Volatile.Write(ref _inputMessages, messages);
 153
 154    /// <summary>
 155    /// Records the aggregated output response produced by the agent for this run.
 156    /// Pass <see langword="null"/> when no response was produced.
 157    /// </summary>
 158    public void RecordOutputResponse(AgentResponse? response) =>
 46159        Volatile.Write(ref _outputResponse, response);
 160
 161    public void RecordFailure(string? errorMessage)
 162    {
 27163        _succeeded = false;
 27164        _errorMessage = errorMessage;
 27165    }
 166
 167    /// <summary>
 168    /// Sets the execution mode label for these diagnostics.
 169    /// Known values: <c>"FunctionInvokingChatClient"</c>, <c>"IterativeLoop"</c>.
 170    /// </summary>
 171    public void SetExecutionMode(string executionMode) =>
 125172        _executionMode = executionMode;
 173
 174    public IAgentRunDiagnostics Build()
 175    {
 284176        var completedAt = DateTimeOffset.UtcNow;
 177
 284178        return new AgentRunDiagnostics(
 284179            AgentName: AgentName,
 284180            TotalDuration: completedAt - StartedAt,
 284181            AggregateTokenUsage: new TokenUsage(
 284182                InputTokens: Volatile.Read(ref _totalInputTokens),
 284183                OutputTokens: Volatile.Read(ref _totalOutputTokens),
 284184                TotalTokens: Volatile.Read(ref _totalTokens),
 284185                CachedInputTokens: Volatile.Read(ref _cachedInputTokens),
 284186                ReasoningTokens: Volatile.Read(ref _reasoningTokens)),
 211187            ChatCompletions: _chatCompletions.OrderBy(c => c.Sequence).ToArray(),
 110188            ToolCalls: _toolCalls.OrderBy(t => t.Sequence).ToArray(),
 284189            TotalInputMessages: Volatile.Read(ref _totalInputMessages),
 284190            TotalOutputMessages: Volatile.Read(ref _totalOutputMessages),
 284191            InputMessages: Volatile.Read(ref _inputMessages) ?? Array.Empty<ChatMessage>(),
 284192            OutputResponse: Volatile.Read(ref _outputResponse),
 284193            Succeeded: _succeeded,
 284194            ErrorMessage: _errorMessage,
 284195            StartedAt: StartedAt,
 284196            CompletedAt: completedAt,
 284197            ExecutionMode: _executionMode);
 198    }
 199
 200    /// <summary>Clears the builder from the current async flow.</summary>
 1201    public static void ClearCurrent() => CurrentBuilder.Value = null;
 202
 203    /// <summary>
 204    /// Restores the previous builder (if any) to the current async flow. If this builder
 205    /// was created inside a nested sub-agent run, the outer agent's builder is restored.
 206    /// Otherwise equivalent to <see cref="ClearCurrent"/>.
 207    /// </summary>
 307208    public void Dispose() => CurrentBuilder.Value = _previousBuilder;
 209
 210    private void ForwardToSecondarySinks(ChatCompletionDiagnostics diagnostics)
 211    {
 312212        if (_secondarySinks is not { Count: > 0 })
 213        {
 308214            return;
 215        }
 216
 18217        for (var i = 0; i < _secondarySinks.Count; i++)
 218        {
 219            try
 220            {
 5221                _secondarySinks[i].AddChatCompletion(diagnostics);
 3222            }
 2223            catch
 224            {
 225                // Best-effort: secondary sink failures must not break agent execution.
 2226            }
 227        }
 4228    }
 229
 230    private void ForwardToSecondarySinks(ToolCallDiagnostics diagnostics)
 231    {
 177232        if (_secondarySinks is not { Count: > 0 })
 233        {
 173234            return;
 235        }
 236
 20237        for (var i = 0; i < _secondarySinks.Count; i++)
 238        {
 239            try
 240            {
 6241                _secondarySinks[i].AddToolCall(diagnostics);
 4242            }
 2243            catch
 244            {
 245                // Best-effort: secondary sink failures must not break agent execution.
 2246            }
 247        }
 4248    }
 249}