Budgetende, Stufe 2: laufende Laeufe reservieren ihr Budget (#716)
Der offene Rest aus #682: Jeder Lauf pruefte das Budget einzeln gegen den bisher erfassten Verbrauch. Laufende Laeufe waren unsichtbar, gleichzeitig startende passierten die Pruefung gemeinsam und ueberzogen gemeinsam. - BudgetGuard.ReserveAsync prueft und reserviert unter einer Sperre. Die Reservierung ist der Mittelwert der letzten 20 Laeufe des Agenten (Laeufe ohne Verbrauch zaehlen nicht mit) und gilt, bis der Verbrauch des Laufs erfasst ist. - Gegen die Grenze zaehlt: verbraucht + in laufenden Laeufen reserviert. Die eigene Schaetzung eines Laufs zaehlt bewusst nicht mit; ein einzelner Lauf wird weiter nur abgelehnt, wenn das Budget schon verbraucht ist. Sonst fiele eine kleine Aufgabe wegen eines grossen Durchschnitts aus. - Gilt fuer Kosten- und Tokengrenze, je Agent und je Instanz. - Die Ablehnungsmeldung nennt den reservierten Anteil, sonst wirkte "0,60 von 1,00 USD verbraucht" als Ablehnungsgrund falsch. - IUsageRepository.GetRecentForAgentAsync, mit leerer Vorgabe im Interface, damit bestehende Attrappen unberuehrt bleiben. Tests: BudgetReservationTests (8 neu), darunter zehn gleichzeitige Starts, von denen genau vier passieren. 1.246 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5f3a85595b
commit
8a83a67fb4
@@ -125,6 +125,29 @@ public sealed class SqliteUsageRepository : IUsageRepository
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RunUsage>> GetRecentForAgentAsync(string agentId, int limit, CancellationToken ct)
|
||||
{
|
||||
await using var conn = await _storage.OpenConnectionAsync(ct);
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
SELECT Id, AgentId, Model, PromptTokens, CompletionTokens, CachedTokens,
|
||||
CostUsd, CostIsKnown, Status, StepCount, DurationMs, OccurredAt
|
||||
FROM RunUsage
|
||||
WHERE AgentId = @agent COLLATE NOCASE
|
||||
ORDER BY OccurredAt DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("@agent", agentId);
|
||||
cmd.Parameters.AddWithValue("@limit", Math.Clamp(limit, 1, 1000));
|
||||
|
||||
var results = new List<RunUsage>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
while (await reader.ReadAsync(ct))
|
||||
results.Add(Read(reader));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public Task<int> PurgeBeforeAsync(DateOnly date, CancellationToken ct)
|
||||
=> _storage.WriteAsync(async conn =>
|
||||
{
|
||||
|
||||
@@ -53,6 +53,14 @@ public interface IUsageRepository
|
||||
|
||||
Task<IReadOnlyList<RunUsage>> GetRecentAsync(int limit, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// #716: The most recent runs of one agent — the basis for estimating what a run of
|
||||
/// it will cost. Without an implementation there is no estimate, and a reservation
|
||||
/// counts nothing.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<RunUsage>> GetRecentForAgentAsync(string agentId, int limit, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<RunUsage>>([]);
|
||||
|
||||
/// <summary>Entfernt Einträge älter als das angegebene Datum. Gibt die Anzahl zurück.</summary>
|
||||
Task<int> PurgeBeforeAsync(DateOnly date, CancellationToken ct);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user