InterviewPrepKit

Home / Coding / Agent Coding / Reliability & Retries / Reflect and Retry on Tool Failure

Reflect and Retry on Tool Failure

hard 00:00
Solving tips
  • The reflect trick is just feeding the failure back into the conversation as a tool result, so the model's NEXT turn sees the error text and can fix its arguments.
  • Budget per tool name, not globally: a run that touches three tools should tolerate a stumble from each without one flaky tool exhausting everyone's retries.
  • Append the error BEFORE you decide to give up — the transcript should always record why the loop stopped, even on the fatal attempt.

A single tool call failing is not the end of a run. A well-built agent treats the error the way a human would: read what went wrong, then try again with better arguments. The mechanism is deceptively simple — you take the exception, turn it into a tool result message, and hand the whole transcript back to the model. On its next turn the model sees the error and can correct itself. This is the “reflect and retry” loop, and it is what separates an agent that recovers from a bad JSON argument or a divide-by-zero from one that crashes on the first hiccup.

The catch is knowing when to stop. If a tool is genuinely broken (or the model keeps making the same mistake), reflecting forever just burns tokens. So each tool gets a failure budget: reflect up to max_attempts_per_tool times, then give up.

How reflection works

The model does not retry on its own — your loop makes retry possible by writing the error into the conversation:

assistant -> {"type": "tool_call", "tool": "divide", "args": {"a": 10, "b": 0}}
tool      -> {"status": "error", "content": "ZeroDivisionError: division by zero"}
assistant -> {"type": "tool_call", "tool": "divide", "args": {"a": 10, "b": 2}}   # reflected!
tool      -> {"status": "ok", "content": "5.0"}
assistant -> {"type": "final", "content": "The answer is 5.0"}

Because the failed attempt and its error are now part of messages, the model’s second turn has everything it needs to fix the call. Your job is the plumbing: call the model, run the tool, catch the exception, record it as a tool result, and stop once a tool has burned its budget.

Task

Complete reflect_and_retry(model, tools, messages, max_attempts_per_tool):

  1. Loop: call model(messages), then append the returned action as {"role": "assistant", "action": action}.
  2. If action["type"] == "final", return action["content"].
  3. Otherwise it is a tool_call. Look up tools[name] and call it with action["args"].
    • On success, append {"role": "tool", "tool": name, "status": "ok", "content": result} and continue.
    • On failure (the tool raises, or name is not in tools), increment that tool’s failure count and append {"role": "tool", "tool": name, "status": "error", "content": f"{type(exc).__name__}: {exc}"}.
  4. After appending the error, if that tool’s failure count has reached max_attempts_per_tool, raise RuntimeError. Otherwise continue so the model can reflect.

Example

def divide(args):
    return str(args["a"] / args["b"])

tools = {"divide": divide}

script = iter([
    {"type": "tool_call", "tool": "divide", "args": {"a": 10, "b": 0}},  # fails
    {"type": "tool_call", "tool": "divide", "args": {"a": 10, "b": 2}},  # reflected fix
    {"type": "final", "content": "The answer is 5.0"},
])
def model(messages):
    return next(script)

messages = [{"role": "user", "content": "What is 10 divided by 2?"}]
out = reflect_and_retry(model, tools, messages, max_attempts_per_tool=3)
# out == "The answer is 5.0"

# The error was recorded so the model could reflect:
errors = [m for m in messages if m.get("status") == "error"]
# len(errors) == 1
# errors[0]["content"] == "ZeroDivisionError: division by zero"

Constraints

  • The model is the passed-in model stub only — never call a real API, network, or time.sleep.
  • messages is mutated in place; both assistant actions and tool results are appended in the order they occur.
  • Catch tool failures with except Exception (not BaseException), so a KeyboardInterrupt still aborts the loop.
  • An unknown tool name is a failure too: it counts against that name’s budget and is recorded as an error result.
  • The failure budget is per distinct tool name, counted cumulatively across the whole run — not consecutive, and not shared between tools.
  • Always append the error result before raising, so the final transcript explains why the loop stopped.

Write your solution, then hit Run tests to check it — or get a mock grade from the AI coach.

The coach remembers this session — revise your code and ask again, and it grades your progress. It gives hints, not the answer.
Report a bug