This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Fourth entry, and the fuzzer that found entry 3 is still paying out. Same failure family, different corner of the language: valid Python the sandbox refuses to run, with an error that lies about why.
The one-liner every developer writes
best, *rest = scores
Splitting a list into "the first one" and "the rest" is about as ordinary as Python gets. It is PEP 3132, shipped in 2008. Under smolagents' sandbox it fails with:
InterpreterError: Cannot unpack tuple of wrong size
There is no wrong size. scores has four items and the pattern accepts any length of two or more. The message describes a problem that does not exist, so the agent does the only thing a faithful agent can do with a message that is already a lie: it retries the identical, valid code.
And it is not just the starred form. All of these are standard Python and all of them were broken:
first, *rest = "hello" # Cannot unpack tuple of wrong size
a, b = "hi" # Cannot unpack non-tuple value
[a, b] = [1, 2] # silently assigns nothing at all
Why it happens
smolagents runs model-generated code in its own AST interpreter, and assignment targets go through one function, set_value. It handled exactly one shape: a fixed-size ast.Tuple.
elif isinstance(target, ast.Tuple):
if not isinstance(value, tuple):
if hasattr(value, "__iter__") and not isinstance(value, (str, bytes)):
value = tuple(value)
else:
raise InterpreterError("Cannot unpack non-tuple value")
if len(target.elts) != len(value):
raise InterpreterError("Cannot unpack tuple of wrong size")
...
Three separate holes hide in those few lines:
-
Starred targets.
a, *bhas two target elements but the value has three items, solen(target.elts) != len(value)fires. The*marker is never even looked at. -
Strings and bytes. They are explicitly excluded from the iterable path, so
a, b = "hi"is rejected even though CPython unpacks strings happily. -
List-pattern targets.
[a, b] = ...is anast.List, not anast.Tuple, so it matches no branch, falls through, and silently assigns nothing. No error, no values, the worst kind of quiet.
The Sentry angle: count the retries
Same lesson as the last two entries, and Sentry keeps making it visible. The error is handled: the agent catches it and feeds it back to the model as guidance. But the guidance is wrong, so the model cannot act on it, so it loops. One bug, one misleading message, three identical failures burning three steps:
Three events on one issue is the retry loop made countable. Without it you would see a slow run, not a stuck one. Sentry's Seer read the same event and landed on the exact cause:
smolagents' custom Python interpreter does not support starred unpacking (e.g.
best, *rest = scores), treating it as a fixed-size tuple unpack.set_valuecheckslen(target.elts) != len(value)and raises, without handlingast.Starredtargets.
The fix
Rewrite the branch to match CPython instead of guessing:
elif isinstance(target, (ast.Tuple, ast.List)):
elts = target.elts
starred = [i for i, e in enumerate(elts) if isinstance(e, ast.Starred)]
if len(starred) > 1:
raise InterpreterError("multiple starred expressions in assignment")
if not hasattr(value, "__iter__"):
raise InterpreterError(f"cannot unpack non-iterable {type(value).__name__} object")
values = list(value)
if starred:
i = starred[0]
n_after = len(elts) - i - 1
if len(values) < i + n_after:
raise InterpreterError(
f"not enough values to unpack (expected at least {i + n_after}, got {len(values)})"
)
split = len(values) - n_after
# assign head, then the starred target gets the middle as a list, then the tail
...
else:
if len(values) < len(elts):
raise InterpreterError(f"not enough values to unpack (expected {len(elts)}, got {len(values)})")
if len(values) > len(elts):
raise InterpreterError(f"too many values to unpack (expected {len(elts)})")
...
Any iterable now unpacks, a single starred target absorbs the surplus into a list in any position (a, *b, *a, b, a, *b, c), list-pattern targets work, and the size errors read exactly like CPython's, so when the model genuinely does pass the wrong number of values it gets an actionable message instead of a dead end.
After
app: step 1 ok, output = (90, [82, 71, 65])
One step. No loop. best is 90, rest is the tail, the way the model expected all along.
Numbers
- 4 valid unpacking forms fixed: starred targets, string unpacking, list-pattern targets, and the CPython error messages
- Reproduced on current
mainand 1.26.0; 3 wasted agent steps per occurrence, visible only because Sentry counts events - 14 new tests plus one existing test updated to the improved message
- 411 passing, ruff clean
Links
- Issue: https://github.com/huggingface/smolagents/issues/2555
- PR: https://github.com/huggingface/smolagents/pull/2556
The through-line across all four entries has not changed: the dangerous agent bugs are not the loud crashes, they are the polite, well-handled, wrong messages that let the model fail on repeat. Fuzz the sandbox with ordinary valid code, and count your events.

Top comments (7)
I still don't understand the point, nor what's being said here, nor the problem, why the hell even write a,
*b = list, man, it ruins the readability of code, it's the same as starting to writea%6&2!8=@5. in my humble opinion, of courseFair, and to be clear the post isn't arguing you should write a, *b = list, that's a style call and you're free to dislike it. The point is different: the model writes it (it's standard Python, valid since 2008), and the sandbox rejected that valid code with an error blaming a "wrong size" that wasn't real, so the agent retried the same line three times. The bug is a sandbox breaking valid syntax and then lying about why, not the syntax itself. Readability is a separate debate.
Why even bother using any models that run Python? Is it really a shame to waste a couple of megabytes on your computer? I don't know how this model works, but it seems like reinventing the wheel to me.
Yeah, a sandbox is great for security... But... It's kind of weird. I'd use a VM, or since I'm working in a browser, or whatever, more importantly, I'd try compiling the interpreter to WASM, which is certainly very difficult, but less crazy than making my own. If I'm missing something, please correct me
The useful distinction here is not just that the sandbox rejected valid Python, but that it produced an explanation an agent would trust. A wrong error message is worse than a raw failure because it turns the retry loop into a confident waste of time.
That is exactly the kind of bug where trace data matters: how many identical retries happened, what code was reissued, and which error text kept steering the agent back into the same wall.
Did your fix work at the parser layer, or did you also add coverage specifically for retry-shaping error messages?
That retry loop is the expensive part. A deterministic interpreter given byte-identical input returns a byte-identical error, so the second and third attempts can't converge either, and from inside the loop the agent gets no signal that they won't. We hash the code block plus the error string on every tool retry and stop on a repeat, instead of counting attempts. That cut our retry spend 40% in the first week. Your Sentry screenshot is the same thing made countable: three events on one issue is three agent steps buying nothing.
I think I need to track (tool_name, normalized_args, error_type) in my tool loop to prevent same error from happening multiple times.Thanks
That's the move. (tool_name, normalized_args, error_type) is exactly the right key. Normalizing the args is the part people skip, and it's what makes it catch a real loop instead of only exact-string repeats. Escalate after it repeats N times (replan, ask, or fail fast) and you turn a silent budget drain into a signal.