Fuellarbeit: zeitlich flexible Arbeit fuellt ungenutztes Budget (#717, Stufe 1)
Eine einmalige Aufgabe mit fill_by (spaetestens bis) statt Termin laeuft, wenn der Tag Budget uebrig laesst: nach regulaerer und nachgeholter Arbeit, nur wenn nichts vom Agenten wartet, bevorzugt ab 18 Uhr, und nur wenn ihre eigene Schaetzung in den Spielraum aus #716 passt. Kurz vor der Frist wird einmal ein Mensch gefragt; nach der Frist laeuft sie als regulaere Arbeit. Ehrliche Schaetzung (TaskCostEstimator): eigene Laeufe, dann Geschwister unter derselben Elternaufgabe, dann die Angabe des Autors (als Schaetzung gekennzeichnet), zuletzt das Mittel des Agenten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
25f74d9ad6
commit
183e922278
@@ -50,6 +50,11 @@ public sealed class TaskboardTool : IAgentTool
|
||||
The time zone 'tz' is required (e.g. 'Europe/Berlin').
|
||||
|
||||
If 'when' is missing, the task is one-time and due immediately.
|
||||
|
||||
Fill work: large work that is not urgent gets 'fill_by' (at the latest by) instead of
|
||||
a schedule. It runs when the day's budget has room that would otherwise go unused,
|
||||
and before the deadline at the latest. Give 'estimate_usd' if you can judge the cost
|
||||
of one run — it is shown as your estimate until a run has been measured.
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
@@ -116,6 +121,14 @@ public sealed class TaskboardTool : IAgentTool
|
||||
"type": "string",
|
||||
"description": "ID of the task this one serves (its purpose — unlike blocked_by, which is a precondition). Empty string removes it (task_update)."
|
||||
},
|
||||
"fill_by": {
|
||||
"type": "string",
|
||||
"description": "Fill work: at the latest by (ISO timestamp, or a date for the end of that day). Only for one-time tasks without 'when'. Empty string removes it (task_update)."
|
||||
},
|
||||
"estimate_usd": {
|
||||
"type": "number",
|
||||
"description": "Your estimate of what one run of the task costs, in USD. Shown as an estimate until runs have been measured."
|
||||
},
|
||||
"blocked_by": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
@@ -312,6 +325,9 @@ public sealed class TaskboardTool : IAgentTool
|
||||
if (parent.Length > 0 && await CheckParentAsync(null, parent, board, visible, ct) is { } parentProblem)
|
||||
return ToolResult.Fail(parentProblem);
|
||||
|
||||
if (!TryReadFill(input, when, null, out var fillBy, out var estimate, out var fillError))
|
||||
return ToolResult.Fail(fillError!);
|
||||
|
||||
// #674: The same finding a second time is not a second task. It is recorded on the
|
||||
// existing one — so that later one can see how often and for how long.
|
||||
if (GetBool(input, "force") != true)
|
||||
@@ -356,6 +372,8 @@ public sealed class TaskboardTool : IAgentTool
|
||||
// #684: decides the board when the task is for a human or a fresh run.
|
||||
CreatedBy = author,
|
||||
ParentId = parent,
|
||||
FillBy = fillBy,
|
||||
EstimateUsd = estimate,
|
||||
// #697: Where it came from — the engine's knowledge, never an argument of the agent.
|
||||
OriginRunId = context.RunId ?? "",
|
||||
OriginTaskId = context.RunTaskId ?? ""
|
||||
@@ -420,6 +438,9 @@ public sealed class TaskboardTool : IAgentTool
|
||||
if (parent is { Length: > 0 } && await CheckParentAsync(id.Trim(), parent, board, visible, ct) is { } parentProblem)
|
||||
return ToolResult.Fail(parentProblem);
|
||||
|
||||
if (!TryReadFill(input, hasWhen ? when : existing?.When, existing, out var fillBy, out var estimate, out var fillError))
|
||||
return ToolResult.Fail(fillError!);
|
||||
|
||||
var result = await board.UpdateAsync(id.Trim(), current => current with
|
||||
{
|
||||
Title = GetString(input, "title")?.Trim() ?? current.Title,
|
||||
@@ -432,7 +453,9 @@ public sealed class TaskboardTool : IAgentTool
|
||||
Acceptance = GetString(input, "acceptance")?.Trim() ?? current.Acceptance,
|
||||
BlockedBy = input.TryGetProperty("blocked_by", out _) ? GetStringList(input, "blocked_by") : current.BlockedBy,
|
||||
OnlyWhenMarketOpen = GetBool(input, "onlyWhenMarketOpen") ?? current.OnlyWhenMarketOpen,
|
||||
ParentId = parent ?? current.ParentId
|
||||
ParentId = parent ?? current.ParentId,
|
||||
FillBy = fillBy,
|
||||
EstimateUsd = estimate
|
||||
}, ct);
|
||||
|
||||
if (result is null)
|
||||
@@ -516,6 +539,11 @@ public sealed class TaskboardTool : IAgentTool
|
||||
block.AppendLine($" Arose from: {origin}");
|
||||
if (task.BlockedBy.Count > 0)
|
||||
block.AppendLine($" Waiting for: {string.Join(", ", task.BlockedBy)}");
|
||||
// #717: Fill work waits for room, not for a time — say so, or it looks forgotten.
|
||||
if (task.IsFillWork)
|
||||
block.AppendLine($" Fill work: runs when the budget has room, at the latest by {task.FillBy:yyyy-MM-dd HH:mm} UTC");
|
||||
if (task.EstimateUsd is { } usd)
|
||||
block.AppendLine($" Author's estimate: {usd.ToString("0.00##", System.Globalization.CultureInfo.InvariantCulture)} USD per run (not measured)");
|
||||
|
||||
if (sb.Length + block.Length > MaxResultChars)
|
||||
{
|
||||
@@ -535,6 +563,60 @@ public sealed class TaskboardTool : IAgentTool
|
||||
|
||||
// ─── Eingabe lesen ───
|
||||
|
||||
/// <summary>
|
||||
/// #717: 'fill_by' and 'estimate_usd'. Fill work is one-time and lies ahead; a schedule and a
|
||||
/// deadline contradict each other. Missing fields keep what <paramref name="existing"/> has.
|
||||
/// </summary>
|
||||
private static bool TryReadFill(
|
||||
JsonElement input, TaskWhen? when, TaskItem? existing,
|
||||
out DateTime? fillBy, out decimal? estimate, out string? error)
|
||||
{
|
||||
fillBy = existing?.FillBy;
|
||||
estimate = existing?.EstimateUsd;
|
||||
error = null;
|
||||
|
||||
if (input.TryGetProperty("fill_by", out var f))
|
||||
{
|
||||
var text = f.ValueKind == JsonValueKind.String ? f.GetString()?.Trim() : null;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
fillBy = null;
|
||||
else if (TaskFrontmatter.ParseUtc(text) is { } parsed)
|
||||
fillBy = parsed;
|
||||
else
|
||||
{
|
||||
error = $"'fill_by' is not a point in time: '{text}'. Use an ISO timestamp or a date (yyyy-MM-dd).";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fillBy is { } deadline && deadline <= DateTime.UtcNow)
|
||||
{
|
||||
error = $"'fill_by' lies in the past ({deadline:yyyy-MM-dd HH:mm} UTC). Fill work needs a deadline ahead.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fillBy is not null && when is { Kind: TaskWhenKind.Cron or TaskWhenKind.Every })
|
||||
{
|
||||
error = "Fill work is one-time: a task with 'fill_by' has no recurring schedule ('when').";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.TryGetProperty("estimate_usd", out var e))
|
||||
{
|
||||
if (e.ValueKind == JsonValueKind.Null)
|
||||
estimate = null;
|
||||
else if (e.ValueKind == JsonValueKind.Number && e.TryGetDecimal(out var usd) && usd >= 0)
|
||||
estimate = usd;
|
||||
else
|
||||
{
|
||||
error = "'estimate_usd' must be a non-negative number (USD per run).";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadWhen(JsonElement input, out TaskWhen? when, out string? error)
|
||||
{
|
||||
when = null;
|
||||
|
||||
Reference in New Issue
Block a user