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() 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