Skip to content

Commit 566e5ad

Browse files
fix: typeError when serializing multimodal prompts with binary content in Graph/Swarm session persistence (#1870)
1 parent d03311a commit 566e5ad

File tree

4 files changed

+82
-8
lines changed

4 files changed

+82
-8
lines changed

src/strands/multiagent/graph.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from ..types.content import ContentBlock, Messages
5252
from ..types.event_loop import Metrics, Usage
5353
from ..types.multiagent import MultiAgentInput
54+
from ..types.session import decode_bytes_values, encode_bytes_values
5455
from ..types.traces import AttributeValue
5556
from .base import MultiAgentBase, MultiAgentResult, NodeResult, Status
5657

@@ -1158,7 +1159,7 @@ def serialize_state(self) -> dict[str, Any]:
11581159
"interrupted_nodes": [n.node_id for n in self.state.interrupted_nodes],
11591160
"node_results": {k: v.to_dict() for k, v in (self.state.results or {}).items()},
11601161
"next_nodes_to_execute": next_nodes,
1161-
"current_task": self.state.task,
1162+
"current_task": encode_bytes_values(self.state.task),
11621163
"execution_order": [n.node_id for n in self.state.execution_order],
11631164
"_internal_state": {
11641165
"interrupt_state": self._interrupt_state.to_dict(),
@@ -1248,7 +1249,7 @@ def _from_dict(self, payload: dict[str, Any]) -> None:
12481249
self.state.execution_order = [self.nodes[node_id] for node_id in order_node_ids if node_id in self.nodes]
12491250

12501251
# Task
1251-
self.state.task = payload.get("current_task", self.state.task)
1252+
self.state.task = decode_bytes_values(payload.get("current_task", self.state.task))
12521253

12531254
# next nodes to execute
12541255
next_nodes = [self.nodes[nid] for nid in (payload.get("next_nodes_to_execute") or []) if nid in self.nodes]

src/strands/multiagent/swarm.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from ..types.content import ContentBlock, Messages
5252
from ..types.event_loop import Metrics, Usage
5353
from ..types.multiagent import MultiAgentInput
54+
from ..types.session import decode_bytes_values, encode_bytes_values
5455
from ..types.traces import AttributeValue
5556
from .base import MultiAgentBase, MultiAgentResult, NodeResult, Status
5657

@@ -965,7 +966,7 @@ def serialize_state(self) -> dict[str, Any]:
965966
"node_history": [n.node_id for n in self.state.node_history],
966967
"node_results": {k: v.to_dict() for k, v in self.state.results.items()},
967968
"next_nodes_to_execute": next_nodes,
968-
"current_task": self.state.task,
969+
"current_task": encode_bytes_values(self.state.task),
969970
"context": {
970971
"shared_context": getattr(self.state.shared_context, "context", {}) or {},
971972
"handoff_node": self.state.handoff_node.node_id if self.state.handoff_node else None,
@@ -1028,7 +1029,7 @@ def _from_dict(self, payload: dict[str, Any]) -> None:
10281029
logger.exception("Failed to hydrate NodeResult for node_id=%s; skipping.", node_id)
10291030
raise
10301031
self.state.results = results
1031-
self.state.task = payload.get("current_task", self.state.task)
1032+
self.state.task = decode_bytes_values(payload.get("current_task", self.state.task))
10321033

10331034
next_node_ids = payload.get("next_nodes_to_execute") or []
10341035
if next_node_ids:

tests/strands/multiagent/test_graph.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,7 +1986,10 @@ async def stream_without_result(*args, **kwargs):
19861986

19871987
@pytest.mark.asyncio
19881988
async def test_graph_persisted(mock_strands_tracer, mock_use_span):
1989-
"""Test graph persistence functionality."""
1989+
"""Test graph persistence functionality with multimodal input containing binary bytes."""
1990+
import base64
1991+
import json
1992+
19901993
# Create mock session manager
19911994
session_manager = Mock(spec=FileSessionManager)
19921995
session_manager.read_multi_agent().return_value = None
@@ -2011,7 +2014,40 @@ async def test_graph_persisted(mock_strands_tracer, mock_use_span):
20112014
assert "completed_nodes" in state
20122015
assert "node_results" in state
20132016

2014-
# Test apply_state_from_dict with persisted state
2017+
# Build a multimodal prompt with inline binary PDF bytes (the problematic case)
2018+
pdf_bytes = b"%PDF-1.4 binary content"
2019+
multimodal_task = [
2020+
{"text": "Analyze this PDF"},
2021+
{
2022+
"document": {
2023+
"format": "pdf",
2024+
"name": "document.pdf",
2025+
"source": {
2026+
"bytes": pdf_bytes,
2027+
},
2028+
}
2029+
},
2030+
]
2031+
2032+
# Simulate graph having executed with a multimodal task
2033+
graph.state.task = multimodal_task
2034+
2035+
# serialize_state must not raise TypeError for bytes
2036+
serialized = graph.serialize_state()
2037+
assert json.dumps(serialized) # must be JSON-serializable
2038+
2039+
# The bytes should be encoded in the serialized form
2040+
encoded_bytes = serialized["current_task"][1]["document"]["source"]["bytes"]
2041+
assert encoded_bytes == {"__bytes_encoded__": True, "data": base64.b64encode(pdf_bytes).decode()}
2042+
2043+
# deserialize_state must restore bytes back to original
2044+
serialized["next_nodes_to_execute"] = ["test_node"]
2045+
serialized["status"] = "executing"
2046+
graph.deserialize_state(serialized)
2047+
restored_bytes = graph.state.task[1]["document"]["source"]["bytes"]
2048+
assert restored_bytes == pdf_bytes
2049+
2050+
# Test apply_state_from_dict with plain string persisted state (backward compat)
20152051
persisted_state = {
20162052
"status": "executing",
20172053
"completed_nodes": [],

tests/strands/multiagent/test_swarm.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,10 @@ async def failing_execute_swarm(*args, **kwargs):
11061106

11071107
@pytest.mark.asyncio
11081108
async def test_swarm_persistence(mock_strands_tracer, mock_use_span):
1109-
"""Test swarm persistence functionality."""
1109+
"""Test swarm persistence functionality with multimodal input containing binary bytes."""
1110+
import base64
1111+
import json
1112+
11101113
# Create mock session manager
11111114
session_manager = Mock(spec=FileSessionManager)
11121115
session_manager.read_multi_agent.return_value = None
@@ -1127,7 +1130,40 @@ async def test_swarm_persistence(mock_strands_tracer, mock_use_span):
11271130
assert "node_results" in state
11281131
assert "context" in state
11291132

1130-
# Test apply_state_from_dict with persisted state
1133+
# Build a multimodal prompt with inline binary PDF bytes (the problematic case)
1134+
pdf_bytes = b"%PDF-1.4 binary content"
1135+
multimodal_task = [
1136+
{"text": "Analyze this PDF"},
1137+
{
1138+
"document": {
1139+
"format": "pdf",
1140+
"name": "document.pdf",
1141+
"source": {
1142+
"bytes": pdf_bytes,
1143+
},
1144+
}
1145+
},
1146+
]
1147+
1148+
# Simulate swarm having executed with a multimodal task
1149+
swarm.state.task = multimodal_task
1150+
1151+
# serialize_state must not raise TypeError for bytes
1152+
serialized = swarm.serialize_state()
1153+
assert json.dumps(serialized) # must be JSON-serializable
1154+
1155+
# The bytes should be encoded in the serialized form
1156+
encoded_bytes = serialized["current_task"][1]["document"]["source"]["bytes"]
1157+
assert encoded_bytes == {"__bytes_encoded__": True, "data": base64.b64encode(pdf_bytes).decode()}
1158+
1159+
# deserialize_state must restore bytes back to original
1160+
serialized["next_nodes_to_execute"] = ["test_agent"]
1161+
serialized["status"] = "executing"
1162+
swarm.deserialize_state(serialized)
1163+
restored_bytes = swarm.state.task[1]["document"]["source"]["bytes"]
1164+
assert restored_bytes == pdf_bytes
1165+
1166+
# Test apply_state_from_dict with plain string persisted state (backward compat)
11311167
persisted_state = {
11321168
"status": "executing",
11331169
"node_history": [],

0 commit comments

Comments
 (0)