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.