How Forge Retry Logic Handles Validation Failures with Nudges

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. 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, 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, 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 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 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 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:

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:

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 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 and are returned via ValidationResult with needs_retry=True and nudge.kind="retry".
  • ErrorTracker in 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →