retry makes too few attempts and hides the error

Done

/home/devdi/devswarm/evals/tasks/retry_attempts/repo · 2026-11-09 16:04:59 UTC · took 40.8s

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

Usage by agent

Cost per agent
planner: $0.0010 planner planner, Cost: $0.0010 $0.0010 coder: $0.0016 coder coder, Cost: $0.0016 $0.0016 tester: $0.0016 tester tester, Cost: $0.0016 $0.0016 reviewer: $0.0009 reviewer reviewer, Cost: $0.0009 $0.0009
Tokens per agent
  • Cache read
  • Cache write
  • Input
  • Output
planner: 7,550 planner planner, Cache read: 3,877 planner, Cache write: 2,270 planner, Output: 1,395 7,550 coder: 17.8K coder coder, Cache read: 10.9K coder, Cache write: 5,160 coder, Output: 1,698 17.8K tester: 17.4K tester tester, Cache read: 9,742 tester, Cache write: 6,286 tester, Output: 1,405 17.4K reviewer: 5,235 reviewer reviewer, Cache write: 4,648 reviewer, Output: 583 5,235
View as table
AgentCostCache readCache writeInputOutput
planner$0.00103,8772,27081,395
coder$0.001610,9055,160101,698
tester$0.00169,7426,28681,405
reviewer$0.000904,6484583

Timeline

  1. planner iteration 1 Finished 3 calls · 7,550 tokens · $0.0010 · 12.0s

    Loading…

  2. coder iteration 1 Finished 4 calls · 17.8K tokens · $0.0016 · 12.1s

    Loading…

  3. tester iteration 1 Finished 3 calls · 17.4K tokens · $0.0016 · 10.7s

    Loading…

  4. verify iteration 1 Passed 0.5s
    ..............                                                           [100%]
    14 passed in 0.02s
    
  5. reviewer iteration 1 Finished 1 calls · 5,235 tokens · $0.0009 · 4.6s

    Loading…

Final diff

diff --git a/retrying.py b/retrying.py
index a49ffae..e4e6447 100644
--- a/retrying.py
+++ b/retrying.py
@@ -1,7 +1,10 @@
 def retry(fn, attempts: int = 3):
-    for _ in range(attempts - 1):
+    if attempts < 1:
+        raise ValueError("attempts must be at least 1")
+    last_exc = None
+    for _ in range(attempts):
         try:
             return fn()
-        except Exception:
-            pass
-    return None
+        except Exception as e:
+            last_exc = e
+    raise last_exc
diff --git a/test_retrying.py b/test_retrying.py
index 409b641..5fae042 100644
--- a/test_retrying.py
+++ b/test_retrying.py
@@ -1,5 +1,118 @@
+import pytest
+
 from retrying import retry
 
 
 def test_success_first_time():
     assert retry(lambda: 5) == 5
+
+
+@pytest.mark.parametrize("n", [1, 2, 3, 5])
+def test_succeeds_on_nth_attempt(n):
+    calls = {"count": 0}
+
+    def fn():
+        calls["count"] += 1
+        if calls["count"] < n:
+            raise ValueError("fail")
+        return "ok"
+
+    assert retry(fn, attempts=n) == "ok"
+    assert calls["count"] == n
+
+
+def test_succeeds_on_last_attempt_after_failures():
+    attempts = 4
+    calls = {"count": 0}
+
+    def fn():
+        calls["count"] += 1
+        if calls["count"] < attempts:
+            raise RuntimeError("boom")
+        return 42
+
+    assert retry(fn, attempts=attempts) == 42
+    assert calls["count"] == attempts
+
+
+def test_always_fails_calls_exactly_attempts_times_and_raises_last():
+    calls = {"count": 0}
+
+    def fn():
+        calls["count"] += 1
+        raise ValueError(f"n{calls['count']}")
+
+    with pytest.raises(ValueError, match="^n3$"):
+        retry(fn, attempts=3)
+    assert calls["count"] == 3
+
+
+def test_stops_after_first_success():
+    calls = {"count": 0}
+
+    def fn():
+        calls["count"] += 1
+        return "done"
+
+    assert retry(fn, attempts=5) == "done"
+    assert calls["count"] == 1
+
+
+@pytest.mark.parametrize("attempts", [0, -1])
+def test_attempts_less_than_one_raises_value_error(attempts):
+    with pytest.raises(ValueError):
+        retry(lambda: 5, attempts=attempts)
+
+
+def test_raises_exact_instance_from_last_attempt():
+    calls = {"count": 0}
+    excs = []
+
+    def fn():
+        calls["count"] += 1
+        exc = KeyError(calls["count"])
+        excs.append(exc)
+        raise exc
+
+    with pytest.raises(KeyError) as info:
+        retry(fn, attempts=4)
+    assert calls["count"] == 4
+    assert info.value is excs[-1]
+
+
+def test_last_exception_type_wins_over_earlier_types():
+    errors = [ValueError("first"), TypeError("second"), OSError("third")]
+    calls = {"count": 0}
+
+    def fn():
+        exc = errors[calls["count"]]
+        calls["count"] += 1
+        raise exc
+
+    with pytest.raises(OSError, match="^third$"):
+        retry(fn, attempts=3)
+    assert calls["count"] == 3
+
+
+def test_base_exception_propagates_immediately():
+    calls = {"count": 0}
+
+    def fn():
+        calls["count"] += 1
+        raise KeyboardInterrupt()
+
+    with pytest.raises(KeyboardInterrupt):
+        retry(fn, attempts=5)
+    assert calls["count"] == 1
+
+
+def test_attempts_one_calls_once_and_raises():
+    calls = {"count": 0}
+
+    def fn():
+        calls["count"] += 1
+        raise ValueError("only")
+
+    with pytest.raises(ValueError, match="^only$"):
+        retry(fn, attempts=1)
+    assert calls["count"] == 1