A Tool Loop Should End in a Receipt
A tool loop becomes trustworthy when its stopping condition is visible. The useful unit is not “the agent called a tool,” but “the agent made a bounded attempt, checked the result, and left a receipt that tells the next operator what happened.”
This article presents a small DAL challenge: build a loop that retries a filesystem operation without turning a transient failure into an unbounded execution. The solution uses explicit state, a step budget, and a final evidence record. The same shape applies to review queues, scheduled work, and any agent action with an external effect.
The challenge: retry without creating ambient work
Suppose an agent must ensure that a report exists at a destination path. It may need to inspect the directory, create the file, and verify the result. A naïve loop says: “keep trying until the file appears.” That instruction hides three problems:
- A permanent permission error becomes infinite work.
- A repeated execution can create duplicate review items or logs.
- A later operator cannot distinguish a successful retry from an execution that merely stopped.
The challenge is small enough to implement, but representative enough to expose the design rule: every iteration must consume a known amount of budget, and every exit must explain itself.
In DAL-shaped pseudocode, the state can be kept deliberately plain:
``dal let state = { "attempt": 0, "max_attempts": 3, "path": "COO-FILESYSTEM/articles/drafts/example.md", "status": "pending", "evidence": [] } ``
The important field is not the path or even the counter. It is the fact that the loop has a declared terminal boundary before it touches a tool.
Step one: make each attempt observable
A tool call should produce an event that can be inspected independently of the final answer. That event need not be elaborate. It needs an attempt number, an operation, and an outcome.
``dal fn record(state, operation, outcome) { state.evidence = append(state.evidence, { "attempt": state.attempt, "operation": operation, "outcome": outcome }) return state } ``
Now the loop can separate planning from execution. First increment the attempt. Then perform exactly one meaningful operation. Finally record what the operation returned.
```dal while state.status == "pending" and state.attempt < state.max_attempts { state.attempt = state.attempt + 1
let listing = run("test -e '" + state.path + "' && echo present || echo missing") state = record(state, "check_destination", listing)
if contains(listing, "present") { state.status = "verified" } else { let created = run("mkdir -p 'COO-FILESYSTEM/articles/drafts' && printf '%s\\n' '# Draft' > '" + state.path + "'") state = record(state, "create_destination", created) } } ```
This example is intentionally schematic: production code still needs careful escaping and policy checks around writes. Its point is structural. The loop does not treat “tool returned” as “work succeeded.” It records the observation and makes verification a separate state transition.
Step two: distinguish completion from exhaustion
A bounded loop has at least two normal endings: verification succeeded, or the budget ran out. Those outcomes must not collapse into the same boolean.
```dal if state.status == "pending" { state.status = "exhausted" state = record(state, "stop", "attempt budget reached") }
let receipt = { "path": state.path, "status": state.status, "attempts": state.attempt, "evidence": state.evidence } ```
The distinction matters operationally. A verified result can move to the next stage. An exhausted result belongs in review or a repair queue. Neither result requires ambient retries from an unseen worker.
This is also where idempotency enters the design. Before creating or submitting anything, the loop should ask whether the intended artifact or commitment already exists. A check such as “destination present” is useful, but a real workflow may need a stable operation key as well:
``dal let operation_key = "article-draft:2026-09-02:tool-loop" ``
Persist that key with the receipt. If the worker wakes again, it can recognize the prior attempt rather than blindly repeating it. Idempotency does not mean “never retry.” It means “retry as the same operation, with a visible history.”
Step three: put the receipt where the next human can find it
The loop is not complete when it prints a status. It is complete when the status is durable and easy to locate. A compact markdown receipt works well for a small workflow:
```markdown
# Tool-loop receipt
- operation: article-draft:2026-09-02:tool-loop
- path: COO-FILESYSTEM/articles/drafts/example.md
- status: verified
- attempts: 2
Evidence
- 1. check_destination — missing
- 2. create_destination — command completed
- 3. check_destination — present
```
The receipt should answer four questions without replaying the entire agent session:
- 1. What did the agent intend to do?
- 2. Which path or external boundary was involved?
- 3. How many attempts occurred?
- 4. What evidence justifies the terminal status?
For an externally visible action, add a review gate before the effect rather than after it. The agent may prepare a message or file autonomously, but sending or publishing should remain a distinct, approved transition when policy requires it. A receipt then records both preparation and approval, instead of implying that a draft was already delivered.
What this prevents
The design directly addresses a recurring class of operational failure: repeated execution that produces repeated failure records but no new information. A bounded loop will still fail when permissions, credentials, or inputs are wrong. That is acceptable. The failure is now finite, classified, and actionable.
It also improves measurement. “The task ran eleven times” is a weak signal by itself. A receipt can show whether those runs were new attempts, duplicate operation keys, or retries after a verified result. That difference determines whether the fix belongs in the tool, the scheduler, or the deduplication layer.
Finally, the pattern keeps the operator surface calm. Instead of surfacing every intermediate thought, expose one durable result with enough evidence to continue. Noise is not observability; a searchable, bounded record is.
Conclusion: define the exit before the tool call
A practical DAL tool loop needs only a few disciplined ingredients: explicit state, a finite attempt budget, separate verification, idempotency, and a durable receipt. Start by wrapping one real operation. Record the observation after each attempt. Make verified and exhausted different outcomes. Then place the receipt under the workflow’s normal artifact or work-log path.
The next time a tool loop is tempting, write its terminal states first. If you cannot name how it succeeds, how it stops, and what evidence remains, the loop is not ready to run unattended. If you can, the agent has a chance to be both useful and quiet: it drains a commitment predictably, and it leaves the next decision to the person or process authorized to make it.