// std/agent/judge_arbitration — the completion arbitration layer.
//
// A model judge decides whether a run is finished, and two of its refusals can
// be overruled by evidence the runtime already holds. Both overrules live here,
// beside the verdict shapes they read, so the rule that narrows a refusal sits
// next to the rule that narrows the other one and neither can drift from the
// receipt it writes.
//
// `__completion_arbitration_converts` and
// `__completion_contradiction_arbitration_converts` in `std/agent/judge_internals`
// own the CONDITIONS and the reasoning for each clause. This module owns what a
// conversion does to the verdict: it flips the acceptance, clears the feedback
// that no turn could answer, names the party whose answer the verdict now IS,
// and stamps both the typed override and `converted_from` so a reader can count
// overrules without inferring them.
//
// Both conversions overrule a judge, so both write the attribution pair harn#8229
// defined: `source` names the author of the verdict of record, `judge_source`
// stays with the party that produced the judge's own answer, and `override`
// carries the overruling party and the rule in a typed object rather than in
// prose a consumer has to pattern-match.
//
// They did not, and the gap was one-directional in a way that hid it. harn#8229
// instrumented the gate forcing a `continue` over a judge's `done`, which is the
// direction the completion seam takes. These two take the other direction, a
// `done` of record over a judge's `continue`, and stamped only `converted_from`
// while leaving `source` at `llm`. Measured across an eval archive: every one of
// the eight conversions published the gate's answer under the model's name,
// which is the exact reading harn#8229 exists to prevent, surviving on the half
// of the ladder it did not reach.
import { CompletionEvidenceSnapshot } from "std/agent/completion_evidence"
import { CompletionRequirementReport } from "std/agent/completion_requirements"
import {
CompletionJudgeAdmission,
CompletionJudgeGapClass,
CompletionVerificationState,
__completion_arbitration_converts,
__completion_contradiction_arbitration_converts,
} from "std/agent/judge_internals"
/** Which adjudicators actually ran at one completion boundary. */
pub type CompletionJudgeInvocations = {
deterministic: bool,
verify_completion_judge: bool,
turn_end_condition: bool,
}
/** Normalized result from either a deterministic or model completion check. */
pub type CompletionStageVerdict = {
vetoed: bool,
confirm?: bool,
verdict?: string,
feedback?: string?,
reasoning?: string,
next_step?: string,
source?: string,
// The judge's own answer, and the party that produced it. `verdict` and
// `source` above describe the answer of RECORD, which an override may have
// authored; these two stay the model's, so a reader can tell the two apart
// and arbitration can still find a model refusal after a conversion.
judge_verdict?: string?,
judge_source?: string?,
// Typed conversion record: the overriding party and the rule it applied.
// Absent when the verdict of record is the judge's own.
override?: dict?,
trigger?: string?,
reason?: string?,
gap_class?: CompletionJudgeGapClass?,
// Set when a `done` reply that named a gap was re-asked once with the
// contradiction spelled out. Present so the contradiction arbitration can
// tell a first self-contradiction from one that survived being pointed out.
contradiction_reasked?: bool?,
verification?: CompletionVerificationState?,
converted_from?: string?,
escalation_recommended?: bool?,
escalation_target?: string?,
judge_duration_ms?: int | float,
judge_outcome?: string?,
terminal_judge_reason?: string?,
terminal_evidence_preserved?: bool,
admission?: CompletionJudgeAdmission?,
typed_checkpoint?: dict?,
requirement_report?: CompletionRequirementReport?,
}
pub type CompletionPolicyEvaluation = {
verdict: CompletionStageVerdict,
invoked: CompletionJudgeInvocations,
verification_judge_cap_reached: bool,
completion_judge_cap_reached: bool,
// The snapshot as of this stage. Later stages see what earlier stages
// established; a second model judge must not read the gate's facts through the
// first judge's overwritten verdict.
payload: CompletionEvidenceSnapshot,
// The deterministic gate's own reason, held separately because
// `evaluation.verdict` is overwritten by every stage that follows it.
deterministic_reason: string,
// This boundary's acceptance ledger, read with no model assessment in hand,
// so every declared row is pending. Computed once for the boundary rather
// than per stage: both decisions that can end a run without asking the judge
// — the typed-green skip and the judge cap — are boundary-level, and a ledger
// each of them read separately could disagree. Empty when no ledger is
// declared, never absent. See harn#8110.
ledger: CompletionRequirementReport,
}
const __COMPLETION_VERIFICATION_CONVERSION: string = "failed_verification_contradicted_by_gate"
const __COMPLETION_CONTRADICTION_CONVERSION: string =
"gap_contradicts_done_survived_reask_over_passing_verifier"
/**
* The typed override both conversions stamp, in the shape harn#8229 defined.
*
* Shared so the two arbitrations cannot describe the same kind of event
* differently, which is how the prose field they used to write alone drifted
* from the decision it was describing.
*
* `judge_verdict` prefers the field harn#8229 sets upstream and falls back to
* the verdict being converted. The fallback is not decoration: a replayed or
* older-pin verdict reaches here without the pair, and recording the overruled
* answer as absent would leave exactly the hole this closes. The verdict on
* this path is a refusal by construction, so the fallback is the judge's own
* word either way.
*
* @effects: []
* @errors: []
* @api_stability: internal
* @example: __completion_conversion_override(verdict, "failed_verification_contradicted_by_gate")
*/
fn __completion_conversion_override(verdict: CompletionStageVerdict, rule: string) -> dict {
return {
party: "gate",
rule: rule,
judge_verdict: to_string(verdict?.judge_verdict ?? verdict.verdict ?? ""),
}
}
/**
* Refuse a model `continue` that names a failed verification the deterministic
* gate observed passing, and convert it to `done` with a receipt saying so.
*
* This narrows WHAT EVIDENCE a refusal may rest on; it does not weaken refusal.
* Every other `gap_class` vetoes exactly as before, so artifact, manner and
* negative-clause, and authorization gaps keep the judge's full authority — and
* those are where its demonstrated discriminating power lives.
*
* `__completion_arbitration_converts` owns the conditions and why each is
* required. The invocation map is read here rather than re-derived: whether an
* adjudicator actually ran is the invocation map's question, not this
* function's.
*
* @effects: []
* @errors: []
* @api_stability: internal
* @example: __completion_arbitrate_verification(verdict, evaluation)
*/
pub fn __completion_arbitrate_verification(
verdict: CompletionStageVerdict,
evaluation: CompletionPolicyEvaluation,
) -> CompletionStageVerdict {
// Reads `judge_source`, the party that produced the judge's own answer, not
// `source`, which names the author of the verdict of record and therefore
// reads "gate" on exactly the overridden decisions this arbitration exists
// to reconsider. Falls back to `source` for a verdict built before the pair
// existed, so an older shape keeps its previous behaviour rather than
// silently ceasing to arbitrate.
const judged_by = to_string(verdict?.judge_source ?? verdict.source ?? "")
if !verdict.vetoed || judged_by != "llm" {
return verdict
}
if !__completion_arbitration_converts(
to_string(verdict.gap_class ?? "other"),
evaluation.invoked.deterministic,
evaluation.payload.verification?.oracle_expected ?? false,
evaluation.deterministic_reason,
to_string(evaluation.payload.verification?.observed ?? "not_run"),
) {
return verdict
}
// `reasoning` keeps the judge's own words. The record must show that a real
// refusal was overruled, not that no refusal happened.
return verdict
+ {
vetoed: false,
verdict: "done",
feedback: nil,
next_step: "",
// `source` is the author of the verdict of record, and after this line
// that author is the gate. `judge_source` is deliberately untouched: it
// names who produced the judge's own answer, stays `llm` across an
// override, and is what the guard above reads so a later arbitration can
// still recognise a model refusal.
// `source` is the author of the verdict of record, and after this line
// that author is the gate. `judge_source` is deliberately untouched: it
// names who produced the judge's own answer, stays `llm` across an
// override, and is what the guard above reads so a later arbitration can
// still recognise a model refusal.
source: "gate",
override: __completion_conversion_override(verdict, __COMPLETION_VERIFICATION_CONVERSION),
converted_from: __COMPLETION_VERIFICATION_CONVERSION,
reason: __COMPLETION_VERIFICATION_CONVERSION,
}
}
/**
* Refuse a self-contradiction that survived its one re-ask and names nothing,
* on a run the deterministic gate observed verifying green.
*
* `__completion_contradiction_arbitration_converts` owns the conditions and why
* each is required. This is the second half of the harn#8060 fix: the re-ask
* gives the judge one chance to name a real gap or drop the field, and this
* stops an unnamed contradiction that took neither exit from spending the
* invocation budget on feedback no turn can answer.
*
* As with the verification arbitration, `reasoning` keeps the judge's own words,
* so the record shows a refusal that was overruled rather than one that never
* happened.
*
* @effects: []
* @errors: []
* @api_stability: internal
* @example: __completion_arbitrate_contradiction(verdict, evaluation)
*/
pub fn __completion_arbitrate_contradiction(
verdict: CompletionStageVerdict,
evaluation: CompletionPolicyEvaluation,
) -> CompletionStageVerdict {
// Reads `judge_source`, the party that produced the judge's own answer, not
// `source`, which names the author of the verdict of record and therefore
// reads "gate" on exactly the overridden decisions this arbitration exists
// to reconsider. Falls back to `source` for a verdict built before the pair
// existed, so an older shape keeps its previous behaviour rather than
// silently ceasing to arbitrate.
const judged_by = to_string(verdict?.judge_source ?? verdict.source ?? "")
if !verdict.vetoed || judged_by != "llm" {
return verdict
}
if to_string(verdict.reason ?? "") != "completion_judge_gap_contradicts_done" {
return verdict
}
if !__completion_contradiction_arbitration_converts(
to_string(verdict.gap_class ?? "other"),
verdict.contradiction_reasked ?? false,
evaluation.invoked.deterministic,
evaluation.payload.verification?.oracle_expected ?? false,
evaluation.deterministic_reason,
to_string(evaluation.payload.verification?.observed ?? "not_run"),
) {
return verdict
}
return verdict
+ {
vetoed: false,
verdict: "done",
feedback: nil,
next_step: "",
source: "gate",
override: __completion_conversion_override(verdict, __COMPLETION_CONTRADICTION_CONVERSION),
converted_from: __COMPLETION_CONTRADICTION_CONVERSION,
reason: __COMPLETION_CONTRADICTION_CONVERSION,
}
}