remember(name, description, fact) and forget(name, why): control tools for the driver (before the go-ahead too) and conversations, writing only the agent's own vault folder, one note per fact with its provenance. No one agrees first; each change is a line in the thread. The harness keeps the index and puts it into the transcript once per item, with a hash. At an item's close, completed or abandoned, a bounded memory pass (the driver's model, its own transcript and view) records what was decided and learned; it never holds the close up (tuning.memory.max_turns). With the operator: corrections may be paraphrased (L6 changed), the model forgets on its own, and nothing limits what may prompt a note. The walk remembers and forgets, and checks nothing lands outside the agent's folder. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
"""Agent memory (L6.4, M5.2): one note per fact in the agent's folder, an
|
|
index the harness keeps, and a snapshot of it for the transcript."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nisse_dev.memory import Memory
|
|
from nisse_dev.vault import Vault, VaultError, split_frontmatter
|
|
|
|
FOLDER = "memory/nisse-dev"
|
|
|
|
|
|
@pytest.fixture
|
|
def memory(tmp_path: Path) -> Memory:
|
|
return Memory(Vault(tmp_path / "v", {FOLDER: "rw", "notes": "ro"}), FOLDER)
|
|
|
|
|
|
def note(memory: Memory, name: str) -> tuple[dict, str]:
|
|
path = memory._vault.root / FOLDER / f"{name}.md"
|
|
return split_frontmatter(path.read_text())
|
|
|
|
|
|
def test_a_fact_is_a_note_with_its_description_and_provenance(memory: Memory) -> None:
|
|
replaced = memory.remember(
|
|
"prefers-small-prs",
|
|
"The operator prefers small PRs",
|
|
"Don said PRs over 400 lines are hard to review.\n",
|
|
provenance={"source": "work item x"},
|
|
)
|
|
assert not replaced
|
|
front, body = note(memory, "prefers-small-prs")
|
|
assert front["description"] == "The operator prefers small PRs"
|
|
assert front["provenance"]["source"] == "work item x"
|
|
assert body == "Don said PRs over 400 lines are hard to review.\n"
|
|
|
|
|
|
def test_the_same_name_replaces_and_the_index_follows(memory: Memory) -> None:
|
|
memory.remember("b-fact", "Bee", "b", provenance={})
|
|
memory.remember("a-fact", "Ay", "a", provenance={})
|
|
assert memory.remember("a-fact", "Ay, again", "a2", provenance={})
|
|
index = memory.snapshot().text
|
|
assert index.endswith("- [[a-fact]] — Ay, again\n- [[b-fact]] — Bee")
|
|
memory.forget("a-fact")
|
|
assert "a-fact" not in memory.snapshot().text
|
|
with pytest.raises(VaultError, match="no such note"):
|
|
memory.forget("a-fact")
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["index", "Caps", "../x", "a/b", "", "-x"])
|
|
def test_names_are_plain(memory: Memory, name: str) -> None:
|
|
with pytest.raises(VaultError, match="a note's name"):
|
|
memory.remember(name, "d", "f", provenance={})
|
|
|
|
|
|
def test_a_note_needs_a_description_and_a_fact(memory: Memory) -> None:
|
|
with pytest.raises(VaultError, match="description and the fact"):
|
|
memory.remember("x", " ", "f", provenance={})
|
|
|
|
|
|
def test_the_snapshot_is_hashed_and_made_if_missing(memory: Memory) -> None:
|
|
first = memory.snapshot()
|
|
assert first.path == f"{FOLDER}/index.md"
|
|
assert "Nothing yet." in first.text
|
|
assert memory.snapshot().sha == first.sha # same index, same hash
|
|
memory.remember("x", "Ex", "f", provenance={})
|
|
assert memory.snapshot().sha != first.sha
|