Abstimmung: mehrere Agenten pruefen unabhaengig und stimmen blind ab (#702)
A unit can put operations to a blind vote instead of its lead. Not opinions are counted but independent checks enforced. - UnitConfig.votes: operations, voters, approveAt, rejectAt, quorum, hours, perDay - set only by a human (OrgAdministration.SetVotes). - Core/Staging/Votes: routes a covered staged call before the lead; one task per voter with the frozen call and nothing else; cast_vote needs reasoning and evidence. Counted as soon as the result is settled; in between and without the quorum a human decides. The proposer does not vote, voters that read foreign content get no ballot, changes to agents never go to a vote. - Escalation tool: list_ballots, cast_vote. - Approvals view shows each vote with every ballot and the voters' costs; organisation view sets the rule and lists the votes held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d834e5ac38
commit
a5dd503d9b
@@ -41,6 +41,7 @@ public sealed class EscalationTool : IAgentTool
|
||||
spot_checks lists the runs picked for you, with what the audit log shows; inspect_run (run) shows one run in full;
|
||||
check_done (check, verdict ok|finding, note) closes a check. propose_measure (agent, measure, reasoning, check) puts a measure to a human:
|
||||
sharpen_task, change_model, adjust_rights, suspend — or security, if you suspect the agent is steered from outside.
|
||||
As a voter: list_ballots shows the calls you are asked to vote on; cast_vote (vote, choice yes|no, reasoning, evidence) — check against your own data, with source and time. You never see the other votes.
|
||||
""";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
@@ -49,7 +50,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", "list_approvals", "approve_call", "reject_call", "doctrine_read", "doctrine_set", "spot_checks", "inspect_run", "check_done", "propose_measure"]
|
||||
"enum": ["find_decisions", "escalate", "list_cases", "decide", "handle", "forward", "open_conflict", "state_position", "request_budget", "list_approvals", "approve_call", "reject_call", "doctrine_read", "doctrine_set", "spot_checks", "inspect_run", "check_done", "propose_measure", "list_ballots", "cast_vote"]
|
||||
},
|
||||
"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." },
|
||||
@@ -72,7 +73,10 @@ public sealed class EscalationTool : IAgentTool
|
||||
"check": { "type": "integer", "description": "check_done, propose_measure: the spot check number." },
|
||||
"verdict": { "type": "string", "enum": ["ok", "finding"], "description": "check_done: ok, or finding (then 'note' with the evidence)." },
|
||||
"agent": { "type": "string", "description": "propose_measure: the agent it concerns." },
|
||||
"measure": { "type": "string", "enum": ["sharpen_task", "change_model", "adjust_rights", "suspend", "security"], "description": "propose_measure: what should follow." }
|
||||
"measure": { "type": "string", "enum": ["sharpen_task", "change_model", "adjust_rights", "suspend", "security"], "description": "propose_measure: what should follow." },
|
||||
"vote": { "type": "integer", "description": "cast_vote: the vote number (#…)." },
|
||||
"choice": { "type": "string", "enum": ["yes", "no"], "description": "cast_vote: your vote." },
|
||||
"evidence": { "type": "string", "description": "cast_vote: what you checked — source and time. Required: without it a vote is an opinion." }
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
@@ -104,6 +108,10 @@ public sealed class EscalationTool : IAgentTool
|
||||
if (Get(input, "action") is "spot_checks" or "inspect_run" or "check_done" or "propose_measure")
|
||||
return await SpotCheckAsync(input, context, ct);
|
||||
|
||||
// #702: A voter's ballot.
|
||||
if (Get(input, "action") is "list_ballots" or "cast_vote")
|
||||
return await BallotAsync(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);
|
||||
@@ -176,7 +184,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, list_approvals, approve_call, reject_call, doctrine_read, doctrine_set, spot_checks, inspect_run, check_done, propose_measure.");
|
||||
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, doctrine_read, doctrine_set, spot_checks, inspect_run, check_done, propose_measure, list_ballots, cast_vote.");
|
||||
}
|
||||
|
||||
return outcome.Ok ? ToolResult.Ok(outcome.Message) : ToolResult.Fail(outcome.Message);
|
||||
@@ -328,6 +336,35 @@ public sealed class EscalationTool : IAgentTool
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ToolResult> BallotAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.Votes is not { } votes)
|
||||
return ToolResult.Fail("Votes are not available in this instance.");
|
||||
|
||||
if (Get(input, "action") == "list_ballots")
|
||||
{
|
||||
var open = await votes.BallotsForAsync(context.AgentId, ct);
|
||||
if (open.Count == 0)
|
||||
return ToolResult.Ok("No vote waits for you.");
|
||||
var sb = new StringBuilder($"{open.Count} vote(s) wait for you — check each against your own data:\n");
|
||||
foreach (var (vote, call) in open)
|
||||
{
|
||||
sb.AppendLine($"- vote #{vote.Id}: {vote.Operation} from {vote.Proposer}, until {vote.Until:yyyy-MM-dd HH:mm} UTC. The call:");
|
||||
sb.AppendLine(Core.Engine.UntrustedContent.Rahmen(call.Tool, string.IsNullOrWhiteSpace(call.ArgumentsJson) ? call.Proposal : call.ArgumentsJson));
|
||||
}
|
||||
return ToolResult.Ok(sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
if (!input.TryGetProperty("vote", out var v) || v.ValueKind != JsonValueKind.Number || !v.TryGetInt64(out var voteId))
|
||||
return ToolResult.Fail("'vote' is required — the vote number.");
|
||||
var choice = Get(input, "choice");
|
||||
if (choice is not ("yes" or "no"))
|
||||
return ToolResult.Fail("'choice' must be yes or no.");
|
||||
var cast = await votes.CastAsync(voteId, context.AgentId, context.Trust, choice == "yes",
|
||||
Get(input, "reasoning") ?? "", Get(input, "evidence") ?? "", context.RunId, ct);
|
||||
return cast.Ok ? ToolResult.Ok(cast.Message) : ToolResult.Fail(cast.Message);
|
||||
}
|
||||
|
||||
/// <summary>One run as the engine recorded it. The result text is the agent's — marked as such.</summary>
|
||||
private static string Describe(RunInspection run)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user