< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Budget.TokenBudgetTracker
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Budget/TokenBudgetTracker.cs
Line coverage
95%
Covered lines: 70
Uncovered lines: 3
Coverable lines: 73
Total lines: 160
Line coverage: 95.8%
Branch coverage
91%
Covered branches: 42
Total branches: 46
Branch coverage: 91.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
BeginScope(...)100%22100%
BeginScope(...)100%11100%
BeginTrackingScope()100%11100%
BeginChildScope(...)66.66%6692.3%
get_CurrentTokens()100%22100%
get_CurrentInputTokens()100%22100%
get_CurrentOutputTokens()100%22100%
get_MaxTokens()100%22100%
get_MaxInputTokens()100%22100%
get_MaxOutputTokens()100%22100%
get_BudgetCancellationToken()50%22100%
Record(...)100%22100%
Record(...)100%22100%
.ctor(...)100%22100%
get_Name()100%210%
get_MaxInputTokens()100%11100%
get_MaxOutputTokens()100%11100%
get_MaxTotalTokens()100%11100%
get_CurrentInputTokens()100%11100%
get_CurrentOutputTokens()100%11100%
get_CurrentTotalTokens()100%11100%
get_CancellationToken()100%11100%
AddTotal(...)100%22100%
AddDetailed(...)100%22100%
CheckBudget(...)92.85%141488.88%
Dispose()100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Budget/TokenBudgetTracker.cs

#LineLine coverage
 1namespace NexusLabs.Needlr.AgentFramework.Budget;
 2
 3/// <summary>
 4/// <see cref="AsyncLocal{T}"/>-scoped implementation of <see cref="ITokenBudgetTracker"/>
 5/// with granular input/output/total budget tracking.
 6/// </summary>
 7public sealed class TokenBudgetTracker : ITokenBudgetTracker
 8{
 19    private static readonly AsyncLocal<ScopeState?> _current = new();
 10
 11    /// <inheritdoc />
 12    public IDisposable BeginScope(long maxTokens)
 13    {
 2314        if (maxTokens <= 0)
 315            throw new ArgumentOutOfRangeException(nameof(maxTokens), "Budget must be greater than zero.");
 16
 2017        return BeginScope(maxInputTokens: null, maxOutputTokens: null, maxTotalTokens: maxTokens);
 18    }
 19
 20    /// <inheritdoc />
 21    public IDisposable BeginScope(long? maxInputTokens = null, long? maxOutputTokens = null, long? maxTotalTokens = null
 22    {
 3423        var parent = _current.Value;
 3424        var scope = new ScopeState(maxInputTokens, maxOutputTokens, maxTotalTokens, parent);
 3425        _current.Value = scope;
 3426        return scope;
 27    }
 28
 29    /// <inheritdoc />
 30    public IDisposable BeginTrackingScope()
 31    {
 532        return BeginScope(null, null, null);
 33    }
 34
 35    /// <inheritdoc />
 36    public IDisposable BeginChildScope(string name, long? maxTokens = null)
 37    {
 538        ArgumentException.ThrowIfNullOrWhiteSpace(name);
 539        if (maxTokens is <= 0)
 040            throw new ArgumentOutOfRangeException(nameof(maxTokens), "Child scope budget must be greater than zero.");
 41
 542        var parent = _current.Value
 543            ?? throw new InvalidOperationException("Cannot open a child scope without an active parent scope.");
 44
 545        var scope = new ScopeState(
 546            name: name,
 547            maxInputTokens: null,
 548            maxOutputTokens: null,
 549            maxTotalTokens: maxTokens,
 550            parent: parent);
 551        _current.Value = scope;
 552        return scope;
 53    }
 54
 55    /// <inheritdoc />
 4456    public long CurrentTokens => _current.Value?.CurrentTotalTokens ?? 0L;
 57
 58    /// <inheritdoc />
 959    public long CurrentInputTokens => _current.Value?.CurrentInputTokens ?? 0L;
 60
 61    /// <inheritdoc />
 962    public long CurrentOutputTokens => _current.Value?.CurrentOutputTokens ?? 0L;
 63
 64    /// <inheritdoc />
 5465    public long? MaxTokens => _current.Value?.MaxTotalTokens;
 66
 67    /// <inheritdoc />
 2268    public long? MaxInputTokens => _current.Value?.MaxInputTokens;
 69
 70    /// <inheritdoc />
 2271    public long? MaxOutputTokens => _current.Value?.MaxOutputTokens;
 72
 73    /// <inheritdoc />
 74    public CancellationToken BudgetCancellationToken =>
 1575        _current.Value?.CancellationToken ?? CancellationToken.None;
 76
 77    /// <inheritdoc />
 78    public void Record(long tokenCount)
 79    {
 1680        _current.Value?.AddTotal(tokenCount);
 1481    }
 82
 83    /// <inheritdoc />
 84    public void Record(long inputTokens, long outputTokens)
 85    {
 4286        _current.Value?.AddDetailed(inputTokens, outputTokens);
 1587    }
 88
 89    private sealed class ScopeState : IDisposable
 90    {
 91        private long _currentInputTokens;
 92        private long _currentOutputTokens;
 93        private long _currentTotalTokens;
 94        private readonly CancellationTokenSource _cts;
 95        private readonly ScopeState? _parent;
 96
 3997        public ScopeState(long? maxInputTokens, long? maxOutputTokens, long? maxTotalTokens, ScopeState? parent = null, 
 98        {
 3999            MaxInputTokens = maxInputTokens;
 39100            MaxOutputTokens = maxOutputTokens;
 39101            MaxTotalTokens = maxTotalTokens;
 39102            _parent = parent;
 39103            Name = name;
 104
 105            // Link to parent's CTS so parent cancellation cascades to children
 39106            _cts = parent is not null
 39107                ? CancellationTokenSource.CreateLinkedTokenSource(parent.CancellationToken)
 39108                : new CancellationTokenSource();
 39109        }
 110
 0111        public string? Name { get; }
 46112        public long? MaxInputTokens { get; }
 45113        public long? MaxOutputTokens { get; }
 108114        public long? MaxTotalTokens { get; }
 115
 25116        public long CurrentInputTokens => Volatile.Read(ref _currentInputTokens);
 25117        public long CurrentOutputTokens => Volatile.Read(ref _currentOutputTokens);
 40118        public long CurrentTotalTokens => Volatile.Read(ref _currentTotalTokens);
 119
 20120        public CancellationToken CancellationToken => _cts.Token;
 121
 122        public void AddTotal(long tokens)
 123        {
 17124            var newTotal = Interlocked.Add(ref _currentTotalTokens, tokens);
 17125            CheckBudget(CurrentInputTokens, CurrentOutputTokens, newTotal);
 17126            _parent?.AddTotal(tokens);
 3127        }
 128
 129        public void AddDetailed(long inputTokens, long outputTokens)
 130        {
 16131            var newInput = Interlocked.Add(ref _currentInputTokens, inputTokens);
 16132            var newOutput = Interlocked.Add(ref _currentOutputTokens, outputTokens);
 16133            var newTotal = Interlocked.Add(ref _currentTotalTokens, inputTokens + outputTokens);
 16134            CheckBudget(newInput, newOutput, newTotal);
 16135            _parent?.AddDetailed(inputTokens, outputTokens);
 1136        }
 137
 138        private void CheckBudget(long currentInput, long currentOutput, long currentTotal)
 139        {
 33140            if (_cts.IsCancellationRequested)
 0141                return;
 142
 33143            bool exceeded =
 33144                (MaxTotalTokens.HasValue && currentTotal >= MaxTotalTokens.Value) ||
 33145                (MaxInputTokens.HasValue && currentInput >= MaxInputTokens.Value) ||
 33146                (MaxOutputTokens.HasValue && currentOutput >= MaxOutputTokens.Value);
 147
 33148            if (exceeded)
 149            {
 9150                _cts.Cancel();
 151            }
 33152        }
 153
 154        public void Dispose()
 155        {
 39156            _current.Value = _parent;
 39157            _cts.Dispose();
 39158        }
 159    }
 160}