Bisher teilte sich eine Budgetablehnung den Ausgang NichtGelaufen mit dem Herunterfahren. Einmalige Aufgaben wurden den Rest des Tages alle 30 Minuten erneut abgelehnt (jedes Mal ein Beleg) und liefen nach Mitternacht still zu Lasten des neuen Tages. Wiederkehrende verfielen still. - TaskLauf.KeinBudget als eigener Ausgang; der Dispatcher bildet BudgetExceeded darauf ab. - Vier Felder an der Aufgabe: BudgetMissCount, LastBudgetMissAt, DeferredUntil, CatchUpPending (Spalten werden fuer bestehende Datenbanken nachgeruestet). - Einmalige Aufgaben werden mitgenommen: Der Termin bleibt erhalten, faellig ist die Aufgabe wieder ab der naechsten lokalen Mitternacht, nicht vorher. - Wiederkehrende Aufgaben folgen der Regel des Agenten (budget.whenExhausted): expire vermerkt den Ausfall und laesst den Termin verfallen; catchUpOnce fasst alle verpassten Termine zu einem Nachholauf am naechsten Tag zusammen. Ein regulaer faelliger Termin deckt das Nachholen mit ab, es bleibt bei einem Lauf. - Der neue Tag hat Vorfahrt: Mitgenommenes und Nachholaeufe kommen nach allem regulaer Faelligen, unabhaengig von der Prioritaet. - Beim ersten Takt eines neuen Tages eine Zusammenfassung je Agent im Log. - Sichtbar: Die Aufgabenkarte der WebUI zeigt Ausfaelle, Mitnahme und Nachholen; die Regel ist im Desktop je Agent einstellbar. Nebenbei sind "Next:"/"Last:" auf der Karte ins Sprachpaket gewandert. Stufen 2 bis 4 (Reservierung, Spielraum statt pauschalem Anteil, Vorbeugen) folgen. Tests: BudgetMissTests (9 neu). 1.238 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
6.7 KiB
C#
181 lines
6.7 KiB
C#
using ClawdDotNet.App.Services;
|
|
using ClawdDotNet.Core.Accounting;
|
|
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Core.Engine;
|
|
using ClawdDotNet.Core.Security;
|
|
using ClawdDotNet.Core.Staging;
|
|
using ClawdDotNet.Core.Tasks;
|
|
|
|
namespace ClawdDotNet.Web.Contracts;
|
|
|
|
/// <summary>
|
|
/// Die eine Stelle, an der Kerntypen zu Übertragungstypen werden.
|
|
///
|
|
/// <para>Eine Stelle, weil die Maskierung von Zugangsdaten hier hängt. Verstreut über
|
|
/// zehn Endpunkte wäre die Frage „kommt hier ein Passwort heraus?" nicht mehr zu
|
|
/// beantworten, sondern nur noch zu hoffen.</para>
|
|
/// </summary>
|
|
internal static class Mapping
|
|
{
|
|
public static AgentDto ToDto(
|
|
this AgentConfig agent,
|
|
IReadOnlyDictionary<string, string> toolDescriptions,
|
|
bool isRunning)
|
|
=> new(
|
|
agent.AgentId,
|
|
agent.DisplayName,
|
|
agent.Model,
|
|
agent.Description,
|
|
agent.PromptCaching,
|
|
agent.MaxToolResultChars,
|
|
agent.LoopGuard.MaxSteps,
|
|
agent.LoopGuard.MaxContextTokens,
|
|
agent.LoopGuard.TimeoutSeconds,
|
|
agent.Budget.DailyCostUsd,
|
|
agent.Budget.DailyTokens,
|
|
isRunning,
|
|
AgentAdministration.KnownTools
|
|
.Select(name => new AgentToolDto(
|
|
name,
|
|
toolDescriptions.GetValueOrDefault(name) ?? "",
|
|
agent.Tools.ContainsKey(name)))
|
|
.ToList());
|
|
|
|
/// <summary>
|
|
/// Die Einstellungen eines Werkzeugs — <b>ohne die Geheimnisse</b>.
|
|
///
|
|
/// <para>Welcher Schlüssel eines ist, entscheidet <see cref="ConfigSecrets"/>: der
|
|
/// Kern weiß es bereits, und eine zweite Liste hier wäre eine, die beim nächsten
|
|
/// neuen Werkzeug vergessen wird. Statt des Werts steht in <c>Secrets</c>, ob
|
|
/// überhaupt einer gesetzt ist.</para>
|
|
/// </summary>
|
|
public static ToolSettingsDto ToDto(
|
|
string toolName, IReadOnlyDictionary<string, object?> settings)
|
|
{
|
|
var values = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
|
var secrets = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var (key, value) in settings)
|
|
{
|
|
if (ConfigSecrets.IsSecretKey(key))
|
|
secrets[key] = value is not null && value is not string { Length: 0 };
|
|
else
|
|
values[key] = value;
|
|
}
|
|
|
|
return new ToolSettingsDto(toolName, values, secrets);
|
|
}
|
|
|
|
public static ChatEntryDto ToDto(this ChatEntry entry)
|
|
=> new(entry.Role, entry.Content, entry.Timestamp, entry.Source);
|
|
|
|
public static TaskDto ToDto(this TaskOverview overview)
|
|
=> overview.Task.ToDto() with
|
|
{
|
|
NextRunUtc = overview.NextRunUtc,
|
|
LastRun = overview.LastRun is { } lauf
|
|
? new TaskLastRunDto(lauf.OccurredAt, lauf.Status, lauf.CostUsd, lauf.CostIsKnown, lauf.ResultRef)
|
|
: null,
|
|
Hinweis = overview.Hinweis
|
|
};
|
|
|
|
public static TaskInput ToInput(this TaskInputDto input)
|
|
=> new(
|
|
input.Title ?? "",
|
|
input.Body ?? "",
|
|
input.Assignee ?? "",
|
|
input.WhenKind,
|
|
input.WhenValue,
|
|
input.TimeZone,
|
|
input.Acceptance ?? "",
|
|
input.Priority);
|
|
|
|
public static ForecastDto ToDto(this InstanzPrognose prognose)
|
|
=> new(
|
|
prognose.Aufgaben.Select(a => new ForecastTaskDto(
|
|
a.TaskId, a.Title, a.AgentId, a.Model, a.LaeufeProTag, a.EinmaligOffen,
|
|
a.KostenJeLauf, a.Grundlage.ToString(), a.Stichprobe, a.KostenProTag)).ToList(),
|
|
prognose.AufgabenProTag,
|
|
prognose.SonstigeProTag,
|
|
prognose.SummeProTag,
|
|
prognose.SummeProMonat,
|
|
prognose.EinmaligOffen,
|
|
prognose.OhneKostenbasis,
|
|
prognose.Budgetwarnungen.Select(w => new ForecastBudgetDto(w.AgentId, w.KostenProTag, w.BudgetProTag)).ToList());
|
|
|
|
public static ServiceDto ToDto(this ServiceConfig service)
|
|
=> new(
|
|
service.ServiceId,
|
|
service.Name,
|
|
service.Type,
|
|
service.Port,
|
|
service.Enabled,
|
|
service.BuiltIn,
|
|
service.Description);
|
|
|
|
public static TaskDto ToDto(this TaskItem task)
|
|
=> new(
|
|
task.Id,
|
|
task.Title,
|
|
task.Status.ToString(),
|
|
task.Type.ToString(),
|
|
task.Priority,
|
|
task.Assignee,
|
|
task.RequireApproval,
|
|
task.Acceptance,
|
|
task.BlockedBy,
|
|
task.Body,
|
|
task.CreatedAt,
|
|
task.UpdatedAt,
|
|
task.When is { } when
|
|
? new TaskWhenDto(when.Kind.ToString().ToLowerInvariant(), when.Value, when.TimeZone)
|
|
: null,
|
|
BudgetMiss: task.BudgetMissCount > 0 || task.CatchUpPending || task.DeferredUntil is not null
|
|
? new TaskBudgetMissDto(task.BudgetMissCount, task.LastBudgetMissAt, task.DeferredUntil,
|
|
task.CatchUpPending, task.IsCarriedOver)
|
|
: null);
|
|
|
|
/// <summary>
|
|
/// Ein wartender Aufruf samt seiner eingefrorenen Argumente.
|
|
///
|
|
/// <para>Die Argumente gehen <b>roh</b> heraus, und das ist der ganze Sinn der
|
|
/// Freigabe: Wer entscheidet, muss sehen, was genau ausgeführt würde — nicht eine
|
|
/// Zusammenfassung davon. Sie sind zugleich Fremdtext aus einem Modell; die Anzeige
|
|
/// setzt sie als Text ein, nie als HTML.</para>
|
|
/// </summary>
|
|
public static StagedCallDto ToDto(this StagedCall call, int bisherFreigegeben = 0, bool dauerfreigabenAktiv = false)
|
|
{
|
|
var erlaubbar = dauerfreigabenAktiv && StagingService.IstDauerhaftErlaubbar(call);
|
|
return new(
|
|
call.Id,
|
|
call.RunId,
|
|
call.AgentId,
|
|
call.Tool,
|
|
call.Action,
|
|
call.Proposal,
|
|
call.Status.ToString(),
|
|
call.ArgumentsJson,
|
|
call.CreatedAt,
|
|
bisherFreigegeben,
|
|
erlaubbar,
|
|
erlaubbar && bisherFreigegeben >= StagingService.VorschlagAb);
|
|
}
|
|
|
|
public static StandingApprovalDto ToDto(this Dauerfreigabe d)
|
|
=> new(d.Id, d.AgentId, d.Tool, d.Operation, d.ErteiltVon, d.ErteiltAm, d.AnlassId, d.WiderrufenAm, d.WiderrufenVon);
|
|
|
|
public static UsageDto ToDto(this RunUsage usage)
|
|
=> new(
|
|
usage.Id,
|
|
usage.AgentId,
|
|
usage.Model,
|
|
usage.PromptTokens,
|
|
usage.CompletionTokens,
|
|
usage.CachedTokens,
|
|
usage.CostUsd,
|
|
usage.CostIsKnown,
|
|
usage.Status,
|
|
usage.StepCount,
|
|
usage.OccurredAt);
|
|
}
|