import { CompletionEvidenceSnapshot } from "std/agent/completion_evidence"
import { JudgeCheckpointRequest, __judge_run_checkpoint } from "std/agent/judge_internals"
import {
CompletionJudgeVerdict,
__judge_completion_verdict_contradicts_itself,
__judge_completion_verdict_read,
__judge_ungrounded_gap_quotes,
} from "std/agent/judge_verdict"
import { agent_emit_event } from "std/agent/state"
/** Judge response after at most one self-contradiction re-ask. */
pub type JudgeContradictionResolution = {data: dict, reasked: bool}
/**
* The instruction added to the user turn on the one contradiction re-ask.
*
* It names the contradiction and states both exits, because a judge that
* populated `gap_class` beside `done` is not choosing between them — it is
* filling a field it was offered. Telling it only "do not do that" leaves the
* same reply available; telling it which half to keep does not.
*/
const __JUDGE_CONTRADICTION_RETRY_PROMPT: string =
"\n\nYour previous reply set `verdict` to \"done\" and also populated `gap_class`. Those cannot both be true: `gap_class` names an obligation that is still unmet, and `done` says none is. Answer again, keeping exactly one. If the work is finished, reply `done` and OMIT `gap_class` entirely. If an obligation is still unmet, reply `continue`, name that obligation in `gap_class`, and say in `detail` what remains to be done."
/**
* Resolve a judge response that accepted completion while naming a gap.
*
* `done` beside a populated `gap_class` is a self-contradiction the caller
* refuses (harn#7910). Refusing it is right; refusing it N times is not. The
* re-ask prompt is unchanged between invocations, so a judge whose reply is
* stable returns the identical contradiction on every call, the actor is handed
* feedback that asserts the work is finished, and the loop spends its whole
* invocation budget before dying at the cap on a run nothing ever refused.
*
* So the contradiction is resolved the way an unitemized approval already is:
* re-ask the JUDGE exactly once, with the contradiction named, on the user turn
* so the cached stable system prefix stays byte-identical. Whatever comes back
* is the reply of record. `reasked` says the resolution was attempted, which is
* what lets a later arbitration distinguish a first contradiction from one that
* survived being pointed out.
*
* `contradictory` is the caller's reading of the first reply, because the caller
* owns the verdict vocabulary. A re-ask whose checkpoint could not be read keeps
* the first reply rather than inventing one: the contradiction then stands and
* the caller refuses exactly as it does today.
*
* @effects: [llm, event]
* @errors: []
* @api_stability: internal
* @example: __judge_resolve_contradiction(harness, request, data, true)
*/
pub fn __judge_resolve_contradiction(
harness: Harness,
request: JudgeCheckpointRequest,
data: dict,
contradictory: bool,
) -> JudgeContradictionResolution {
if !contradictory {
return {data: data, reasked: false}
}
return __judge_reask_once(
harness,
request,
data,
__JUDGE_CONTRADICTION_RETRY_PROMPT,
"contradiction",
)
}
/**
* Resolve a refusal that quoted evidence the judge was never shown.
*
* Same treatment and the same reasoning as the contradiction above: the reply
* is not usable, and handing the actor a complaint sourced to a line that is
* not in the file view is worse than asking again. Measured 2026-09-05 on two
* scheduler trials, where the judge refused ten times over a file view it had
* been shown, phrasing the gap against "the current scheduler.go shown in the
* live edited window".
*
* The re-ask names the offending quotes, because a judge told only "that was
* wrong" has the same reply available to it.
*
* @effects: [llm, event]
* @errors: []
* @api_stability: internal
* @example: __judge_resolve_ungrounded_quotes(harness, request, data, ["queue = append(queue, s)"])
*/
pub fn __judge_resolve_ungrounded_quotes(
harness: Harness,
request: JudgeCheckpointRequest,
data: dict,
quotes: list<string>,
) -> JudgeContradictionResolution {
if len(quotes) == 0 {
return {data: data, reasked: false}
}
return __judge_reask_once(
harness,
request,
data,
__judge_ungrounded_quote_retry_prompt(quotes),
"ungrounded_quote",
)
}
/**
* The instruction added to the user turn when a refusal quoted absent evidence.
*
* Named as paraphrase rather than as invention, because that is what was
* measured: the ungroundable quotes are sentences the judge composed about the
* evidence, not lines it hallucinated. The best line-level match for a rejected
* quote scored 0.50, which is the signature of a summary of a real region, not
* of a fabricated one. "Quote only lines that are actually there" does not
* reach that failure: a judge that believes it is describing what it saw reads
* the instruction as already satisfied and returns the same paraphrase.
*
* So the re-ask asks for a copied span, puts the judge's own words where they
* belong, and refuses the exit that costs nothing. An empty quote is an exit:
* the grounding check skips empty quotes by design, so a judge that cannot find
* a span can drop the field and keep the refusal. The prompt closes that by
* naming it, and requires either a span or a `continue` whose gap is stated in
* words without claiming evidence for it.
*
* Exported so the four instructions can be pinned by a test without a model.
*
* @effects: []
* @errors: []
* @api_stability: internal
* @example: __judge_ungrounded_quote_retry_prompt(["the queue is still unsorted"])
*/
pub fn __judge_ungrounded_quote_retry_prompt(quotes: list<string>) -> string {
return "\n\nYour previous reply refused the work and attributed it to evidence above, "
+ "but these quotes do not appear anywhere in the evidence you were given:\n"
+ join(quotes.map({ quote -> " - " + quote }), "\n")
+ "\nThese are paraphrases. You summarized what the evidence showed instead of "
+ "copying from it, so what you wrote is your sentence about the evidence and "
+ "not a part of the evidence itself.\n"
+ "Answer again under these rules:\n"
+ "- `evidence_quote` must be a span copied character for character from the "
+ "transcript or tool output above. Copy it; do not compose, summarize, "
+ "reword, join separate lines, or fill in an ellipsis.\n"
+ "- Put what you think the span shows in `detail`, in your own words. "
+ "`detail` is where your reading of the evidence belongs; the quote is only "
+ "the raw span it is a reading of.\n"
+ "- An empty `evidence_quote` is not an accepted answer here. Leaving it out "
+ "does not withdraw the refusal and does not resolve this. If no span "
+ "supports the gap, reply `continue` with the gap named in `detail` and no "
+ "evidence claimed for it, so the gap stands on your judgement rather than "
+ "on a quote.\n"
+ "If the evidence does not in fact show the gap you had in mind, that gap is not "
+ "supported and you should not raise it; if the evidence shows the work IS done, reply `done`."
}
/**
* One re-ask on the user turn, with the reason named.
*
* Both re-ask reasons share this so the retry stays a single behaviour: the
* stable system prefix is untouched (so the cached prompt prefix stays
* byte-identical), a re-ask whose checkpoint could not be read keeps the first
* reply rather than inventing one, and the attempt is recorded either way.
*/
fn __judge_reask_once(
harness: Harness,
request: JudgeCheckpointRequest,
data: dict,
prompt: string,
reason: string,
) -> JudgeContradictionResolution {
const reask = __judge_run_checkpoint(harness, request + {user: request.user + prompt})
const readable = reask.checkpoint?.ok ?? false
agent_emit_event(
harness.agent,
request.payload?.session_id,
"typed_checkpoint",
{
schema: "harn.completion_judge_contradiction_retry.v1",
reask_status: to_string(reask.checkpoint?.status ?? "ok"),
reask_readable: readable,
reask_reason: reason,
},
)
if readable {
return {data: reask.checkpoint?.data ?? {}, reasked: true}
}
return {data: data, reasked: true}
}
/**
* A judge reply after every re-ask, with the reply it came from.
*
* `contradiction_reasked` is deliberately NOT "either re-ask happened". The
* arbitration ladder reads it to tell a FIRST self-contradiction from one that
* survived being pointed out, and a grounding re-ask says nothing about that.
* Folding the two together would let a quote correction overrule a judge whose
* contradiction had never been challenged.
*/
pub type JudgeSettledReply = {
decoded: CompletionJudgeVerdict,
reply_of_record: dict,
contradiction_reasked: bool,
}
/**
* Run both re-ask reasons in order and return the reply of record.
*
* Two things can make a reply unusable, and both are readable without a model:
*
* 1. `done` beside a named gap. The judge accepted and refused inside one
* object (harn#7910, harn#8060).
* 2. A refusal quoting evidence that is not in the packet the judge was handed
* (#8100). Measured on two scheduler trials, where the judge refused ten
* times over a file view that already showed the fix.
*
* Each is re-asked exactly once, in that order, because a grounding check
* should run against the reply the contradiction pass settled on rather than
* against one already superseded. `reply_of_record` is whichever reply survived
* both, so callers never read a clause off a superseded one.
*
* The grounding pass only applies to a refusal: an approval names no gap, so it
* quotes nothing and there is nothing to ground.
*
* @effects: [llm, event]
* @errors: []
* @api_stability: internal
* @example: __judge_settle_reply(harness, request, contract, payload, data, decoded)
*/
pub fn __judge_settle_reply(
harness: Harness,
request: JudgeCheckpointRequest,
contract: unknown,
payload: CompletionEvidenceSnapshot,
data: dict,
decoded: CompletionJudgeVerdict,
) -> JudgeSettledReply {
const contradiction = __judge_resolve_contradiction(
harness,
request,
data,
__judge_completion_verdict_contradicts_itself(decoded),
)
const after_contradiction = if contradiction.reasked {
__judge_completion_verdict_read(contradiction.data, contract, payload.judge_evidence_packet)
} else {
decoded
}
// `payload.judge_evidence` is the RENDERED packet the judge was handed, so
// this compares a quote against what it saw and never against what the
// workspace happens to hold now.
const grounding = if after_contradiction.verdict == "continue" {
__judge_resolve_ungrounded_quotes(
harness,
request,
contradiction.data,
__judge_ungrounded_gap_quotes(after_contradiction.gaps, payload.judge_evidence),
)
} else {
{data: contradiction.data, reasked: false}
}
if grounding.reasked {
return {
decoded: __judge_completion_verdict_read(
grounding.data,
contract,
payload.judge_evidence_packet,
),
reply_of_record: grounding.data,
contradiction_reasked: contradiction.reasked,
}
}
return {
decoded: after_contradiction,
reply_of_record: contradiction.data,
contradiction_reasked: contradiction.reasked,
}
}