# How Forge Retry Logic Handles Validation Failures with Nudges

> Discover how Forge retry logic handles validation failures with nudges. Learn how Forge prompts model correction and enforces retry limits for robust error management.

- Repository: [Antoine/forge](https://github.com/antoinezambelli/forge)
- Tags: how-to-guide
- Published: 2026-05-22

---

**When a model outputs plain text instead of a valid tool call, Forge automatically generates a retry nudge that prompts the model to correct its response, while the ErrorTracker increments a counter that the WorkflowRunner monitors to enforce retry limits.**

The `antoinezambelli/forge` repository implements a guardrail system that intercepts malformed outputs before they disrupt the workflow. When validation fails—specifically when the model returns free-form text rather than the expected tool invocation—the system leverages structured **nudges** to guide the model back on track. Understanding how this retry logic handles validation failures with nudges enables developers to build autonomous AI agents that self-correct parsing errors within defined retry budgets.

## Detection of Validation Failures

At the core of the failure detection mechanism lies the **ResponseValidator** class in [`src/forge/guardrails/response_validator.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/guardrails/response_validator.py). This component evaluates every model response against the expected schema for the current workflow step.

When the validator receives a **TextResponse** while the workflow expects a **ToolCall**, it first attempts *rescue parsing* to extract a JSON-encoded tool call from the text content. If rescue parsing fails to produce a valid tool invocation, the validator constructs a **ValidationResult** with `needs_retry` set to `True` and attaches a **Nudge** object whose `kind` attribute equals `"retry"`. This behavior is verified in [`tests/unit/test_response_validator.py`](https://github.com/antoinezambelli/forge/blob/main/tests/unit/test_response_validator.py), where the `test_plain_text_returns_retry_nudge` test confirms that plain text responses trigger the retry pathway rather than immediate failure.

## The Retry Nudge Lifecycle

### Nudge Template Generation

The content of the retry instruction originates from [`src/forge/prompts/nudges.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/prompts/nudges.py), which contains the **retry_nudge** template. When instantiated, this template generates a message instructing the model that it must respond with a tool call rather than free-form text. The **Nudge** class encapsulates this instruction along with metadata that categorizes it as a retry-type correction, distinguishing it from other nudge variants like step enforcement prompts.

### ValidationResult Structure

The `validate()` method returns a **ValidationResult** object that serves as the contract between the validation layer and the orchestration layer. Key attributes include:

- **`needs_retry`**: Boolean flag indicating validation failure
- **`nudge`**: A **Nudge** instance containing the retry instruction
- **`tool_calls`**: Empty when validation fails, populated when successful

This structure allows the calling code to check `result.needs_retry` and immediately access `result.nudge.content` to append the corrective instruction to the conversation history.

## Retry Counting and Orchestration

### ErrorTracker Implementation

The **ErrorTracker** class in [`src/forge/guardrails/error_tracker.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/guardrails/error_tracker.py) maintains the state necessary to prevent infinite retry loops. It exposes the **`record_retry()`** method, which increments an internal counter each time the workflow processes a validation failure. The tracker persists this count across turns, allowing the system to enforce `max_retries` limits configured at the workflow level.

When the **WorkflowRunner** in [`src/forge/core/runner.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/core/runner.py) detects a successful **ToolCall** following a series of retries, it invokes the tracker's reset logic, ensuring that subsequent validation failures start with a fresh retry count rather than accumulating from previous exchanges.

### WorkflowRunner Loop Logic

The **Guardrails** facade in [`src/forge/guardrails/guardrails.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/guardrails/guardrails.py) unifies validation, step enforcement, and error tracking behind a simplified API. Its **`check()`** method delegates to `ResponseValidator.validate()`, while its **`record()`** methods interface with the **ErrorTracker**.

Inside the runner's main loop, the integration flows as follows:

1. The runner calls `guardrails.check(response)` after receiving model output
2. If the result indicates `needs_retry`, the runner invokes `guardrails.record_retry(nudge)`
3. The **ErrorTracker** increments its counter and the nudge content is added to the prompt
4. The loop continues until either a valid tool call passes validation or the retry limit is exceeded

## Implementation Example

The following example demonstrates how the components interact when processing an invalid text response:

```python
from forge.guardrails import ResponseValidator, ValidationResult
from forge.core.messages import TextResponse

# Initialize validator expecting a "search" tool

validator = ResponseValidator(tool_names=["search"])

# Simulate a model returning plain text instead of a tool call

text_response = TextResponse(content="I will search for that information")
result: ValidationResult = validator.validate(text_response)

if result.needs_retry:
    print(f"Retry required: {result.needs_retry}")  # True

    print(f"Nudge kind: {result.nudge.kind}")        # "retry"

    print(f"Instruction: {result.nudge.content}")
    # Output: Your previous response was not a valid tool call...

```

Within a workflow runner, the retry handling appears as:

```python
from forge.core.runner import WorkflowRunner
from forge.guardrails import Guardrails

guardrails = Guardrails(tool_names=["search"], max_retries=3)

while not completion:
    response = await inference.generate(prompt)
    validation = guardrails.check(response)
    
    if validation.needs_retry:
        # Record retry increments the ErrorTracker counter

        prompt = guardrails.record_retry(validation.nudge)
        continue
        
    # Success path resets the retry counter

    await process_tool_calls(validation.tool_calls)
    prompt = guardrails.record_success()

```

## Summary

- **ResponseValidator** in [`src/forge/guardrails/response_validator.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/guardrails/response_validator.py) detects validation failures when rescue parsing fails to extract a valid tool call from text responses.
- **Retry nudges** originate from [`src/forge/prompts/nudges.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/prompts/nudges.py) and are returned via `ValidationResult` with `needs_retry=True` and `nudge.kind="retry"`.
- **ErrorTracker** in [`src/forge/guardrails/error_tracker.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/guardrails/error_tracker.py) counts consecutive retries via `record_retry()` and resets on successful validation.
- **WorkflowRunner** orchestrates the loop, checking guardrails after each inference and either continuing the loop with the nudge appended or exiting on success.
- The retry mechanism ensures that invalid **TextResponse** objects automatically trigger corrective instructions without manual intervention, while enforcing configured retry limits to prevent infinite loops.

## Frequently Asked Questions

### What is the difference between a retry nudge and a step nudge?

A **retry nudge** specifically addresses validation failures where the model output format is incorrect—typically when a **TextResponse** is received but a **ToolCall** was expected. A **step nudge**, handled by the **StepEnforcer**, prevents the model from prematurely terminating the workflow with a terminal tool call before completing required intermediate steps. While retry nudges increment the **ErrorTracker** counter, step nudges do not affect retry statistics.

### How does the retry counter reset after a successful validation?

The **WorkflowRunner** calls the **ErrorTracker**'s reset method (implicitly via `guardrails.record_success()`) immediately after `ResponseValidator.validate()` returns a result with `needs_retry=False`. This occurs when the model finally outputs a properly formatted **ToolCall** that passes both schema validation and step enforcement checks, ensuring that subsequent failures in the same session start with a zero retry count.

### Where can I customize the retry nudge message?

The retry nudge message template resides in [`src/forge/prompts/nudges.py`](https://github.com/antoinezambelli/forge/blob/main/src/forge/prompts/nudges.py) within the **retry_nudge** function or constant. By modifying this template, you can alter the instruction text that gets injected into the conversation history when validation fails. Changes here affect all workflows using the default guardrail configuration.

### What happens when the maximum number of retries is exceeded?

When the **ErrorTracker**'s internal counter reaches the `max_retries` threshold configured in the **Guardrails** facade, the **WorkflowRunner** exits the retry loop and propagates the validation failure to the caller. Rather than generating another nudge, the system raises an exception or returns a final error result, allowing upstream code to implement fallback logic or terminate the workflow gracefully.