Skip to content

fix(graph): prevent reset_executor_state from corrupting MultiAgentBase state#1988

Open
giulio-leone wants to merge 2 commits intostrands-agents:mainfrom
giulio-leone:fix/graphnode-reset-executor-state
Open

fix(graph): prevent reset_executor_state from corrupting MultiAgentBase state#1988
giulio-leone wants to merge 2 commits intostrands-agents:mainfrom
giulio-leone:fix/graphnode-reset-executor-state

Conversation

@giulio-leone
Copy link
Copy Markdown
Contributor

Summary

Fixes #1775GraphNode.reset_executor_state() corrupts MultiAgentBase executor state by overwriting GraphState with AgentState.

Root Cause

reset_executor_state() checks hasattr(self.executor, 'state') without verifying the state type:

# Before (broken)
if hasattr(self.executor, 'state'):
    self.executor.state = AgentState(self._initial_state.get())  # Overwrites GraphState!

__post_init__ already had the correct guard (hasattr(self.executor.state, 'get')), but reset_executor_state() was missing it.

Impact

Two call sites affected:

Call site Trigger
_execute_node reset_on_revisit=True with nested graph in a cycle
deserialize_state Always — iterates all nodes when restoring a completed run

Fix

Mirror the __post_init__ guard:

# After (fixed)
if hasattr(self.executor, 'state') and hasattr(self.executor.state, 'get'):
    self.executor.state = AgentState(self._initial_state.get())

Test

Added test_reset_executor_state_preserves_graph_state_for_nested_graph — creates a GraphNode with a nested Graph executor and verifies reset_executor_state() preserves the GraphState type.

All 49 graph tests pass.

⚠️ This reopens #1896 which was accidentally closed due to fork deletion.

giulio-leone and others added 2 commits March 21, 2026 03:31
…se state

GraphNode.reset_executor_state() checked hasattr(self.executor, 'state')
but did not verify the state type before overwriting it with AgentState.
When the executor is a MultiAgentBase (e.g. a nested Graph), its state
is a GraphState, and overwriting it with AgentState corrupts the
executor.

The __post_init__ method already had the correct guard:
  hasattr(self.executor.state, 'get')
but reset_executor_state() was missing it.

This affected two call sites:
- _execute_node (when reset_on_revisit is enabled with nested graphs)
- deserialize_state (unconditionally resets all nodes on completed runs)

Fixes strands-agents#1775
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mkmeral
Copy link
Copy Markdown
Contributor

mkmeral commented Apr 14, 2026

/strands review

@github-actions
Copy link
Copy Markdown

Assessment: Approve

Clean, well-scoped bugfix that correctly mirrors the existing __post_init__ guard into reset_executor_state(). The root cause analysis in the PR description is excellent, and the regression test covers the exact scenario that was broken.

Review Details
  • Correctness: The fix is sound — GraphState (a dataclass) lacks a get() method while AgentState (JSONSerializableDict) has one, so the hasattr(self.executor.state, "get") guard correctly prevents the type corruption.
  • Robustness: The duck-typing guard (hasattr(..., "get")) is fragile by nature — if GraphState ever gains a get() method, this breaks silently. An isinstance check or executor-type check would be more future-proof. That said, this is a pre-existing pattern, and this PR correctly keeps the two call sites in sync. See inline comment for a suggestion to improve both sites together in a follow-up.
  • Testing: The new test is focused and verifies the exact regression scenario.

Nice fix — minimal and well-targeted. 👍

self.executor.messages = copy.deepcopy(self._initial_messages)

if hasattr(self.executor, "state"):
if hasattr(self.executor, "state") and hasattr(self.executor.state, "get"):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (non-blocking): The hasattr(self.executor.state, "get") guard relies on duck typing to distinguish AgentState from GraphState. This works today because GraphState is a dataclass without a get() method, but it would silently break if GraphState ever gains one.

A more explicit guard — such as isinstance(self.executor.state, AgentState) or not isinstance(self.executor, MultiAgentBase) — would make the intent clearer and be more resilient to future changes.

Since __post_init__ uses the same pattern, this could be addressed as a follow-up to improve both sites together.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this actually, can we update the condition? This opens the door for a possible bug later on

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] GraphNode.reset_executor_state() corrupts MultiAgentBase executor state

2 participants