From bb81d17d1b268831dcb7674ee41a4989f8773088 Mon Sep 17 00:00:00 2001 From: Fernando Celmer Date: Sat, 15 Aug 2026 14:01:05 -0300 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=AA=B2=20BUG-#18:=20Reset=20provider?= =?UTF-8?q?=20fallback=20index=20at=20the=20start=20of=20each=20run()=20ca?= =?UTF-8?q?ll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pycodeloop/core/agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycodeloop/core/agent.py b/pycodeloop/core/agent.py index c039492..1754929 100644 --- a/pycodeloop/core/agent.py +++ b/pycodeloop/core/agent.py @@ -359,6 +359,8 @@ def run( or `cancel_event` is set — checked at each turn boundary and before every tool call, never mid-request, so an in-flight provider call always finishes first.""" + self._provider_index = 0 + self.provider = self._provider_chain[0] session = session or Session(system_prompt=self.system_prompt) session.add_user(prompt, images=images) self._notify_message() From 442d79f8be7baa552bd71ab7e5b15b85e8445fdd Mon Sep 17 00:00:00 2001 From: Fernando Celmer Date: Sat, 15 Aug 2026 14:01:05 -0300 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9D=A4=EF=B8=8F=20TEST-#18:=20Verify=20a?= =?UTF-8?q?=20new=20run()=20retries=20the=20primary=20after=20a=20prior=20?= =?UTF-8?q?fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/core/test_agent.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/core/test_agent.py b/tests/core/test_agent.py index b2b0307..f2a27ca 100644 --- a/tests/core/test_agent.py +++ b/tests/core/test_agent.py @@ -614,6 +614,34 @@ def test_raises_when_every_provider_in_the_chain_fails(self): self.assertEqual(primary.calls, 4) self.assertEqual(secondary.calls, 4) + def test_a_new_run_call_retries_the_primary_even_after_a_prior_fallback( + self, + ): + """Issue #18: falling back within one run() must not be permanent + across the agent's lifetime — a later run() call (a new user + message) should give the primary another chance instead of + staying stuck on the fallback forever.""" + primary = FlakyProvider(fail_times=4) + secondary = FlakyProvider(fail_times=0) + agent = Agent( + provider=primary, + fallback_providers=[secondary], + tools=[], + ) + + first = agent.run("hi") + self.assertEqual(first, "ok") + self.assertIs(agent.provider, secondary) + self.assertEqual(primary.calls, 4) + self.assertEqual(secondary.calls, 1) + + second = agent.run("hi again") + + self.assertEqual(second, "ok") + self.assertIs(agent.provider, primary) + self.assertEqual(primary.calls, 5) + self.assertEqual(secondary.calls, 1) + def test_second_turn_after_fallback_does_not_retry_the_dead_primary(self): """Regression: rebuilding `[self.provider, *self.fallback_providers]` fresh on every `_complete` call put the now-active fallback in the