Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions python/packages/core/agent_framework/_harness/_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
DEFAULT_MEMORY_CONSOLIDATION_INTERVAL: Final[timedelta] = timedelta(hours=24)
DEFAULT_MEMORY_INDEX_HEADER = "# MEMORY"
DEFAULT_MEMORY_NO_TOPICS_TEXT = "- none yet"

# Suffix marking a genuine memory whose exact text equals the empty-list
# placeholder, so a singleton real memory is distinguishable from a written
# placeholder line on read. Unmarked files keep their previous meaning.
_MEMORY_GENUINE_PLACEHOLDER_SUFFIX = " <!-- genuine -->"
DEFAULT_MEMORY_EXTRACTION_PROMPT = """You extract durable memory candidates from an agent transcript delta.

Return only JSON with this exact shape:
Expand Down Expand Up @@ -446,7 +451,7 @@ def to_markdown(self) -> str:
# content (or LLM-supplied consolidation output) for a section delimiter.
escaped_summary_lines = [_escape_markdown_line(line) for line in self.summary.splitlines()]
escaped_summary = "\n".join(escaped_summary_lines)
memory_lines = [f"- {_escape_markdown_line(memory)}" for memory in self.memories] or [
memory_lines = [f"- {_escape_markdown_line(self._encode_memory_text(memory))}" for memory in self.memories] or [
f"- {DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]}"
]
return "\n".join([
Expand All @@ -462,6 +467,18 @@ def to_markdown(self) -> str:
*memory_lines,
]).rstrip()

@staticmethod
def _encode_memory_text(memory: str) -> str:
"""Mark a genuine memory equal to the placeholder so a reader keeps it.

The write side emits the bare placeholder line only for an empty memory
list; a real memory whose exact text equals it gets a distinguishing
suffix so a singleton round trip cannot collapse to empty.
"""
if memory == DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]:
return memory + _MEMORY_GENUINE_PLACEHOLDER_SUFFIX
return memory

@classmethod
def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> MemoryTopicRecord:
"""Parse one topic record from the canonical markdown format.
Expand Down Expand Up @@ -515,9 +532,20 @@ def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> M
continue
if current_section == "memories" and stripped.startswith("- "):
memory_text = _unescape_markdown_line(stripped[2:].strip())
if memory_text and memory_text != DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]:
if memory_text:
memories.append(memory_text)

# Decode the genuine-marker suffix first so a marked memory can never be
# mistaken for the placeholder, then drop a bare placeholder only when it
# is the sole entry: that is the only shape the write side emits it in,
# and unmarked files keep their previous meaning.
placeholder = DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]
marked = placeholder + _MEMORY_GENUINE_PLACEHOLDER_SUFFIX
was_marked = [memory == marked for memory in memories]
memories = [placeholder if is_marked else memory for memory, is_marked in zip(memories, was_marked)]
if memories == [placeholder] and not was_marked[0]:
memories = []

if topic is None:
raise ValueError("Memory topic markdown is missing a '# <topic>' heading.")
return cls(
Expand Down
75 changes: 75 additions & 0 deletions python/packages/core/tests/core/test_harness_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,81 @@ def test_memory_file_store_encodes_non_ascii_owner_ids(tmp_path) -> None:
assert roots[0] != roots[1]


def test_memory_topic_record_keeps_a_memory_that_matches_the_placeholder(tmp_path) -> None:
"""A real memory whose text equals the empty-list placeholder must survive a
round trip; only the placeholder written for an empty memory list is dropped."""
record = MemoryTopicRecord(
topic="status",
summary="s",
memories=["none yet", "real fact"],
updated_at="2026-10-26T00:00:00+00:00",
)

parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="status")

assert parsed.memories == ["none yet", "real fact"]


def test_memory_topic_record_round_trips_a_genuine_singleton_placeholder_memory() -> None:
"""A real memories=[none yet] singleton must not collapse to an empty list.

The write side marks a genuine memory whose exact text equals the
placeholder with a distinguishing suffix, and the reader decodes the mark
before applying the sole-placeholder drop, so only a bare placeholder line
written for an empty list reads back empty.
"""
record = MemoryTopicRecord(
topic="notes",
summary="s",
memories=["none yet"],
updated_at="2026-10-26T00:00:00+00:00",
)
parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="notes")
assert parsed.memories == ["none yet"]


def test_memory_topic_record_marks_genuine_placeholder_on_write() -> None:
"""The written form of a genuine placeholder-valued memory carries the
distinguishing suffix, so it cannot be confused with a written placeholder."""
record = MemoryTopicRecord(
topic="notes",
summary="s",
memories=["none yet", "real fact"],
updated_at="2026-10-26T00:00:00+00:00",
)
markdown = record.to_markdown()
assert "- none yet <!-- genuine -->" in markdown
assert "- real fact" in markdown


def test_memory_topic_record_keeps_unmarked_placeholder_behavior_for_legacy_files() -> None:
"""Files written before the marker keep their previous meaning: a sole bare
placeholder line still reads back as an empty memory list."""
legacy = "\n".join([
"# notes",
"",
"Updated: 2026-10-26T00:00:00+00:00",
"Sessions: -",
"",
"## Summary",
"s",
"",
"## Memories",
"- none yet",
])
parsed = MemoryTopicRecord.from_markdown(legacy, fallback_topic="notes")
assert parsed.memories == []


def test_memory_topic_record_still_drops_the_placeholder_for_an_empty_list() -> None:
"""An empty memory list writes the placeholder line and reads back empty."""
record = MemoryTopicRecord(topic="empty", summary="s", memories=[], updated_at="2026-10-26T00:00:00+00:00")

parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="empty")

assert parsed.memories == []


def test_memory_file_store_uses_literal_folders_for_safe_identifiers(tmp_path) -> None:
"""Safe identifiers are readable on disk rather than opaque encoded segments."""
store = MemoryFileStore(tmp_path, owner_prefix="user_", owner_state_key="owner_id")
Expand Down
Loading