diff --git a/src/strands/multiagent/graph.py b/src/strands/multiagent/graph.py index 966d2a0b3..1873d171a 100644 --- a/src/strands/multiagent/graph.py +++ b/src/strands/multiagent/graph.py @@ -184,11 +184,15 @@ def reset_executor_state(self) -> None: This is useful when nodes are executed multiple times and need to start fresh on each execution, providing stateless behavior. + + For MultiAgentBase executors (e.g. nested Graph or Swarm), the state is a + GraphState/SwarmState dataclass — not an AgentState dict — so we skip the + state reset to avoid corrupting it with an incompatible type. """ if hasattr(self.executor, "messages"): self.executor.messages = copy.deepcopy(self._initial_messages) - if hasattr(self.executor, "state"): + if hasattr(self.executor, "state") and not isinstance(self.executor, MultiAgentBase): self.executor.state = AgentState(self._initial_state.get()) # Reset execution status diff --git a/tests/strands/multiagent/test_graph.py b/tests/strands/multiagent/test_graph.py index 8158bf4b1..94c3908b7 100644 --- a/tests/strands/multiagent/test_graph.py +++ b/tests/strands/multiagent/test_graph.py @@ -738,8 +738,14 @@ async def test_node_reset_executor_state(): # Also modify execution status and result node.execution_status = Status.COMPLETED + agent_result = AgentResult( + message={"role": "assistant", "content": [{"text": "test"}]}, + stop_reason="end_turn", + state={}, + metrics=None, + ) node.result = NodeResult( - result="test result", + result=agent_result, execution_time=100, status=Status.COMPLETED, accumulated_usage={"inputTokens": 10, "outputTokens": 20, "totalTokens": 30}, @@ -793,6 +799,49 @@ async def test_node_reset_executor_state(): assert multi_agent_node.result is None +def test_reset_executor_state_preserves_multiagent_state_type(): + """Test that reset_executor_state does not overwrite MultiAgentBase.state with AgentState. + + Regression test for https://github.com/strands-agents/sdk-python/issues/1775. + GraphNode.reset_executor_state() was unconditionally assigning AgentState(...) to + executor.state, corrupting the GraphState dataclass on nested Graph/Swarm executors. + """ + multi_agent = create_mock_multi_agent("nested_graph") + # Simulate a MultiAgentBase that has a GraphState (like a nested Graph) + original_state = GraphState(task="original task") + multi_agent.state = original_state + + node = GraphNode("nested_node", multi_agent) + + # Modify execution status as if the node had run + node.execution_status = Status.COMPLETED + agent_result = AgentResult( + message={"role": "assistant", "content": [{"text": "test"}]}, + stop_reason="end_turn", + state={}, + metrics=None, + ) + node.result = NodeResult( + result=agent_result, + execution_time=100, + status=Status.COMPLETED, + accumulated_usage={}, + accumulated_metrics={}, + execution_count=1, + ) + + # Reset should NOT corrupt the state type + node.reset_executor_state() + + # The state must still be a GraphState, not an AgentState dict + assert isinstance(multi_agent.state, GraphState), ( + f"Expected GraphState but got {type(multi_agent.state).__name__}; " + "reset_executor_state must not overwrite MultiAgentBase state with AgentState" + ) + assert node.execution_status == Status.PENDING + assert node.result is None + + def test_graph_dataclasses_and_enums(): """Test dataclass initialization, properties, and enum behavior.""" # Test Status enum