Skip to content

Commit f515b88

Browse files
CopilotTaoChenOSU
andcommitted
Changes before error encountered
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
1 parent 64446f9 commit f515b88

3 files changed

Lines changed: 114 additions & 32 deletions

File tree

python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# Checkpoint serialization helpers
1111
MODEL_MARKER = "__af_model__"
1212
DATACLASS_MARKER = "__af_dataclass__"
13+
PRESERVED_MARKER = "__af_preserved__"
1314

1415
# Guards to prevent runaway recursion while encoding arbitrary user data
1516
_MAX_ENCODE_DEPTH = 100
@@ -92,9 +93,22 @@ def _enc(v: Any, stack: set[int], depth: int) -> Any:
9293
return _CYCLE_SENTINEL
9394
stack.add(oid)
9495
try:
95-
json_dict: dict[str, Any] = {}
96+
# Check if this dict looks like a marker pattern (has marker key + "value" key)
97+
# If so, preserve it to prevent confusion during deserialization
98+
has_model_marker = MODEL_MARKER in v_dict
99+
has_dataclass_marker = DATACLASS_MARKER in v_dict
100+
has_value_key = "value" in v_dict
101+
if (has_model_marker or has_dataclass_marker) and has_value_key:
102+
# This is user data that looks like a marker pattern - preserve it
103+
json_dict: dict[str, Any] = {}
104+
for k_any, val_any in v_dict.items(): # type: ignore[assignment]
105+
k_str: str = str(k_any)
106+
json_dict[k_str] = _enc(val_any, stack, depth + 1)
107+
return {PRESERVED_MARKER: True, "value": json_dict}
108+
109+
json_dict = {}
96110
for k_any, val_any in v_dict.items(): # type: ignore[assignment]
97-
k_str: str = str(k_any)
111+
k_str = str(k_any)
98112
json_dict[k_str] = _enc(val_any, stack, depth + 1)
99113
return json_dict
100114
finally:
@@ -132,6 +146,13 @@ def decode_checkpoint_value(value: Any) -> Any:
132146
"""Recursively decode values previously encoded by encode_checkpoint_value."""
133147
if isinstance(value, dict):
134148
value_dict = cast(dict[str, Any], value) # encoded form always uses string keys
149+
150+
# Handle preserved marker first - this was user data that looked like a marker pattern
151+
if PRESERVED_MARKER in value_dict and "value" in value_dict:
152+
preserved_value = value_dict.get("value")
153+
# Decode as a regular dict without marker interpretation
154+
return _decode_as_regular_dict(preserved_value)
155+
135156
# Structured model marker handling
136157
if MODEL_MARKER in value_dict and "value" in value_dict:
137158
type_key: str | None = value_dict.get(MODEL_MARKER) # type: ignore[assignment]
@@ -196,6 +217,23 @@ def decode_checkpoint_value(value: Any) -> Any:
196217
return value
197218

198219

220+
def _decode_as_regular_dict(value: Any) -> Any:
221+
"""Decode value as a regular dict/list without marker interpretation.
222+
223+
Used to recover preserved user data that looked like marker patterns.
224+
"""
225+
if isinstance(value, dict):
226+
value_dict = cast(dict[str, Any], value)
227+
decoded: dict[str, Any] = {}
228+
for k_any, v_any in value_dict.items():
229+
decoded[k_any] = _decode_as_regular_dict(v_any)
230+
return decoded
231+
if isinstance(value, list):
232+
value_list: list[Any] = value # type: ignore[assignment]
233+
return [_decode_as_regular_dict(v_any) for v_any in value_list]
234+
return value
235+
236+
199237
def _class_supports_model_protocol(cls: type[Any]) -> bool:
200238
"""Check if a class type supports the model serialization protocol.
201239

python/packages/core/tests/workflow/test_checkpoint_decode.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from agent_framework._workflows._checkpoint_encoding import (
88
DATACLASS_MARKER,
99
MODEL_MARKER,
10+
PRESERVED_MARKER,
1011
decode_checkpoint_value,
1112
encode_checkpoint_value,
1213
)
@@ -154,19 +155,23 @@ def test_encode_allows_value_key_without_marker_key() -> None:
154155

155156

156157
def test_encode_allows_marker_with_value_key() -> None:
157-
"""Test that encoding a dict with marker and 'value' keys is allowed.
158+
"""Test that encoding a dict with marker and 'value' keys wraps it in preservation envelope.
158159
159-
This is allowed because legitimate encoded data may contain these keys,
160-
and security is enforced at deserialization time by validating class types.
160+
User data that looks like a marker pattern is preserved to prevent confusion
161+
during deserialization.
161162
"""
162163
dict_with_both = {
163164
MODEL_MARKER: "some.module:SomeClass",
164165
"value": {"data": "test"},
165166
"strategy": "to_dict",
166167
}
167168
encoded = encode_checkpoint_value(dict_with_both)
168-
assert MODEL_MARKER in encoded
169+
# Should be wrapped in preservation envelope
170+
assert PRESERVED_MARKER in encoded
171+
assert encoded[PRESERVED_MARKER] is True
169172
assert "value" in encoded
173+
# Original dict is inside the envelope
174+
assert MODEL_MARKER in encoded["value"]
170175

171176

172177
class NotADataclass:
@@ -221,10 +226,10 @@ def test_decode_rejects_non_model_with_model_marker() -> None:
221226

222227

223228
def test_encode_allows_nested_dict_with_marker_keys() -> None:
224-
"""Test that encoding allows nested dicts containing marker patterns.
229+
"""Test that encoding wraps nested dicts containing marker patterns in preservation envelope.
225230
226-
Security is enforced at deserialization time, not serialization time,
227-
so legitimate encoded data can contain markers at any nesting level.
231+
User data that looks like marker patterns is preserved at any nesting level
232+
to prevent confusion during deserialization.
228233
"""
229234
nested_data = {
230235
"outer": {
@@ -235,4 +240,6 @@ def test_encode_allows_nested_dict_with_marker_keys() -> None:
235240

236241
encoded = encode_checkpoint_value(nested_data)
237242
assert "outer" in encoded
238-
assert MODEL_MARKER in encoded["outer"]
243+
# Nested dict should be wrapped in preservation envelope
244+
assert PRESERVED_MARKER in encoded["outer"]
245+
assert encoded["outer"][PRESERVED_MARKER] is True

python/packages/core/tests/workflow/test_checkpoint_encode.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
_CYCLE_SENTINEL,
88
DATACLASS_MARKER,
99
MODEL_MARKER,
10+
PRESERVED_MARKER,
11+
decode_checkpoint_value,
1012
encode_checkpoint_value,
1113
)
1214

@@ -296,43 +298,44 @@ def test_encode_list_with_self_reference() -> None:
296298

297299

298300
# --- Tests for reserved keyword handling ---
299-
# Note: Security is enforced at deserialization time by validating class types,
300-
# not at serialization time. This allows legitimate encoded data to be re-encoded.
301+
# User data containing marker keys + "value" is preserved in a special envelope
302+
# during serialization and recovered during deserialization.
301303

302304

303-
def test_encode_allows_dict_with_model_marker_and_value() -> None:
304-
"""Test that encoding a dict with MODEL_MARKER and 'value' is allowed.
305-
306-
Security is enforced at deserialization time, not serialization time.
307-
"""
305+
def test_encode_preserves_dict_with_model_marker_and_value() -> None:
306+
"""Test that user dict with MODEL_MARKER and 'value' is preserved in envelope."""
308307
data = {
309308
MODEL_MARKER: "some.module:SomeClass",
310309
"value": {"data": "test"},
311310
}
312311
result = encode_checkpoint_value(data)
313-
assert MODEL_MARKER in result
312+
# Should be wrapped in preservation envelope
313+
assert PRESERVED_MARKER in result
314+
assert result[PRESERVED_MARKER] is True
314315
assert "value" in result
316+
# The inner value should contain the original dict
317+
assert MODEL_MARKER in result["value"]
318+
assert result["value"]["value"] == {"data": "test"}
315319

316320

317-
def test_encode_allows_dict_with_dataclass_marker_and_value() -> None:
318-
"""Test that encoding a dict with DATACLASS_MARKER and 'value' is allowed.
319-
320-
Security is enforced at deserialization time, not serialization time.
321-
"""
321+
def test_encode_preserves_dict_with_dataclass_marker_and_value() -> None:
322+
"""Test that user dict with DATACLASS_MARKER and 'value' is preserved in envelope."""
322323
data = {
323324
DATACLASS_MARKER: "some.module:SomeClass",
324325
"value": {"field": "test"},
325326
}
326327
result = encode_checkpoint_value(data)
327-
assert DATACLASS_MARKER in result
328+
# Should be wrapped in preservation envelope
329+
assert PRESERVED_MARKER in result
330+
assert result[PRESERVED_MARKER] is True
328331
assert "value" in result
332+
# The inner value should contain the original dict
333+
assert DATACLASS_MARKER in result["value"]
334+
assert result["value"]["value"] == {"field": "test"}
329335

330336

331-
def test_encode_allows_nested_dict_with_marker_keys() -> None:
332-
"""Test that encoding nested dict with marker keys is allowed.
333-
334-
Security is enforced at deserialization time, not serialization time.
335-
"""
337+
def test_encode_preserves_nested_dict_with_marker_keys() -> None:
338+
"""Test that nested dict with marker keys is preserved in envelope."""
336339
nested_data = {
337340
"outer": {
338341
MODEL_MARKER: "some.module:SomeClass",
@@ -341,27 +344,61 @@ def test_encode_allows_nested_dict_with_marker_keys() -> None:
341344
}
342345
result = encode_checkpoint_value(nested_data)
343346
assert "outer" in result
344-
assert MODEL_MARKER in result["outer"]
347+
# The nested dict should be wrapped in preservation envelope
348+
assert PRESERVED_MARKER in result["outer"]
349+
assert result["outer"][PRESERVED_MARKER] is True
350+
351+
352+
def test_decode_recovers_preserved_dict_with_model_marker() -> None:
353+
"""Test that preserved dict with MODEL_MARKER is recovered correctly."""
354+
original_data = {
355+
MODEL_MARKER: "some.module:SomeClass",
356+
"value": {"data": "test"},
357+
}
358+
encoded = encode_checkpoint_value(original_data)
359+
decoded = decode_checkpoint_value(encoded)
360+
# Should recover the original dict structure
361+
assert MODEL_MARKER in decoded
362+
assert decoded[MODEL_MARKER] == "some.module:SomeClass"
363+
assert decoded["value"] == {"data": "test"}
364+
365+
366+
def test_decode_recovers_preserved_dict_with_dataclass_marker() -> None:
367+
"""Test that preserved dict with DATACLASS_MARKER is recovered correctly."""
368+
original_data = {
369+
DATACLASS_MARKER: "some.module:SomeClass",
370+
"value": {"field": "test"},
371+
}
372+
encoded = encode_checkpoint_value(original_data)
373+
decoded = decode_checkpoint_value(encoded)
374+
# Should recover the original dict structure
375+
assert DATACLASS_MARKER in decoded
376+
assert decoded[DATACLASS_MARKER] == "some.module:SomeClass"
377+
assert decoded["value"] == {"field": "test"}
345378

346379

347380
def test_encode_allows_marker_without_value() -> None:
348-
"""Test that a dict with marker key but without 'value' key is allowed."""
381+
"""Test that a dict with marker key but without 'value' key is NOT preserved."""
349382
data = {
350383
MODEL_MARKER: "some.module:SomeClass",
351384
"other_key": "allowed",
352385
}
353386
result = encode_checkpoint_value(data)
387+
# Should NOT be wrapped (no "value" key present)
388+
assert PRESERVED_MARKER not in result
354389
assert MODEL_MARKER in result
355390
assert result["other_key"] == "allowed"
356391

357392

358393
def test_encode_allows_value_without_marker() -> None:
359-
"""Test that a dict with 'value' key but without marker is allowed."""
394+
"""Test that a dict with 'value' key but without marker is NOT preserved."""
360395
data = {
361396
"value": {"nested": "data"},
362397
"other_key": "allowed",
363398
}
364399
result = encode_checkpoint_value(data)
400+
# Should NOT be wrapped (no marker key present)
401+
assert PRESERVED_MARKER not in result
365402
assert "value" in result
366403
assert result["other_key"] == "allowed"
367404

0 commit comments

Comments
 (0)