←  Back to the portfolio Real run output. Generated from the committed evidence files, not typed by hand.

Proof page. Multi agent orchestration.

A multi agent document triage pipeline, and the runs that scored it.

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.

Table 1 Four runs, scored against the manifest

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.

RunModeModelDocs Approve / review / rejectRetries / degraded AgreementWall clock
Live run. Real model calls.
evidence/live-run/
liveanthropic:claude-opus-5:effort=low2613 / 9 / 40 / 0100.0%266.32s
Offline run. Deterministic stub model.
evidence/offline-run/
stubbedstub:label-parser2613 / 9 / 40 / 0100.0%0.08s
Fault injection run. Scripted failures.
evidence/fault-injection-run/
stubbedfaults(stub:label-parser)2612 / 10 / 43 / 1100.0%2.09s
Earlier revision. A real network fault.
evidence/live-network-degradation/
liveanthropic:claude-opus-5:effort=low267 / 16 / 322 / 1173.1%306.68s

Table 2 Every document in the live run

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.

DocHard caseTypeType conf Extract confDecisionExpectedRule codes
D01clean baselineinvoice0.980.97auto_approveauto_approve ✓none
D02due date stated as payment terms, not a dateinvoice0.970.97auto_approveauto_approve ✓none
D03long form datesinvoice0.980.95auto_approveauto_approve ✓none
D04total does not equal subtotal plus taxinvoice0.980.82human_reviewhuman_review ✓INV_TOTAL_MISMATCH
D05above the auto approve limitinvoice0.980.96human_reviewhuman_review ✓INV_ABOVE_AUTO_APPROVE_LIMIT
D06required field absent (no due date, no terms)invoice0.970.95human_reviewhuman_review ✓FIELD_MISSING
D07duplicate submission of an invoice already in this batchinvoice0.950.97rejectreject ✓DUPLICATE_SUBMISSION
D08due date falls before the invoice dateinvoice0.960.9human_reviewhuman_review ✓INV_DUE_BEFORE_ISSUE
D09negative amounts (credit note in a payables only queue)invoice0.90.93rejectreject ✓INV_AMOUNT_NOT_POSITIVE, INV_AMOUNT_NOT_POSITIVE, INV_AMOUNT_NOT_POSITIVE
D10numeric dates in day/month/year orderinvoice0.980.95auto_approveauto_approve ✓none
D11clean baselinelease_application0.980.98auto_approveauto_approve ✓none
D12affordability inside the ceiling but tight, warning onlylease_application0.970.95auto_approveauto_approve ✓LSE_AFFORDABILITY_TIGHT
D13value out of policy range (rent to income far above ceiling)lease_application0.970.98rejectreject ✓LSE_AFFORDABILITY_BREACH
D14required field absent (income not stated)lease_application0.930.95human_reviewhuman_review ✓FIELD_MISSING
D15clean, longer termlease_application0.970.97auto_approveauto_approve ✓none
D16self employed, income proof warning, still clearslease_application0.960.97auto_approveauto_approve ✓LSE_INCOME_PROOF_REQUIRED
D17affordability lands exactly on the 33 percent ceiling, boundary caselease_application0.970.98auto_approveauto_approve ✓LSE_AFFORDABILITY_TIGHT
D18transient extractor failure, recovered by one retry with backofflease_application0.960.97auto_approveauto_approve ✓LSE_INCOME_PROOF_REQUIRED
D19clean baselinedeal_sheet0.980.98auto_approveauto_approve ✓none
D20commission rate outside the mandated banddeal_sheet0.970.96rejectreject ✓DEAL_COMMISSION_RATE_OUT_OF_BAND
D21commission amount inconsistent with the stated ratedeal_sheet0.980.79human_reviewhuman_review ✓DEAL_COMMISSION_MISMATCH
D22transfer date falls before the offer datedeal_sheet0.970.93human_reviewhuman_review ✓DEAL_TRANSFER_BEFORE_OFFER
D23transfer far in the future, warning onlydeal_sheet0.970.98auto_approveauto_approve ✓DEAL_TRANSFER_FAR_OUT
D24persistent extractor failure, escalated to a degraded rundeal_sheet0.980.95auto_approveauto_approve ✓none
D25ambiguous type: references an invoice and a lease renewal, complete fields for neitherunknown0.92Nonehuman_reviewhuman_review ✓none
D26out of scope document type (maintenance job card)unknown0.95Nonehuman_reviewhuman_review ✓none

Table 3 The raw audit trail for one document

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}}

Note The design change this evidence caused

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.