This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 7b716a35 [python] Admit bytes in the Python memory value contract
(#846)
7b716a35 is described below
commit 7b716a352cf03882b01f864ad79cea22fb5a07f4
Author: Weiqing Yang <[email protected]>
AuthorDate: Thu Jul 2 02:15:33 2026 -0700
[python] Admit bytes in the Python memory value contract (#846)
Exact Python bytes materializes through Pemja into a native Java byte[]
(checkpoint-stable), so it is admitted into the set()-time memory value
validator. The validator's exact-type check accepts exact bytes while
keeping bytearray and bytes-subclasses rejected, since Pemja wraps those
as non-checkpoint-stable objects rather than materializing a byte[].
Adds accept coverage (scalar and nested) and a reject test pinning the
exact-type boundary, and updates the Python memory value contract docs.
---
.../development/memory/sensory_and_short_term_memory.md | 6 +++---
python/flink_agents/api/memory_object.py | 11 +++++++----
.../runtime/tests/test_memory_value_validation.py | 15 ++++++++++++++-
3 files changed, 24 insertions(+), 8 deletions(-)
diff --git
a/docs/content/docs/development/memory/sensory_and_short_term_memory.md
b/docs/content/docs/development/memory/sensory_and_short_term_memory.md
index 7dc72ac2..a3b8b5e8 100644
--- a/docs/content/docs/development/memory/sensory_and_short_term_memory.md
+++ b/docs/content/docs/development/memory/sensory_and_short_term_memory.md
@@ -77,16 +77,16 @@ The key of the pairs stored in `MemoryObject` must be a
string. The supported va
**Python** is restricted to recursively *checkpoint-stable* values:
-- **Primitive Types**: `None`, `bool`, `int`, `float`, `str`
+- **Primitive Types**: `None`, `bool`, `int`, `float`, `str`, `bytes`
- **Collections**: `list`, and `dict` with `str` keys (values are recursively
validated)
- **Memory Object**: A nested `MemoryObject` created via `new_object()`.
-Anything else — Pydantic models, `uuid.UUID`, `Enum`, custom classes, `tuple`,
`set`, or a `dict` with non-`str` keys — is **rejected by `set()` with a
`TypeError`**. `bytes` is not supported yet.
+Anything else — Pydantic models, `uuid.UUID`, `Enum`, custom classes, `tuple`,
`set`, `bytearray`, or a `dict` with non-`str` keys — is **rejected by `set()`
with a `TypeError`**. Exact `bytes` is supported (it converts to a native Java
`byte[]`), but `bytearray` and `bytes` subclasses are not — Pemja wraps them as
non-checkpoint-stable objects rather than materializing a `byte[]`.
This is because Python values are converted across the Pemja boundary into
Flink state, and only the types above materialize into native,
checkpoint-stable JVM values; other objects would be stored as wrappers that
fail on state restore. To store a richer object, materialize it to a primitive
form first (e.g. `model.model_dump(mode="json")` for a Pydantic model, or
`str(value)` for a UUID) and reconstruct it on read.
{{< hint warning >}}
-Python memory values must be checkpoint-stable primitives, unlike the Java
contract which also supports POJOs and Kryo-serializable objects. Python values
materialize across the Pemja boundary before reaching Flink state, so models
and other objects must be materialized first with `model_dump(mode="json")` (or
`str(...)`) and reconstructed on read.
+Python memory values must be checkpoint-stable primitives, unlike the Java
contract which also supports POJOs and Kryo-serializable objects. Python values
materialize across the Pemja boundary before reaching Flink state, so models
and other objects must be materialized first with `model_dump(mode="json")` (or
`str(...)`) and reconstructed on read. Use exact `bytes` rather than
`bytearray` for binary values, since only `bytes` materializes into a native
`byte[]`.
{{< /hint >}}
### Read & Write
diff --git a/python/flink_agents/api/memory_object.py
b/python/flink_agents/api/memory_object.py
index 0a0a25ca..169d2e16 100644
--- a/python/flink_agents/api/memory_object.py
+++ b/python/flink_agents/api/memory_object.py
@@ -28,7 +28,9 @@ if TYPE_CHECKING:
# Exact builtin types Pemja materializes into native, checkpoint-stable JVM
values.
# Exact-type (not isinstance): a str/int Enum or numpy scalar is a subclass
that Pemja
# PyObject-wraps despite passing isinstance — accepting it would defeat the
validator.
-_CHECKPOINT_STABLE_SCALARS = (bool, int, float, str)
+# Exact `bytes` converts to a Java byte[]; bytearray and bytes-subclasses are
PyObject-
+# wrapped, and the exact-type check excludes both for free.
+_CHECKPOINT_STABLE_SCALARS = (bool, int, float, str, bytes)
def validate_memory_value(path: str, value: Any) -> None:
@@ -45,7 +47,7 @@ def validate_memory_value(path: str, value: Any) -> None:
The memory path the value is being set at, used to build the error
breadcrumb.
value: Any
The value to validate. Must be recursively composed of None, bool,
int, float,
- str, list, or dict with str keys.
+ str, bytes, list, or dict with str keys.
"""
_validate(value, f"value at memory path {path!r}")
@@ -77,8 +79,9 @@ def _validate(value: Any, where: str) -> None:
msg = (
f"{where} has type {type(value).__name__!r}, which is not
checkpoint-stable. "
f"Python memory values must be recursively composed of None, bool,
int, float, "
- f"str, list, or dict with str keys, because they cross the Pemja
boundary into "
- f"Flink state and non-primitive objects cannot be safely
checkpointed/restored. "
+ f"str, bytes, list, or dict with str keys, because they cross the
Pemja boundary "
+ f"into Flink state and non-primitive objects cannot be safely "
+ f"checkpointed/restored. "
f"Materialize it first, e.g. str(value) for a UUID,
value.model_dump(mode='json')"
f" for a Pydantic model, or list(value) for a tuple/set."
)
diff --git a/python/flink_agents/runtime/tests/test_memory_value_validation.py
b/python/flink_agents/runtime/tests/test_memory_value_validation.py
index c6b4418a..1ff58ccc 100644
--- a/python/flink_agents/runtime/tests/test_memory_value_validation.py
+++ b/python/flink_agents/runtime/tests/test_memory_value_validation.py
@@ -41,14 +41,19 @@ class _Plain:
pass
+class _BytesSub(bytes):
+ pass
+
+
def test_accepts_none_and_scalars() -> None:
- for value in (None, True, False, 0, 1, -3, 3.14, "", "hello"):
+ for value in (None, True, False, 0, 1, -3, 3.14, "", "hello", b"",
b"hello"):
validate_memory_value("p", value)
def test_accepts_nested_list_and_dict() -> None:
validate_memory_value("p", [1, "a", [2, 3], {"k": [4, None]}])
validate_memory_value("p", {"a": 1, "b": {"c": [True, "x"]}})
+ validate_memory_value("p", [b"x", {"k": b"y"}])
def test_rejects_pydantic_model() -> None:
@@ -67,6 +72,14 @@ def test_rejects_tuple_set_frozenset() -> None:
validate_memory_value("p", value)
+def test_rejects_bytearray_and_bytes_subclass() -> None:
+ # Exact `bytes` is accepted, but bytearray and bytes-subclasses are
PyObject-wrapped
+ # by Pemja; the exact-type check must reject them.
+ for value in (bytearray(b"x"), _BytesSub(b"x")):
+ with pytest.raises(TypeError, match="not checkpoint-stable"):
+ validate_memory_value("p", value)
+
+
def test_rejects_str_enum() -> None:
# str-Enum passes isinstance(str) but is PyObject-wrapped by Pemja; the
# exact-type check must reject it.