- Werkzeugkontext kennt Lauf und Aufgabe des Laufs (RunId, RunTaskId); die Delegationskette traegt den Lauf des Aufrufers weiter (RunId, CallerRunId) - Aufgaben tragen origin_run / origin_task (Datei, DB, Taskboard "Arose from"), gestempelt, nie vom Agenten behauptet - Kommentare nennen den Lauf, in dem sie geschrieben wurden - Laufbeleg: CallerRunId; Laufansicht fuehrt zu "Delegiert von" und "Delegiert an" - #677: task_update ersetzt den Rumpf nicht mehr, Abnahmekriterien aendert nur der Ersteller; Kommentare der Fremdinhalt-Stufe auf fremden Aufgaben kommen gerahmt an - WebUI: Herkunft auf der Aufgabenkarte mit Link zum Lauf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3f90e0d0cd
commit
f812dbd856
@@ -75,7 +75,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Description/assignment — passed as task instructions to executing agent."
|
||||
"description": "task_create: description/assignment — passed as task instructions to executing agent. Not in task_update: the body is only ever added to, with task_comment."
|
||||
},
|
||||
"assignee": {
|
||||
"type": "string",
|
||||
@@ -110,7 +110,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "string",
|
||||
"description": "Acceptance criteria against which the result is verified."
|
||||
"description": "Acceptance criteria against which the result is verified. In task_update only by the task's creator."
|
||||
},
|
||||
"parent": {
|
||||
"type": "string",
|
||||
@@ -331,7 +331,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
await board.AddCommentAsync(
|
||||
dublette.Id, author,
|
||||
$"Observed again — {author} wanted to create the same task: \"{title.Trim()}\"",
|
||||
ct);
|
||||
ct, context.RunId);
|
||||
|
||||
return ToolResult.Fail(
|
||||
$"Not created: {dublette.Id} \"{dublette.Title}\" has been open since {dublette.CreatedAt:dd.MM. HH:mm} " +
|
||||
@@ -355,7 +355,10 @@ public sealed class TaskboardTool : IAgentTool
|
||||
OnlyWhenMarketOpen = GetBool(input, "onlyWhenMarketOpen") ?? false,
|
||||
// #684: decides the board when the task is for a human or a fresh run.
|
||||
CreatedBy = author,
|
||||
ParentId = parent
|
||||
ParentId = parent,
|
||||
// #697: Where it came from — the engine's knowledge, never an argument of the agent.
|
||||
OriginRunId = context.RunId ?? "",
|
||||
OriginTaskId = context.RunTaskId ?? ""
|
||||
};
|
||||
|
||||
var created = await board.CreateAsync(definition, ct);
|
||||
@@ -393,10 +396,22 @@ public sealed class TaskboardTool : IAgentTool
|
||||
return ToolResult.Fail($"No task found with ID '{id}'.");
|
||||
|
||||
// #715: No changes of status or fields on the tasks of someone above.
|
||||
if (await board.GetAsync(id.Trim(), ct) is { } current
|
||||
&& Upward(AssigneeOf(current), UpwardKind.Change, context) is { } notUp)
|
||||
var existing = await board.GetAsync(id.Trim(), ct);
|
||||
if (existing is not null && Upward(AssigneeOf(existing), UpwardKind.Change, context) is { } notUp)
|
||||
return ToolResult.Fail(notUp);
|
||||
|
||||
// #677/#697: The body holds the order and every comment on it. Replacing it would erase
|
||||
// the trail back to the original — it is only ever added to.
|
||||
if (input.TryGetProperty("body", out _))
|
||||
return ToolResult.Fail("The body of a task is not replaced — it holds the order and the whole discussion. " +
|
||||
"Add to it with task_comment.");
|
||||
|
||||
// #677: Whoever is measured against the criteria does not get to move them.
|
||||
if (existing is not null && input.TryGetProperty("acceptance", out _)
|
||||
&& !string.Equals(existing.CreatedBy, context.AgentId, StringComparison.OrdinalIgnoreCase))
|
||||
return ToolResult.Fail("Only the task's creator changes its acceptance criteria. Comment with task_comment " +
|
||||
"if you think they should change.");
|
||||
|
||||
if (!TryReadWhen(input, out var when, out var whenError))
|
||||
return ToolResult.Fail(whenError!);
|
||||
var hasWhen = input.TryGetProperty("when", out _);
|
||||
@@ -408,7 +423,6 @@ public sealed class TaskboardTool : IAgentTool
|
||||
var result = await board.UpdateAsync(id.Trim(), current => current with
|
||||
{
|
||||
Title = GetString(input, "title")?.Trim() ?? current.Title,
|
||||
Body = GetString(input, "body")?.Trim() ?? current.Body,
|
||||
Status = ParseStatusFilter(GetString(input, "status")) ?? current.Status,
|
||||
Type = input.TryGetProperty("type", out _) ? TaskText.Type(GetString(input, "type")) : current.Type,
|
||||
Priority = GetInt(input, "priority") ?? current.Priority,
|
||||
@@ -440,15 +454,21 @@ public sealed class TaskboardTool : IAgentTool
|
||||
|
||||
// #715: A comment goes up one level at most, and only from the standard tier; reopening
|
||||
// is a change of status, which never goes up.
|
||||
if (await board.GetAsync(id.Trim(), ct) is { } target
|
||||
&& Upward(AssigneeOf(target), GetBool(input, "reopen") == true ? UpwardKind.Change : UpwardKind.Comment, context) is { } notUp)
|
||||
if (await board.GetAsync(id.Trim(), ct) is not { } target)
|
||||
return ToolResult.Fail($"No task found with ID '{id}'.");
|
||||
if (Upward(AssigneeOf(target), GetBool(input, "reopen") == true ? UpwardKind.Change : UpwardKind.Comment, context) is { } notUp)
|
||||
return ToolResult.Fail(notUp);
|
||||
|
||||
var text = GetString(input, "text");
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return ToolResult.Fail("'text' is required — what should be commented?");
|
||||
|
||||
var result = await board.AddCommentAsync(id.Trim(), author, text.Trim(), ct);
|
||||
// #715: A comment is part of the body, and the body goes into the assignee's next run. From
|
||||
// an agent that reads foreign content, on someone else's task, it arrives framed as data.
|
||||
var commentText = string.Equals(AssigneeOf(target), author, StringComparison.OrdinalIgnoreCase)
|
||||
? text.Trim()
|
||||
: ReportingLine.Frame(context.Trust, author, text.Trim());
|
||||
var result = await board.AddCommentAsync(id.Trim(), author, commentText, ct, context.RunId);
|
||||
if (result is null)
|
||||
return ToolResult.Fail($"No task found with ID '{id}'.");
|
||||
|
||||
@@ -463,6 +483,19 @@ public sealed class TaskboardTool : IAgentTool
|
||||
|
||||
// ─── Darstellung ───
|
||||
|
||||
/// <summary>#697: "amazon-lead, while working on t-3, run 4f…" — or <c>null</c> for a task from a human.</summary>
|
||||
private static string? Origin(TaskItem task)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(task.CreatedBy))
|
||||
parts.Add(task.CreatedBy);
|
||||
if (!string.IsNullOrWhiteSpace(task.OriginTaskId))
|
||||
parts.Add($"while working on {task.OriginTaskId}");
|
||||
if (!string.IsNullOrWhiteSpace(task.OriginRunId))
|
||||
parts.Add($"run {task.OriginRunId}");
|
||||
return parts.Count == 0 ? null : string.Join(", ", parts);
|
||||
}
|
||||
|
||||
private static string Render(IReadOnlyList<TaskItem> tasks)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@@ -478,6 +511,9 @@ public sealed class TaskboardTool : IAgentTool
|
||||
block.AppendLine($" Assignee: {task.Assignee} | Priority: {task.Priority}{FormatWhen(task.When)}");
|
||||
if (!string.IsNullOrWhiteSpace(task.ParentId))
|
||||
block.AppendLine($" Serves: {task.ParentId}");
|
||||
// #697: The origin travels with the task; the content stays where it is.
|
||||
if (Origin(task) is { } origin)
|
||||
block.AppendLine($" Arose from: {origin}");
|
||||
if (task.BlockedBy.Count > 0)
|
||||
block.AppendLine($" Waiting for: {string.Join(", ", task.BlockedBy)}");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user