Skip to content

[BUG] Nested agent-as-tool interrupts don't resume across rehydration (stateless / distributed execution) #3076

Description

@seanalbert

Checks

  • I have updated to the lastest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched ./issues and there are no duplicates of my issue

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

  1. Create a sub-agent with a tool guarded by a BeforeToolCallEvent hook that raises an interrupt (event.interrupt(...)) for human confirmation.
  2. Expose it on an orchestrator via orchestrator = Agent(tools=[sub_agent.as_tool(preserve_context=True)]).
  3. Turn 1: invoke the orchestrator so the model calls the sub-agent tool, which triggers the interrupt. Confirm result.stop_reason == "interrupt".
  4. 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.
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1area-hilHuman in the loop and suspend/resumearea-multiagentMulti-agent relatedbugSomething isn't workingbug-validatedValiated reproducible code snippetspythonPull requests that update python code

    Type

    Fields

    Language

    Python

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions