⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/strands/multiagent/swarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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

Expand Down
95 changes: 95 additions & 0 deletions tests/strands/multiagent/test_swarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1347,3 +1347,98 @@ 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()