Dissecting AI
From the paper: Agent-Reflex: Teaching Language Agents to Act Reflectively

Mining Environment-Level Recovery Skills

Tool failures make agents repeat bad moves; Agent-Reflex grounds failures in observed tool errors and contrasts same-task successes with failures to extract reusable, environment-specific recovery rules.

Read the paper ↗
The short version

What it is

  • Observed tool failures Collected tool errors supply realistic failure messages. Each tool response independently has a configured chance of being replaced by a compatible error.
  • Same-task contrasts Different models attempt tasks in noisy environments. Successful and failed attempts are labeled, then paired within the same task—not assigned those labels just because an attempt was clean or noisy.
  • Environment recovery rules An analyzer turns recurring failure patterns into corrective rules tied to an environment's tools and objects, rather than generic advice to retry.

Why it matters

A timeout, missing object, and permission error need not call for the same response. Contrasting verified successes and failures helps extract recovery guidance suited to the environment, although discovering a rule is separate from teaching an agent to use it.

The short version is that Agent-Reflex collects realistic tool errors, uses them to perturb executable environments, and compares attempts that succeed with attempts that fail on the same task. It turns recurring differences into environment-specific recovery rules. The important distinction is that adding noise creates a testing condition; it does not determine whether an attempt counts as a success or a failure.

  1. Realistic failures: Consider a tool returning “not found” for a file identifier. Repeating the identical request is a different recovery strategy from checking the identifier or searching for the file. Observed error returns supply failure families such as missing resources, permission errors, and timeouts, so the injected problems resemble actual tool failures rather than arbitrary text corruption.

  2. Meaningful comparison: Imagine two attempts to complete the same file-related task: one succeeds and the other keeps using a wrong identifier. That shared task makes the behavioral difference interpretable. Attempts come from a capability-diverse model pool, and their outcomes are jointly labeled by an analyzer and an environment verifier. A noisy attempt can succeed; a failed attempt is not simply whichever run received injected noise.

  3. Reusable, local knowledge: A rule about checking identifiers is more useful when it can refer to the environment's actual tools and business objects. The extracted rule connects a recurring bad pattern to a corrective action and lives alongside that environment's tool descriptions and observed patterns. Think of it as a local troubleshooting guide, not a universal recovery algorithm.

So how does an error enter the interaction? Each tool-return position independently gets a chance of replacement, controlled by the injection probability. If replacement is selected, the returned message is drawn from observed failures compatible with that tool, with sampling balanced across failure families. The knob controls replacement opportunities, not a guarantee that every attempt fails. Nor does replacing a response itself specify that the underlying tool operation was undone. That leaves the central question: what did the agent actually do after seeing the response?

To answer it, the method gathers attempts under the noisy sampling protocol before building pairs. It keeps agreed success and error labels, records the failed attempt's diagnosis and first erroneous turn, and then matches successes to failures on the same task. Pair counts are capped per task, and each environment must contribute pairs from at least five distinct tasks. I find this distinction essential: a clean/noisy comparison would mix up the testing condition with the outcome. But what can a shared task tell us beyond one isolated mistake?

The analyzer looks across these contrasts for recurring patterns and converts them into recovery rules. Those rules can be reused across tasks within that environment; they do not automatically transfer to unfamiliar tools. The remaining question is whether an acting agent will use the guidance appropriately—that belongs to the later training stages, not skill discovery itself.

Read the deep dive

Why People Wanted It

A tool-using agent can get stuck on an unavailable service, a wrong identifier, or an unchecked write. More reasoning is not necessarily the missing ingredient: the agent may need to recognize the failure and change its next action.

The problem is that clean interactions do not expose these recovery decisions. But adding errors alone is not enough either. An injected error describes what the agent encountered, not whether the agent ultimately failed. So the real question was never just how to make tools unreliable. It was how to turn observed differences between successful and failed behavior into reusable recovery guidance.

The Core Move

Perturb executable environments with observed tool failures, sample attempts in those noisy environments, and contrast jointly labeled successes and failures on the same task to extract environment-specific recovery rules.

The noise has an explicit control: each tool-return position is independently selected for replacement with probability pinjp_{\mathrm{inj}}. A selected response is replaced with a compatible observed failure; otherwise the true response is retained. Success and failure labels are assigned afterward, not inferred from whether noise was injected.

Quiz
How does Agent-Reflex inject grounded tool failures?
Use the observed error fraction as the task's failure label.
Independently select each tool return for possible replacement by a compatible observed error.
Inject errors only after a rollout has been labeled unsuccessful.
Replace every tool return in any task selected to fail.

Why the Noise Comes from Tool Use

Here is the part that gets technical: failure injection operates on tool returns, using error forms collected from actual tool interactions rather than arbitrary invented messages.

Collected errors are grouped into eight actionable families: unavailable servers, authentication or permission failures, timeouts, missing resources, invalid arguments, execution exceptions, rate limits, and network errors. The renderer selects failures compatible with the invoked tool and balances sampling across families so that rare families receive recovery examples too.

Let rtr_t be the true return from tool ftf_t, and let J(ft,rt)J(f_t,r_t) denote its replacement by a compatible mined failure. The response shown to the agent is:

bt∼Bernoulli⁡(pinj),r~t={J(ft,rt),bt=1,rt,bt=0.b_t \sim \operatorname{Bernoulli}(p_{\mathrm{inj}}),\qquad \widetilde r_t = \begin{cases} J(f_t,r_t), & b_t=1,\\ r_t, & b_t=0. \end{cases}

The draws are independent across tool-return positions. Thus pinjp_{\mathrm{inj}} is a per-return replacement probability, not a per-task failure probability and not the observed fraction of error returns in the mining corpus. Replacing a response also does not, by itself, imply rolling back the tool's state transition.

Environment construction retains clean tasks with verifiable solutions and perturbed tasks with verifiable recovery. This is an eligibility check on the tasks, not a rule that pairs a successful clean rollout with a failed noisy rollout.

Why Success and Failure Must Share a Task

Now the deeper sampling detail: correct and erroneous trajectories are outcome-labeled samples from the noisy ecosystem, not labels attached in advance to clean and perturbed runs.

A capability-diverse pool of nine models spanning five families attempts every task under the same sampling protocol. An analyzer and the environment verifier jointly label each rollout; disagreements are discarded. A failed rollout carries an error diagnosis, its first erroneous turn, and an environment-state snapshot taken before that turn.

Let CqC_q contain the agreed correct trajectories for task qq, and let FqF_q contain the agreed erroneous trajectories and their annotations. Candidate contrasts are drawn from:

Pq={(q,τ+,τ−,e,t⋆,s⋆):τ+∈Cq,  (τ−,e,t⋆,s⋆)∈Fq}.\mathcal P_q = \{(q,\tau^+,\tau^-,e,t^\star,s^\star): \tau^+\in C_q,\; (\tau^-,e,t^\star,s^\star)\in F_q\}.

Here ee is the diagnosis, t⋆t^\star the first erroneous turn, and s⋆s^\star its pre-action state. Pair contributions are capped per task, and each environment draws pairs from at least five distinct tasks. A task without both kinds of labeled attempt cannot supply a contrast.

Sharing a task makes the behavioral comparison interpretable. It does not imply that the attempts share a model, identical injected errors, or identical intermediate states.

Why the Extracted Rule Belongs to an Environment

Here is the deepest layer: the object being extracted is a mapping from a recurring bad pattern to a corrective action, not merely a summary of one failed attempt.

An analyzer examines paired trajectories, identifies recurring patterns, and converts them into executable recovery rules. For example, the illustrated file-and-comment environment connects wrong identifiers and unchecked writes with checking identifiers and verifying writes.

Each environment stores a record ζm=(dm,Tm,Bm,Km)\zeta_m=(d_m,T_m,B_m,K_m): its description and business objects, tools, observed bad patterns, and extracted reflection skills. This context gives a rule concrete objects and actions to refer to. Reuse is therefore across tasks within the environment; applicability to other environments is not automatic.

Minimal Causal Code

This deliberately unoptimized PyTorch sketch separates response injection, outcome labeling, and same-task pairing. The injected-response probability and pair cap are inputs. The harness helpers handle executable tools, joint labeling, and rule extraction; the loop makes the relevant state and dataset updates explicit.

import torch


def rollout(agent, task, env, failure_bank, p_inj, max_turns):
    state = env.reset(task)
    history, trace = [], []
    for _ in range(max_turns):
        action = agent.next_action(task, history)
        if action.is_final:
            trace.append((state, action, None))
            break

        before = env.snapshot(state)
        state, true_return = env.execute(state, action)
        shown_return = true_return
        if torch.rand(()).item() < p_inj:
            shown_return = failure_bank.draw_compatible_balanced(
                action.tool_name
            )
        # Replacing the message does not roll back the tool state.
        trace.append((before, action, shown_return))
        history.append((action, shown_return))
    return trace, state


def mine(env, tasks, model_pool, failure_bank, p_inj,
         max_turns, pair_cap, analyzer, verifier):
    assert 0.0 <= p_inj <= 1.0
    pairs = []
    contributing_tasks = set()

    for task in tasks:  # Already checked for solution and recovery.
        correct, errors = [], []
        for agent in model_pool:
            trace, final_state = rollout(
                agent, task, env, failure_bank, p_inj, max_turns
            )
            # Returns None on disagreement; otherwise an agreed label.
            label = joint_label(
                analyzer, verifier, task, trace, final_state
            )
            if label is None:
                continue
            if label.correct:
                correct.append(trace)
            else:
                errors.append((trace, label.error,
                               label.first_error_turn,
                               label.pre_error_state))

        # Only same-task, outcome-labeled attempts enter this pool.
        candidates = [(task, good, *bad)
                      for good in correct for bad in errors]
        selected = candidates[:pair_cap]  # Illustrative capped selection.
        pairs.extend(selected)
        if selected:
            contributing_tasks.add(task.id)

    if len(contributing_tasks) < 5:
        return None  # Does not meet the environment coverage requirement.
    return analyzer.extract_rules(env.description, env.tools, pairs)

A successful attempt and a failed attempt both come from the noisy sampling process. The code does not treat a recovery-validated noisy attempt as a failed trajectory, and it does not manufacture a contrast when one outcome pool is empty.

What You Gain, and What You Lose

Observed failure forms give the perturbations practical grounding. Same-task contrasts give the analyzer a shared objective against which to compare behavior. Environment-specific rules can refer to actual tools and objects rather than saying only “try again.”

The tradeoff is specificity and dependence on the collection process. Constructed perturbations do not cover every deployment failure, and response replacement does not simulate every possible service-side malfunction. Joint labeling filters disagreement but does not make the remaining diagnoses infallible. Requiring both outcomes also limits which tasks contribute contrasts.

Finally, a recovery library is knowledge, not a guarantee of reflective behavior. Later training stages teach local repair, global recovery, and responses to user corrections; they are separate from mining the rules.

Quiz
Which pairing correctly supplies a contrast for recovery-skill extraction?
Any two attempts for which a recovery verifier confirms success.
An agreed successful attempt and an agreed failed attempt on the same task, sampled in the noisy environment.
A clean attempt labeled successful by default and a noisy attempt labeled failed by default.
A success and a failure from unrelated tasks, provided they use the same model.
Core Takeaway

Inject failures independently at tool-return positions, then label outcomes: recovery rules come from same-task successes and failures, not from assuming clean means success and noisy means failure.