Skip to content

Audit

Reasoning audits

Every specific in the agent's written rationale, checked against the rows that run actually served. A figure, a name, a date or an event that appears in none of them is quoted back with its position and scored.

BUILDING The package is pit-audit, Apache-2.0, Python 3.11 or newer, with no runtime dependencies. It installs from the repository and runs offline; metered pricing per decision audited is not live yet.

  • pip
  • pit-audit run
  • CLI · Python
  • exit 1 on unsupported

The artifact

A figure that appears in no row the run served

Our own test agent, walking the SVB window over four sessions in March 2023, wrote that the 8-K announced “an urgent $2.25B capital raise”. The rows it had been served that session were filing metadata, whose entire content is strings like SVB FINANCIAL GROUP 8-K. The figure is correct and it came out of the model's training data, since it appears in nothing the run served. The trajectory ships with the package, so pit-audit calibrate reproduces this on your machine with no key and no model.

pit-audit run --trajectory svb-2023-03.jsonlexit 1

unsupported: 9 flagged of 12 claims over 5 rationales (5 at or above entity), worst number
  2023-03-08 main/pit number[number] unmatched '$2.25B'
  2023-03-08 main/pit number[number] unmatched '$2.25 billion'
  2023-03-08 main/pit event[entity] unmatched 'Form 8-K (2023-03-08) announcing large …'
  2023-03-07 main/pit vague[vague] unmatched 'The recent 10-K highlights mounting …'
  cell main/pit: unsupported, 6 flagged, worst number
  cell redacted/pit: grounded, 1 flagged, worst vague
  cell shifted/pit: grounded, 2 flagged, worst vague

The finding is the part that makes the verdict checkable rather than asserted. It reports how many served values were considered, names the nearest one, and gives the gap:

findings.json · one findingrung none

{
  "claim_id": "940eadc8ea9c109c",
  "context": "Form 8-K (2023-03-08) announcing large realized portfolio losses and an urgent $2.25B capital raise",
  "decision": {
    "arm": "main",
    "clock": "pit",
    "known_at": "2023-03-08T23:59:59Z",
    "seq": 9,
    "session_date": "2023-03-08"
  },
  "detail": {
    "quantity": {
      "currency": "USD",
      "kind": "money",
      "raw": "2.25",
      "scale": "b",
      "value": 2250000000.0
    }
  },
  "end": 85,
  "extractor": "rules",
  "field": "meta.key_signal",
  "flagged": true,
  "kind": "number",
  "match": {
    "candidates_considered": 966,
    "claimed_value": 2250000000.0,
    "matched": false,
    "nearest_served": {
      "absolute_gap": 405447766.0,
      "raw": "1844552234",
      "relative_gap": 0.180199,
      "value": 1844552234.0
    },
    "rung": "none"
  },
  "quote": "$2.25B",
  "severity": "number",
  "start": 79,
  "unmatched": true
}

966 candidate values were considered for that cell. The closest was 1,844,552,234, a magnitude derived from an accession number, 18.0% away and outside the loosest 5% rung. The redacted and shifted arms read the same 86 rows with the issuer removed and with every date moved ten years forward, and neither produced an unmatched number or entity; their findings are all at vague.

The failure it prevents

An agent that remembers the answer and calls it analysis

Control arms bound contamination at the level of a score. They tell you the main arm beat the shifted arm by some margin. They do not tell you which sentence did it, and a team reading a leaderboard has nothing to point at.

A score gap is an aggregate

It moves for many reasons and averages over every decision in the run. A single fabricated figure can carry a whole arm and leave the delta looking ordinary.

A quoted span is evidence

The finding carries the claim verbatim, the sentence it sat in, character offsets into the field it came from, and the arm, clock and session it was written at, which is enough to go and read the original.

How it works

Extract with a model if you like, match without one

The served set is rebuilt from the trajectory

The set a claim is checked against is rebuilt from the trajectory's recorded tool results, prompt text and probe items. It is cumulative and per cell: a claim made on the 2023-03-08 session in main on the PIT clock is checked against everything that cell had been served by 2023-03-08, never against a row the run revealed on 2023-03-10 and never against what redacted saw. Prompts count as served, because a prompt states the session date and an agent quoting it back is quoting something it was handed.

Matching runs strict to generous, and reports where it stopped

RungClears when
exactthe claimed magnitude equals a served magnitude
roundedthey differ by less than half the last digit either was written to, in the units it was written in
scale_shiftthey differ by a factor of 100 up to 1e12 within 0.5%
within_0p5pctthe relative gap is 0.5% or less
within_2pctthe relative gap is 2% or less
within_5pctthe relative gap is 5% or less

The loose rungs are there because a model writing from thin context reconstructs figures it half-remembers rather than copying served bytes (arXiv 2504.14765). An exact-string matcher would report ordinary paraphrase as fabrication, and the fabrications that matter would sit underneath the noise. The rung that cleared a claim is written into the finding, so a reader who wants a stricter audit can see which claims cleared only on the loose rungs, and pass a shorter ladder.

A percentage is only ever compared with a percentage. Entities match on alias forms, so SVB clears against a served SVB FINANCIAL GROUP, and the alias table ships empty because an alias asserts something the trajectory does not.

Severity, and what fails a build

Claims are typed number, entity, date, event or vague, and severity runs number above entity above date above vague. The verdict is unsupported when the count of flagged findings at or above --severity (default entity) exceeds --max-flagged (default 0). run exits 1 on unsupported, so a CI gate can fail on it.

Nothing is averaged across cells. A control arm answering “only the filing metadata is knowable” scores badly against the filing metadata, and a run-level number would blend its hedging into the main arm's fabrications.

The anachronism check

KindFires whenEvidence
future_datethe rationale writes a date later than the decision instantthe rationale's own words
refused_rowthe rationale quotes a row key a tool refused as not_yet_knowablethe trajectory's own refusal record
future_eventthe rationale names a term in a supplied event calendar dated after the decision instantthe calendar entry's source string

The first two need nothing external. The event calendar ships empty and is never inferred, and the report records its name, provenance and entry count so a reader can see what the check ran against.

Integration

Point it at a trajectory

terminaloffline, no key

$ cd audit && uv venv && uv pip install -e '.[dev]'
$ pit-audit run --trajectory trajectory.jsonl \
    --out findings.json --certificate audit.json
$ pit-audit calibrate          # grade the extractor on nine findings a person made

--certificate audit.json writes the document that fills the reasoning-audit slot in a contamination certificate. pit_harness.certify owns that block and recomputes its counts from the findings rather than copying them, so a certificate cannot state a total its own findings do not support.

Extraction can be model-assisted, and matching never is. A model chooses spans; typing, matching and severity are computed here. Every returned quote must be a verbatim substring of the rationale or it is dropped and counted, and a provider error falls back to the rule extractor and is counted too.

What it measures, and what it does not

  • A cleared rung means the run served a close-enough value. Whether that row was right, and whether the agent read it correctly, are outside what this checks.
  • Vague findings are noisy by construction. An agent stating what it cannot see scores badly against what it was shown, so those findings are reported and left below the default threshold rather than dropped.
  • A run with no written rationale cannot be audited. The report says so in scope.decisions_with_rationale rather than reading as clean.
  • Contamination that never reached the written text does not show up here. The control arms and the positive-control probe cover that.

Related

What sits either side of it

Upstream

Evaluation Harness

The trajectory this reads, and the receipt it is bound to.

Alongside

Benchmark Datasets

The redacted and shifted arms whose rationales this compares against the main one.

Downstream

Contamination Certificates

The signed file this fills the reasoning-audit slot in.