Rueckmeldung nach Vertrauensstufe: drei Wege nach oben (#715)
- IAgentTool.ReadsForeignContent: Werkzeuge mit Aussenkontakt erklaeren sich selbst (WebFetch, WebMonitor, DirectAPI, Mail, RocketChat, Telegram, TelegramClient, SocialMediaManager, FTP, Database); AgentConfig.TreatAsForeignReader kann nur verschaerfen - ReportingLine: Stufe aus den Werkzeugen, "wer steht darueber", Pruefung des Wegs nach oben (nie Aenderungen an Aufgaben darueber, nur eine Ebene hoch, Fremdinhalt-Stufe gar kein Freitext) und Rahmung als untrusted_content - Engine: Nachricht und Spawn nach oben geprueft, Text der Fremdinhalt-Stufe kommt gerahmt an - Taskboard: task_create/update/comment, reopen und die Dubletten-Notiz folgen dem Berichtsweg - Dispatcher: Aufgaben von Agenten der Fremdinhalt-Stufe gehen gerahmt in den Lauf - TaskReports: Erledigt-Signal und Systemmeldung (in Pruefung, blockiert, abgebrochen) an den Auftraggeber, je Zustand einmal, gesammelt in einer offenen Aufgabe je Empfaenger - OrgBriefing nennt den Berichtsweg; WebUI zeigt Stufe und Schalter am Agenten Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e2ffe4b380
commit
3857a7cab2
@@ -1,5 +1,6 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using ClawdDotNet.Core.Tasks;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -166,6 +167,11 @@ public sealed class TaskboardTool : IAgentTool
|
||||
&& CheckAssignee(GetString(input, "assignee"), context) is { } outOfReach)
|
||||
return ToolResult.Fail(outOfReach);
|
||||
|
||||
// #715: A new task for someone above is free text going up — the same way as a message.
|
||||
if (action is "task_create" or "task_update"
|
||||
&& Upward(TaskAssignee.AgentId(GetString(input, "assignee") ?? ""), UpwardKind.Message, context) is { } notUp)
|
||||
return ToolResult.Fail(notUp);
|
||||
|
||||
// #684: The boards this agent sees; null means all of them (no units).
|
||||
var visible = TaskBoard.VisibleTo(context.Organisation, context.AgentId);
|
||||
|
||||
@@ -173,11 +179,11 @@ public sealed class TaskboardTool : IAgentTool
|
||||
{
|
||||
return action switch
|
||||
{
|
||||
"task_create" => await CreateAsync(input, board, context.AgentId, visible, ct),
|
||||
"task_create" => await CreateAsync(input, board, context.AgentId, visible, context, ct),
|
||||
"task_find" => await FindAsync(input, board, visible, ct),
|
||||
"task_list" => await ListAsync(input, board, visible, ct),
|
||||
"task_update" => await UpdateAsync(input, board, visible, ct),
|
||||
"task_comment" => await CommentAsync(input, board, context.AgentId, visible, ct),
|
||||
"task_update" => await UpdateAsync(input, board, visible, context, ct),
|
||||
"task_comment" => await CommentAsync(input, board, context.AgentId, visible, context, ct),
|
||||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
@@ -208,6 +214,19 @@ public sealed class TaskboardTool : IAgentTool
|
||||
: "Assign it to @human.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #715: The way up, for a task whose assignee is <paramref name="targetAgentId"/>. Tasks
|
||||
/// for a human or for the agent itself are never "up".
|
||||
/// </summary>
|
||||
private static string? Upward(string targetAgentId, UpwardKind kind, AgentToolContext context)
|
||||
=> targetAgentId.Length == 0
|
||||
? null
|
||||
: ReportingLine.CheckUpward(context.Organisation, context.Trust, context.AgentId, targetAgentId, kind);
|
||||
|
||||
/// <summary>#715: The agent the task's words and changes reach — its assignee.</summary>
|
||||
private static string AssigneeOf(TaskItem task)
|
||||
=> TaskAssignee.KindOf(task.Assignee) == TaskAssigneeKind.Human ? "" : TaskAssignee.AgentId(task.Assignee);
|
||||
|
||||
// ─── Aktionen ───
|
||||
|
||||
/// <summary>
|
||||
@@ -280,7 +299,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
=> visible is null || (await board.GetAsync(id, ct) is { } task && visible(task));
|
||||
|
||||
private static async Task<ToolResult> CreateAsync(
|
||||
JsonElement input, TaskboardService board, string author, Func<TaskItem, bool>? visible, CancellationToken ct)
|
||||
JsonElement input, TaskboardService board, string author, Func<TaskItem, bool>? visible, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var title = GetString(input, "title");
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -302,6 +321,13 @@ public sealed class TaskboardTool : IAgentTool
|
||||
|
||||
if (dublette is not null)
|
||||
{
|
||||
// #715: The note carries the agent's title — free text, so it stays off the
|
||||
// tasks of someone above when the way up is closed to this agent.
|
||||
if (Upward(AssigneeOf(dublette), UpwardKind.Comment, context) is not null)
|
||||
return ToolResult.Fail(
|
||||
$"Not created: {dublette.Id} \"{dublette.Title}\" has been open since {dublette.CreatedAt:dd.MM. HH:mm} " +
|
||||
"and refers to the same issue.");
|
||||
|
||||
await board.AddCommentAsync(
|
||||
dublette.Id, author,
|
||||
$"Observed again — {author} wanted to create the same task: \"{title.Trim()}\"",
|
||||
@@ -358,7 +384,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> UpdateAsync(
|
||||
JsonElement input, TaskboardService board, Func<TaskItem, bool>? visible, CancellationToken ct)
|
||||
JsonElement input, TaskboardService board, Func<TaskItem, bool>? visible, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var id = GetString(input, "id");
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
@@ -366,6 +392,11 @@ public sealed class TaskboardTool : IAgentTool
|
||||
if (!await CanSeeAsync(board, id.Trim(), visible, ct))
|
||||
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)
|
||||
return ToolResult.Fail(notUp);
|
||||
|
||||
if (!TryReadWhen(input, out var when, out var whenError))
|
||||
return ToolResult.Fail(whenError!);
|
||||
var hasWhen = input.TryGetProperty("when", out _);
|
||||
@@ -399,7 +430,7 @@ public sealed class TaskboardTool : IAgentTool
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> CommentAsync(
|
||||
JsonElement input, TaskboardService board, string author, Func<TaskItem, bool>? visible, CancellationToken ct)
|
||||
JsonElement input, TaskboardService board, string author, Func<TaskItem, bool>? visible, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var id = GetString(input, "id");
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
@@ -407,6 +438,12 @@ public sealed class TaskboardTool : IAgentTool
|
||||
if (!await CanSeeAsync(board, id.Trim(), visible, ct))
|
||||
return ToolResult.Fail($"No task found with ID '{id}'.");
|
||||
|
||||
// #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)
|
||||
return ToolResult.Fail(notUp);
|
||||
|
||||
var text = GetString(input, "text");
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return ToolResult.Fail("'text' is required — what should be commented?");
|
||||
|
||||
Reference in New Issue
Block a user