Checks
SDK Language
Python
Strands Version
1.41.0
Language Runtime Version
python 12
Operating System
macOS 26.5.1
Installation Method
pip
Steps to Reproduce
- Create a sub-agent with a tool guarded by a
BeforeToolCallEvent hook that raises an interrupt (event.interrupt(...)) for human confirmation.
- Expose it on an orchestrator via
orchestrator = Agent(tools=[sub_agent.as_tool(preserve_context=True)]).
- Turn 1: invoke the orchestrator so the model calls the sub-agent tool, which triggers the interrupt. Confirm
result.stop_reason == "interrupt".
- Simulate a restart: discard the orchestrator and sub-agent objects and rebuild both fresh from the session store (new process / new instances), exactly as a stateless handler would on the next request.
- Turn 2: resume by invoking the orchestrator with the interrupt response:
orchestrator([{"interruptResponse": {"interruptId": <id>, "response": "APPROVE"}}]).
Minimal illustration of the "restart":
# Turn 1 (process A)
orchestrator = build_orchestrator(session_id="s1") # sub-agent wrapped via .as_tool()
r1 = orchestrator("do the thing that needs confirmation")
assert r1.stop_reason == "interrupt"
interrupt_id = next(iter(orchestrator._interrupt_state.interrupts))
# ---- process A dies ----
# Turn 2 (process B): everything rebuilt from storage
orchestrator = build_orchestrator(session_id="s1") # fresh objects, restored from session
r2 = orchestrator([{"interruptResponse": {"interruptId": interrupt_id, "response": "APPROVE"}}])
Expected Behavior
On Turn 2, the sub-agent resumes the original pending tool call (same toolUseId), applies the human's response, executes the confirmed tool once, and the orchestrator completes (r2.stop_reason == "end_turn").
Actual Behavior
The sub-agent does not receive the response. It restarts its turn from scratch, the model emits a new toolUseId, the BeforeToolCallEvent hook recomputes a new interrupt id, and the confirmation is requested again. r2.stop_reason is "interrupt" again — an infinite re-prompt loop; the confirmed tool never runs.
Additional Context
Nested resume relied on shared object identity:
The tool executor registers the sub-agent's live Interrupt object into the orchestrator's _interrupt_state by reference (setdefault).
_InterruptState.resume() sets .response on the orchestrator's copy, while _AgentAsTool._build_interrupt_responses() reads .response off the sub-agent's copy.
In one process these are the same object, so it "just works." After rehydration they are two independent objects with the same id, so the response written to the orchestrator's copy is invisible to the sub-agent's copy → _build_interrupt_responses() returns an empty list → the sub-agent re-interrupts.
This is compounded by BeforeToolCallEvent._interrupt_id being derived from the random toolUseId, so a restarted sub-agent can't even produce a matching id once it regenerates the tool call.
Possible Solution
#3008
Related Issues
No response
Checks
SDK Language
Python
Strands Version
1.41.0
Language Runtime Version
python 12
Operating System
macOS 26.5.1
Installation Method
pip
Steps to Reproduce
BeforeToolCallEventhook that raises an interrupt (event.interrupt(...)) for human confirmation.orchestrator = Agent(tools=[sub_agent.as_tool(preserve_context=True)]).result.stop_reason == "interrupt".orchestrator([{"interruptResponse": {"interruptId": <id>, "response": "APPROVE"}}]).Minimal illustration of the "restart":
Expected Behavior
On Turn 2, the sub-agent resumes the original pending tool call (same toolUseId), applies the human's response, executes the confirmed tool once, and the orchestrator completes (r2.stop_reason == "end_turn").
Actual Behavior
The sub-agent does not receive the response. It restarts its turn from scratch, the model emits a new toolUseId, the BeforeToolCallEvent hook recomputes a new interrupt id, and the confirmation is requested again. r2.stop_reason is "interrupt" again — an infinite re-prompt loop; the confirmed tool never runs.
Additional Context
Nested resume relied on shared object identity:
The tool executor registers the sub-agent's live Interrupt object into the orchestrator's _interrupt_state by reference (setdefault).
_InterruptState.resume() sets .response on the orchestrator's copy, while _AgentAsTool._build_interrupt_responses() reads .response off the sub-agent's copy.
In one process these are the same object, so it "just works." After rehydration they are two independent objects with the same id, so the response written to the orchestrator's copy is invisible to the sub-agent's copy → _build_interrupt_responses() returns an empty list → the sub-agent re-interrupts.
This is compounded by BeforeToolCallEvent._interrupt_id being derived from the random toolUseId, so a restarted sub-agent can't even produce a matching id once it regenerates the tool call.
Possible Solution
#3008
Related Issues
No response