5 · Intake and enrichment
What this step adds: the first two stages of the pipeline. One of them contains no model.
The new idea
Section titled “The new idea”A pipeline is a fixed sequence of named stages, each with a defined input and output. Part 2 has five:
Intake -> Enrichment -> Risk -> Case -> Human gateThe sequence is written in ordinary Python and never changes:
def triage_alert(alert, audit, auto=False): case = agents.run_intake(alert, audit) agents.run_enrichment(case, audit) agents.run_risk(case, audit) agents.run_case(case, audit)
_present_case(case) case["human_decision"] = _human_gate(case, audit, auto=auto) return caseCompare that to Part 1, where the model decided what happened next on every turn. Here the model does not choose the order of anything. It has autonomy inside the enrichment stage — which tools, how many turns — and none at all about the shape of the process.
That is the autonomy trade made concrete: full autonomy where the work is genuinely open-ended, no autonomy where the sequence is known in advance. There is no reason to let a model rediscover a pipeline you already know the shape of.
Intake needs no model
Section titled “Intake needs no model”def run_intake(alert, audit): """Deterministic: normalise the raw alert into a case file. No model needed.""" audit.record(alert["alert_id"], "INTAKE", "stage_start", f"Alert {alert['alert_id']} on rule '{alert['rule']}'") ...The stage takes a raw alert and produces a case file. That is a data transformation with one correct answer, so it is a function.
This sounds obvious written down, and it is the single most common mistake in agent projects. The instinct is to make everything an agent. You have a model, so each stage becomes a prompt, and now you have five non-deterministic components where you needed two — five things to eval, five that can hallucinate, five that make the run cost more and take longer.
The question to ask at every stage: does this need judgment? Intake doesn’t. Neither does scoring, as the next step shows.
Enrichment is where the model earns its place
Section titled “Enrichment is where the model earns its place”ENRICHMENT_INSTRUCTIONS = ( "You are a fraud-enrichment agent. Gather ALL available context for this " "alert by calling your tools: transaction history, customer profile and " "device fingerprint. Do not guess any fact you can look up.")
def run_enrichment(case, audit): case["enrichment"] = _run_tool_loop( case, audit, "ENRICHMENT", ENRICHMENT_INSTRUCTIONS, ["get_customer_profile", "check_device_fingerprint", "get_transaction_history"], )Three tools, one loop, results into case["enrichment"].
Note the fourth tool — search_similar_cases — is not in that list, because
it needs signals that don’t exist yet. Each stage gets only the tools it can
usefully call. Same least-privilege instinct as the registry, applied one level
up.
Why the stage boundaries are the point
Section titled “Why the stage boundaries are the point”Naming the stages does something that has nothing to do with the code: it makes failure locatable.
When a Part 1 agent produces a bad answer, the failure is “the agent got it wrong”. When this one does, you can say “enrichment skipped the device check” or “the case summary contradicts the score” — and those are different problems with different fixes. The audit trail tags every event with its stage for exactly this reason.
Run it
Section titled “Run it”cd part2uv run --with ollama run.pyINTAKE | stage_start | Alert ALERT-001 on rule 'HIGH_VALUE_NEW_PAYEE_CROSS_BORDER'ENRICHMENT | tool_call | get_customer_profile({'customer_id': 'CUST-1001'})ENRICHMENT | tool_result | {"home_country": "AU", "avg_txn_amount": 2100.0, ...}What’s still wrong
Section titled “What’s still wrong”The system gathers facts and still has no opinion. The obvious next move is to ask the model for a risk score — and that is exactly the wrong move.