car-verify
Static plan verification for Agent IR in the Common Agent Runtime.
Deterministic graph and dataflow algorithms — no solver, no proof term. The checks
differ in strength (some are decision procedures, some are heuristics, some sample),
and every result carries a VerificationEvidence bundle naming what each check did
and did not establish. Read that rather than trusting valid alone; the module docs
in src/lib.rs spell out the taxonomy and the known blind spots.
That taxonomy is also carried in the data. Every VerifyIssue and CheckRecord
tags itself with an EvidenceTier — DecisionProcedure, Heuristic, or
Sampled — so a caller can tell an exact set-membership failure from the
count >= 3 loop rule without recognising the message string, and modules whose
findings all share one tier expose report.evidence_tier().
DecisionProcedure means the check decides the property it reports over the
inputs it was given. It is not a proof, not a soundness claim, and not a
prediction that the plan will run. Whether those inputs describe what actually
happens at runtime is a separate axis — the forward walk applies only what an
action declares — and that axis stays in cannot_verify / assumptions /
untested_regions, where it already was.
The tiers are kinds, not grades: EvidenceTier derives no Ord, because
Heuristic and Sampled fail in different directions and neither is
categorically stronger. Compare tiers by equality or match on them; filtering
findings down to DecisionProcedure throws away the only signal this crate has
for the things no decision procedure here covers.
Why carry the tier at all rather than leaving it in prose:
docs/proposals/shepherd-substrate-adoption.md,
"What we should also steal: how to state a formal claim" — the argument is that
a stronger claim ships honestly by being tiered and attached to each run,
which is the half CAR's existing "never say formal, never say sound" discipline
did not cover.
What it does
Statically analyzes action proposals without executing them. Verifies precondition satisfiability, detects write conflicts, checks state dependency availability, and flags repeated tool calls (loop detection). Can also simulate final state, test proposal equivalence, and optimize DAG parallelism by pruning phantom dependencies.
Usage
use ;
use ActionProposal;
let result = verify;
assert!;
let final_state = simulate;
let optimized = optimize;
Monte Carlo rollout
simulate answers "what state does this plan leave behind, assuming every
dispatched tool succeeds?" simulate_monte_carlo answers the question an
operator actually asks before running a plan against production: "how often does
this work, and when it doesn't, what breaks first?"
use ;
// Per-tool empirical success rates, from what actually happened. The daemon
// records a trajectory per execution; `Runtime::tool_feedback(30)` derives
// dispatch-conditional rates over the last 30 days.
let rates = runtime.tool_feedback.unwrap.tool_success_rates;
let mc = simulate_monte_carlo;
println!;
println!;
// Which failure hurts most, rather than which is most likely.
let worst = mc.action_outcomes.iter
.max_by;
The rollout kernel is the same one simulate uses, with one change: a
tool_call succeeds with probability p rather than always. Everything else —
pre-dispatch gating on preconditions and state dependencies, and the cascade
where a failure starves its dependents — is inherited, so the two agree exactly
when every rate is 1.0 (there is a test asserting this).
Sampling is a seeded SplitMix64 stream rather than rand, keeping this crate at
car-ir + serde and making runs byte-for-byte reproducible from
MonteCarloConfig::seed.
Deliberately not modelled, matching simulate's documented scope:
failure_behavior, partial effects, and correlated failure — draws are
independent, so a plan calling one flaky tool repeatedly reads more
optimistically here than it behaves when that tool's service is down. See the
montecarlo module docs for the full boundary.
External verifiers
VerificationEvidence describes the checks this crate runs. verifier opens that
bundle to checks it does not own — a test suite, a type checker, a browser
evidence collector, a human — so their verdicts land in the same
VerificationEvidence.checks vocabulary instead of travelling as prose.
use ;
// "The test suite must pass, and a model judge's opinion does not count as one."
let req = new.accepting;
let verdict = pass;
let decision = admit;
assert_eq!;
// decision.records folds straight into VerificationEvidence.checks
Three orthogonal axes, deliberately not collapsed into one: EvidenceTier (how
strong is this check?), VerifierAuthority (whose word is this — Advisory,
Binding, Operator?), and VerifierCost (what does running it cost?). admit
ignores cost entirely and treats authority as a kind of claim, not a grade.
The load-bearing default: a passing Operator verdict does not satisfy a
"tests" requirement unless that requirement opts in with operator_override. A
human clicking approve is a decision to proceed, not a demonstration that the
suite passes. Fail-closed throughout — Inconclusive and Skipped never
satisfy, and a requirement with no matching verdict is unmet rather than
vacuously true.
This module runs nothing; it defines the vocabulary and the fold. Invoking
verifiers belongs to whoever owns the process boundary (car-engine's
AdmissionGate, the supervision.* surface, a CI harness).
Attempt ledger
attempt answers the question a planner needs before it proposes: have we
already tried an equivalent approach under equivalent assumptions, and has
anything changed since?
use ;
let mut ledger = new;
ledger.record;
// Same assumptions — a decided dead end.
assert!;
// The declared retry condition changed — worth trying again.
assert!;
Matching is exact over normalised labels — it does not decide semantic
equivalence, and a caller that invents a fresh label per attempt honestly gets
Untried every time. SimilarFailure is a distinct answer for "relevant history
exists but the recorder declared no retry condition", kept separate so it is
rounded neither to a hard block nor to a clean slate.
Advisory, never a gate: a KnownFailure is a strong reason to pick another
branch, not a prohibition. Blocking belongs to the admission gates, which have
the authority model for it.
Verified exclusions
An Attempt carries the VerifierVerdicts that decided its outcome, which is
what makes a retained failure citable rather than merely recorded:
let attempt = failure
.with_verdicts;
assert!;
// ledger.verified_exclusions() lists only these — the routes foreclosed on evidence
A failure backed by a verifier with authority to decide has foreclosed a route.
A failure backed by nothing, or by an advisory opinion, is a note about one bad
afternoon — it still steers consult, but it is not evidence, and
verified_exclusions() omits it. AttemptAdvice::KnownFailure reports which
kind it found via its verified flag.
The distinction matters because a route abandoned on evidence and a route abandoned on vibes read identically once written into prose. Keeping them apart in the data is what lets a later decision to change course cite why — the input a contract amendment names as its justification.
Design and the wider audit this came from:
docs/proposals/argus-control-layer.md.
Part of CAR -- see the main repo for full documentation.