Repository navigation
fix(interrupt): restore a pending tool execution only for an activated state, and validate it (Python) - #5654
Open
strandly-the-agent wants to merge 1 commit into
Conversation
…d state, and validate it Port of strands-agents#5628 (TS). _InterruptState.from_dict drops a pending tool execution when the state is not activated and raises SessionException naming the field when the restored data is malformed. Well-formed state the event loop could not replay (no tool uses, a result for an unknown tool use) is dropped with a warning instead of failing the session load. Closes strands-agents#5651
1 task done
zastrowm
approved these changes
Oct 29, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by: no human requester — automated run (daily parity sync, window 2026-10-28 UTC)
Human Overview
Description
Python port of the restore-path half of #5628.
_InterruptState.from_dictnow restorespending_tool_execution(typed, or the legacycontext.tool_use_messagemigration) only when the state isactivated, and validates what it restores: the message must be a dict with acontentlist androle: assistant;completed_tool_resultsmust be a list of dicts withstatusand acontentlist. Malformed data raisesSessionExceptionwith the TSfield=<...> | ...messages (snake_case paths). The replay gate itself (event_loop.pychecksactivated and pending_tool_execution is not None) already existed; #5628 added the Python test for it.One deliberate deviation from TS, forced by an independent review of this branch: a well-formed execution the loop cannot replay (no tool uses, or a result for an unknown tool use) is dropped with a
logger.warninginstead of raising. See the decision log.Related Issues
Closes #5651. Source: #5628.
Documentation PR
None needed: #5628's
site/edits are shared prose already onmain.Type of Change
Bug fix
Testing
tests/strands/test_interrupt.py(matrix below); existing fixtures gained atoolUseblock because an emptycontentis now invalid input (it matches what_stop_for_interruptsactually persists, cf.test_event_loop.py:2016).pytestover interrupt/event_loop/session/multiagent/background_tasks/snapshot/agent_as_tool/subagent: 982 passed;ruff check,ruff format --check,mypy: clean (verbatim below).hatch run prepare(hatch unavailable in the sandbox; ran ruff + mypy + pytest directly)Additional Details
Traceability matrix (TS #5628 `interrupt.test.ts` → Python)
tests/strands/test_interrupt.py)fromJSONround-trip with the sharedpendingToolExecutionfixturetest_interrupt_state_from_dict(:87),test_interrupt_state_pending_tool_execution_round_trip_is_independent..._drops_pending_tool_execution_when_not_activated(:256), paramsNone/_PENDING_TOOL_EXECUTIONit.each: a non-assistant message..._rejects_invalid_pending_tool_execution[non-assistant message](:280)it.each: an unparseable message[unparseable message](:285)it.each: non-object completed tool results[non-list completed tool results](:290) — list in Pythonit.each: an unparseable completed tool result[completed tool result without status](:295),[completed tool result without content list](:300),[non-dict completed tool result](:305)it.each: a message without tool uses..._drops_unreplayable_pending_tool_execution[message without tool uses](:322) — drops + warns instead of raising (decision log)it.each: a completed tool result for an unknown tool use[completed tool result for an unknown tool use](:341) — drops + warns (decision log)cause..._wraps_invalid_pending_tool_execution(:227) covers thePendingToolExecution(**data)path only; the content-not-a-list branch raises without__cause__(nothing to chain: Python has no message parser)getPendingExecutionreturns undefined when not activatedevent_loop.py:263/:894, tested bytest_event_loop_cycle_ignores_pending_tool_execution_when_interrupt_state_not_activated(added by #5628)getPendingExecutionreturns the reconstructed execution when activatedagent.interrupt.test.ts(agent-level gating,activate()before yieldingInterruptEvent)_stop_for_interruptsbefore the stop event[tool use without a toolUseId](:333),..._accepts_state_written_for_a_partial_tool_batch(:357),..._validates_legacy_tool_context(:372)Gate output (verbatim)
Selection:
tests/strands/test_interrupt.py tests/strands/event_loop tests/strands/session tests/strands/multiagent tests/strands/background_tasks tests/strands/agent/test_snapshot.py tests/strands/agent/test_agent_as_tool.py tests/strands/vended_tools/test_subagent.py --ignore=tests/strands/multiagent/a2a(a2a extra not installed in the sandbox).tests/strands/agent/test_agent.py::test_baggage_attributes_stamped_on_agent_spanfails onmainin this sandbox too (missingopentelemetry.processor), unrelated.Structural map and decision log
InterruptState.fromJSONgatedata.activated && data.pendingToolExecutionfrom_dict:if activated and pending_tool_execution_data is not None(interrupt.py:~222)validatePendingToolExecution+parseRestoredData_restore_pending_tool_execution(interrupt.py:~238)SessionErrorSessionException(lazy import, as before)completedToolResultsobject keyed by idcompleted_tool_resultslist ofToolResultdicts (existing Python shape)FileSessionManager, that the SDK itself can persist a pending execution with no tool uses:models/openai.pymapsfinish_reason == "tool_calls"tostopReason: tool_usewithout requiring atoolUseblock, and aBeforeToolsEventinterrupt then stores the text-only message. A faithful raise made such a session fail on every load with no recovery. Dropping degrades to "no replay, call the model again" because the loop already guards onpending_tool_execution is not None. Structural problems (non-dict message, non-list content/results, non-assistant role, result withoutstatus/content) still raise, as in TS. TS has the same write path (agent.ts_executeToolsstoresassistantMessage.toJSON()unconditionally), so this may be worth mirroring there — flagged as an open question.context.tool_use_messagepath is gated and validated too. TS has no legacy path. The legacy pair was written by the same_stop_for_interruptswith the same invariants, and before29dfce6b9the replay gate was literallyactivated and "tool_use_message" in context, so gating restores the original semantics.Message.fromMessageData. Python messages are TypedDicts with no runtime parser; unknown extra blocks are tolerated, only the fields the replay path reads are checked._agent_tool_streamer.pyandbackground_tasks/_manager.pycallfrom_dictonly to read.interrupts. Left unchanged: with (1) they can only raise on genuinely corrupt data, andfrom_dictapplies the retained-response filter they rely on.TypeErrormessage was re-shaped to the housefield=<pending_tool_execution> | ...form; its test updated.New dependencies / capabilities: none. Sensitive surface:
Agent(..., session_manager=...),Agent.load_snapshot, Graph/Swarm session restore and_AgentToolStreamercan now raiseSessionExceptionon malformed pending state (TS already does); a non-activated pending execution is no longer restored (it was never replayed).Open questions
_stop_for_interrupts/_executeToolsguard on empty tool uses)?_agent_tool_streamer.pending_interrupt_idsread ids directly rather than viafrom_dict; not done here for the reason in (4).Validation: an independent validator pass (test matrix, gates, 28 adversarial shapes — two
TypeErrorleaks found and fixed) and an independent reviewer pass (BLOCKER on decision 1, fixed; nits applied). Evidence uploaded to the portal under issue #5651.Reviewer: @zastrowm — rule 2 (org member who approved the source PR). Rule 1 did not apply: the #5628 author
opieter-awsis not a public strands-agents member as visible to this token (GET /orgs/strands-agents/members/opieter-aws→ 404, not inpublic_members).Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.