Skip to content

Build1 publisher2 min readPublished

Agent runtime's Verify phase uses schemas and domain invariants, with an optional guard for extra business-logic checks

A dev.to design splits the agent loop into observe, propose, verify and commit phases. The Zod schema check is mandatory, the domain-invariant guard is optional per tool, and a retry counter on state is what ends a runaway run.

The Engineer · Build desk

What happened

  • A dev.to post lists what the naive agent loop does in production: repeats failing tool calls, hallucinates arguments when a tool's output schema shifts, and emits intermediate outputs that violate domain invariants.
  • Its replacement decouples execution into four deterministic phases named Observe, Propose, Verify and Commit, with the model confined to proposing one candidate state transition.
  • Verification happens in a deterministic runtime gate built from Zod schemas, AST parsers and domain invariants, and it inspects the proposed call before the tool executes.
  • The agent's history is an immutable journal of five typed events, among them VERIFICATION_FAILED for a rejected proposal and AGENT_HALTED for a stopped run.
  • AgentState holds a six-value status enum alongside two plain numeric fields, consecutiveFailures and maxRetries.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Anyone adopting this has to decide whether one retry budget per run is the right granularity, because maxRetries sits on the run's state and a single flaky endpoint can spend the whole allowance.
  • cost Reliability is priced per tool: a schema for every one, and a second hand-written function wherever domain invariants matter. The bill falls on whoever adds tools, not on whoever writes prompts.
  • capability Because four of the five event types are written without a tool having run, an operator can replay what the planner attempted, including the rejections, without re-triggering any side effect.

Read the `guard?` field in the `VerifiableTool` interface before the flowchart. Zod handles the shape of the arguments; the guard is where domain logic goes, and the question mark makes it optional per tool [13]. A tool declared without one is checked once before commit, and that check only establishes that the arguments parse [8][2]. The post calls the gate an intermediate firewall between the model's output and downstream services [17]. Each tool author can decline to write that firewall's rule set.

The guard's signature bounds what it can enforce: it receives the proposed input and the event history, and returns `{ valid, reason? }` [13]. Invariants that are functions of those two things are straightforward to write. An invariant that depends on the current row in a database, or on a third party's state, is not, unless the guard performs a read, at which point the deterministic gate the design asks for [8] is calling something whose answer changes between runs.

Observe has the same boundary. It reads current state and active constraints from a read-only event journal [6], so the planner's view of the world is whatever `ACTION_COMMITTED` results were recorded there [9][11]. A downstream service can change underneath the agent between commits, and the loop finds that out from the next failed tool call.

The post's diagnosis is architectural: engineers "treat AI agents like deterministic functions, while executing their state mutations non-deterministically without real boundary validation" [3]. The thing that stops the runaway retry is two numbers on the state object. `AgentState` carries `consecutiveFailures` and `maxRetries` next to the status enum [12]. Those two fields are the halting condition. The phases decide what gets journaled when the ceiling trips, and `AGENT_HALTED` is one of the five event types [11].

The commit phase runs the tool, then appends the result to the immutable journal [9]. A process that dies between those two statements has performed the mutation without recording it. A lost append leads to the same duplicated mutation the post attributes to blind re-prompting after a half-successful call, such as creating a user record but failing to send the email [4].

The post describes what it builds as a deterministic, production-ready runtime in TypeScript [16] and puts the cost of the naive loop at hundreds of dollars in API credits [2], without publishing the run behind that figure. Arguments that parse and are still wrong pass a schema check. For it to transfer, your failures have to be the kind a gate can see: proposals that break a schema after tool output drifts, or that violate an invariant you have already written down [2].

What to watch

  • Whether a follow-up publishes measured retry counts or token spend for an OPVC runtime against the naive loop it replaces.
  • Whether the verify() implementation, which the published excerpt cuts off mid-method, permits I/O inside a guard.
  • Whether the guard field becomes required in the tool interface, since an optional one defaults to no invariant checking.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories