Eskalation, Konflikt und Beschluss als eigene Vorgaenge (#689, Stufe 1+2)
- Cases/Decisions in der Instanz-DB; CaseDesk fuehrt die Regeln, der Brett-Task traegt den Vorgang zum jeweiligen Halter - Eskalation nur nach oben, eine Ebene je Schritt; die Leitung entscheidet (mit Begruendung, bindet ihren Bereich), erledigt selbst oder reicht mit Einschaetzung weiter - Erst nachsehen: bindende Beschluesse werden vor dem Eskalieren gezeigt; dieselbe Frage aus derselben Einheit schliesst sich dem offenen Vorgang an - Tagesdeckel je Einheit; nach der Frist wird ein Mensch gerufen (Anschluss fuer #644) - Konflikt: zuerst die Gegenseite, dann Zustellung an die niedrigste gemeinsame Leitung; Ebenen dazwischen vermerkt, nicht befragt - Fremdinhalt-Stufe (#715): Eskalation ohne eigenen Text, kein Konflikt mit Position - Jede Ebene haengt an (#697), beim Halter gerahmt als Daten - Escalation ist ein implizites Werkzeug jedes Agenten; Briefing nennt den Weg - Scanner behaelt einen im Lauf gesetzten Status blockiert/abgebrochen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c03664cf29
commit
36fe1ad2aa
@@ -0,0 +1,157 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Cases;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
|
||||
namespace ClawdDotNet.Tools.Taskboard;
|
||||
|
||||
/// <summary>
|
||||
/// #689: Escalate, decide, disagree — the agent's side of <see cref="ICaseDesk"/>. Every agent
|
||||
/// has it (<see cref="ToolRegistry.ImplicitTools"/>): being able to say "I cannot get on" is
|
||||
/// part of working in an organisation, not a permission.
|
||||
///
|
||||
/// <para>The rules are the desk's, not the tool's: who the next holder is, who decides a
|
||||
/// conflict, what a decision binds. The tool only reads the arguments and passes on what the
|
||||
/// engine knows — the caller, its tier, its run.</para>
|
||||
/// </summary>
|
||||
public sealed class EscalationTool : IAgentTool
|
||||
{
|
||||
public string Name => CaseDesk.ToolName;
|
||||
|
||||
public string Description => """
|
||||
Escalate what you cannot decide, and decide what is escalated to you.
|
||||
- find_decisions: decisions that bind you and answer a question. escalate shows them anyway.
|
||||
- escalate: you cannot get on and it is not yours to decide. Goes to your lead (or a human). Name the task you are stuck on; it waits for the answer.
|
||||
- list_cases: the open cases you hold, raised, are a side of, or are told of.
|
||||
As the holder of a case: decide (answer + reasoning; binds your area), handle (you took care of it), or forward (to your own lead, with your assessment). Never send a case back down unresolved.
|
||||
- open_conflict: you disagree with an agent you cannot instruct and that cannot instruct you. It goes to the lowest place above both of you, after the other side stated its position.
|
||||
- state_position: answer a request for your position or a statement.
|
||||
After the deadline a human decides.
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["find_decisions", "escalate", "list_cases", "decide", "handle", "forward", "open_conflict", "state_position"]
|
||||
},
|
||||
"subject": { "type": "string", "description": "What it is about, in a few words — like a title. escalate, open_conflict, find_decisions." },
|
||||
"question": { "type": "string", "description": "escalate: the question that has to be decided." },
|
||||
"tried": { "type": "string", "description": "escalate: what you tried, and what you found (or did not find)." },
|
||||
"needed": { "type": "string", "description": "escalate: what you would need to get on." },
|
||||
"task": { "type": "string", "description": "escalate, open_conflict: the task you are stuck on. It waits for the answer." },
|
||||
"force": { "type": "boolean", "description": "escalate: although a decision seems to answer it — say in 'tried' why it does not fit." },
|
||||
"case": { "type": "string", "description": "The case id (c-…) for decide, handle, forward, state_position." },
|
||||
"answer": { "type": "string", "description": "decide: the decision." },
|
||||
"reasoning": { "type": "string", "description": "decide: why. Required — a decision without reasons cannot be applied." },
|
||||
"note": { "type": "string", "description": "handle: what you did about it." },
|
||||
"assessment": { "type": "string", "description": "forward: your view, added to the case." },
|
||||
"with": { "type": "string", "description": "open_conflict: the agent you disagree with." },
|
||||
"position": { "type": "string", "description": "open_conflict, state_position: your side, with reasons." }
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
""").RootElement.Clone();
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.Cases is not { } desk)
|
||||
return ToolResult.Fail("Escalation is not available in this instance.");
|
||||
|
||||
var me = context.AgentId;
|
||||
var run = context.RunId;
|
||||
var caseId = Get(input, "case") ?? "";
|
||||
|
||||
CaseOutcome outcome;
|
||||
switch (Get(input, "action"))
|
||||
{
|
||||
case "find_decisions":
|
||||
{
|
||||
var subject = Get(input, "subject") ?? Get(input, "question");
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
return ToolResult.Fail("'subject' is required — what should be looked for?");
|
||||
var found = await desk.FindDecisionsAsync(me, subject, Get(input, "question"), ct);
|
||||
return ToolResult.Ok(found.Count == 0
|
||||
? $"No decision that binds you answers \"{subject.Trim()}\"."
|
||||
: "Decisions that bind you:\n" + string.Join("\n", found.Take(10).Select(Describe)));
|
||||
}
|
||||
|
||||
case "escalate":
|
||||
outcome = await desk.EscalateAsync(new EscalationRequest(
|
||||
me, context.Trust, Get(input, "subject") ?? "", Get(input, "question") ?? "",
|
||||
Get(input, "tried") ?? "", Get(input, "needed") ?? "",
|
||||
Get(input, "task") ?? context.RunTaskId, run, GetBool(input, "force")), ct);
|
||||
break;
|
||||
|
||||
case "list_cases":
|
||||
{
|
||||
var cases = await desk.CasesConcerningAsync(me, ct);
|
||||
if (cases.Count == 0)
|
||||
return ToolResult.Ok("No open case concerns you.");
|
||||
var sb = new StringBuilder($"{cases.Count} open case(s):\n");
|
||||
foreach (var c in cases)
|
||||
sb.AppendLine($"- {c.Id} [{c.Kind.ToString().ToLowerInvariant()}] {c.Subject} — {Role(c, me)}; " +
|
||||
$"with {(c.HeldByHuman ? "a human" : c.Holder)}; deadline {c.DeadlineUtc:yyyy-MM-dd HH:mm} UTC");
|
||||
return ToolResult.Ok(sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
case "decide":
|
||||
outcome = await desk.DecideAsync(caseId, me, Get(input, "answer") ?? "", Get(input, "reasoning") ?? "", run, ct);
|
||||
break;
|
||||
|
||||
case "handle":
|
||||
outcome = await desk.HandleAsync(caseId, me, Get(input, "note") ?? "", run, ct);
|
||||
break;
|
||||
|
||||
case "forward":
|
||||
outcome = await desk.ForwardAsync(caseId, me, context.Trust, Get(input, "assessment") ?? "", run, ct);
|
||||
break;
|
||||
|
||||
case "open_conflict":
|
||||
{
|
||||
var other = (Get(input, "with") ?? "").Trim().TrimStart('@');
|
||||
if (other.Length == 0)
|
||||
return ToolResult.Fail("'with' is required — who do you disagree with?");
|
||||
outcome = await desk.OpenConflictAsync(new ConflictRequest(
|
||||
me, context.Trust, other, Get(input, "subject") ?? "", Get(input, "position") ?? "",
|
||||
Get(input, "task") ?? context.RunTaskId, run), ct);
|
||||
break;
|
||||
}
|
||||
|
||||
case "state_position":
|
||||
outcome = await desk.StatePositionAsync(caseId, me, context.Trust, Get(input, "position") ?? "", run, ct);
|
||||
break;
|
||||
|
||||
default:
|
||||
return ToolResult.Fail("'action' must be one of find_decisions, escalate, list_cases, decide, handle, forward, open_conflict, state_position.");
|
||||
}
|
||||
|
||||
return outcome.Ok ? ToolResult.Ok(outcome.Message) : ToolResult.Fail(outcome.Message);
|
||||
}
|
||||
|
||||
private static string Describe(Decision d)
|
||||
=> CaseDesk.DecisionLine(d) + (string.IsNullOrWhiteSpace(d.Reasoning) ? "" : $"\n Reasoning: {d.Reasoning}");
|
||||
|
||||
private static string Role(CaseRecord c, string me)
|
||||
{
|
||||
if (string.Equals(c.Holder, me, StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(c.PendingStatements.GetValueOrDefault(me)))
|
||||
return c.PendingStatements.Count > 0 ? "you will decide it once the sides are heard" : "you hold it";
|
||||
if (c.PendingStatements.Keys.Any(k => string.Equals(k, me, StringComparison.OrdinalIgnoreCase)))
|
||||
return "your position is asked for";
|
||||
if (c.Parties.Any(p => string.Equals(p, me, StringComparison.OrdinalIgnoreCase)))
|
||||
return "you are a side";
|
||||
if (c.Informed.Any(p => string.Equals(p, me, StringComparison.OrdinalIgnoreCase)))
|
||||
return "it is decided in your area — you are told, not asked";
|
||||
return "you raised it";
|
||||
}
|
||||
|
||||
private static string? Get(JsonElement input, string name)
|
||||
=> input.ValueKind == JsonValueKind.Object && input.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
||||
? v.GetString()
|
||||
: null;
|
||||
|
||||
private static bool GetBool(JsonElement input, string name)
|
||||
=> input.ValueKind == JsonValueKind.Object && input.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True;
|
||||
}
|
||||
Reference in New Issue
Block a user