Freigabe durch die Leitung innerhalb von Grenzen (#687)
- UnitConfig.Approvals: Operationen, Hoechstzahl je Tag, Stunden je Freigabe; nur der Mensch setzt sie (OrgAdministration.SetApprovals mit Audit, Teilmengenregel nach oben, Senken oben senkt unten mit; Agentenaenderungen/Sonderbudget nie freigebbar) - LeadApprovals: Vorlage geht an die naechste Leitung, deren Einheit die Operation erlaubt; nie eigene Aufrufe, nie eine Leitung mit Fremdinhalt, Tagesdeckel, Frist. Entscheidung ueber Escalation approve_call/reject_call, im Audit als lead:<id> - Der Mensch sieht alles und kann zuerst entscheiden; danach wird die Leitungsaufgabe geschlossen. Nach der Frist nur noch der Mensch. - StagedCalls: Approver, ApproverUntil, ApproverTaskId (Nachruestung) - WebUI: Hinweis "bei Leitung" in der Freigabestelle, Zaehler ohne diese, Quote je Leitung; Karte in der Organisationsansicht; i18n en/de - Tests: LeadApprovalsTests (13), OrgAdministrationTests (+2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dafb8241a0
commit
f51186269c
@@ -31,6 +31,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
- state_position: answer a request for your position or a statement.
|
||||
After the deadline a human decides.
|
||||
- request_budget: a special budget for one large task and its parts (task, amount_usd, reasoning). A human always decides; it does not raise your daily limit.
|
||||
As a lead whose unit may approve: list_approvals shows the calls waiting for you; approve_call (call) or reject_call (call, reason). Never your own calls; if unsure, leave it to the human.
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
@@ -39,7 +40,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["find_decisions", "escalate", "list_cases", "decide", "handle", "forward", "open_conflict", "state_position", "request_budget"]
|
||||
"enum": ["find_decisions", "escalate", "list_cases", "decide", "handle", "forward", "open_conflict", "state_position", "request_budget", "list_approvals", "approve_call", "reject_call"]
|
||||
},
|
||||
"amount_usd": { "type": "number", "description": "request_budget: the amount for the task and all its parts, in USD." },
|
||||
"subject": { "type": "string", "description": "What it is about, in a few words — like a title. escalate, open_conflict, find_decisions." },
|
||||
@@ -54,7 +55,9 @@ public sealed class EscalationTool : IAgentTool
|
||||
"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." }
|
||||
"position": { "type": "string", "description": "open_conflict, state_position: your side, with reasons." },
|
||||
"call": { "type": "integer", "description": "approve_call, reject_call: the approval number (#…)." },
|
||||
"reason": { "type": "string", "description": "reject_call: why — the agent must know what to change." }
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
@@ -70,6 +73,10 @@ public sealed class EscalationTool : IAgentTool
|
||||
if (Get(input, "action") == "request_budget")
|
||||
return await SetUpBudgetAsync(input, context, ct);
|
||||
|
||||
// #687: A lead deciding the approvals its unit grants it.
|
||||
if (Get(input, "action") is "list_approvals" or "approve_call" or "reject_call")
|
||||
return await ApprovalAsync(input, context, ct);
|
||||
|
||||
if (context.Cases is not { } desk)
|
||||
return ToolResult.Fail("Escalation is not available in this instance.");
|
||||
|
||||
@@ -138,7 +145,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
break;
|
||||
|
||||
default:
|
||||
return ToolResult.Fail("'action' must be one of find_decisions, escalate, list_cases, decide, handle, forward, open_conflict, state_position, request_budget.");
|
||||
return ToolResult.Fail("'action' must be one of find_decisions, escalate, list_cases, decide, handle, forward, open_conflict, state_position, request_budget, list_approvals, approve_call, reject_call.");
|
||||
}
|
||||
|
||||
return outcome.Ok ? ToolResult.Ok(outcome.Message) : ToolResult.Fail(outcome.Message);
|
||||
@@ -196,6 +203,30 @@ public sealed class EscalationTool : IAgentTool
|
||||
"Runs in that task tree draw on it, not on your daily budget; once it is used up, they count against the day again.");
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> ApprovalAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.Approvals is not { } approvals)
|
||||
return ToolResult.Fail("Approvals by leads are not available in this instance.");
|
||||
|
||||
var action = Get(input, "action");
|
||||
if (action == "list_approvals")
|
||||
{
|
||||
var waiting = await approvals.PendingForAsync(context.AgentId, ct);
|
||||
if (waiting.Count == 0)
|
||||
return ToolResult.Ok("No approval waits for you.");
|
||||
var sb = new StringBuilder($"{waiting.Count} approval(s) wait for you:\n");
|
||||
foreach (var c in waiting)
|
||||
sb.AppendLine($"- #{c.Id} {c.Tool}{(string.IsNullOrWhiteSpace(c.Action) ? "" : "." + c.Action)} from {c.AgentId}, " +
|
||||
$"until {c.ApproverUntil:yyyy-MM-dd HH:mm} UTC — task {c.ApproverTaskId}");
|
||||
return ToolResult.Ok(sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
if (!input.TryGetProperty("call", out var n) || !n.TryGetInt64(out var callId))
|
||||
return ToolResult.Fail("'call' is required — the approval number.");
|
||||
var decided = await approvals.DecideAsync(callId, context.AgentId, context.Trust, action == "approve_call", Get(input, "reason") ?? "", ct);
|
||||
return decided.Ok ? ToolResult.Ok(decided.Message) : ToolResult.Fail(decided.Message);
|
||||
}
|
||||
|
||||
private static string Describe(Decision d)
|
||||
=> CaseDesk.DecisionLine(d) + (string.IsNullOrWhiteSpace(d.Reasoning) ? "" : $"\n Reasoning: {d.Reasoning}");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user