Skip to content

Commit 2e4c82b

Browse files
giulio-leonegiulio-leone
andauthored
fix(graph): only evaluate outbound edges from completed nodes (#1846)
Co-authored-by: giulio-leone <giulio.leone@users.noreply.github.com>
1 parent 39c8c19 commit 2e4c82b

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

src/strands/multiagent/graph.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -827,9 +827,16 @@ async def _handle_node_timeout(self, node: GraphNode, event_queue: asyncio.Queue
827827
return timeout_exception
828828

829829
def _find_newly_ready_nodes(self, completed_batch: list["GraphNode"]) -> list["GraphNode"]:
830-
"""Find nodes that became ready after the last execution."""
830+
"""Find nodes that became ready after the last execution.
831+
832+
Only evaluates destination nodes of outbound edges from the completed batch,
833+
instead of iterating over all nodes in the graph.
834+
"""
835+
# Collect unique candidate nodes reachable from the completed batch
836+
candidates = {edge.to_node for edge in self.edges if edge.from_node in completed_batch}
837+
831838
newly_ready = []
832-
for _node_id, node in self.nodes.items():
839+
for node in candidates:
833840
if self._is_node_ready_with_conditions(node, completed_batch):
834841
newly_ready.append(node)
835842
return newly_ready

tests/strands/multiagent/test_graph.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,3 +2405,38 @@ async def stream_async(self, prompt=None, **kwargs):
24052405
assert result.completed_nodes == 2
24062406
assert "custom_node" in result.results
24072407
assert "regular_node" in result.results
2408+
2409+
2410+
def test_find_newly_ready_nodes_only_evaluates_outbound_edges():
2411+
"""Verify _find_newly_ready_nodes only checks destinations of outbound edges from completed batch.
2412+
2413+
Previously, it iterated over ALL nodes, which could cause nodes to fire
2414+
before their actual dependencies completed.
2415+
2416+
See: https://github.com/strands-agents/sdk-python/issues/685
2417+
"""
2418+
# Build a graph: A -> B -> C, D -> E (independent chain)
2419+
node_a = GraphNode(node_id="A", executor=create_mock_agent("A"))
2420+
node_b = GraphNode(node_id="B", executor=create_mock_agent("B"))
2421+
node_c = GraphNode(node_id="C", executor=create_mock_agent("C"))
2422+
node_d = GraphNode(node_id="D", executor=create_mock_agent("D"))
2423+
node_e = GraphNode(node_id="E", executor=create_mock_agent("E"))
2424+
2425+
graph = Graph.__new__(Graph)
2426+
graph.nodes = {"A": node_a, "B": node_b, "C": node_c, "D": node_d, "E": node_e}
2427+
graph.edges = [
2428+
GraphEdge(from_node=node_a, to_node=node_b),
2429+
GraphEdge(from_node=node_b, to_node=node_c),
2430+
GraphEdge(from_node=node_d, to_node=node_e),
2431+
]
2432+
graph.state = GraphState()
2433+
2434+
# When A completes, only B should be ready (not E)
2435+
ready = graph._find_newly_ready_nodes([node_a])
2436+
ready_ids = {n.node_id for n in ready}
2437+
assert ready_ids == {"B"}, f"Expected only B, got {ready_ids}"
2438+
2439+
# When D completes, only E should be ready (not B or C)
2440+
ready = graph._find_newly_ready_nodes([node_d])
2441+
ready_ids = {n.node_id for n in ready}
2442+
assert ready_ids == {"E"}, f"Expected only E, got {ready_ids}"

0 commit comments

Comments
 (0)