Skip to content
Simulation · not operational100% synthetic data · the demos’ human decisions were simulated in batch
planlang

Demo A · medical prior authorisation · sprint 3 run v1.5, 2026-10-04

The authorization agent, as it ran

First, what it is: its goal, what it takes in, what it does and what it delivers. Then its real graph against the plan, node by node, with the detail each profile asks for.

The agent at a glance

Read as
Goal

Resolve prior-authorization requests for medical procedures: approve the clear ones in seconds, in full or up to the service's cap, and bring every full denial and every doubtful case to a human auditor. It never fully denies on its own, never reveals member data and never obeys instructions hidden in the request.

Takes in

the request
  • ran in run v1.5
    The physician's free textWhat is requested, why, how urgent and at what cost; in all 200 cases.
  • ran in run v1.5
    The attached medical orderTo check it against the text: on A-009, A-054, A-070, A-099, A-139, A-154, A-175 and A-186 they did not match and a person decided.
  • ran in run v1.5
    Minimal member dataAge, sex and diagnosis. Name and ID are masked before the model (D1).
  • ran in run v1.5
    The benefits plan40 procedures as readable rules: 5 need no authorization and 6 carry an exclusion with a stated cause.

Does

7 activities, in order
  1. 1
    Classifies the care: an emergency or an exempt service?enrutador · 200 of 200 cases
    ran in run v1.5
  2. 2
    If so, authorizes it without a coverage checkenrutador → redactor · 23 emergencies and 12 exempt
    ran in run v1.5
  3. 3
    Reads the request and turns it into data, with its confidenceextractor · 165 cases
    ran in run v1.5
  4. 4
    If data is missing, asks the physician, up to 2 timesaclaracion · 30 cases, 47 questions
    ran in run v1.5
  5. 5
    Checks coverage with fixed rulesverificador_cobertura · 155 cases, no model
    ran in run v1.5
  6. 6
    Decides: go on alone or to the auditordecision pausa_humana · 70 of 200 to a person
    ran in run v1.5
  7. 7
    Writes the answer, filters it and, if adverse, issues its documentredactor guardia_salida · 31 documents in ES and EN
    ran in run v1.5

Delivers

four answers and the trace
  • ran in run v1.5
    ApprovalWith an AI notice for the member: 169 of 200.
  • ran in run v1.5
    Escalation to the medical auditorWith the 10 fields the plan requires: 70 pauses; the simulated auditor approved 48 and denied 22.
  • ran in run v1.5
    Partial approvalUp to the service's cap; a rule (RB-08) denies the excess, with its document in ES and EN: 9 of 200. With Texas mode on, a person decides it.
  • ran in run v1.5
    Denial with an enumerated causeOnly after the auditor, with its document in ES and EN: 22 of 200, all with a person (C1).
  • ran in run v1.5
    Each case's traceplanlang-trace/v1 with a fingerprint: signals, routes, times and tokens for all 200.

ran in run v1.5Today: all 7 activities ran across sprint 3’s 200 cases

Can

seen in the run
  • ran in run v1.5
    Read free text and turn it into data in a fixed format165 cases; no off-format answer
  • ran in run v1.5
    Ask for what is missing and go on with the answer47 questions in 30 cases
  • ran in run v1.5
    Check coverage with rules anyone can read155 cases, 0 tokens
  • ran in run v1.5
    Stop, wait for a person and resume where it was70 pauses and 70 resumes
  • ran in run v1.5
    Ignore orders hidden in the requestA-016, A-034, A-044, A-075, A-105, A-115, A-155, A-180 and A-200: the planted order changed nothing

Never

plan rules
  • Fully deny without a person reviewing itD2 · C1 · CA SB 1120 · TX SB 815 · EU AI Act art. 14
  • See the member's name or IDD1 · masked before the model
  • Reveal member data in the answerC2 · output guard
  • Let a case's text change what it doesC6 · text is data, never instruction
  • Require authorization for an emergencyC4 · Colombian Law 1751, art. 14

The human pause, the output guard and the graph contract enforce them. In run v1.5 none was broken: C1, C2, C4 and C6 were met.

Who takes part

  • Requesting physicianasks for the authorization and answers clarifications · synthetic
  • Member (synthetic)receives the answer · synthetic
  • Human medical auditordecides at the pause · simulated in this demo
  • Authorization agentthis system, at a synthetic insurer
  • Synthetic benefits plan (readable rules)readable coverage rules · synthetic

Measured capacity

8.1 sper case, medianfrom 3.2 to 49.5 s · the plan asks ≤ 30 s for a typical case (C7): met
0.025 USDper case, nominal4.92 USD for all 200; not billed: it comes out of the Claude Code subscription quota
6,007tokens per case, medianfrom 1,563 to 18,289 · the longest asked for two clarifications
70 of 200cases to a personabout 840 auditor minutes: 12 min per case per the plan
196 of 200paths matching the known truthsame decision and same pause as the synthetic case's truth
≈ 37 mina batch of 200 casesin series, adding up this run's 200 cases (11.0 s per case); 4.9 USD nominal

Goal, activities and deliveries: demo A plan v1.5. Status and figures: sprint 3 run suscripcion-planlang-a-002-200-v1.5 (2026-10-04), 200 synthetic cases, sonnet model via the Claude Code subscription. real · run v1.5

Architecture

Pattern
router + extractor, coverage checker and writer + clarification and decision
Decision
D4, two-way: it can go back to a single agent
Baseline
single agent at equal budget on the same batch: S3 was refuted, the multi-agent version was slower than the single agent
Orchestration
LangGraph 1.x: StateGraph, add_conditional_edges with an explicit path_map, interrupt + Command(resume=…); no langgraph-supervisor

Graph

Plan contract
8 nodes · 12 conditional edges · 1 human pause (auditor role, 10 minimum fields)
Sprint 3
8 nodes + start and end · 14 edges · 9 conditional (get_graph().to_json())
Matches
8 of 8 nodes · 12 of 12 rules · 0 nodes outside the contract
Graph fingerprint
056407bf4c1238ca…

Model and provider

Adapter
ChatClaudeCode(BaseChatModel) → claude -p
Model
alias sonnet on the 3 model nodes
Regime
the author's Claude Code subscription; batches of 20 outside CI; switch to API (Haiku 4.5) or Groq (D6)
Isolation
no tools · empty MCP · no settings · clean temp directory · never --bare

Run versions

LangGraph · LangChain
1.2.12 · 1.4.2 (core 1.6.5)
LangSmith SDK · Pydantic
0.14.1 · 2.13.5
Claude Code
2.1.282
Python
3.12.14

State

Type
TypedDict Estado: 15 plan signals under their contract names + 15 working keys
Working keys
caso_identradaumbrales_aplicadosaclaraciones_hechasaclaracionesextraccioncoberturapausasrevisionborradorsalida_finaldocumento_adversoguardia_salidapasosdecisiones_de_arista
Signals required in the trace
tipo_atencionservicio_exentocarga_detectadasenal_confianzacampos_faltantes_countciclos_aclaracioncosto_estimadocontradiccion_orden_textopropuestamodo_texasdecision_finalpausa_humananodos_visitadosseveridad_accionlatencia_total_stokenserror_proveedorproveedor_no_disponible

Persistence and traces

Checkpointer
SqliteSaver in runs (mode 600, private derivative) · InMemorySaver in tests
Thread
one thread_id per case
Traces
own, planlang-trace/v1, one per case with its fingerprint; LangSmith only as a mirror

Evaluation

Verifier
deterministic, no model: 10 criteria, 10 risk detectors, 3 assumptions, graph contract
Cross-check
RF-09.2: 1124 edge decisions redone in TypeScript, 0 differences
Batches
200 cases, not repeated + single-agent baseline
Plan
6 decisions · 10 risks · 3 assumptions · 10 criteria · 4 thresholds

What in the plan touches each node

Each row ties a contract node to the decisions, risks, assumptions, criteria and thresholds that govern it, and says in how many of the 200 cases it ran.

enrutadorDecisionsD4RisksR6 · R7AssumptionsS3CriteriaC4Thresholds—In the runran in run v1.5200 of 200
extractorDecisionsD1 · D6RisksR3 · R5 · R8 · R9AssumptionsS1CriteriaC5 · C6ThresholdsU1 (signal)In the runran in run v1.5165 of 200
aclaracionDecisionsD5 · D6RisksR4 · R9AssumptionsS2Criteria—ThresholdsU3In the runran in run v1.530 of 200
verificador_coberturaDecisions—RisksR6Assumptions—CriteriaC4 · C8ThresholdsU2In the runran in run v1.5155 of 200
decisionDecisionsD2 · D3RisksR1 · R3 · R5 · R10Assumptions—CriteriaC1 · C3 · C10ThresholdsU1 · U2 · U4In the runran in run v1.5155 of 200
pausa_humanaDecisionsD2 · D3RisksR1 · R9 · R10Assumptions—CriteriaC1 · C3 · C9 · C10ThresholdsU1 · U2 · U3 · U4In the runran in run v1.570 of 200
redactorDecisionsD1 · D6RisksR2 · R8Assumptions—CriteriaC7 · C8Thresholds—In the runran in run v1.5200 of 200
guardia_salidaDecisionsD1RisksR2 · R3Assumptions—CriteriaC2 · C6Thresholds—In the runran in run v1.5200 of 200

The author's reading of plan v1.5, checked against the plan: every id exists and no element is left without a node. The counts are real. declared

The graph stateestado.py · lines 20–54 (agents/src/app_agents/demo_a/estado.py)
class Estado(TypedDict, total=False):    caso_id: str    entrada: dict[str, Any]    umbrales_aplicados: dict[str, Any]    # Señales del plan (nombres del contrato de grafo).    tipo_atencion: str    servicio_exento: bool    # M-16 (plan v1.5): la guardia de entrada, sin modelo; solo si el plan declara la señal.    carga_detectada: bool    senal_confianza: float | None    campos_faltantes_count: int | None    ciclos_aclaracion: int    costo_estimado: int | None    contradiccion_orden_texto: bool | None    propuesta: str | None    modo_texas: bool    decision_final: str | None    pausa_humana: bool    severidad_accion: int | None    # AU-9 (plan v1.4): el nodo que llama al modelo deja si el proveedor respondió, y con qué falló si no.    proveedor_no_disponible: bool    error_proveedor: str | None    # Trabajo del grafo.    aclaraciones_hechas: int    aclaraciones: Annotated[list[dict[str, Any]], operator.add]    extraccion: dict[str, Any] | None    cobertura: dict[str, Any] | None    pausas: Annotated[list[dict[str, Any]], operator.add]    revision: dict[str, Any] | None    borrador: dict[str, Any] | None    salida_final: dict[str, Any] | None    documento_adverso: dict[str, Any] | None    guardia_salida: dict[str, Any] | None    pasos: Annotated[list[dict[str, Any]], operator.add]    decisiones_de_arista: Annotated[list[dict[str, Any]], operator.add]

What ran, against its plan

real · exported graph
8 of 8contract nodes are in the graphNone missing and none extra. The spike had 3 of 8 (below, at the end).
12 of 12contract edge rules, with their signalEvery branch the agent takes leaves its signal and threshold in the trace before routing.
1124 · 0decisions redone in another language · differencesThe playground (TypeScript) evaluated the same rules over the traces and took the same branches (RF-09.2).

Plan v1.5 graph contract against the graph exported by run v1.5 (fingerprint 056407bf4c12…).

The graph

Code generates it from the compiled graph and the plan (the house diagrammer, contract 0.5.0). Tap a node or a rule: its detail by profile appears below.

Real Demo A · medical prior authorisation graph, sprint 3, run v1.5: the 8 contract nodes and their 12 edge rules Six layers from left to right, from input to output. The list by layer says the same in text. 6 rules: hidden instruction · U1 · U2 · contradiction ·deny · Texas RTR · 2 rulesenrutador RTR · 6 rulesdecision MOD · sonnetextractor MOD · sonnetaclaracion MOD · sonnetredactor RULEverificador_cobertura RULEguardia_salida HUM · auditorpausa_humana
Slide · layer

sequenceconditional · signal · operator · value; “else”, when no rule holdsresume · a person answershop · two arrows that cross without joining

Selection detail

enrutador

RTR · 2 rulesreal · run v1.5

It classifies the care and decides whether the case needs a coverage check.

Takes in

The attached order and the benefits plan

the type of care and the procedure code
RTR · 2 rulesenrutador
Delivers

The path: to the extractor, or straight to the writer

if it is an emergency or an exempt service
Why it exists
An emergency does not wait: the law requires authorizing it without a coverage check, and the same goes for services the plan exempts. This step sets them apart before spending time on the rest.
How it works
With fixed rules, no model: it reads the type of care from the attached order and looks the procedure up in the benefits plan's exempt list.
What it decides
Two plan rules, in order: emergency → writer; exempt service → writer. If neither holds, the case goes on to the extractor.
If it fails
It would require authorization for an emergency or an exempt service (risk R6), or blur each agent's role (R7).
How it is measured
Every emergency and every exempt service is authorized without a coverage check (criterion C4).
In the 200 cases
165 went on to the extractor. 23 emergencies and 12 exempt services went straight to the writer, skipping coverage. C4 was met.

Contract and state

Node
enrutador · enrutador · layer 02, orchestration
Reads
the attached order and the benefits plan
Writes
carga_detectadadecisiones_de_aristamodo_texaspasosservicio_exentotipo_atencion
Comes from
start · sequence
Goes to
extractor · redactor
Plan rules
tipo_atencion · igual_a · urgencia → redactor · servicio_exento · igual_a · true → redactor · else → extractor

Why it uses no model

Code first
the urgency is written on the order and the exempt list is a table: there is no free text to interpret

What the plan requires

  • D4Multi-agent with router or single agent?Two-way
  • R6Authorizing an exempt service or denying an emergencyAP highS8 · O3 · D2 · RPN 48 · legal control (table: low)Did not occur
  • R7Multi-agent failure: role confusion or inter-agent misalignmentAP lowS6 · O4 · D4 · RPN 96Did not occur
  • S3The router with three specialists does no worse than a single agent at no larger budget.Refuted
  • C4Every emergency is authorized without coverage check; no exempt service goes through coverage.Met

Observed in run v1.5

Cases · visits
200 of 200 · 200
Model
none: 0 tokens, 0 USD, code
Paths
12 servicio_exento → redactor · 165 → extractor · 23 tipo_atencion → redactor

Before: the spike, against the same contract

real · spike 2026-09-26

This is what the plan requires and the spike's graph did not have. The 2026-09-26 spike ran with 3 of the 8 pieces and one node outside the contract; the other 5 appear with a dashed border and the “required” mark. The graph above, the published run's, has them all.

3 of 8contract nodesaclaracion, verificador_cobertura, decision, redactor and guardia_salida were missing
1 of 12edge rulesU1’s, but on enrutador rather than decision
1node outside the contractaprobar, which approved without going through decision
The spike's graph against the plan's graph contract The pieces the plan requires and the spike lacked have a dashed border and the “required” mark. RTRrequireddecision RTR · 1 ruleenrutador MOD · sonnetextractor MODrequiredaclaracion MODrequiredredactor RULErequiredverificador_cobertura RULErequiredguardia_salida RULEunplannedaprobar HUM · auditorpausa_humana
  1. 01InputWhat does the agent receive?

    request (terminal)

    request → extractor · sequence

  2. 02OrchestrationWho decides the next step?
    1. RTR · required by the plan, missing from the graphdecision
    2. RTR · 1 ruleenrutador

      senal_confianza < 0.75 → pausa_humana · else → aprobar

  3. 03Specialised agentsWho reasons, and with which model?
    1. MOD · sonnetextractor

      extractor → enrutador · sequence

    2. MOD · required by the plan, missing from the graphaclaracion
    3. MOD · required by the plan, missing from the graphredactor
  4. 04Rules and guardsWhat does the code check without asking a model?
    1. RULE · required by the plan, missing from the graphverificador_cobertura
    2. RULE · required by the plan, missing from the graphguardia_salida
    3. RULEaprobar

      aprobar → end · sequence

  5. 05Human pauseWhen does it stop and wait for a person?
    1. HUM · auditorpausa_humana

      pausa_humana → end · sequence

  6. 06OutputWhat does it deliver, and with what guarantees?

    end (terminal): the filtered answer and the trace

Read from the spike's code: threshold spike.py:44 · rule spike.py:198 · edges spike.py:228 · pause spike.py:207 · node types spike.py:189-218