Skip to content

9 min read

Inspectability does not require a planner to buy it

On this page

The #supply-weekly bot pulls the vendor scorecard from the analytics API at 09:00 on Mondays, writes six lines of summary, and posts it to a Slack channel. It is a fused loop with three tools and roughly forty lines of glue around one model call. The pull request that landed in April replaced it with a planner and an executor, and the justification in the description was a single sentence: with an explicit plan we can see what the agent decided to do.

The diff was 610 added lines across five files, because a plan needs a schema, the schema needs a validator, the validator needs the tool registry wired to it, the run state needs somewhere to live, and the recovery path needs a policy and a budget even if it never fires. Median wall time went from 4.1 seconds to 6.0, the difference being one planner call against a 1,400-token prompt. In the ninety days before the PR the bot had failed eleven times: seven Slack 429s that the retry policy absorbed, three 502s from the analytics gateway that the retry policy also absorbed, and one Monday where the upstream ETL job, the nightly extract that loads the scorecard tables, had not finished and the scorecard came back empty, so the bot posted a summary of nothing. That last one is a real defect and its fix is a schedule change plus a precondition on row count, both of which fit inside the loop that already exists.

None of the eleven involved a failed step that invalidated later steps. None required a remainder to be re-derived from completed work. The machinery from the previous four chapters exists to handle a class of failure this bot has never produced, and the PR was reviewed on the strength of an argument that had nothing to do with failures at all.

The trace was the thing wanted, and the plan was one way to get it

What the PR author wanted is legitimate. On a Monday when the summary reads oddly, the question is which scorecard the bot fetched, with which week parameter, and what it expected back — and a transcript of assistant messages interleaved with raw tool results is a poor way to answer that, because the only record of intent is whatever the model happened to narrate. A plan object answers it directly, since each step names a tool, its bound arguments and a postcondition before anything runs.

A fused loop can produce the same record. The loop already knows the tool name and the arguments a fraction of a second before it dispatches them; what it does not do by default is write them down as an intention separate from the call. Adding two required fields to every tool schema and one durable write before dispatch closes that gap.

python
# every tool schema gains two required parameters
INTENT_FIELDS = {
    "why":     {"type": "string", "description": "what this call is for, one sentence"},
    "expects": {"type": "string", "description": "what a successful result looks like"},
}

def call_tool(name, args, *, run_id, seq):
    why, expects = args.pop("why"), args.pop("expects")
    log.emit("agent.intent", run=run_id, seq=seq, tool=name,
             args=redact(args), why=why, expects=expects, at=now())
    verdict = authorize(name, args, actor=RUN_ACTOR)      # same policy table as a split
    if not isinstance(verdict, Allow):
        log.emit("agent.denied", run=run_id, seq=seq, reason=str(verdict))
        return {"error": "denied", "detail": str(verdict)}
    try:
        result = TOOLS[name].fn(**args)
    except ToolError as e:
        log.emit("agent.result", run=run_id, seq=seq, ok=False, code=e.code)
        raise
    log.emit("agent.result", run=run_id, seq=seq, ok=True,
             summary=summarise(result), ms=elapsed())
    return result

The expects field is the part that does the work, because it forces a claim the result can be checked against, and the check is available afterwards to a person reading the log or to an assertion running over stored traces. It is weaker than the postcondition chapter one described, since the model writes it in prose and it is evaluated by eye rather than by a probe, and for a Monday morning report bot that is an appropriate amount of rigour.

text
09:00:02 r_5f21 seq 0  analytics.scorecard(week="2026-W15", group="tier1")
                       why     "get last week's tier-1 vendor scores"
                       expects "one row per tier-1 vendor, about 14 rows"
                       -> ok, 14 rows, 820 ms
09:00:04 r_5f21 seq 1  llm.summarise(rows=<14>, style="six_lines")
                       why     "condense the table for a channel post"
                       expects "six lines, worst three vendors named"
                       -> ok, 61 words, 1,910 ms
09:00:06 r_5f21 seq 2  slack.post(channel="#supply-weekly", text=<61w>)
                       why     "deliver the summary"
                       expects "a message ts returned"
                       -> ok, ts=1744099206.4471, 240 ms

That is thirty lines of code, no additional model call, no second prompt and no boundary to recover across. Against the 610-line PR it answers the same question for the same reader.

What the plan artefact was wanted forFused loop with an intent logWhat the split adds beyond that
Which tool ran, with which argumentsRecorded before dispatch, in the same record as the resultNothing
Why that action was chosenThe why field, written by the model at the moment of choosingThe planner's reasoning, covering the sequence rather than the step
Whether the result matched the intentionThe expects field, checked by eye or by an assertion over stored tracesA machine-checked postcondition written by a different component
Seeing the whole sequence before any of it runsNot available — the loop chooses step n after step n−1 returnsThis is the actual purchase
Permission and ceiling checks before dispatchYes; the policy table is a property of the tool, not of the architectureThe literal-valued arguments can be checked once, over the plan
Re-deriving the remaining work after a failureNot available — nothing in the loop states what the remainder wasRun state, completed work, and a re-planning call

Two rows in that table are empty on the left, and they are the two things worth paying for. Everything else in the column is available for thirty lines and no latency.

What has to be true before the sequence needs deciding in advance

The split earns its cost when something needs to happen to the sequence as a whole, before or after execution, rather than to the individual calls.

Committing in advance is the first case. A plan that can be shown to a person, priced against a budget, or checked for whether it touches a particular tool at all is a plan that has to exist before execution begins, and a fused loop has no such artefact to offer, since it decides each action from the results of the last one. When the question is "will this run issue a credit, and how large", only a plan answers it in time to matter.

Recovery is the second, and it is the one the previous chapters built. It matters when a mid-sequence failure leaves the remainder void and re-deriving that remainder is a different operation from repeating the failed call — which requires the sequence to be long enough for a remainder to exist, and its steps to leave state behind that the next attempt has to reckon with. A three-step read–summarise–post has neither property, because failing at any of the three leaves nothing behind that a re-plan would need to route around.

The third is cost shape rather than correctness. Chapter five's token numbers came from a pipeline where most executor steps were dispatch rather than generation, and that only happens when the step count is high enough for the fused loop's transcript growth to dominate. On a three-step task the loop's transcript never gets large and the planner call is pure overhead.

A rough gate covers most cases: if the run has four or fewer steps, if no step leaves an effect that a later attempt must account for, and if the failures observed in production have been transient rather than plan-invalidating, the machinery has nothing to do. The #supply-weekly bot fails that gate on all three counts, which is the answer the PR review should have reached in a few minutes.

Two refund agents that want different architectures

Refunds are where the "high stakes, therefore split" argument shows up most often, and the stakes are not what decides it. Consider two agents that both process refund requests.

The consumer app's refund agent handles order-level refunds under €50 against a single payment provider. A run looks up the order, evaluates a policy that is four rules in code rather than in a prompt, calls payments.refund once, and sends a notification. It is a fused loop with four tools, an approval gate above €50, and an idempotency key on the refund call. Nothing about the sequence needs deciding in advance, because there is one ordering and it is fixed.

The enterprise billing team's refund agent handles a prorated refund across two to five invoices in a contract's current term, which means computing the unused amount per invoice, issuing a credit note in the ERP, adjusting the tax position for the jurisdiction, cancelling any dunning in progress, updating the CRM, and notifying both the customer and the account manager. The step count runs from six to nine and depends on the contract. Half the steps write to systems that reject re-entry, the amounts derive from reads taken during the run, and finance requires that the full set of amounts is approved before anything posts to the ledger. That approval requirement alone settles it, because there is no version of a fused loop that shows the whole set of postings before making the first one.

The distinguishing property in that pair is that the sequence itself is contested — its shape varies per contract, it must be agreed as a unit before any of it commits, and a failure in the middle leaves a partially refunded customer whose remaining steps have to be recomputed from what actually posted. The euro value of the operations is the same kind of thing in both agents.

Safety propertyMechanism that provides itNeeds a separate planner
No refund over €500 without a humanApproval gate declared on the toolNo
A repeated call cannot double the refundIdempotency key derived from run, step and canonical argumentsNo
The agent cannot touch accounts outside its scopeScopes on the actor, checked at dispatchNo
A wrong refund can be reversedCompensating credit note declared on the toolNo
Someone approves the whole set of postings before the first oneA plan artefact that exists before executionYes
The remainder is recomputed after a mid-sequence failureRun state, completed work, a re-planning callYes

Every mechanism in the first four rows is a property of the tool and its policy, and each one is available to a single loop without changing its architecture, because the permission table and the tool registry from the earlier chapters were never coupled to the split. The split is about who decides the sequence and when that decision becomes reviewable.

The middle position and where it gets uncomfortable

There is an arrangement between the two that gets less attention than it deserves: a single loop that emits its intended sequence as a structured list on its first turn, then executes inside the same loop with the same context. It costs no extra model call, and it produces an artefact that can be shown for approval before the first tool runs, which covers the first of the two purchases without any of the second's machinery.

The weakness is enforcement, since nothing stops the loop from deviating from its own list once results start arriving, and a plan the agent may silently abandon is worse than no plan because it reads as a commitment. Making it binding means the dispatcher rejects any call not present in the announced list, at which point the announced list is a plan, the dispatcher is a validator, and a deviation has to route somewhere. That somewhere is a re-planning call with completed and remaining work, which is the split arriving through the back door. The middle position is stable when approval is the whole requirement and recovery genuinely is not, and it collapses toward the split as soon as deviation has to be handled properly.

The variants worth knowing about when you do build the split are collected in LangGraph's planning-agents material, which covers plan-and-execute alongside ReWOO, where the plan uses variable substitution to avoid re-reading context, and LLMCompiler, which schedules independent steps in parallel from the same dependency information. Both are answers to costs this topic measured, and both assume the plan artefact already exists.

The first thing to instrument on an existing agent is the rate at which a final step failure invalidates work that was still queued, counted over a month of real runs, alongside the count of steps whose effects a second attempt has to reckon with. Those two numbers decide the architecture more reliably than any argument made in a review, and neither requires building anything to obtain. Start the loop fused, log the intent before every call, and let a specific failure that the fused shape cannot answer be the thing that buys the planner.

Planner–executor — read the current version at https://daviddominguez.dev/learn/en/planner-executor/inspectability-does-not-require-a-planner-to-buy-it

Back to top