fix(agent): preserve Unicode separators in saved state (#594)
Some checks are pending
CI / OpenClaw plugin / Validate OpenClaw plugin (push) Waiting to run
CI / Python packages / Build and verify distributions (push) Waiting to run
CI / Python quality / GitHub Actions (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / Python tests / Unit Tests - py3.14 (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
CI and Release / Docker / Build and test / amd64 (push) Waiting to run
CI and Release / Docker / Build and test / arm64 (push) Waiting to run
CI and Release / Docker / Publish multi-platform tags (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run

Co-authored-by: pei711 <199601925+pei711@users.noreply.github.com>
This commit is contained in:
pei711 2026-10-05 11:25:37 +08:00 • committed by GitHub
parent 090c8c24ab
commit dc12798526
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 1 deletions

View file

@ -65,7 +65,9 @@ class AsStateHandler:
async def load(self) -> AgentState:
"""Read an AgentState back from ``self.path``."""
async with aiofiles.open(self.path, encoding="utf-8") as f:
lines = (await f.read()).splitlines()
# Unicode line separators are valid JSON string content, not JSONL delimiters.
content = await f.read()
lines = content.split("\n") if content else []
if not lines:
return AgentState()

View file

@ -0,0 +1,56 @@
"""AgentScope state persistence must follow JSONL record boundaries."""
from pathlib import Path
import pytest
from agentscope.message import UserMsg
from agentscope.state import AgentState
from reme.utils import AsStateHandler
@pytest.mark.asyncio
@pytest.mark.parametrize("separator", ["ordinary", "\n", "\r\n", "\u0085", "\u2028", "\u2029"])
@pytest.mark.parametrize("field", ["summary", "context"])
async def test_state_round_trip_preserves_text_separators(tmp_path: Path, separator: str, field: str):
"""Valid Unicode JSON string content never creates an extra JSONL record."""
text = f"before{separator}after"
state = AgentState(
session_id="unicode-session",
reply_id="saved-reply",
cur_iter=3,
summary=text if field == "summary" else "ordinary summary",
context=[UserMsg(name="user", content=text if field == "context" else "ordinary message")],
)
handler = AsStateHandler.for_session(tmp_path, state.session_id)
await handler.dump(state)
restored = await handler.load()
assert restored.model_dump() == state.model_dump()
@pytest.mark.asyncio
@pytest.mark.parametrize("newline", ["\n", "\r\n"])
async def test_state_load_accepts_lf_and_crlf_records(tmp_path: Path, newline: str):
"""Existing LF and CRLF files retain their header and context records."""
state = AgentState(session_id="existing-session", summary="summary", context=[UserMsg(name="user", content="text")])
handler = AsStateHandler.for_session(tmp_path, state.session_id)
await handler.dump(state)
content = handler.path.read_text(encoding="utf-8")
handler.path.write_bytes(content.replace("\n", newline).encode("utf-8"))
assert (await handler.load()).model_dump() == state.model_dump()
@pytest.mark.asyncio
async def test_state_load_accepts_empty_file(tmp_path: Path):
"""An empty state file continues to load as a fresh AgentState."""
handler = AsStateHandler.for_session(tmp_path, "empty-session")
handler.path.write_text("", encoding="utf-8")
restored = await handler.load()
assert restored.summary == ""
assert restored.context == []
assert restored.cur_iter == 0
assert restored.session_id