- Neu PolyTrader.Core/Agents: AgentBudget (Steps/Tokens/Timeout), LoopGuard (thread-safe, AgentBudgetExceededException mit Kind), PermissionGate (Tool-Allow-List, null = alle erlaubt). Aus ClawdDotNet portiert, NICHT als Abhaengigkeit (.NET 10 vs 8). - SupervisorAgent nutzt LoopGuard (Default-Steps = MaxIterations=8, rueckwaerts- kompatibel) + PermissionGate (Allow-List = angebotene Tools; nicht freigegebene Calls liefern Fehlertext statt Ausfuehrung). SupervisorProfile.Budget ueber- schreibt den Deckel. Abbruch graceful mit Grund (Steps/Tokens/Zeit). - Tests: AgentGuardTests (LoopGuard/PermissionGate) + Token-Abbruch am Agenten. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.7 KiB
C#
72 lines
2.7 KiB
C#
using System;
|
||
using System.Diagnostics;
|
||
using System.Threading;
|
||
|
||
namespace PolyTrader.Core.Agents
|
||
{
|
||
/// <summary>Welche Budget-Dimension überschritten wurde.</summary>
|
||
public enum AgentBudgetKind
|
||
{
|
||
Steps,
|
||
Tokens,
|
||
Time
|
||
}
|
||
|
||
/// <summary>Wird geworfen, sobald eine Budget-Dimension eines Agenten-Laufs überschritten ist.</summary>
|
||
public sealed class AgentBudgetExceededException : Exception
|
||
{
|
||
public AgentBudgetKind Kind { get; }
|
||
|
||
public AgentBudgetExceededException(AgentBudgetKind kind, string message) : base(message)
|
||
{
|
||
Kind = kind;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Erzwingt ein <see cref="AgentBudget"/> für EINEN Agenten-Lauf (nicht wiederverwenden – pro Lauf neu
|
||
/// instanziieren). Aus ClawdDotNet übernommen und um ein Zeitbudget ergänzt. Thread-safe (Interlocked),
|
||
/// damit auch parallele Tool-Aufrufe sauber gezählt werden.
|
||
/// </summary>
|
||
public sealed class LoopGuard
|
||
{
|
||
private readonly AgentBudget _budget;
|
||
private readonly Stopwatch _clock = Stopwatch.StartNew();
|
||
private int _steps;
|
||
private int _tokens;
|
||
|
||
public LoopGuard(AgentBudget budget) => _budget = budget ?? AgentBudget.Default;
|
||
|
||
public int Steps => Volatile.Read(ref _steps);
|
||
public int Tokens => Volatile.Read(ref _tokens);
|
||
public TimeSpan Elapsed => _clock.Elapsed;
|
||
|
||
/// <summary>Zählt eine Modell-Runde. Wirft bei Überschreitung der Schrittzahl oder des Zeitbudgets.</summary>
|
||
public void RecordStep()
|
||
{
|
||
if (Interlocked.Increment(ref _steps) > _budget.MaxSteps)
|
||
throw new AgentBudgetExceededException(AgentBudgetKind.Steps,
|
||
$"Maximale Schrittzahl ({_budget.MaxSteps}) überschritten.");
|
||
ThrowIfExpired();
|
||
}
|
||
|
||
/// <summary>Addiert verbrauchte Tokens. Wirft bei Überschreitung des Token-Budgets.</summary>
|
||
public void RecordTokens(int count)
|
||
{
|
||
if (count <= 0) return;
|
||
int total = Interlocked.Add(ref _tokens, count);
|
||
if (total > _budget.MaxTokens)
|
||
throw new AgentBudgetExceededException(AgentBudgetKind.Tokens,
|
||
$"Maximales Token-Budget ({_budget.MaxTokens}) überschritten (verbraucht: {total}).");
|
||
}
|
||
|
||
/// <summary>Wirft, wenn das Zeitbudget abgelaufen ist.</summary>
|
||
public void ThrowIfExpired()
|
||
{
|
||
if (_clock.Elapsed > _budget.Timeout)
|
||
throw new AgentBudgetExceededException(AgentBudgetKind.Time,
|
||
$"Zeitbudget ({_budget.Timeout.TotalSeconds:0}s) überschritten.");
|
||
}
|
||
}
|
||
}
|