Skip to content

7 · The case worker and the gate

What this step adds: the Case worker, which writes the analyst-facing summary, and the human gate that ends every run.

(Two commits — the worker, then the orchestrator and gate.)

CASE_INSTRUCTIONS = (
"You are a fraud case-assembly agent. Write a short, analyst-ready summary "
"of this case in a few sentences. You recommend only; a human analyst "
"decides. Do not invent any fact that is not in the case."
)

Note what it is asked for: prose, and only prose. The score already exists. The recommendation already exists. The model’s entire job is to turn a list of signals into something a human can read at 2am.

That is the right job for a language model, and it is the only job in this system it is trusted with alone.

On ALERT-002 — the clean one, the CLEAR — the model wrote a summary that described risk-reducing signals as evidence of high risk. It had known_device and home_location, both of which subtract points, and narrated them as concerning.

Then it recommended CLEAR. Correctly. Because it wasn’t the one deciding.

Run it yourself and you will find similar confusion on ALERT-003:

…driven by positive signals from new_payee and elevated_mcc, offset slightly by negative signals from above_average_amount and home_location.

That sentence has the sign backwards on three of the four signals, and above_average_amount is grouped with the wrong ones entirely. The number was 35 either way.

This is the strongest single argument in the course for the architecture, and it is stronger than any version anyone could invent, because it happened:

  • If the model had produced the score, that confusion would be the score.
  • Because the score came from rules.py, the confusion is a wording defect in a paragraph — visible to any reviewer, fixable with a better prompt or a bigger model, and consequential to nothing.

You have not eliminated the model’s unreliability. You have relocated it to where it costs least. That is the achievable goal; eliminating it is not.

def _human_gate(case, audit, auto=False):
"""The approval gate. The agent never acts; the human decides."""
rec = case["risk"]["recommendation"]
...
print(f" The agent recommends: {rec}. You are the accountable decision-maker.")
answer = input(" Approve (a) / Override (o) / Request more digging (d)? ")
mapping = {"a": "APPROVE_RECOMMENDATION", "o": "OVERRIDE",
"d": "REQUEST_MORE_INFO"}
action = mapping.get(answer, "APPROVE_RECOMMENDATION")
note = ""
if action == "OVERRIDE":
note = input(" Override reason: ").strip()
audit.record(case["alert_id"], "HUMAN_GATE", "human_decision",
f"{action} (agent recommended {rec}) {note}".strip())
return {"action": action, "by": "analyst", "note": note}

Three differences from Part 1’s yes/no gate, each deliberate:

Three options, not two. Approve, override, or send it back for more work. A binary gate forces a decision the analyst may not be ready to make, and “request more digging” is what a real reviewer actually wants a third of the time.

Override captures a reason. The most valuable data the system produces is the set of cases where a human disagreed with it and said why. That is your next eval set, your prompt-tuning corpus, and your evidence that the gate is real rather than ceremonial.

Nothing is actioned. The gate records a decision and returns. No payment is blocked, no card is frozen. Whatever does that is a separate system with its own controls — and this one is honest about the boundary, unlike the Part 1 router that claimed to have blocked a card it never touched.

The docstring at the top of orchestrator.py makes the argument that actually lands in a bank:

Under the Financial Accountability Regime a named executive is personally accountable for this function — you cannot delegate that to a model. So the agent recommends; the human decides and owns the call.

This is worth internalising because it reframes the entire question. The gate is not a hedge against the model being imperfect — as if a good enough model would let you remove it. Accountability attaches to a person by law. The gate is where that person exercises it. A better model makes them faster and better-informed; it does not make them unnecessary, because it legally cannot.

Terminal window
cd part2
uv run --with ollama run.py

run.py passes auto=True so the demo doesn’t block. To meet the real gate, call triage_alert(alert, audit) without it and answer the prompt yourself.

Every decision so far has scrolled past in a terminal. Nothing is preserved.