prompting
Tool call retries with deterministic guards
Tool call retries waste tokens on errors that won't resolve through repetition; deterministic guards validate inputs before execution to prevent futile retry cycles.

Tool calls fail. Not occasionally—routinely. An LLM agent decides to read a file that doesn't exist, calls a function with the wrong parameter shape, or attempts an API request that times out. The question isn't whether your system will encounter these failures, but how you handle the inevitable retry storm that follows without burning tokens on mistakes the model can't see.
Most retry strategies treat all failures identically: tool call failed, decrement attempt counter, re-prompt the model with error context. This burns tokens on categories of errors that won't resolve with repetition. If an agent calls read_file("/src/config.yaml") but the file lives at /config/yaml, three retries teach the model nothing. The filesystem state hasn't changed. The model receives the same error message each time and often cycles through minor variations of the same incorrect path.
Deterministic guards—constraints enforced before execution rather than after failure—shift this equation. Instead of retrying malformed tool calls repeatedly, you validate inputs against a schema, check file paths before read attempts, and verify API contract compliance before the HTTP round trip. When combined with differentiated retry budgets and structured feedback, guards turn retry loops from token sinks into productive corrections.
The hidden cost of undifferentiated retries
A naive retry policy treats all failures identically. This creates a fundamental mismatch between error types and recovery strategies. The model sees its own mistake in the error message, but nothing in the environment has changed. It still has the same context window, the same biased prior that "componets" might be a valid directory name, and no new information to break the loop.
Worse, each retry compounds the cost. By attempt three on a simple typo, you've tripled your token spend and added 15+ seconds of user-facing latency. If your agent runs 50 tool calls in a session with a 10% per-call failure rate, undifferentiated retries mean a >99% chance of at least one failure cascading into wasted work.
Consider the difference between a network timeout on an external API call versus a schema validation error. The timeout might resolve on retry—a different egress route, a recovered downstream service, or simple jitter in request timing could succeed. The schema error will fail identically until the model receives new information about what the contract actually expects.
Deterministic guards let you separate "will never succeed without new information" from "might succeed with different timing or luck." File path validation happens before the filesystem call. Schema validation happens before JSON serialization. These checks are cheap, deterministic, and eliminate entire categories of retry-doomed operations.
Schema validation as a pre-execution gate
Schema violations are the most common preventable retry trigger. A model that correctly called create_file(path="/src/main.py") five times might suddenly emit create_file(filepath="/src/main.py") on attempt six. Your tool executor expects exact parameter names. The mismatch crashes, you retry, and the model guesses at corrections without seeing the actual contract.
A validation layer catches this at plan time. Using Pydantic in Python:
from pydantic import BaseModel, Field, ValidationError
class CreateFileParams(BaseModel):
path: str = Field(min_length=1)
content: str
mode: int = 0o644
def execute_tool(tool_name: str, params: dict):
if tool_name == "create_file":
try:
validated = CreateFileParams(**params)
return create_file(validated.path, validated.content, validated.mode)
except ValidationError as e:
return {
"error": "validation_failed",
"details": e.errors() # [{"loc": ["path"], "msg": "field required", ...}]
}
When the model sends {"filepath": "/src/main.py", "content": "..."}, Pydantic immediately returns a structured error:
{
"error": "validation_failed",
"details": [
{"loc": ["path"], "msg": "field required", "type": "value_error.missing"}
]
}
The model now sees the exact field name that's missing, not a Python stack trace. This is the contract it agreed to when it picked the tool. The validation error surfaces immediately with specificity, and the retry prompt can reference the concrete violation rather than asking the model to reverse-engineer requirements from symptoms.
The tradeoff: you're adding latency and complexity to your tool execution path. Every call pays the validation tax. For high-volume systems, this overhead matters—typically microseconds for schema checks, but it compounds across hundreds of calls. You're betting that preventing bad calls is cheaper than retrying them. For LLM inference costs, this is usually true. For microsecond-scale tool executions, the calculus depends on your failure rate.
Differentiated retry budgets by error class
Not all failures deserve the same retry treatment. Assign attempt budgets based on failure type rather than blanket limits:
class GuardViolation(Exception):
"""Deterministic failure - do not retry"""
pass
class TransientFailure(Exception):
"""Worth retrying with backoff"""
pass
class StructuredFeedbackRequired(Exception):
"""Retry once with detailed context"""
pass
Your tool execution wrapper catches these and routes accordingly. When a tool raises GuardViolation (missing file, invalid schema), the agent loop moves to the next plan step rather than burning retries on an impossible call. A TransientFailure (network timeout, 503 from an API) gets three attempts with exponential backoff. A StructuredFeedbackRequired error (compile error, test failure) gets one retry with enriched error context.
Practical budgets from production systems:
- Schema validation failures: 1 retry with structured error details
- Missing resources (file not found, undefined variable): 0 retries—feed the error forward and replan
- Network/I/O transients: 3-5 retries with exponential backoff
- Compile/lint failures: 2-3 retries with compiler output in context
The cost here is engineering complexity. You've replaced one retry counter with a decision tree of error handlers. Debugging becomes harder because the same tool call might retry three times or zero times depending on error taxonomy. You need telemetry that tracks not just retry counts but retry reasons to understand system behavior.
Compile gates for code generation tools
Code-generating agents produce especially brittle tool calls. An agent might emit a git commit command with unescaped quotes, or generate a snippet that references non-existent imports. Retrying these after execution wastes time parsing LSP errors the model struggles to interpret.
Goatfied's agent loop runs compile/lint/test gates before considering a change complete. The pattern is plan → constrain → edit → validate → retry. If you're generating Python, the agent output passes through mypy and ruff before entering the diff queue. TypeScript goes through tsc --noEmit. These are deterministic guards—the code either type-checks or it doesn't, and no amount of retries without new context will change the outcome.
When validation fails, the agent gets structured compiler feedback and retries with that context:
const result = await runTypeCheck(editedFile);
if (!result.success) {
return {
status: "needs_retry",
feedback: result.errors, // structured: file, line, message
attemptsRemaining: maxAttempts - currentAttempt
};
}
The retry isn't blind—you're not re-running the same broken code hoping for different results. You're giving the model specific information about type mismatches or lint violations to inform the next attempt.
The tradeoff is latency. Type-checking on every edit slows the loop, especially for large TypeScript projects where a full compile can take 200-500ms. You're betting that catching errors before commit is cheaper than debugging broken PRs post-merge. For teams prioritizing reproducibility and audit trails, this trades throughput for correctness. For rapid prototyping, you might run lightweight guards (schema + basic lint) in the hot path and full compile gates only before final commit.
Constraining retries with concrete options
When a guard rejects a tool call, the retry prompt must communicate why and how to fix it without relying on the model to infer constraints. Generic error messages fail: "your path is wrong" gives the model nothing actionable. Concrete enumerations succeed.
If the model called editFile("componets/Button.tsx") and the path doesn't exist, your guard can list valid paths matching the pattern:
TOOL CALL REJECTED: Path not found.
Available paths matching **/Button.tsx:
- src/components/Button.tsx
- src/ui/Button.tsx
Retry with one of the valid paths above.
This is enum exhaustion: remove the invalid option from the search space and present the model with a bounded choice set. After a failed attempt, your guard dynamically updates the tool schema to include only valid values for the failing parameter. The model can't repeat the typo because "componets/Button.tsx" no longer exists in the option set.
For schema violations, show the exact contract excerpt that failed:
Validation error in create_file call:
- Missing required field: "content"
- You provided: {"path": "/src/main.py", "mode": 420}
Expected schema:
{
"path": "string (required)",
"content": "string (required)",
"mode": "integer (optional, default 0o644)"
}
This pattern works because it replaces inference with lookup. The model doesn't need to guess what went wrong—it sees the exact mismatch and the contract it should match.
Logging and retry provenance
When retries happen automatically, you need visibility into what changed between attempts. A tool call that succeeds on the third try might have succeeded for the wrong reasons—the model got lucky with random parameter choices rather than understanding the fix.
Log every attempt with full context:
- Tool call parameters (raw and validated)
- Guard results (which guards ran, which failed)
- Error messages and structured feedback
- Model response after retry
- Final outcome (success, escalation, or exhaustion)
This creates an audit trail you can reconstruct to understand why a change happened and whether the agent actually learned from feedback or stumbled into success. The data weight grows quickly—a chatty agent making 50 tool calls with 3 retries each generates 150 log entries per session. At scale, you're indexing gigabytes of retry telemetry.
The tradeoff is operational cost versus debuggability. For compliance-heavy domains or regulated environments, this cost is justified. You need to prove what the agent did and why. For rapid prototyping, you might sample retry logs (e.g., 10% of sessions) instead of capturing everything.
In Goatfied's managed environment, guard telemetry surfaces in the replay UI. You see the exact validation error that triggered a retry, then step forward to see how the model corrected itself. Self-hosted deployments get structured logs you can ingest into existing observability stacks—Datadog, Honeycomb, or your internal metrics system.
Chaining guards with multi-step validation
Schema guards catch structural errors. Compile gates catch syntax and type errors. Neither catches semantic failures—code that compiles but does the wrong thing, or valid parameters that violate runtime constraints.
An agent might generate syntactically valid SQL that performs a cartesian join on billion-row tables. The query type-checks, passes your SQL linter, and explodes only at execution time with an OOM error. Guards can't predict runtime cost, but they can enforce heuristics: no joins without WHERE clauses, no SELECT * on tables above a size threshold, query plan estimation before execution.
The practical pattern layers three validation stages:
1. Schema guards (microseconds): validate structure, required fields, type contracts
2. Logical guards (milliseconds): check business rules, resource existence, quota limits
3. Execution guards (variable): compile checks, test runs, resource consumption estimates
Each stage has different retry budgets. Schema failures get one retry with structured feedback. Logical failures might get zero retries if they represent impossible operations (e.g., delete a file that doesn't exist—better to replan than retry). Execution failures get 2-3 retries with full error output, since they often contain actionable information the model can use.
This creates natural escalation paths. If a tool call fails schema validation twice, you escalate to replanning. If it passes schema but fails compile checks three times, you might surface to a human or suggest a simpler implementation strategy.
Handling retry exhaustion
After N attempts, some operations genuinely can't succeed. The model might fundamentally misunderstand the task, or you've hit a real environmental constraint (API quota, missing dependency, filesystem permissions). Define escalation paths:
- Flag for human review: Attach full retry history and surface in your ops tooling
- Request new capabilities: Let the agent propose a missing tool or parameter it needs
- Fall back to simpler operations: Try
write_fileinstead ofatomic_replace, or skip optional steps
Hard-cap retries per tool call at 2-3 attempts. Set a session-wide budget (e.g., 10 total retries across a PR review task) to prevent runaway loops. Track which tools exhaust retries most often—this tells you where to improve schemas, add guards, or refine prompts.
The goal isn't perfect automation. It's making failures debuggable and retries productive, so your agent spends tokens improving outputs rather than repeating mistakes. Small, reversible diffs—core to Goatfied's design—help here. Each edit is a bounded operation with clear before/after state. If a change fails, the agent can retry with a different approach without unwinding unrelated edits.
When deterministic guards aren't enough
Guards catch mechanically preventable failures. They can't predict all semantic errors or guarantee the model understands the task. You still need runtime observability and circuit breakers that kill runaway operations regardless of how many retries remain in budget.
The pragmatic stack combines three layers:
- Deterministic guards handle schema violations, missing files, syntax errors—things that are false before execution and will stay false without new information
- Differentiated retry budgets handle transient issues like network timeouts and rate limits, with policies tuned to error type
- Hard resource caps (execution time, memory, token spend) serve as the final safety layer when both guards and retries fail to prevent pathological cases
This isn't a perfect system—LLMs will still surprise you. But it shifts the failure mode from "silent corruption or infinite retries" to "fast, debuggable failures with clear escalation paths." You're building a system that fails gracefully and provides enough telemetry to improve over time.
