# How the @contract Decorator Retry Mechanism Works in SymbolicAI

> Discover how SymbolicAI's @contract decorator retries LLM outputs up to 8 times with exponential back-off and remedy LLMs, ensuring robust Pydantic schema validation.

- Repository: [ExtensityAI/symbolicai](https://github.com/extensityai/symbolicai)
- Tags: internals
- Published: 2026-03-01

---

**The `@contract` decorator in SymbolicAI automatically retries failed LLM outputs up to 8 times by default, using exponential back-off and a remedy LLM to fix validation errors against Pydantic schemas before optionally failing gracefully.**

The `extensityai/symbolicai` library implements Design-by-Contract principles for neurosymbolic programming. When you apply the `@contract` decorator to a class, it wraps the `forward` method with a sophisticated retry mechanism that validates JSON outputs against declared `LLMDataModel` schemas. If validation fails, the system does not immediately raise an error; instead, it enters a retry loop that invokes a remedy LLM to correct the output, making the retry mechanism with the `@contract` decorator a critical resilience feature for production LLM applications.

## Where Retry Parameters Are Configured

The default retry behavior lives in [`symai/strategy.py`](https://github.com/extensityai/symbolicai/blob/main/symai/strategy.py) within the `contract` class definition. The decorator maintains a class-level dictionary that specifies how many attempts to make and how long to wait between them:

```python

# symai/strategy.py – default retry defaults

_default_retry_params: ClassVar[dict[str, int | float | bool]] = {
    "tries": 8,
    "delay": 0.015,
    "backoff": 1.25,
    "jitter": 0.0,
    "max_delay": 0.25,
    "graceful": False,
}

```

*Lines 37-44* define these defaults, which are merged with any user-supplied `remedy_retry_params` during decorator instantiation. The merged configuration is stored in `self.remedy_retry_params` and passed to the internal `TypeValidationFunction` instance:

```python
self.f_type_validation_remedy = TypeValidationFunction(
    accumulate_errors=accumulate_errors,
    verbose=verbose,
    retry_params=remedy_retry_params,
)

```

This initialization occurs at *lines 20-24* of the `contract.__init__` method, ensuring that every wrapped class carries its own retry policy.

## The Core Retry Loop in TypeValidationFunction

The actual retry logic resides in `TypeValidationFunction._run_validation_attempts` inside [`symai/strategy.py`](https://github.com/extensityai/symbolicai/blob/main/symai/strategy.py). This method implements a "try-catch-repeat" pattern that attempts validation up to `tries + 1` times (the initial attempt plus the specified retries):

```python
total_attempts = self.retry_params["tries"] + 1
for attempt in range(total_attempts):
    # try to parse the JSON into the output model

    result = self.output_data_model.model_validate_json(json_str, ...)
    # optional semantic checks …

    break          # success → exit loop

except Exception as error:
    # failure → invoke remedy handling

    json_str = self._handle_failed_validation_attempt(...)

```

*Lines 85-100* contain this loop structure. On the first iteration, the system attempts to validate the zero-shot LLM output. If `model_validate_json` raises an exception—indicating malformed JSON or schema violations—the flow jumps to the exception handler, which triggers the remedy pipeline and schedules a retry.

## Exponential Back-Off and Jitter Implementation

Between failed attempts, the decorator applies intelligent back-off to avoid overwhelming the LLM API. The `_pause` method at *lines 28-36* calculates the sleep duration using exponential back-off with optional jitter and a hard ceiling:

```python
def _pause(self, attempt):
    base = self.retry_params["delay"] * (self.retry_params["backoff"] ** attempt)
    jit = (
        np.random.uniform(*self.retry_params["jitter"])
        if isinstance(self.retry_params["jitter"], tuple)
        else self.retry_params["jitter"]
    )
    _delay = min(base + jit, self.retry_params["max_delay"])
    time.sleep(_delay)

```

This implementation multiplies the base `delay` by `backoff` raised to the power of the current attempt number, adds random jitter if configured (accepting either a float or a tuple for range specification), and caps the result at `max_delay`. For example, with the defaults (`delay=0.015`, `backoff=1.25`), the delays between attempts progress roughly as 0.015s, 0.019s, 0.024s, up to the 0.25s maximum.

## Remedy LLM Invocation

When validation fails, `_handle_failed_validation_attempt` (referenced at *lines 58-68*) constructs a remedy prompt that explains the validation errors to a secondary LLM call. This "remedy function" attempts to repair the malformed JSON:

```python
context = self.remedy_prompt(prompt=prompt, output=json_str,
                            errors="\n".join(errors) if self.accumulate_errors else error_str)
self.remedy_function.clear()
self.remedy_function.adapt(context)
json_str = self.remedy_function(seed=remedy_seeds[attempt_index], **kwargs).value

```

The `remedy_function` is a pre-configured `Function` instance from [`symai/components.py`](https://github.com/extensityai/symbolicai/blob/main/symai/components.py) that calls the active LLM engine. It receives the original output, the specific validation errors, and optionally accumulated error history (if `accumulate_errors=True`). The remedied JSON string then feeds back into the next iteration of the validation loop.

## Graceful Failure Handling

If all retry attempts exhaust without successful validation, `_handle_validation_failure` at *lines 18-24* determines the final behavior based on the `graceful` parameter:

```python
if self.retry_params["graceful"]:
    return
raise TypeValidationError(...)

```

When `graceful=True`, the decorator silently returns `None` rather than raising a `TypeValidationError`, allowing applications to degrade gracefully. When `graceful=False` (the default), the system raises a comprehensive error containing the last validation failure details.

## Practical Example with Custom Retry Settings

You can override the default retry policy by passing a custom `remedy_retry_params` dictionary to the decorator. This example configures 4 attempts with aggressive back-off and random jitter:

```python
from symai import contract, LLMDataModel, field

class AnswerModel(LLMDataModel):
    answer: str = field(..., description="The answer to the question")

@contract(
    pre_remedy=True,
    post_remedy=True,
    remedy_retry_params={
        "tries": 4,
        "delay": 0.1,
        "backoff": 2.0,
        "jitter": (0.0, 0.05),
        "max_delay": 2.0,
        "graceful": False,
    },
    verbose=True,
)
class QA:
    def __init__(self, engine):
        self.engine = engine

    def forward(self, input: AnswerModel) -> AnswerModel:
        return self.engine.run_prompt(f"Answer the question: {input.question}")

qa = QA(engine=my_neurosymbolic_engine)
result = qa.forward(AnswerModel(question="What is 2+2?"))

```

In this configuration:
1. The decorator creates a `TypeValidationFunction` with `tries=4`, meaning 5 total validation attempts (initial plus 4 retries).
2. The back-off multiplier is `2.0`, creating delays of 0.1s, 0.2s, 0.4s, and 0.8s (capped at 2.0s).
3. Random jitter between 0.0 and 0.05 seconds prevents thundering herd problems.
4. If all attempts fail, the decorator raises `TypeValidationError` because `graceful` is disabled.

## Summary

- **[`symai/strategy.py`](https://github.com/extensityai/symbolicai/blob/main/symai/strategy.py)** houses the `contract` decorator and `TypeValidationFunction`, which orchestrate the retry mechanism.
- **Default parameters** provide 8 retry attempts with 1.25x exponential back-off, starting at 0.015s and capped at 0.25s.
- **The retry loop** validates JSON against Pydantic models; on failure, it pauses using `_pause`, invokes the remedy LLM via `remedy_function`, and re-validates.
- **Graceful degradation** is controlled by the `graceful` boolean flag, which determines whether to raise `TypeValidationError` or return silently after exhaustion.
- **Customization** occurs through the `remedy_retry_params` argument, accepting `tries`, `delay`, `backoff`, `jitter`, `max_delay`, and `graceful` keys.

## Frequently Asked Questions

### How many retry attempts does the @contract decorator perform by default?

By default, the decorator performs **8 retry attempts** plus the initial validation attempt, totaling 9 tries. This is controlled by the `"tries": 8` default in `_default_retry_params` located at *lines 37-44* of [`symai/strategy.py`](https://github.com/extensityai/symbolicai/blob/main/symai/strategy.py). You can override this by passing a custom `remedy_retry_params` dictionary with your desired `"tries"` value.

### What happens to the delay between retry attempts?

The delay grows exponentially according to the formula `delay * (backoff ** attempt)`, clamped at `max_delay`. The `_pause` method at *lines 28-36* implements this calculation and adds optional random jitter. For example, with `delay=0.1` and `backoff=2.0`, the second retry waits 0.2 seconds, the third waits 0.4 seconds, and so on, up to the specified ceiling.

### Can the @contract decorator silently ignore validation failures?

Yes. Setting `"graceful": True` in the `remedy_retry_params` dictionary instructs the `_handle_validation_failure` method to return `None` instead of raising `TypeValidationError` when all retries exhaust. This is useful for applications that must continue execution even when LLM outputs fail schema validation, though it requires careful handling of `None` returns in downstream logic.

### Does the remedy LLM use the same engine as the primary forward call?

The remedy LLM uses the currently registered engine through the `remedy_function` instance, which is a standard `Function` object from [`symai/components.py`](https://github.com/extensityai/symbolicai/blob/main/symai/components.py). It inherits the active backend configuration from [`symai/backend/settings.py`](https://github.com/extensityai/symbolicai/blob/main/symai/backend/settings.py). While it typically uses the same engine as the primary `forward` method, the decorator clears and re-adapts the function context specifically for the remedy prompt, ensuring the correction request is handled independently of the original conversation state.