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
@@ -322,7 +322,9 @@ public sealed record StagedCallDto(
|
||||
bool SuggestAllowSimilar = false,
|
||||
// #687: the lead that may decide it instead of the human, and until when.
|
||||
string? WithLead = null,
|
||||
DateTime? LeadUntil = null);
|
||||
DateTime? LeadUntil = null,
|
||||
// #702: the blind vote on it, if one was held.
|
||||
VoteDto? Vote = null);
|
||||
|
||||
/// <summary>#687: How a lead decided the approvals it was given.</summary>
|
||||
public sealed record LeadApprovalStatsDto(string LeadId, int Routed, int Approved, int Rejected, int Lapsed, double? ApprovalRate);
|
||||
|
||||
@@ -22,7 +22,9 @@ public sealed record UnitDto(
|
||||
// #687: what the lead may approve instead of the human; null: nothing.
|
||||
UnitApprovalsDto? Approvals = null,
|
||||
// #693: the lead's spot checks; null: none.
|
||||
UnitInspectionDto? Inspection = null);
|
||||
UnitInspectionDto? Inspection = null,
|
||||
// #702: what the unit puts to a blind vote; null: nothing.
|
||||
UnitVotesDto? Votes = null);
|
||||
|
||||
/// <summary>#698: One version of a unit's doctrine.</summary>
|
||||
public sealed record DoctrineDto(int Version, string Text, string SetBy, DateTime SetAt, string? RunId, string Reason);
|
||||
@@ -40,6 +42,20 @@ public sealed record InspectionInputDto(bool Enabled, double? Rate, double? Dens
|
||||
|
||||
public sealed record FindingDto(string Kind, string Text);
|
||||
|
||||
/// <summary>#702: What a unit puts to a blind vote, and who votes.</summary>
|
||||
public sealed record UnitVotesDto(IReadOnlyList<string> Operations, IReadOnlyList<string> Voters, int ApproveAt, int RejectAt, int Quorum, double Hours, int PerDay);
|
||||
|
||||
public sealed record VotesInputDto(IReadOnlyList<string>? Operations, IReadOnlyList<string>? Voters, int? ApproveAt, int? RejectAt, int? Quorum, double? Hours, int? PerDay, string Reason = "");
|
||||
|
||||
/// <summary>#702: One ballot as the human sees it — the voters never see each other's.</summary>
|
||||
public sealed record BallotDto(string Voter, string? Choice, string Reasoning, string Evidence, DateTime? CastAt, string? RunId);
|
||||
|
||||
/// <summary>#702: A vote on a staged call — the tally, every ballot and what the voters' runs cost.</summary>
|
||||
public sealed record VoteDto(
|
||||
long Id, long CallId, string Proposer, string Operation, int ApproveAt, int RejectAt, int Quorum,
|
||||
DateTime Until, DateTime CreatedAt, string Outcome, string OutcomeText, int Yes, int No, int Missing,
|
||||
IReadOnlyList<BallotDto> Ballots, decimal? CostUsd);
|
||||
|
||||
/// <summary>#693: A run picked for the lead, with what the audit log showed and the lead's verdict.</summary>
|
||||
public sealed record SpotCheckDto(
|
||||
long Id, string Lead, string AgentId, string RunId, string Why, IReadOnlyList<FindingDto> Findings,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using ClawdDotNet.Core.Staging;
|
||||
|
||||
namespace ClawdDotNet.Web.Contracts;
|
||||
|
||||
/// <summary>#702: A vote for the human — with what the voters' runs cost, so a vote can be set against what it prevented.</summary>
|
||||
public static class VoteMapping
|
||||
{
|
||||
public static async Task<VoteDto> ToDtoAsync(VoteRecord vote, Votes votes, CancellationToken ct)
|
||||
{
|
||||
var cost = await votes.CostAsync(vote, ct);
|
||||
return new VoteDto(
|
||||
vote.Id, vote.CallId, vote.Proposer, vote.Operation, vote.ApproveAt, vote.RejectAt, vote.Quorum,
|
||||
vote.Until, vote.CreatedAt, vote.Outcome ?? "open", Votes.Describe(vote.Outcome),
|
||||
vote.YesVotes, vote.NoVotes, vote.Outstanding,
|
||||
vote.Ballots.Select(b => new BallotDto(b.Voter, b.Yes is null ? null : b.Yes.Value ? "yes" : "no", b.Reasoning, b.Evidence, b.CastAt, b.RunId)).ToList(),
|
||||
cost);
|
||||
}
|
||||
}
|
||||
@@ -347,9 +347,18 @@ internal static class ApiEndpoints
|
||||
return Unavailable("Keine Freigabestelle in dieser Instanz.");
|
||||
|
||||
var pending = await service.ListPendingAsync(ct);
|
||||
// #702: the vote beside each call it was held on — the human decides with every ballot in view.
|
||||
var votes = host.Engine?.Votes is Core.Staging.Votes v
|
||||
? await v.ForCallsAsync(pending.Select(c => c.Id), ct)
|
||||
: new Dictionary<long, Core.Staging.VoteRecord>();
|
||||
var liste = new List<StagedCallDto>(pending.Count);
|
||||
foreach (var call in pending)
|
||||
liste.Add(call.ToDto(await service.BisherFreigegebenAsync(call, ct), service.KenntDauerfreigaben));
|
||||
{
|
||||
var dto = call.ToDto(await service.BisherFreigegebenAsync(call, ct), service.KenntDauerfreigaben);
|
||||
if (votes.TryGetValue(call.Id, out var vote))
|
||||
dto = dto with { Vote = await VoteMapping.ToDtoAsync(vote, (Core.Staging.Votes)host.Engine!.Votes!, ct) };
|
||||
liste.Add(dto);
|
||||
}
|
||||
return Results.Ok(liste);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ internal static class OrganisationEndpoints
|
||||
new UnitDefaultsDto(u.Defaults.Model, u.Defaults.DailyCostUsd, u.Defaults.DailyTokens, u.Defaults.WhenExhausted),
|
||||
chart.PathOf(u.Id), chart.Depth(u.Id),
|
||||
u.Approvals is { } ap ? new UnitApprovalsDto(ap.Operations, ap.PerDay, ap.Hours) : null,
|
||||
u.Inspection is { } ins ? new UnitInspectionDto(ins.Rate, ins.DenseRate, ins.NewDays, ins.PerDay) : null))
|
||||
u.Inspection is { } ins ? new UnitInspectionDto(ins.Rate, ins.DenseRate, ins.NewDays, ins.PerDay) : null,
|
||||
u.Votes is { } v ? new UnitVotesDto(v.Operations, v.Voters, v.ApproveAt, v.RejectAt, v.EffectiveQuorum, v.Hours, v.PerDay) : null))
|
||||
.ToList();
|
||||
|
||||
var agents = host.Instance.Agents
|
||||
@@ -104,6 +105,20 @@ internal static class OrganisationEndpoints
|
||||
return Results.Ok(new OutcomeDto(true, set.IsEmpty ? $"Doctrine of '{unit.Name}' removed." : $"Doctrine of '{unit.Name}' set (version {set.Version})."));
|
||||
});
|
||||
|
||||
// #702: What the unit puts to a blind vote, and the votes held.
|
||||
org.MapPut("/units/{unitId}/votes", (AppHost host, string unitId, VotesInputDto input) =>
|
||||
host.Organisation.SetVotes(unitId, input.Operations, input.Voters, input.ApproveAt, input.RejectAt, input.Quorum, input.Hours, input.PerDay, input.Reason).ToResponse());
|
||||
|
||||
org.MapGet("/units/{unitId}/votes", async (AppHost host, string unitId, CancellationToken ct) =>
|
||||
{
|
||||
if (host.Engine?.Votes is not Core.Staging.Votes votes)
|
||||
return Results.Ok(Array.Empty<VoteDto>());
|
||||
var list = new List<VoteDto>();
|
||||
foreach (var vote in await votes.RecentAsync(unitId, 20, ct))
|
||||
list.Add(await VoteMapping.ToDtoAsync(vote, votes, ct));
|
||||
return Results.Ok(list);
|
||||
});
|
||||
|
||||
// #693: The lead's spot checks — how often, what came of them, and the measures it proposed.
|
||||
org.MapPut("/units/{unitId}/inspection", (AppHost host, string unitId, InspectionInputDto input) =>
|
||||
host.Organisation.SetInspection(unitId, input.Enabled, input.Rate, input.DenseRate, input.NewDays, input.PerDay, input.Reason).ToResponse());
|
||||
|
||||
Reference in New Issue
Block a user