From 69cafa5426a79c48927cafd800d85ea49dde3835 Mon Sep 17 00:00:00 2001 From: Strands Agent <217235299+strands-agent@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:03:21 +0000 Subject: [PATCH 1/3] fix(swarm): accumulate execution_time across interrupt/resume cycles Change execution_time calculation from assignment to accumulation (= to +=) to match the behavior in graph.py. This ensures execution_time reflects the total time across all invocations rather than being reset on resume. Add test to verify execution_time accumulates across interrupt/resume cycles. --- src/strands/multiagent/swarm.py | 2 +- tests/strands/multiagent/test_swarm.py | 50 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/strands/multiagent/swarm.py b/src/strands/multiagent/swarm.py index 6c1149624..82e45a4a0 100644 --- a/src/strands/multiagent/swarm.py +++ b/src/strands/multiagent/swarm.py @@ -406,7 +406,7 @@ async def stream_async( self.state.completion_status = Status.FAILED raise finally: - self.state.execution_time = round((time.time() - self.state.start_time) * 1000) + self.state.execution_time += round((time.time() - self.state.start_time) * 1000) await self.hooks.invoke_callbacks_async(AfterMultiAgentInvocationEvent(self, invocation_state)) self._resume_from_session = False diff --git a/tests/strands/multiagent/test_swarm.py b/tests/strands/multiagent/test_swarm.py index f2abed9f7..f76d62878 100644 --- a/tests/strands/multiagent/test_swarm.py +++ b/tests/strands/multiagent/test_swarm.py @@ -1347,3 +1347,53 @@ def test_swarm_interrupt_on_agent(agenerator): assert tru_status == exp_status agent.stream_async.assert_called_once_with(responses, invocation_state={}) + + +def test_swarm_execution_time_accumulates_across_interrupt_resume(interrupt_hook): + """Test that execution_time accumulates across interrupt/resume cycles. + + This test verifies that the execution_time in SwarmResult is not reset on resume + but instead accumulates across all invocations (initial + resume). + + Related to: https://github.com/strands-agents/sdk-python/issues/1501 + """ + agent = create_mock_agent("test_agent", "Task completed") + swarm = Swarm([agent], hooks=[interrupt_hook]) + + # First invocation - should be interrupted + multiagent_result = swarm("Test task") + + # Store the execution time from first invocation + first_execution_time = multiagent_result.execution_time + + tru_status = multiagent_result.status + exp_status = Status.INTERRUPTED + assert tru_status == exp_status + + # Add a delay before resume to ensure time passes between invocations + time.sleep(0.1) # 100ms delay + + # Resume with interrupt response + interrupt = multiagent_result.interrupts[0] + responses = [ + { + "interruptResponse": { + "interruptId": interrupt.id, + "response": "test_response", + }, + }, + ] + multiagent_result = swarm(responses) + + tru_status = multiagent_result.status + exp_status = Status.COMPLETED + assert tru_status == exp_status + + # The key assertion: execution_time after resume should be >= first_execution_time + # because it should accumulate, not reset. The time.sleep is outside the invocation + # so it doesn't add to execution_time, but we verify the accumulated value is at + # least the first invocation time plus some additional processing time. + assert multiagent_result.execution_time >= first_execution_time, ( + f"execution_time should accumulate: got {multiagent_result.execution_time}ms, " + f"expected >= {first_execution_time}ms (first invocation)" + ) From 85ca92a5d403f26aaf49b51e95bf7fb316d59558 Mon Sep 17 00:00:00 2001 From: Strands Agent <217235299+strands-agent@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:26:30 +0000 Subject: [PATCH 2/3] fix(swarm): include accumulated time in should_continue timeout check Update elapsed time calculation in SwarmState.should_continue to include accumulated execution_time from previous invocations, matching the pattern in GraphState.should_continue. This ensures timeout checks account for total execution time across interrupt/resume cycles. Add test to verify timeout check includes accumulated time. --- src/strands/multiagent/swarm.py | 4 +-- tests/strands/multiagent/test_swarm.py | 45 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/strands/multiagent/swarm.py b/src/strands/multiagent/swarm.py index 82e45a4a0..c67288f45 100644 --- a/src/strands/multiagent/swarm.py +++ b/src/strands/multiagent/swarm.py @@ -198,8 +198,8 @@ def should_continue( if len(self.node_history) >= max_iterations: return False, f"Max iterations reached: {max_iterations}" - # Check timeout - elapsed = time.time() - self.start_time + # Check timeout (include accumulated time from previous invocations) + elapsed = self.execution_time / 1000 + time.time() - self.start_time if elapsed > execution_timeout: return False, f"Execution timed out: {execution_timeout}s" diff --git a/tests/strands/multiagent/test_swarm.py b/tests/strands/multiagent/test_swarm.py index f76d62878..7336ceac8 100644 --- a/tests/strands/multiagent/test_swarm.py +++ b/tests/strands/multiagent/test_swarm.py @@ -1397,3 +1397,48 @@ def test_swarm_execution_time_accumulates_across_interrupt_resume(interrupt_hook f"execution_time should accumulate: got {multiagent_result.execution_time}ms, " f"expected >= {first_execution_time}ms (first invocation)" ) + + +def test_swarm_state_should_continue_elapsed_time_includes_accumulated(): + """Test that should_continue elapsed time includes accumulated execution_time. + + This verifies that timeout checks account for total time across interrupt/resume + cycles, not just the current invocation. + + Related to: https://github.com/strands-agents/sdk-python/issues/1501 + """ + state = SwarmState( + current_node=None, + task="test", + completion_status=Status.EXECUTING, + shared_context=SharedContext(), + ) + + # Simulate previous invocation took 5 seconds (5000ms) + state.execution_time = 5000 + + # Current invocation just started + state.start_time = time.time() + + # With a 6 second timeout, should_continue should return True + # (5s accumulated + ~0s current = ~5s < 6s timeout) + should_continue, reason = state.should_continue( + max_handoffs=100, + max_iterations=100, + execution_timeout=6.0, + repetitive_handoff_detection_window=0, + repetitive_handoff_min_unique_agents=0, + ) + assert should_continue is True, f"Expected to continue, got: {reason}" + + # With a 4 second timeout, should_continue should return False + # (5s accumulated + ~0s current = ~5s > 4s timeout) + should_continue, reason = state.should_continue( + max_handoffs=100, + max_iterations=100, + execution_timeout=4.0, + repetitive_handoff_detection_window=0, + repetitive_handoff_min_unique_agents=0, + ) + assert should_continue is False + assert "timed out" in reason.lower() From b1b5d86b2a073027de5e2ac546a89f43a6e765d6 Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Tue, 20 Jan 2026 12:46:38 -0500 Subject: [PATCH 3/3] clean up --- src/strands/multiagent/swarm.py | 2 +- tests/strands/multiagent/test_swarm.py | 99 ++------------------------ 2 files changed, 5 insertions(+), 96 deletions(-) diff --git a/src/strands/multiagent/swarm.py b/src/strands/multiagent/swarm.py index c67288f45..8368f5936 100644 --- a/src/strands/multiagent/swarm.py +++ b/src/strands/multiagent/swarm.py @@ -198,7 +198,7 @@ def should_continue( if len(self.node_history) >= max_iterations: return False, f"Max iterations reached: {max_iterations}" - # Check timeout (include accumulated time from previous invocations) + # Check timeout elapsed = self.execution_time / 1000 + time.time() - self.start_time if elapsed > execution_timeout: return False, f"Execution timed out: {execution_timeout}s" diff --git a/tests/strands/multiagent/test_swarm.py b/tests/strands/multiagent/test_swarm.py index 7336ceac8..aae11b709 100644 --- a/tests/strands/multiagent/test_swarm.py +++ b/tests/strands/multiagent/test_swarm.py @@ -1243,6 +1243,8 @@ def test_swarm_interrupt_on_before_node_call_event(interrupt_hook): multiagent_result = swarm("Test task") + first_execution_time = multiagent_result.execution_time + tru_status = multiagent_result.status exp_status = Status.INTERRUPTED assert tru_status == exp_status @@ -1279,6 +1281,8 @@ def test_swarm_interrupt_on_before_node_call_event(interrupt_hook): exp_message = "Task completed" assert tru_message == exp_message + assert multiagent_result.execution_time >= first_execution_time + def test_swarm_interrupt_on_agent(agenerator): exp_interrupts = [ @@ -1347,98 +1351,3 @@ def test_swarm_interrupt_on_agent(agenerator): assert tru_status == exp_status agent.stream_async.assert_called_once_with(responses, invocation_state={}) - - -def test_swarm_execution_time_accumulates_across_interrupt_resume(interrupt_hook): - """Test that execution_time accumulates across interrupt/resume cycles. - - This test verifies that the execution_time in SwarmResult is not reset on resume - but instead accumulates across all invocations (initial + resume). - - Related to: https://github.com/strands-agents/sdk-python/issues/1501 - """ - agent = create_mock_agent("test_agent", "Task completed") - swarm = Swarm([agent], hooks=[interrupt_hook]) - - # First invocation - should be interrupted - multiagent_result = swarm("Test task") - - # Store the execution time from first invocation - first_execution_time = multiagent_result.execution_time - - tru_status = multiagent_result.status - exp_status = Status.INTERRUPTED - assert tru_status == exp_status - - # Add a delay before resume to ensure time passes between invocations - time.sleep(0.1) # 100ms delay - - # Resume with interrupt response - interrupt = multiagent_result.interrupts[0] - responses = [ - { - "interruptResponse": { - "interruptId": interrupt.id, - "response": "test_response", - }, - }, - ] - multiagent_result = swarm(responses) - - tru_status = multiagent_result.status - exp_status = Status.COMPLETED - assert tru_status == exp_status - - # The key assertion: execution_time after resume should be >= first_execution_time - # because it should accumulate, not reset. The time.sleep is outside the invocation - # so it doesn't add to execution_time, but we verify the accumulated value is at - # least the first invocation time plus some additional processing time. - assert multiagent_result.execution_time >= first_execution_time, ( - f"execution_time should accumulate: got {multiagent_result.execution_time}ms, " - f"expected >= {first_execution_time}ms (first invocation)" - ) - - -def test_swarm_state_should_continue_elapsed_time_includes_accumulated(): - """Test that should_continue elapsed time includes accumulated execution_time. - - This verifies that timeout checks account for total time across interrupt/resume - cycles, not just the current invocation. - - Related to: https://github.com/strands-agents/sdk-python/issues/1501 - """ - state = SwarmState( - current_node=None, - task="test", - completion_status=Status.EXECUTING, - shared_context=SharedContext(), - ) - - # Simulate previous invocation took 5 seconds (5000ms) - state.execution_time = 5000 - - # Current invocation just started - state.start_time = time.time() - - # With a 6 second timeout, should_continue should return True - # (5s accumulated + ~0s current = ~5s < 6s timeout) - should_continue, reason = state.should_continue( - max_handoffs=100, - max_iterations=100, - execution_timeout=6.0, - repetitive_handoff_detection_window=0, - repetitive_handoff_min_unique_agents=0, - ) - assert should_continue is True, f"Expected to continue, got: {reason}" - - # With a 4 second timeout, should_continue should return False - # (5s accumulated + ~0s current = ~5s > 4s timeout) - should_continue, reason = state.should_continue( - max_handoffs=100, - max_iterations=100, - execution_timeout=4.0, - repetitive_handoff_detection_window=0, - repetitive_handoff_min_unique_agents=0, - ) - assert should_continue is False - assert "timed out" in reason.lower()