Building Autonomous AI Agents: Tool Calling, State Machines, and ReAct Loops
Designing deterministic AI workflows with JSON schema function calling, error recovery, and safety guardrails.
1. The ReAct (Reason + Act) Loop Architecture
The ReAct pattern structures an agent's execution as an interleaved loop of reasoning and action: the model produces a thought about what it needs to do next, selects and calls a tool based on that thought, observes the tool's result, and feeds that observation back into the next reasoning step, repeating until it has enough information to produce a final answer. This is a meaningful departure from a single-shot prompt-response call, because it lets the model course-correct based on real intermediate results rather than having to plan an entire multi-step task correctly in one pass.
Implementing this loop reliably requires an explicit state machine, not an implicit while loop, because production agents need well-defined transitions between states (reasoning, awaiting tool result, tool error, awaiting human approval, terminal) that can be logged, resumed after a crash, and reasoned about independently of the LLM call itself. Treating the loop as just a for loop around an API call works for a demo but breaks down the moment you need to persist state, retry a failed tool call without re-running the whole reasoning chain, or pause for human approval mid-task.
The state machine also needs an explicit iteration budget from the start, both a maximum number of loop iterations and a maximum wall-clock time, because an agent that gets stuck in an unproductive reasoning pattern (repeatedly calling the same tool with slightly different arguments, for example) will otherwise run until it hits a token or cost ceiling rather than failing fast and surfacing the problem.
2. Enforcing Strict Schema Validation with Zod
Function calling APIs from LLM providers describe tool arguments with a JSON schema, but the model is not guaranteed to produce arguments that actually satisfy that schema, and treating the raw tool-call arguments as trusted input is one of the most common sources of silent failures and, in the worst case, real security issues (a path traversal argument passed straight into a file-read tool, for instance). Every tool call's arguments must be validated against a strict runtime schema before execution, not just at the type level during development.
Zod is the standard choice in a TypeScript stack because a single schema definition serves double duty: it generates the JSON schema the LLM sees describing what arguments the tool expects, and it validates the actual arguments returned at runtime, which keeps the two in sync by construction instead of maintaining a hand-written JSON schema and a separate hand-written validator that can silently drift apart.
When validation fails, the correct behavior is not to throw and terminate the agent loop, but to feed the validation error back to the model as a tool result, in the same shape a normal tool error would take, which lets the model see exactly what was wrong (a missing required field, a value out of range) and retry with corrected arguments. This turns schema validation into a self-correcting feedback loop rather than a hard failure, which measurably reduces the fraction of tasks that fail purely on malformed tool arguments.

