retry makes too few attempts and hides the error

Done

/home/devdi/devswarm/evals/tasks/retry_attempts/repo · 2026-11-09 16:05:41 UTC · took 38.5s

  1. Plan (done)
  2. Code (done)
  3. Test (done)
  4. Review (done)
  5. Finish (done)
Cost
$0.0037
Tests
Passed
Iterations
1
Re-plans
0
LLM calls
10
Prompt read from cache
80%

Usage by agent

Cost per agent
planner: $0.0007 planner planner, Cost: $0.0007 $0.0007 coder: $0.0016 coder coder, Cost: $0.0016 $0.0016 tester: $0.0014 tester tester, Cost: $0.0014 $0.0014
Tokens per agent
  • Cache read
  • Cache write
  • Input
  • Output
planner: 7,375 planner planner, Cache read: 5,545 planner, Cache write: 614 planner, Output: 1,208 7,375 coder: 18.8K coder coder, Cache read: 13.4K coder, Cache write: 3,305 coder, Output: 2,086 18.8K tester: 16.7K tester tester, Cache read: 11.3K tester, Cache write: 3,827 tester, Output: 1,624 16.7K
View as table
AgentCostCache readCache writeInputOutput
planner$0.00075,54561481,208
coder$0.001613,3863,305102,086
tester$0.001411,2903,82781,624

Timeline

  1. planner iteration 1 Finished 3 calls · 7,375 tokens · $0.0007 · 9.6s

    Loading…

  2. coder iteration 1 Finished 4 calls · 18.8K tokens · $0.0016 · 15.0s

    Loading…

  3. tester iteration 1 Finished 3 calls · 16.7K tokens · $0.0014 · 12.8s

    Loading…

  4. verify iteration 1 Passed 0.5s
    .........                                                                [100%]
    9 passed in 0.03s
    

Final diff

diff --git a/retrying.py b/retrying.py
index a49ffae..483f943 100644
--- a/retrying.py
+++ b/retrying.py
@@ -1,7 +1,14 @@
 def retry(fn, attempts: int = 3):
-    for _ in range(attempts - 1):
+    """Call ``fn`` up to ``attempts`` times and return its first successful result.
+
+    If every attempt raises an ``Exception``, the exception from the last
+    attempt is re-raised. ``attempts`` values below 1 are treated as 1, so
+    ``fn`` is always called at least once. No delay is applied between attempts.
+    """
+    last_exc = None
+    for _ in range(max(1, attempts)):
         try:
             return fn()
-        except Exception:
-            pass
-    return None
+        except Exception as exc:
+            last_exc = exc
+    raise last_exc
diff --git a/test_retrying.py b/test_retrying.py
index 409b641..238130c 100644
--- a/test_retrying.py
+++ b/test_retrying.py
@@ -1,5 +1,106 @@
+import pytest
+
 from retrying import retry
 
 
 def test_success_first_time():
     assert retry(lambda: 5) == 5
+
+
+def test_succeeds_after_failures():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        if len(calls) < 3:
+            raise ValueError("boom")
+        return "ok"
+
+    assert retry(fn, attempts=3) == "ok"
+    assert len(calls) == 3
+
+
+def test_always_failing_called_attempts_times_and_raises_last():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        raise RuntimeError(f"attempt {len(calls)}")
+
+    with pytest.raises(RuntimeError, match="attempt 3"):
+        retry(fn, attempts=3)
+    assert len(calls) == 3
+
+
+def test_success_first_time_calls_once():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        return 42
+
+    assert retry(fn, attempts=3) == 42
+    assert len(calls) == 1
+
+
+def test_stops_calling_after_success_on_attempt_k():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        if len(calls) < 2:
+            raise ValueError("first")
+        return "done"
+
+    assert retry(fn, attempts=5) == "done"
+    assert len(calls) == 2
+
+
+def test_raises_exception_object_from_last_attempt():
+    excs = []
+
+    def fn():
+        exc = KeyError(len(excs))
+        excs.append(exc)
+        raise exc
+
+    with pytest.raises(KeyError) as info:
+        retry(fn, attempts=4)
+    assert len(excs) == 4
+    assert info.value is excs[-1]
+    assert info.value is not excs[0]
+
+
+def test_attempts_zero_runs_once():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        raise ValueError("nope")
+
+    with pytest.raises(ValueError):
+        retry(fn, attempts=0)
+    assert len(calls) == 1
+
+
+def test_negative_attempts_runs_once():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        return "x"
+
+    assert retry(fn, attempts=-2) == "x"
+    assert len(calls) == 1
+
+
+def test_base_exception_propagates_immediately():
+    calls = []
+
+    def fn():
+        calls.append(1)
+        raise KeyboardInterrupt()
+
+    with pytest.raises(KeyboardInterrupt):
+        retry(fn, attempts=3)
+    assert len(calls) == 1