From 4cdc2d201b455ce7adacf830c0915641abc957a3 Mon Sep 17 00:00:00 2001 From: Harsh-617 Date: Fri, 6 Mar 2026 04:09:44 +0800 Subject: [PATCH 1/2] fix: prevent in-place mutation of current_plan.tool_calls in ReWOOReasoning --- mesa_llm/reasoning/rewoo.py | 5 ++- tests/test_reasoning/test_rewoo.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/mesa_llm/reasoning/rewoo.py b/mesa_llm/reasoning/rewoo.py index e3abe87..01e3eac 100644 --- a/mesa_llm/reasoning/rewoo.py +++ b/mesa_llm/reasoning/rewoo.py @@ -1,3 +1,4 @@ +import copy from typing import TYPE_CHECKING from mesa_llm.reasoning.reasoning import ( @@ -115,7 +116,7 @@ def plan( ) self.remaining_tool_calls -= 1 tool_call = [self.current_plan.tool_calls[index_of_tool]] - current_plan = self.current_plan + current_plan = copy.copy(self.current_plan) current_plan.tool_calls = tool_call return Plan(llm_plan=current_plan, step=self.current_obs.step, ttl=ttl) @@ -175,7 +176,7 @@ async def aplan( ) self.remaining_tool_calls -= 1 tool_call = [self.current_plan.tool_calls[index_of_tool]] - current_plan = self.current_plan + current_plan = copy.copy(self.current_plan) current_plan.tool_calls = tool_call return Plan(llm_plan=current_plan, step=self.current_obs.step, ttl=ttl) diff --git a/tests/test_reasoning/test_rewoo.py b/tests/test_reasoning/test_rewoo.py index a4d7d76..8e3747d 100644 --- a/tests/test_reasoning/test_rewoo.py +++ b/tests/test_reasoning/test_rewoo.py @@ -576,6 +576,65 @@ def test_remaining_tool_calls_decrement(self): assert result3.llm_plan.tool_calls == [mock_tool_3] # index 2 (3-1=2) + def test_sequential_replay_dispatches_distinct_tools(self): + """Regression: plan() replay must dispatch A→B→C, not A→A→A. + + Before the fix, `current_plan = self.current_plan` was an alias, so + `current_plan.tool_calls = [tool_a]` mutated self.current_plan.tool_calls + in-place. On step 2, len became 1 and index -1 (Python wrap) returned + tool_a again. This test sets current_plan ONCE and never resets it. + """ + mock_agent = Mock() + mock_agent.step_prompt = None + + tool_a = Mock(name="tool_a") + tool_b = Mock(name="tool_b") + tool_c = Mock(name="tool_c") + + mock_plan = Mock() + mock_plan.tool_calls = [tool_a, tool_b, tool_c] + + reasoning = ReWOOReasoning(mock_agent) + reasoning.current_plan = mock_plan + reasoning.current_obs = Observation(step=1, self_state={}, local_state={}) + reasoning.remaining_tool_calls = 3 + + result1 = reasoning.plan() + result2 = reasoning.plan() + result3 = reasoning.plan() + + assert result1.llm_plan.tool_calls == [tool_a], "Step 1 should dispatch tool A" + assert result2.llm_plan.tool_calls == [tool_b], "Step 2 should dispatch tool B, not A" + assert result3.llm_plan.tool_calls == [tool_c], "Step 3 should dispatch tool C, not A" + assert reasoning.remaining_tool_calls == 0 + + def test_sequential_replay_dispatches_distinct_tools_async(self): + """Async regression: aplan() replay must dispatch A→B→C, not A→A→A.""" + mock_agent = Mock() + mock_agent.step_prompt = None + + tool_a = Mock(name="tool_a") + tool_b = Mock(name="tool_b") + tool_c = Mock(name="tool_c") + + mock_plan = Mock() + mock_plan.tool_calls = [tool_a, tool_b, tool_c] + + reasoning = ReWOOReasoning(mock_agent) + reasoning.current_plan = mock_plan + reasoning.current_obs = Observation(step=1, self_state={}, local_state={}) + reasoning.remaining_tool_calls = 3 + + result1 = asyncio.run(reasoning.aplan()) + result2 = asyncio.run(reasoning.aplan()) + result3 = asyncio.run(reasoning.aplan()) + + assert result1.llm_plan.tool_calls == [tool_a], "Step 1 should dispatch tool A" + assert result2.llm_plan.tool_calls == [tool_b], "Step 2 should dispatch tool B, not A" + assert result3.llm_plan.tool_calls == [tool_c], "Step 3 should dispatch tool C, not A" + assert reasoning.remaining_tool_calls == 0 + + class TestReWOOSignatureConsistency: def test_plan_accepts_obs_kwarg(self): """plan() must accept obs= keyword without raising TypeError.""" From e37c7ab5dad627382a18ee1ad56583eb4239f788 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:10:55 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_reasoning/test_rewoo.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/test_reasoning/test_rewoo.py b/tests/test_reasoning/test_rewoo.py index 8e3747d..bec9788 100644 --- a/tests/test_reasoning/test_rewoo.py +++ b/tests/test_reasoning/test_rewoo.py @@ -575,7 +575,6 @@ def test_remaining_tool_calls_decrement(self): assert reasoning.remaining_tool_calls == 0 assert result3.llm_plan.tool_calls == [mock_tool_3] # index 2 (3-1=2) - def test_sequential_replay_dispatches_distinct_tools(self): """Regression: plan() replay must dispatch A→B→C, not A→A→A. @@ -604,8 +603,12 @@ def test_sequential_replay_dispatches_distinct_tools(self): result3 = reasoning.plan() assert result1.llm_plan.tool_calls == [tool_a], "Step 1 should dispatch tool A" - assert result2.llm_plan.tool_calls == [tool_b], "Step 2 should dispatch tool B, not A" - assert result3.llm_plan.tool_calls == [tool_c], "Step 3 should dispatch tool C, not A" + assert result2.llm_plan.tool_calls == [tool_b], ( + "Step 2 should dispatch tool B, not A" + ) + assert result3.llm_plan.tool_calls == [tool_c], ( + "Step 3 should dispatch tool C, not A" + ) assert reasoning.remaining_tool_calls == 0 def test_sequential_replay_dispatches_distinct_tools_async(self): @@ -630,8 +633,12 @@ def test_sequential_replay_dispatches_distinct_tools_async(self): result3 = asyncio.run(reasoning.aplan()) assert result1.llm_plan.tool_calls == [tool_a], "Step 1 should dispatch tool A" - assert result2.llm_plan.tool_calls == [tool_b], "Step 2 should dispatch tool B, not A" - assert result3.llm_plan.tool_calls == [tool_c], "Step 3 should dispatch tool C, not A" + assert result2.llm_plan.tool_calls == [tool_b], ( + "Step 2 should dispatch tool B, not A" + ) + assert result3.llm_plan.tool_calls == [tool_c], ( + "Step 3 should dispatch tool C, not A" + ) assert reasoning.remaining_tool_calls == 0