← gadi cohen

Case study · Granted Health · founding team · 2025–now

Case Automation

I designed and lead the agent platform that resolves a patient's medical-billing case at Granted Health. A case is a long-running, adversarial process: documents arrive out of order, deadlines are real, and a wrong action costs the patient money. So the interesting engineering isn't the model call. It's everything wrapped around it.

Multi-agent workflows · event-sourced state · human gates · durable execution · eval harness

IntakeDocumentsdenials · EOBs · itemized billsPatient messagesPayer responsesRead pipelineparse · classify · extract, provenance attachedStateFact storeevent-sourced, one source of truth per caseContext engineregistry: playbooks · gate state · scoped docsDecide · ActPlannerbounded read-only loop, proposes one actionPreconditionschecked in code, not in proseHuman gateanything consequential waits for a personExecutoridempotent actions: appeals · requests · messagesresults → eventsEvals & tracingboots this whole graph on seeded cases, side effects suppressed · every decision traced
The system: state below the model, guarantees outside it

One case, end to end

The architecture is abstract until you follow a single denial through it.

  1. 01

    A denial letter arrives

    The read pipeline parses it, classifies the denial, and extracts the facts that matter: codes, amounts, deadlines. Every extracted fact carries its source.

  2. 02

    Facts land in the store

    Each one is an event, not a row update: what was learned, where it came from, how much to trust it. The case's history is replayable from the log.

  3. 03

    The planner wakes up

    It reads the case through the context engine: the relevant playbook, the current gate state, the facts. It never sees one giant prompt.

  4. 04

    It proposes exactly one action

    Say, request an itemized bill from the provider. The proposal is a typed object, not prose: action, arguments, preconditions, an idempotency key.

  5. 05

    Code checks the preconditions

    Is the deadline still open? Did we already ask? If a precondition fails, the proposal dies before anything happens.

  6. 06

    A person approves the consequential ones

    Sending an appeal or contacting a payer waits for human sign-off. The reviewer sees what the agent knew and why it proposed the action.

  7. 07

    The executor acts, once

    Idempotency boundaries mean a retry can't send two appeals. The result is appended to the fact store as a new event, and the loop continues.

  8. 08

    The whole run is traced and evaled

    The same graph boots against seeded cases with side effects suppressed. Regressions show up as changed decisions, not changed sentiment.

The seam between model and world

The design reduces to one contract. The model reasons; code decides what reasoning is allowed to touch.

// The seam between model and world. The planner can only
// return one of these; everything else is enforced in code.
type ProposedAction = {
  kind: "request_itemized_bill" | "draft_appeal" | "message_patient" | /* … */;
  args: Record<string, unknown>;   // validated against the action's schema
  basis: FactRef[];                // which facts justified it, by event id
  preconditions: Check[];          // evaluated in code before execution
  approval: "auto" | "human";      // consequential ⇒ human
  idempotencyKey: string;          // a retry can never act twice
};
Shape, not source: the production types are richer, but the contract is this.

Failures, and what catches them

Every mechanism above exists because something broke without it.

Failure we hitWhat catches it now
Instruction X quietly degrades instruction Y as the prompt growsContext registry: the agent reads what it needs per task; the prompt stays flat
The agent re-asks and contradicts itself across turnsEvent-sourced fact store: one source of truth, with provenance
You can't tell what the agent decided, let alone enforce itPlanner/executor split: every decision is a typed, logged proposal
A retry fires the same action twiceIdempotency keys at the executor boundary
An ambiguous document produces a confident wrong answerRetrieval answers carry scope; ambiguity escalates to a person
A prompt change silently regresses last month's casesEvals boot the real graph on seeded cases and score decisions

Why it's shaped this way

The shortest honest account is the sequence of failures. Margin notes carry the deeper reason or the paper behind a move.

One prompt, one call

The simplest agent is one model call per turn: hand it the case, a set of tools, and a prompt telling it what to do. It holds up until you add the second capability, and the third. Each new rule expands the prompt, and at some point adding instruction X quietly degrades instruction Y.Not anecdotal: “never do X” rules measurably stop being followed as a conversation runs long (Constraint Decay, arXiv 2604.20911), while “always do X” rules hold near 100%. A bigger prompt is a worse prompt. So the first move is to stop stuffing it: the agent reads context on demand from a registry (the relevant playbook, the current gate state) instead of carrying all of it at once.Close to what Agent-S (2503.15520) calls a global action repository, and to writing prompts as named, ordered routines (2501.11613). The prompt stays small and flat as the system grows.

Move the facts below the model

Now it reads context, but the facts of the case still live in the conversation, so the agent re-derives what it already knew every turn. It re-asks, contradicts itself, drifts. The fix is to move state below the model: a canonical, event-sourced fact store where each fact carries where it came from and how much to trust it.The pattern recent work calls event sourcing for autonomous agents (2602.23193), and the memory/compute split behind DeepSeek's Engram: facts become a lookup, not a recomputation. The agent reads from one source of truth instead of reconstructing it each time.

Separate deciding from doing

The agent still decides and speaks in the same breath: the action is just whichever tool it happened to call. You can't enforce a precondition in prose, and you can't even measure what it decided. So separate proposing from doing. A bounded planner proposes one action in a short, read-only loop; a single executor carries it out; preconditions are checked in code; and anything consequential waits for a person.The tempting wrong turn is to make the agent “reliable” by rebuilding it as a deterministic state machine. That just relocates the non-determinism into the resolver and the queue. Keep the agent; enforce the guarantee at the source.

Make failure visible

None of this is real until you can catch it breaking. Tool-match and similarity scores miss the failures that matter: asking to escalate twice, inventing a fact. So the eval boots the real execution graph against seeded data with side effects suppressed, and scores the decision the agent made, not the words it said.The simulacrum idea behind τ-bench (2406.12045); on methodology, Anthropic's Demystifying Evals (pass^k over whole trajectories).

Built on the Vercel AI SDK and Claude, with Langfuse for tracing, Vellum for retrieval, an event-sourced fact store on Gel, and durable execution on Temporal. Ask my agent about any of it on the home page.

Gadi Cohen