Grossaufgaben und Sonderbudget (#717, Stufe 2+3)
Eine Aufgabe, deren eigene Schaetzung (gemessen, Geschwister oder Autor) ueber der Haelfte der Tageskostengrenze liegt, bekommt zuerst einen Planungslauf: teilen (Teile unter der Elternaufgabe) oder Sonderbudget beantragen. Danach wartet sie auf ihre Teile bzw. die Freigabe und laeuft einmal zum Zusammenfuehren. Sonderbudget ueber Escalation.request_budget, ausnahmslos freigabepflichtig (auch gegen Werkzeugkonfiguration und Dauerfreigabe). Laeufe im Baum ziehen den Topf statt der Tagesgrenzen, tragen BudgetPot in Verbrauch und Beleg und sind in der Kostenanalyse (nach Budget) und auf der Aufgabenkarte gesondert sichtbar. Aufgebraucht zaehlen sie wieder gegen den Tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
183e922278
commit
67649559f5
@@ -54,6 +54,9 @@ public sealed class BudgetGuard
|
||||
private readonly Dictionary<string, RunEstimate> _reservedByAgent = new(StringComparer.OrdinalIgnoreCase);
|
||||
private RunEstimate _reservedInstance;
|
||||
|
||||
/// <summary>#717: What runs in flight have reserved of each special budget.</summary>
|
||||
private readonly Dictionary<string, RunEstimate> _reservedByPot = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public BudgetGuard(IUsageRepository usage, Func<DateTime>? now = null)
|
||||
{
|
||||
_usage = usage;
|
||||
@@ -109,7 +112,7 @@ public sealed class BudgetGuard
|
||||
_reservedInstance += estimate;
|
||||
}
|
||||
|
||||
return (status, new Reservation(this, agent.AgentId, estimate));
|
||||
return (status, new Reservation(() => Release(agent.AgentId, estimate)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -117,9 +120,49 @@ public sealed class BudgetGuard
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #717: Reserves a run on a special budget instead of the daily limits. Refused once what
|
||||
/// the pot's runs used, plus what runs in flight reserved, has reached the amount — the
|
||||
/// same rule as for the daily limit: a run is not refused for its own estimate.
|
||||
/// </summary>
|
||||
public async Task<IDisposable?> ReservePotAsync(SpecialBudget pot, decimal spent, string agentId, CancellationToken ct)
|
||||
{
|
||||
var estimate = await EstimateRunAsync(agentId, ct);
|
||||
|
||||
await _reserveGate.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
lock (_reservedLock)
|
||||
{
|
||||
var reserved = _reservedByPot.GetValueOrDefault(pot.TaskId);
|
||||
if (spent + reserved.CostUsd >= pot.AmountUsd)
|
||||
return null;
|
||||
_reservedByPot[pot.TaskId] = reserved + estimate;
|
||||
}
|
||||
|
||||
return new Reservation(() => ReleasePot(pot.TaskId, estimate));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_reserveGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleasePot(string potId, RunEstimate estimate)
|
||||
{
|
||||
lock (_reservedLock)
|
||||
{
|
||||
var left = _reservedByPot.GetValueOrDefault(potId) - estimate;
|
||||
if (left.CostUsd <= 0 && left.Tokens <= 0)
|
||||
_reservedByPot.Remove(potId);
|
||||
else
|
||||
_reservedByPot[potId] = left;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Today's usage of an agent, or with an empty id of the whole instance.</summary>
|
||||
public Task<DailyUsage> GetUsedTodayAsync(string agentId, CancellationToken ct)
|
||||
=> _usage.GetDailyAsync(DateOnly.FromDateTime(_now()), agentId, ct);
|
||||
=> _usage.GetDailyForBudgetAsync(DateOnly.FromDateTime(_now()), agentId, ct);
|
||||
|
||||
/// <summary>What is currently reserved by runs in flight — for the agent, or with an
|
||||
/// empty id for the whole instance.</summary>
|
||||
@@ -159,14 +202,14 @@ public sealed class BudgetGuard
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Reservation(BudgetGuard guard, string agentId, RunEstimate estimate) : IDisposable
|
||||
private sealed class Reservation(Action release) : IDisposable
|
||||
{
|
||||
private int _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
guard.Release(agentId, estimate);
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +223,7 @@ public sealed class BudgetGuard
|
||||
var agentBudget = agent.Budget;
|
||||
if (agentBudget.HasAnyLimit)
|
||||
{
|
||||
var used = await _usage.GetDailyAsync(today, agent.AgentId, ct);
|
||||
var used = await _usage.GetDailyForBudgetAsync(today, agent.AgentId, ct);
|
||||
|
||||
if (Exceeds(agentBudget.DailyCostUsd, used.CostUsd + agentReserved.CostUsd))
|
||||
{
|
||||
@@ -203,7 +246,7 @@ public sealed class BudgetGuard
|
||||
// Instanzgrenze — gilt für alle Agenten zusammen
|
||||
if (instanceBudget.HasAnyLimit)
|
||||
{
|
||||
var used = await _usage.GetDailyAsync(today, agentId: "", ct);
|
||||
var used = await _usage.GetDailyForBudgetAsync(today, agentId: "", ct);
|
||||
|
||||
if (Exceeds(instanceBudget.DailyCostUsd, used.CostUsd + instanceReserved.CostUsd))
|
||||
{
|
||||
@@ -232,7 +275,7 @@ public sealed class BudgetGuard
|
||||
if (agent.Budget.DailyCostUsd <= 0)
|
||||
return null;
|
||||
|
||||
var used = await _usage.GetDailyAsync(DateOnly.FromDateTime(_now()), agent.AgentId, ct);
|
||||
var used = await _usage.GetDailyForBudgetAsync(DateOnly.FromDateTime(_now()), agent.AgentId, ct);
|
||||
return Math.Max(0m, agent.Budget.DailyCostUsd - used.CostUsd);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user