Proof page. Multi agent orchestration.
Four agents (classifier, extractor, validator, router) plus a supervisor that owns retry, escalation and graceful degradation, wired as a LangGraph state graph in Python. Every document ends with a decision, a written reason and an audit trail. The tables below are read directly out of the committed run files, so what you see is what the run produced.
What is real and what is not. The pipeline, the model calls and every
number on this page are real. The 26 input documents are synthetic, generated by a
script in the repo, and every email address in them is on a .example domain. There is no
real company, person, supplier, tenant or deal anywhere in the project. Accuracy figures are measured
against that synthetic corpus, which was written alongside the rules, so they show the pipeline does
what it says on data of this shape. They are not a claim about real supplier invoices.
Which framework, stated plainly. LangGraph is genuinely imported and
used (StateGraph, a typed state schema, conditional edges and a cyclic edge back to the
supervisor). Model calls go straight to the Anthropic SDK with a JSON schema structured output
contract. LangChain chat wrappers are not used, and CrewAI and AutoGen are not
installed, not imported and not claimed.
The live and the offline run reach the same 13 / 9 / 4 split and give the same stated reason on every document, because the rules and the routing are pure functions of the extracted fields. The model can only influence classification and extraction. The last row is an earlier revision kept on purpose: the provider connection dropped mid run, so the degradation path fired on a real fault rather than a scripted one. It disagrees with the manifest by design, because 11 documents were marked degraded and sent to a person instead of being auto approved on data the pipeline could not read.
| Run | Mode | Model | Docs | Approve / review / reject | Retries / degraded | Agreement | Wall clock |
|---|---|---|---|---|---|---|---|
| Live run. Real model calls. evidence/live-run/ | live | anthropic:claude-opus-5:effort=low | 26 | 13 / 9 / 4 | 0 / 0 | 100.0% | 266.32s |
| Offline run. Deterministic stub model. evidence/offline-run/ | stubbed | stub:label-parser | 26 | 13 / 9 / 4 | 0 / 0 | 100.0% | 0.08s |
| Fault injection run. Scripted failures. evidence/fault-injection-run/ | stubbed | faults(stub:label-parser) | 26 | 12 / 10 / 4 | 3 / 1 | 100.0% | 2.09s |
| Earlier revision. A real network fault. evidence/live-network-degradation/ | live | anthropic:claude-opus-5:effort=low | 26 | 7 / 16 / 3 | 22 / 11 | 73.1% | 306.68s |
All 26 documents from the live run: what the classifier decided, what the extractor reported for its own confidence, which business rules fired, and the routing decision against the expected one from the ground truth manifest. Type accuracy is scored over 25, not 26, because D25 is deliberately ambiguous and the manifest asserts no single correct type for it.
| Doc | Hard case | Type | Type conf | Extract conf | Decision | Expected | Rule codes |
|---|---|---|---|---|---|---|---|
| D01 | clean baseline | invoice | 0.98 | 0.97 | auto_approve | auto_approve ✓ | none |
| D02 | due date stated as payment terms, not a date | invoice | 0.97 | 0.97 | auto_approve | auto_approve ✓ | none |
| D03 | long form dates | invoice | 0.98 | 0.95 | auto_approve | auto_approve ✓ | none |
| D04 | total does not equal subtotal plus tax | invoice | 0.98 | 0.82 | human_review | human_review ✓ | INV_TOTAL_MISMATCH |
| D05 | above the auto approve limit | invoice | 0.98 | 0.96 | human_review | human_review ✓ | INV_ABOVE_AUTO_APPROVE_LIMIT |
| D06 | required field absent (no due date, no terms) | invoice | 0.97 | 0.95 | human_review | human_review ✓ | FIELD_MISSING |
| D07 | duplicate submission of an invoice already in this batch | invoice | 0.95 | 0.97 | reject | reject ✓ | DUPLICATE_SUBMISSION |
| D08 | due date falls before the invoice date | invoice | 0.96 | 0.9 | human_review | human_review ✓ | INV_DUE_BEFORE_ISSUE |
| D09 | negative amounts (credit note in a payables only queue) | invoice | 0.9 | 0.93 | reject | reject ✓ | INV_AMOUNT_NOT_POSITIVE, INV_AMOUNT_NOT_POSITIVE, INV_AMOUNT_NOT_POSITIVE |
| D10 | numeric dates in day/month/year order | invoice | 0.98 | 0.95 | auto_approve | auto_approve ✓ | none |
| D11 | clean baseline | lease_application | 0.98 | 0.98 | auto_approve | auto_approve ✓ | none |
| D12 | affordability inside the ceiling but tight, warning only | lease_application | 0.97 | 0.95 | auto_approve | auto_approve ✓ | LSE_AFFORDABILITY_TIGHT |
| D13 | value out of policy range (rent to income far above ceiling) | lease_application | 0.97 | 0.98 | reject | reject ✓ | LSE_AFFORDABILITY_BREACH |
| D14 | required field absent (income not stated) | lease_application | 0.93 | 0.95 | human_review | human_review ✓ | FIELD_MISSING |
| D15 | clean, longer term | lease_application | 0.97 | 0.97 | auto_approve | auto_approve ✓ | none |
| D16 | self employed, income proof warning, still clears | lease_application | 0.96 | 0.97 | auto_approve | auto_approve ✓ | LSE_INCOME_PROOF_REQUIRED |
| D17 | affordability lands exactly on the 33 percent ceiling, boundary case | lease_application | 0.97 | 0.98 | auto_approve | auto_approve ✓ | LSE_AFFORDABILITY_TIGHT |
| D18 | transient extractor failure, recovered by one retry with backoff | lease_application | 0.96 | 0.97 | auto_approve | auto_approve ✓ | LSE_INCOME_PROOF_REQUIRED |
| D19 | clean baseline | deal_sheet | 0.98 | 0.98 | auto_approve | auto_approve ✓ | none |
| D20 | commission rate outside the mandated band | deal_sheet | 0.97 | 0.96 | reject | reject ✓ | DEAL_COMMISSION_RATE_OUT_OF_BAND |
| D21 | commission amount inconsistent with the stated rate | deal_sheet | 0.98 | 0.79 | human_review | human_review ✓ | DEAL_COMMISSION_MISMATCH |
| D22 | transfer date falls before the offer date | deal_sheet | 0.97 | 0.93 | human_review | human_review ✓ | DEAL_TRANSFER_BEFORE_OFFER |
| D23 | transfer far in the future, warning only | deal_sheet | 0.97 | 0.98 | auto_approve | auto_approve ✓ | DEAL_TRANSFER_FAR_OUT |
| D24 | persistent extractor failure, escalated to a degraded run | deal_sheet | 0.98 | 0.95 | auto_approve | auto_approve ✓ | none |
| D25 | ambiguous type: references an invoice and a lease renewal, complete fields for neither | unknown | 0.92 | None | human_review | human_review ✓ | none |
| D26 | out of scope document type (maintenance job card) | unknown | 0.95 | None | human_review | human_review ✓ | none |
D07 is a duplicate of an invoice already seen earlier in the same batch. Below is
every audit line the run wrote for it, unedited from evidence/live-run/events.jsonl
with only the absolute file path shortened. Read top to bottom it is the whole control flow: the
supervisor dispatches each stage, the classifier and extractor call the model and report their own
confidence, the validator applies the rules with no model call and finds the duplicate as a fatal
finding, and the router turns that into a decision carrying a policy version.
{"ts": "2026-07-30T11:09:01.168+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "supervisor", "event": "dispatch", "detail": {"target": "classify", "delay_s": 0.0, "note": "dispatching classify", "attempts": {}, "degraded": false}}
{"ts": "2026-07-30T11:09:01.168+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "classifier", "event": "start", "attempt": 1, "detail": {"model": "anthropic:claude-opus-5:effort=low"}}
{"ts": "2026-07-30T11:09:05.456+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "classifier", "event": "classified", "attempt": 1, "detail": {"doc_type": "invoice", "confidence": 0.95, "reason": "The email embeds a full tax invoice with invoice number INV-2201, dates, line item, VAT and total due of R8 050.00.", "usage": {"model": "claude-opus-5", "input_tokens": 981, "output_tokens": 71, "latency_ms": 4287}}}
{"ts": "2026-07-30T11:09:05.460+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "supervisor", "event": "dispatch", "detail": {"target": "extract", "delay_s": 0.0, "note": "dispatching extract", "attempts": {"classify": 1}, "degraded": false}}
{"ts": "2026-07-30T11:09:05.464+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "extractor", "event": "start", "attempt": 1, "detail": {"doc_type": "invoice", "model": "anthropic:claude-opus-5:effort=low"}}
{"ts": "2026-07-30T11:09:10.847+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "extractor", "event": "extracted", "attempt": 1, "detail": {"doc_type": "invoice", "confidence": 0.97, "fields_populated": 11, "fields_requested": 11, "usage": {"model": "claude-opus-5", "input_tokens": 2734, "output_tokens": 166, "latency_ms": 5382}}}
{"ts": "2026-07-30T11:09:10.849+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "supervisor", "event": "dispatch", "detail": {"target": "validate", "delay_s": 0.0, "note": "dispatching validate", "attempts": {"classify": 1, "extract": 1}, "degraded": false}}
{"ts": "2026-07-30T11:09:10.850+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "validator", "event": "start", "attempt": 1, "detail": {"doc_type": "invoice"}}
{"ts": "2026-07-30T11:09:10.850+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "validator", "event": "validated", "attempt": 1, "detail": {"passed": false, "severity_counts": {"fatal": 1}, "findings": [{"code": "DUPLICATE_SUBMISSION", "severity": "fatal", "message": "Same business key as document D01, already processed in this run.", "field": null}], "policy_version": "2026.07.1"}}
{"ts": "2026-07-30T11:09:10.850+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "supervisor", "event": "dispatch", "detail": {"target": "route", "delay_s": 0.0, "note": "dispatching route", "attempts": {"classify": 1, "extract": 1, "validate": 1}, "degraded": false}}
{"ts": "2026-07-30T11:09:10.851+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "router", "event": "routed", "attempt": 1, "detail": {"decision": "reject", "reasons": ["DUPLICATE_SUBMISSION: Same business key as document D01, already processed in this run."], "policy_version": "2026.07.1", "confidence": 0.95, "degraded": false}}
{"ts": "2026-07-30T11:09:10.851+00:00", "run_id": "live-final-verified", "doc_id": "D07", "node": "supervisor", "event": "finish", "detail": {"target": null, "delay_s": 0.0, "note": "all stages complete", "attempts": {"classify": 1, "extract": 1, "validate": 1, "route": 1}, "degraded": false}}
D06 is an invoice with no due date. In the first schema revision the business
required fields were not nullable, so the model had no way to report the absence and supplied a date
centuries out instead. The arithmetic downstream was then perfectly correct about nonsense, reporting
unusually long payment terms. Nothing crashed and the document still went to a person, because the
extractor honestly dropped its confidence to 0.50 and the confidence floor caught it, but the stated
reason described a commercial observation instead of a missing field. A schema that cannot express
absence does not prevent a missing value, it converts one into a fabricated one, and a fabricated
value that is well formed is invisible to shape checking. The two concerns were separated: every
field is nullable on the wire so the model can tell the truth, requiredness is enforced as a business
rule in Python, and a plausibility rule was added ahead of the payment terms warning as a backstop.
In the run above D06 reports FIELD_MISSING on due_date and extractor
confidence is 0.95, because the model no longer has to guess. Both changes are covered by regression
tests that name the observed failure.