skip to content>_lesfer
12
Modules

of 14 · Oct 22, 2026 · 10 min read

When the Next Step Depends on What You Find

Some questions need a sequence of lookups, and which lookup comes next depends on the last result. Let the model choose each step from a few read-only tools, and let code run the tools, enforce access, cap the steps, and check what gets cited.

Builds on the routes from Part 9, Part 10 and Part 11.

Add this module only if some questions need several lookups, where each one depends on what the previous one found.

A customer walks into a dealership with a C-3 and has heard about a battery recall. The technician reads the serial number off the pack and asks:

A customer's C-3 has battery serial 2509-0412. Is it affected by a recall, and what should I do?

With Part 11's code, the search route supplies the battery recall ("Battery packs BP-36-2 with serial numbers 2509-0001 to 2509-0800 may swell...") and a customer FAQ entry about warranty coverage. Three things are missing. Nothing says the C-3 uses BP-36-2: that's in the C-3 specification, which the question doesn't match. The recall's second section, on what to record in the service record, didn't make the cut. And whether 2509-0412 falls inside the range is left to the model.

Ask the same question about a T-7, and search supplies the same recall, alone. The serial is inside its range, so a model would very likely answer "affected". It isn't: the T-7 uses a different pack, BP-48-1, which has no recall.

Why no route fits

Answering takes up to three lookups, and each depends on the one before:

  1. Which pack does this bike use? (In the bike's specification.)
  2. Is this serial inside a recall for that pack? (A check, which code should do, as in Part 10.)
  3. Only if it is: what does the recall say to do?

For a C-3 with serial 2509-0412, all three steps. For serial 2509-0950, the check says no and there's nothing more to read. For a T-7, step 1 finds a pack with no recall at all.

Every route so far has a fixed shape (search, list, calculate, follow one hop), chosen before anything is retrieved. Here, the shape isn't known until the first result comes back. So for these questions, we let the model decide each step after seeing the previous result. That's an agent loop.

The loop

The loop has two actors with different jobs:

  • The model chooses which tool to call next, with which arguments, or whether it has enough to finish. It decides only from the question and what earlier steps returned.
  • Code does everything else: it runs the tool, applies the user's access, records the step, enforces a budget, and checks what the model cites.
flowchart LR
  Q[Question] --> P{"Model: next step?"}
  P -- "call a tool" --> T["Code runs the tool (user's access)"]
  T -- "result" --> P
  P -- "finish (evidence)" --> V["Code keeps only evidence a tool showed"]
  V --> A[Answer, as in the other routes]
  T -. "6 steps, or the same call twice" .-> X[Stop: a person should look]

The model sees the tools as short descriptions, exactly as the code gives them:

TOOLS = {    "search": "search(query): the best passages for a query, each with its source and product.",    "recall_status": "recall_status(part, serial): whether a pack's serial number is in a recall, checked in code.",    "read": "read(source_id): every section of one document that you may read.",    "finish": "finish(evidence) or finish(clarify): stop, naming the sources the answer rests on, "              "or the question to ask the user.",}

search and read reuse what earlier parts built. recall_status follows Part 10's rule that comparisons belong in code: at ingestion, a recall that states a serial range becomes a record (part BP-36-2, first 2509-0001, last 2509-0800, source BULLETIN-2026-09 §1), and the tool checks a serial against it. All four tools only read. That matters, and we'll come back to it.

In the companion code, the model is a stand-in, ToyPlanner: a few rules that pick the next call from the question and the previous results. A real implementation gives the same descriptions to a model with tool calling, where the model returns a structured request such as {"tool": "search", "args": {"query": "C-3 battery pack"}} instead of text. The loop around it is the same either way.

This is where the stand-ins show the least. A rule-based planner always chooses sensibly; a real model sometimes picks the wrong tool, writes a poor query, or finishes too early, which are exactly the cases the guards below exist for. A second version of the companion repository, with real models behind the same interfaces, is planned, and the agent loop is where comparing the two will teach the most.

Following one run

For the C-3 with serial 2509-0412, each step is a span in the trace, with the model's stated reason, the call, and what the tool returned:

step 1  thought: The serial alone doesn't say which pack it is. Find the pack the C-3 uses.        search(query='C-3 battery pack')        saw:  SPEC-C3-R5#8, SPEC-C3-R5#6
step 2  thought: The C-3 uses BP-36-2. Check serial 2509-0412 against recalls of BP-36-2.        recall_status(part='BP-36-2', serial='2509-0412')        saw:  affected: yes. Recall BULLETIN-2026-09#1 covers BP-36-2 serial numbers              2509-0001 to 2509-0800; serial 2509-0412 is inside that range.
step 3  thought: It is affected. Read BULLETIN-2026-09 for what to do.        read(source_id='BULLETIN-2026-09')        saw:  BULLETIN-2026-09#1, BULLETIN-2026-09#2
step 4  thought: I have the pack, the recall check, and the instructions.        finish(evidence=['SPEC-C3-R5#8', 'BULLETIN-2026-09#1', 'BULLETIN-2026-09#2'])

BP-36-2 appears in neither the question nor the first query: the model read it in step 1's result and used it in step 2. Step 3 happened only because step 2 said "affected: yes". That's the difference from Part 11's follow route, where code followed a link chosen in advance.

The answer is then built like every other route's: the cited evidence becomes labeled sources, the code-checked result becomes a stated fact, and the number and citation checks run. With our stand-in model:

- affected: yes. Recall BULLETIN-2026-09#1 covers BP-36-2 serial numbers 2509-0001 to 2509-0800; serial 2509-0412 is inside that range.- The C-3 uses the 36 V battery pack BP-36-2, mounted in the frame's battery tube. [SPEC-C3-R5, section 8]- Battery packs BP-36-2 with serial numbers 2509-0001 to 2509-0800 may swell after a deep discharge. [BULLETIN-2026-09, section 1]- Record the pack serial number and the frame number in the service record. [BULLETIN-2026-09, section 2]

The other questions take shorter paths, decided along the way:

C-3, serial 2509-0950:  search -> recall_status: "affected: no ... outside that range" -> finishT-7, serial 2509-0412:  search -> recall_status: "BP-48-1: no recall found." -> finish

For the T-7, the answer cites the T-7 specification and never supplies the recall; test case Q23-other-pack checks exactly that. And without a product ("Battery serial 2509-0412: is it affected by a recall?"), the model can't take step 1 and asks: "Which product is this about?"

Keeping the loop safe

A loop that a model drives can circle, wander, cite something it never saw, or reach for documents its user may not read. Each guard is a few lines of code, and the tests drive the loop with deliberately bad stand-in planners to prove they work.

A repeated call or a spent budget stops the loop. Repeating a call means the model isn't learning anything new; six steps is the ceiling whatever it wants:

step 2   search(query='battery')   stopped: repeated callI couldn't finish this: the same step came up twice. A person should look at it.
I couldn't finish this: all 6 steps were used. A person should look at it.

Both end with status gave_up, which puts the request in Part 8's review queue: a loop that gives up usually points to a missing tool or a confusing question.

Only what a tool showed can be cited. Code keeps only evidence that a tool returned during this run. A stand-in planner that finishes at once, citing the price list it never saw:

evidence   cited=[], dropped=['PRICE-PARTS-2026#1']I couldn't find evidence for this in the documents you can read.

Tools apply access. The model gets tools, not a database connection, and every tool checks the user's groups. Asked to read the price list for service staff, read returns "No document PRICE-PARTS-2026 that you can read", which doesn't even confirm that the document exists.

What it costs

Every step is a model call. The C-3 question took five: three to choose the calls, one to finish, one to write the answer. The routes take one. With a real model, each call adds latency and cost, and Part 8's trace records them per step.

That's why the agent loop is a fifth route, not a replacement for the router. The router sends a question here only when it needs it (in our code, when it contains a battery serial number). In Part 8's replayed week, no question did: all 24 had a fixed path that fit.

One loop, many processes

Our loop serves one business process: checking a battery against recalls. A real dealer assistant faces many (warranty claims, recalls, returns, service scheduling), each with its own steps and rules. A route per process works for a few. An orchestrator handing each process to its own specialist agent works for more, at the price of more model calls and harder traces. Both mean writing code every time a process is added or changes.

A more flexible option keeps one capable model and one set of tools, and describes each process in writing. Such a description is often called a skill: a short document saying when it applies, which steps to take, which tools to use, and what to check. The model sees only each skill's name and description, and loads the full text when a question matches:

---name: battery-recall-checkdescription: Use when a technician asks whether a specific battery pack is affected by a recall.---1. Find which pack the bike uses (search its specification). Never infer it from the serial.2. Check the serial with recall_status. Never compare serial ranges yourself.3. If affected, read the recall and include what to record in the service record.4. If the product or serial is missing, ask for it before searching.

The open Agent Skills format standardizes this: a folder with a SKILL.md file and optional scripts or references, loaded in stages so unused skills cost almost nothing.

Three lessons from this series still apply. Choosing a skill is routing again, so it needs test cases like the router's. A skill is instructions, not enforcement: access, budgets, and checks stay in code. And skills are knowledge the assistant depends on, so they get the treatment Part 9 gave curated records: versioned, and reviewed by the people who own the process.

Reading is not acting

Every tool in this part reads. The worst a bad step can do is waste a call or supply the wrong evidence, and the guards catch most of that.

The obvious next request is "and create the replacement order". A tool that changes something (an order, a service record, an email to the customer) can't be undone by ignoring an answer. It needs different controls: a person confirming each action, permissions per tool, a way to undo, calls that are safe to repeat. Agents that act, and skills in depth, are the subject of a future series, still a work in progress. This one stays on the side of answering.

The system now

flowchart LR
  Q[Question] --> R{Router}
  R -- "search" --> S[Search, top k]
  R -- "list" --> L[Every matching record]
  R -- "calculate" --> C[Metric computed in code]
  R -- "follow" --> F[Best evidence + one hop of links]
  R -- "agent" --> G["Model picks each step: search, recall_status, read, finish"]:::new
  G --> GU{{"Guards: 6 steps, no repeats, cite only what was seen, access in every tool"}}:::new
  K[(Records, including recall ranges)] --> G
  S --> M[Answer]
  L --> M
  C --> M
  F --> M
  GU --> M
  classDef new stroke-width:3px
routes: search, list, calculate, follow, agentagent: 4 read-only tools, at most 6 steps

Stop here if

Some questions need several dependent lookups, a handful of read-only tools covers them, and the extra calls are acceptable for the share of traffic that takes this route. Each tool is a deliberate addition: a description the model can understand, access checks inside it, and test cases for the paths it opens.

One kind of question remains that none of the routes handle well: comparing two whole documents, or answering from material too large or too interconnected to split into pieces. That's Part 13.