import {
CompletionRequirementAssessment,
CompletionRequirementEvidenceRecord,
CompletionRequirementReport,
completion_requirement_contract,
completion_requirement_pending_feedback,
completion_requirement_report,
} from "std/agent/completion_requirements"
import { COMPLETION_JUDGE_DEFAULT_CAP } from "std/agent/judge"
// std/agent/completion_gate — a configured done-time gate built by composing
// `verify_completion` (deterministic) with `verify_completion_judge` or
// `turn_end_condition` (bounded LLM). Domain facts remain host callbacks; this module
// owns their validation, evaluation, persistent veto budget, and option bundle.
import {
CompletionVerificationState,
__completion_gate_apply_budget,
__completion_gate_classify_writes,
__completion_gate_combine_verify,
__completion_gate_feedback_keys,
__completion_gate_ladder,
} from "std/agent/judge_internals"
import {
CompletionObligations,
completion_obligations_from_messages,
completion_obligations_requirements,
} from "std/agent/obligations"
import { CompletionRequirementContract, JudgeConfig } from "std/agent/options_types"
/**
* A host write-classification label. `"source"` and `"cosmetic"` are load-bearing; other kinds are treated as non-source.
*/
pub type WriteKind = string
/** One host-supplied write fact. */
pub type CompletionWriteFact = {path?: string, diff?: string, kind?: WriteKind}
/**
* A host-supplied verifier verdict. `findings` is optional red-detail text.
* `command` optionally names what the oracle ran, so the reading threaded to a
* model judge can say WHICH verification passed rather than only that one did.
*/
pub type CompletionVerifyVerdict = {ok?: bool, findings?: string, command?: string}
/**
* Facts a host supplies (via the `facts` callback) for one gate evaluation.
* All fields optional: absent write facts disable the evidence gate for that
* turn (never a fabricated pass), absent `verify` falls back to the
* `verify_command` callback. Rust twin: none — stdlib-owned.
*/
pub type CompletionFacts = {
consecutive_failed_after_write?: int,
source_write_count?: int,
cosmetic_write_count?: int,
writes?: list<CompletionWriteFact>,
verify?: CompletionVerifyVerdict | list<CompletionVerifyVerdict> | nil,
verification_failures_converging?: bool,
requires_write?: bool,
oracle_expected?: bool,
verification_command?: string,
verification_current?: bool,
verifier_available?: bool,
verification_discriminates?: bool,
observed_failure_required?: bool,
observed_failure?: bool,
requirement_contract?: CompletionRequirementContract,
requirement_assessments?: list<CompletionRequirementAssessment>,
requirement_evidence?: list<CompletionRequirementEvidenceRecord>,
}
/** Typed facts-callback input for one proposed completion. */
pub type CompletionGateContext = {
session_id: string,
task: string,
stop_reason: string,
text: string,
messages: list,
/*
* What the run is CURRENTLY held to: the opening task amended by every
* accepted mid-run steer (harn#7795). `task` stays the goal as written at
* session start, so a host reading it keeps today's behavior; a host that
* wants the amended target reads this instead of re-deriving it.
*
* Carried through from the loop rather than derived by each host on purpose.
* The context already carries `messages`, so every host COULD call
* `completion_obligations_from_messages` itself, but that would put the
* supersession rule in each host and let them drift from the judge's
* reading of the same run. One derivation, one owner.
*
* That owner reads the session's typed control record (harn#7875), so this
* also carries an accepted stop, which no reading of `messages` can see.
*
* An untouched run renders an empty steer list and no stop, so this is a
* no-op for every gate that exists today.
*/
obligations?: CompletionObligations,
}
/** Structured gate verdict delivered to a feedback decorator. */
pub type CompletionGateVerdict = {
ok?: bool,
reason?: string,
feedback?: string,
strict?: bool,
converted_from?: string,
escalation_recommended?: bool,
escalation_target?: string,
requirement_report?: CompletionRequirementReport,
}
pub type CompletionFactsProvider = fn(CompletionGateContext) -> CompletionFacts?
pub type CompletionWriteClassifier = fn(string, string?) -> WriteKind
pub type CompletionVerifier = fn() -> CompletionVerifyVerdict?
pub type CompletionFeedbackDecorator = fn(string, string, CompletionGateVerdict) -> string
/**
* Options for `completion_gate`. Domain facts enter as callbacks; policy knobs
* are plain data. Rust twin: none — stdlib-owned.
*
* Host-fact callbacks (all optional):
* - `facts(ctx) -> CompletionFacts` — the primary fact supplier. `ctx` carries
* `{session_id, task, stop_reason, text, messages}`.
* - `classify_write(path, diff?) -> WriteKind` — classifies a single write when
* `facts` returns a `writes` list without counts.
* - `verify_command() -> CompletionVerifyVerdict` — runs the verifier oracle when
* `facts` does not carry a `verify` verdict.
* - `feedback_decorator(reason, feedback, verdict) -> string` — decorates a
* veto message after the ladder and budget resolve it. `reason` is the stable
* ladder key; `verdict` carries the structured escalation fields.
*
* Policy knobs:
* - `require_source_write` (default true) — enforce the evidence gate ⛔#5.
* - `requires_write` — override the per-task "needs a source change" fact.
* - `max_vetoes` (default 3) — per-session
* soft-veto budget; 0 disables.
* - `requirement_contract` — declared task requirements, assessed from the typed
* requirement rows and evidence returned by `facts`. Missing, unsupported or
* unmet rows block completion even when a separate verifier passed.
* - `judge` (default off) — attach a bounded LLM completion judge (`true` or a
* JudgeConfig-shaped dict); defaults its cap to the compatibility value 5.
* - `judge_seam` (default `"verify_completion_judge"`) — which capped LLM seam
* the judge rides (`"verify_completion_judge"` or `"turn_end_condition"`).
* - `feedback_templates` (optional) — a dict overriding the rendered veto feedback
* strings per ladder rule (the coding-domain prose is host content, not
* mechanism). Keys are the recognized rules (`__completion_gate_feedback_keys`):
* `no_source_write_cosmetic`, `no_source_write_absent`,
* `verification_after_write_red` and `failed_verification` (support a
* `{findings}` token), `repeated_verification_failures` (supports `{attempts}`
* and `{findings}`), and `missing_verification`. Values must be strings.
* Absent keys keep the defaults.
* - `escalation_threshold` (default 3) — consecutive failed verifications needed
* before the streak-aware ladder recommends escalation.
* - `escalation_target` (optional) — a host routing channel copied onto an
* escalated verdict and its `judge_decision` event.
*/
pub type CompletionGateOptions = {
facts?: CompletionFactsProvider,
classify_write?: CompletionWriteClassifier,
verify_command?: CompletionVerifier,
feedback_decorator?: CompletionFeedbackDecorator,
require_source_write?: bool,
requires_write?: bool,
max_vetoes?: int,
exhaustion?: "allow" | "stop_unverified",
requirement_contract?: CompletionRequirementContract,
judge?: bool | JudgeConfig,
judge_seam?: string,
feedback_templates?: dict,
escalation_threshold?: int,
escalation_target?: string,
}
/**
* Evaluate write and verification facts without charging a retry or running callbacks.
* Hosts supply observations; the same ladder owns previews and live completion.
* @effects: []
* @errors: []
*/
pub fn completion_gate_verdict(
facts: CompletionFacts,
options: CompletionGateOptions = {},
) -> CompletionGateVerdict {
const counts = __completion_gate_write_counts(facts, options.classify_write)
const verify = __completion_gate_combine_verify(facts.verify)
const saw_source = counts.source_known && counts.source_write_count > 0
const green = verify != nil && (verify.ok ?? false)
if saw_source && facts.verifier_available == false {
return {
ok: false,
strict: true,
reason: "verifier_unavailable",
feedback: "The verifier could not start on this host. The change remains unverified.",
}
}
if saw_source && green {
if facts.verification_current == false {
return {
ok: false,
strict: true,
reason: "stale_verification",
feedback: "Verification predates the latest change. Run it again before finishing.",
}
}
if facts.verification_discriminates == false {
return {
ok: false,
strict: true,
reason: "non_discriminating_verification",
feedback:
"This verification already passed without the change. Exercise the changed behavior and observe a failing and passing result.",
}
}
if (facts.observed_failure_required ?? false) && !(facts.observed_failure ?? false) {
return {
ok: false,
strict: true,
reason: "required_failure_unobserved",
feedback:
"The task requires a failing check before the fix. Observe that failure, then verify the fix.",
}
}
}
return __completion_gate_ladder(
{
source_known: counts.source_known,
source_write_count: counts.source_write_count,
cosmetic_write_count: counts.cosmetic_write_count,
requires_write: options.requires_write ?? facts.requires_write
?? options.require_source_write
?? true,
verify: if facts.verification_current == false {
nil
} else {
verify
},
oracle_expected: facts.oracle_expected
?? (facts.verify != nil || options.verify_command != nil),
consecutive_failed_after_write: facts.consecutive_failed_after_write,
verification_failures_converging: facts.verification_failures_converging ?? false,
escalation_threshold: options.escalation_threshold ?? 3,
escalation_target: options.escalation_target,
},
options.feedback_templates,
)
}
pub type CompletionGateVetoCounts = {total: int, by_class: dict}
pub type CompletionGateBudget = {used: int, exhausted: bool, stop_unverified: bool}
/**
* Read the retry window for the current phase without changing its counters.
* A source change starts a fresh window; earlier no-source refusals remain visible.
* Strict failures can end unverified, but can never become a successful completion.
* @effects: []
* @errors: []
*/
pub fn completion_gate_budget(
counts: CompletionGateVetoCounts,
saw_source: bool,
strict: bool,
options: CompletionGateOptions = {},
) -> CompletionGateBudget {
const used = if saw_source {
max(0, counts.total - (to_int(counts.by_class.no_source_write) ?? 0))
} else {
counts.total
}
const cap = options.max_vetoes ?? 3
const stop_unverified = options.exhaustion == "stop_unverified"
return {
used: used,
exhausted: cap > 0 && used >= cap && (!strict || stop_unverified),
stop_unverified: stop_unverified,
}
}
/**
* A host-fact callback may be an anonymous closure or a named `fn`; the runtime
* reports these as "closure", "function", or "fn" (mirrors std/agent/goal's
* `__goal_callable`). Gating on "closure" alone would reject named-fn callbacks.
*/
fn __completion_gate_callable(value: any) -> bool {
const kind = type_of(value)
return kind == "closure" || kind == "function" || kind == "fn"
}
fn __completion_gate_options(options: CompletionGateOptions) -> CompletionGateOptions {
const contract = completion_requirement_contract(options.requirement_contract)
if contract == nil {
return options
}
return options + {requirement_contract: contract}
}
/** Validate a CompletionGateOptions at config time. */
fn __completion_gate_validate_options(opts: dict) {
for field in ["facts", "classify_write", "verify_command", "feedback_decorator"] {
const value = opts[field]
if value != nil && !__completion_gate_callable(value) {
throw "agent_completion_gate: `" + field + "` must be a callable or nil; got "
+ type_of(value)
}
}
if opts?.requirement_contract != nil && opts?.facts == nil {
throw "agent_completion_gate: `requirement_contract` needs a `facts` callback that supplies requirement assessments and evidence."
}
if opts?.classify_write != nil && opts?.facts == nil {
throw "agent_completion_gate: `classify_write` only classifies the writes `facts` returns, so it does nothing without `facts` — set `facts` too or remove `classify_write`."
}
if opts?.escalation_threshold != nil
&& (type_of(opts.escalation_threshold) != "int"
|| opts.escalation_threshold
< 1) {
throw "agent_completion_gate: `escalation_threshold` must be a positive int; got "
+ to_string(opts.escalation_threshold)
}
if opts?.escalation_target != nil
&& (type_of(opts.escalation_target) != "string"
|| trim(opts.escalation_target)
== "") {
throw "agent_completion_gate: `escalation_target` must be a non-empty string; got "
+ to_string(opts.escalation_target)
}
if opts?.judge_seam != nil
&& opts.judge_seam != "verify_completion_judge"
&& opts.judge_seam != "turn_end_condition" {
throw "agent_completion_gate: `judge_seam` must be \"verify_completion_judge\" or \"turn_end_condition\"; got "
+ to_string(opts.judge_seam)
}
const templates = opts?.feedback_templates
if templates != nil {
if type_of(templates) != "dict" {
throw "agent_completion_gate: `feedback_templates` must be a dict; got " + type_of(templates)
}
const known = __completion_gate_feedback_keys()
for key in keys(templates) {
if !contains(known, key) {
throw "agent_completion_gate: unknown `feedback_templates` key `" + key
+ "`; expected one of "
+ join(known, ", ")
}
if type_of(templates[key]) != "string" {
throw "agent_completion_gate: `feedback_templates." + key + "` must be a string; got "
+ type_of(templates[key])
}
}
}
}
/** Run the host feedback decorator only for a veto that will be delivered. */
fn __completion_gate_decorate_feedback(verdict: dict, decorator: any) {
if decorator == nil || (verdict?.ok ?? false) {
return verdict
}
const decorated = decorator(
to_string(verdict?.reason ?? ""),
to_string(verdict?.feedback ?? ""),
verdict,
)
if type_of(decorated) != "string" {
throw "agent_completion_gate: `feedback_decorator` must return a string; got "
+ type_of(decorated)
}
return verdict + {feedback: decorated}
}
fn __completion_gate_veto_key(session_id: string) {
return "harn.completion_gate." + session_id + ".veto_count"
}
fn __completion_gate_veto_class_key(session_id: string, reason: any) {
return "harn.completion_gate." + session_id + ".veto_count." + reason
}
const __COMPLETION_GATE_VETO_CLASSES = [
"requirements_unmet",
"failed_verification",
"missing_verification",
"stale_verification",
"non_discriminating_verification",
"required_failure_unobserved",
"no_source_write",
"repeated_verification_failures",
"verification_after_write_red",
]
fn __completion_gate_vetoes_used(runtime: HarnessRuntime, session_id: string) {
if session_id == "" {
return 0
}
return to_int(runtime.store_get(__completion_gate_veto_key(session_id)) ?? 0) ?? 0
}
fn __completion_gate_class_vetoes_used(runtime: HarnessRuntime, session_id: string, reason: any) {
if session_id == "" {
return 0
}
return to_int(runtime.store_get(__completion_gate_veto_class_key(session_id, reason)) ?? 0) ?? 0
}
/**
* Read the completion-gate veto counters for a session.
*
* @effects: []
* @errors: []
* @api_stability: experimental
* @example: agent_completion_gate_veto_counts(session_id).by_class.no_source_write
*/
pub fn agent_completion_gate_veto_counts(
runtime: HarnessRuntime,
session_id: string?,
) -> CompletionGateVetoCounts {
const sid = to_string(session_id ?? "")
let by_class = {}
for reason in __COMPLETION_GATE_VETO_CLASSES {
by_class = by_class + {[reason]: __completion_gate_class_vetoes_used(runtime, sid, reason)}
}
return {by_class: by_class, total: __completion_gate_vetoes_used(runtime, sid)}
}
/**
* Charge a delivered refusal, including its class, using the gate's own store.
* @effects: [store]
* @errors: [runtime]
*/
pub fn agent_completion_gate_record_veto(
runtime: HarnessRuntime,
session_id: string,
reason: string = "",
) -> nil {
if session_id == "" {
return nil
}
runtime.store_set(
__completion_gate_veto_key(session_id),
__completion_gate_vetoes_used(runtime, session_id) + 1,
)
if reason != "" {
runtime.store_set(
__completion_gate_veto_class_key(session_id, reason),
__completion_gate_class_vetoes_used(runtime, session_id, reason) + 1,
)
}
}
fn __completion_gate_write_counts(raw: dict, classify_write: any) {
if raw?.source_write_count != nil {
return {
source_write_count: to_int(raw.source_write_count) ?? 0,
cosmetic_write_count: to_int(raw?.cosmetic_write_count ?? 0) ?? 0,
source_known: true,
}
}
if raw?.writes != nil {
const counts = __completion_gate_classify_writes(raw.writes, classify_write)
return counts + {source_known: true}
}
return {source_write_count: 0, cosmetic_write_count: 0, source_known: false}
}
fn __completion_gate_verification(
verify: CompletionVerifyVerdict?,
facts: CompletionFacts,
oracle_expected: bool,
) -> CompletionVerificationState {
const accepted = facts.verification_current != false && facts.verifier_available != false
&& facts.verification_discriminates != false
&& (!(facts.observed_failure_required ?? false) || (facts.observed_failure ?? false))
const observed_command = to_string(verify?.command ?? "").trim()
return {
oracle_expected: oracle_expected,
command: if observed_command != "" {
observed_command
} else {
to_string(facts.verification_command ?? "").trim()
},
observed: if verify == nil || !accepted {
"not_run"
} else if verify.ok ?? false {
"passed"
} else {
"failed"
},
observed_at_evidence_index: nil,
}
}
fn __completion_gate_evaluate(
runtime: HarnessRuntime,
payload: dict,
opts: CompletionGateOptions,
facts_available: any,
charge: bool = true,
) {
const session_id = to_string(payload?.session_id ?? "")
const classify_write = opts.classify_write
const verify_command = opts.verify_command
const facts = opts.facts
const max_vetoes = opts.max_vetoes ?? 3
if !facts_available {
// This `confirm` means "I could not look", not "I looked and it passed".
// The reading it ships says exactly that, so a consumer cannot mistake the
// confirm for a positive observation.
return {
confirm: true,
reason: "facts_unavailable",
verification: {
oracle_expected: false,
command: "",
observed: "not_run",
observed_at_evidence_index: nil,
},
}
}
const messages = json_parse(to_string(payload?.transcript ?? "[]")) ?? []
const task = to_string(payload?.task ?? "")
const ctx = {
session_id: session_id,
task: task,
stop_reason: to_string(payload?.stop_reason ?? ""),
text: to_string(payload?.text ?? ""),
messages: messages,
// The loop already derived this from the session's typed control record,
// which is the only reading that can see an accepted stop. Re-deriving it
// from `messages` here would silently downgrade to the transcript reading
// and lose the stop. The fallback covers a host that calls the gate with a
// bare payload rather than the loop's evidence snapshot.
obligations: payload?.obligations ?? completion_obligations_from_messages(messages, task),
}
const raw = if facts != nil {
facts(ctx) ?? {}
} else {
{}
}
const counts = __completion_gate_write_counts(raw, classify_write)
const verify_raw = if raw?.verify != nil {
raw.verify
} else if verify_command != nil {
verify_command()
} else {
nil
}
const verify = __completion_gate_combine_verify(verify_raw)
// A retarget retires the rows declared under the replaced objective, exactly
// as it does for the judge; the gate must not veto on work the user withdrew.
const requirement_report = completion_requirement_report(
if raw?.requirement_contract != nil {
completion_requirement_contract(raw.requirement_contract)
} else {
completion_obligations_requirements(opts.requirement_contract, ctx.obligations).contract
},
raw?.requirement_assessments,
{requirement_evidence: raw?.requirement_evidence ?? []},
)
const requires_write = if opts.requires_write != nil {
opts.requires_write
} else if raw?.requires_write != nil {
raw.requires_write
} else {
opts.require_source_write ?? true
}
const derived = {
consecutive_failed_after_write: raw?.consecutive_failed_after_write,
source_known: counts.source_known,
source_write_count: counts.source_write_count,
cosmetic_write_count: counts.cosmetic_write_count,
requires_write: requires_write,
verify: verify,
verification_failures_converging: raw?.verification_failures_converging ?? false,
oracle_expected: raw?.oracle_expected ?? (verify_command != nil || raw?.verify != nil),
escalation_threshold: opts.escalation_threshold ?? 3,
escalation_target: opts.escalation_target,
}
// The gate already computes this to reach its own verdict. Returning only
// `{confirm, reason}` threw it away, and the model judge downstream was then
// asked a question this oracle had answered, with no way to see the answer.
const verification = __completion_gate_verification(verify, raw, derived.oracle_expected)
const evidence_verdict = completion_gate_verdict(
raw
+ {
source_write_count: if counts.source_known {
counts.source_write_count
} else {
nil
},
cosmetic_write_count: counts.cosmetic_write_count,
verify: verify,
requires_write: requires_write,
oracle_expected: derived.oracle_expected,
},
opts,
)
const verdict = if requirement_report != nil && !requirement_report.complete {
evidence_verdict
+ {
ok: false,
reason: "requirements_unmet",
strict: true,
feedback: join(
[
to_string(evidence_verdict.feedback ?? ""),
completion_requirement_pending_feedback(requirement_report),
]
.filter({ message -> message != "" }),
"\n\n",
),
}
} else {
evidence_verdict
}
const budget = completion_gate_budget(
agent_completion_gate_veto_counts(runtime, session_id),
counts.source_write_count > 0,
verdict.strict ?? false,
opts,
)
if raw?.verifier_available == false && counts.source_write_count > 0 {
return {
confirm: false,
action: "stop_unverified",
reason: "verifier_unavailable",
message: __completion_gate_decorate_feedback(verdict, opts.feedback_decorator).feedback,
verification: verification,
requirement_report: requirement_report,
}
}
if !(verdict.ok ?? false) && budget.exhausted && budget.stop_unverified {
return {
confirm: false,
action: "stop_unverified",
reason: "veto_budget_exhausted",
converted_from: verdict.reason,
message: __completion_gate_decorate_feedback(verdict, opts.feedback_decorator).feedback,
verification: verification,
requirement_report: requirement_report,
}
}
const budgeted = __completion_gate_apply_budget(verdict, budget.used, max_vetoes)
if charge && budgeted.charge {
agent_completion_gate_record_veto(runtime, session_id, verdict.reason ?? "")
if session_id != "" && counts.source_known && counts.source_write_count == 0
&& requires_write
&& verdict.reason != "no_source_write" {
runtime.store_set(
__completion_gate_veto_class_key(session_id, "no_source_write"),
__completion_gate_class_vetoes_used(runtime, session_id, "no_source_write") + 1,
)
}
}
const resolved = __completion_gate_decorate_feedback(
budgeted.verdict + {requirement_report: requirement_report},
opts.feedback_decorator,
)
if resolved?.ok ?? false {
return {
confirm: true,
reason: resolved?.reason ?? "",
converted_from: resolved?.converted_from,
escalation_recommended: resolved?.escalation_recommended,
escalation_target: resolved?.escalation_target,
verification: verification,
requirement_report: requirement_report,
}
}
return {
confirm: false,
message: to_string(resolved?.feedback ?? ""),
reason: resolved?.reason ?? "",
escalation_recommended: resolved?.escalation_recommended,
escalation_target: resolved?.escalation_target,
verification: verification,
requirement_report: requirement_report,
}
}
/**
* Preview the same completion decision without spending a retry.
* Fact and verifier callbacks still execute; only counter mutation is suppressed.
* @effects: [store]
* @errors: [runtime]
*/
pub fn agent_completion_gate_preview(
runtime: HarnessRuntime,
payload: dict,
options: CompletionGateOptions = {},
) -> dict {
const opts = __completion_gate_options(options)
__completion_gate_validate_options(opts)
return __completion_gate_evaluate(
runtime,
payload,
opts,
opts.facts != nil || opts.verify_command != nil,
false,
)
}
/**
* Build a completion-gate option bundle to spread into `agent_loop` options.
*
* @effects: [store]
* @errors: [runtime]
* @api_stability: experimental
* @example: agent_completion_gate(harness.runtime, {facts: host_facts, verify_command: host_verify})
*/
pub fn agent_completion_gate(runtime: HarnessRuntime, options: CompletionGateOptions = {}) -> dict {
const opts = __completion_gate_options(options)
__completion_gate_validate_options(opts)
const facts_available = opts.facts != nil || opts.verify_command != nil
const gate =
fn(payload) { return __completion_gate_evaluate(runtime, payload, opts, facts_available) }
let out = {
verify_completion: gate,
_completion_gate: {
facts_available: facts_available,
max_vetoes: to_int(opts.max_vetoes ?? 3) ?? 3,
require_source_write: opts.require_source_write ?? true,
},
}
const judge = opts.judge
if judge != nil && judge {
const judge_cfg = if type_of(judge) == "dict" {
judge
} else {
{}
}
const seam = opts.judge_seam ?? "verify_completion_judge"
const cap = judge_cfg?.max_invocations ?? COMPLETION_JUDGE_DEFAULT_CAP
out = out + {[seam]: judge_cfg + {max_invocations: cap}}
}
return out
}