// 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, and stamps `converted_from` so a reader can count
// overrules without inferring them.
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,
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,
}
const __COMPLETION_VERIFICATION_CONVERSION: string = "failed_verification_contradicted_by_gate"
const __COMPLETION_CONTRADICTION_CONVERSION: string =
"gap_contradicts_done_survived_reask_over_passing_verifier"
/**
* 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 {
if !verdict.vetoed || verdict.source != "llm" {
return verdict
}
if !__completion_arbitration_converts(
to_string(verdict.gap_class ?? "other"),
evaluation.invoked.deterministic,
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: "",
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 {
if !verdict.vetoed || verdict.source != "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.deterministic_reason,
to_string(evaluation.payload.verification?.observed ?? "not_run"),
) {
return verdict
}
return verdict
+ {
vetoed: false,
verdict: "done",
feedback: nil,
next_step: "",
converted_from: __COMPLETION_CONTRADICTION_CONVERSION,
reason: __COMPLETION_CONTRADICTION_CONVERSION,
}
}