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):
- Loop: call
model(messages), then append the returnedactionas{"role": "assistant", "action": action}. - If
action["type"] == "final", returnaction["content"]. - Otherwise it is a
tool_call. Look uptools[name]and call it withaction["args"].- On success, append
{"role": "tool", "tool": name, "status": "ok", "content": result}and continue. - On failure (the tool raises, or
nameis not intools), increment that tool’s failure count and append{"role": "tool", "tool": name, "status": "error", "content": f"{type(exc).__name__}: {exc}"}.
- On success, append
- After appending the error, if that tool’s failure count has reached
max_attempts_per_tool, raiseRuntimeError. 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
modelstub only — never call a real API, network, ortime.sleep. messagesis mutated in place; both assistant actions and tool results are appended in the order they occur.- Catch tool failures with
except Exception(notBaseException), so aKeyboardInterruptstill 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.