Shared parts, not a shared scheduler (agreed): nisse-review will have its own small scheduler; a common one waits until two exist to compare. - nisse_core.loop.driver: the driver run, moved; the agent names its waiting tools per run (nisse-dev's are WAITING_TOOLS), and execution goes to any Executor (ToolOutcome moves with it). - nisse_core.loop.tools: the tool helper and the tools any agent offers (notes, memory, memory_done, ask_expert); nisse_core.loop.expert. - nisse_core.knowledge.handlers: Knowledge, the vault and memory as control tools, the index and prompt text, out of LoopAgent. - nisse_core.config: file reading, references, people, tuning sections of the agent's own, LLM numbers, the vault. - New core tests: a driver run with a non-workspace executor and the agent's own waiting set; Knowledge alone; the shared config pieces. Docs: the Development Plan marks M7.2 done; Architecture Part L and the nisse-core README name the new parts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""The vault and memory as a loop offers them (L10, L6.4), apart from any
|
|
agent: tools only for what exists, labelled results, failures as results,
|
|
notes only about the item's own project, provenance recorded."""
|
|
|
|
import io
|
|
from pathlib import Path
|
|
|
|
from nisse_core.knowledge.handlers import Knowledge
|
|
from nisse_core.knowledge.memory import Memory
|
|
from nisse_core.knowledge.vault import Vault, split_frontmatter
|
|
from nisse_core.loop.tools import names
|
|
from nisse_core.oplog import OpLog
|
|
|
|
FOLDER = "memory/agent"
|
|
|
|
|
|
def knowledge(tmp_path: Path, *, memory: bool = True) -> Knowledge:
|
|
(tmp_path / "notes").mkdir()
|
|
(tmp_path / "notes" / "Style.md").write_text("Use split.\n")
|
|
vault = Vault(tmp_path, {FOLDER: "rw", "notes": "ro"}, agent="agent")
|
|
return Knowledge(
|
|
vault, Memory(vault, FOLDER) if memory else None, OpLog("t", io.StringIO())
|
|
)
|
|
|
|
|
|
def test_tools_and_prompt_follow_what_exists(tmp_path: Path) -> None:
|
|
both = knowledge(tmp_path)
|
|
assert names(both.tools()) == {
|
|
"read_note",
|
|
"list_notes",
|
|
"search_notes",
|
|
"remember",
|
|
"forget",
|
|
}
|
|
assert "knowledge vault" in both.prompt() and "Your memory" in both.prompt()
|
|
none = Knowledge(None, None, OpLog("t", io.StringIO()))
|
|
assert none.tools() == [] and none.prompt() == "" and none.index("x", None) is None
|
|
assert none.handlers("x", project=None, provenance={}, say=print) == {}
|
|
|
|
|
|
def test_notes_are_read_labelled_and_failures_are_results(tmp_path: Path) -> None:
|
|
handlers = knowledge(tmp_path, memory=False).handlers(
|
|
"x", project=None, provenance={}, say=print
|
|
)
|
|
assert "remember" not in handlers
|
|
assert handlers["read_note"]({"path": "notes/Style.md"}) == (
|
|
"[vault: notes/Style.md]\nUse split.\n",
|
|
True,
|
|
)
|
|
assert handlers["read_note"]({"path": "notes/No.md"})[1] is False
|
|
|
|
|
|
def test_memory_is_about_the_items_own_project_only(tmp_path: Path) -> None:
|
|
k = knowledge(tmp_path)
|
|
said: list[str] = []
|
|
handlers = k.handlers(
|
|
"x", project="demo", provenance={"source": "work item x"}, say=said.append
|
|
)
|
|
fact = {"name": "short-prs", "description": "Small PRs", "fact": "Don likes them"}
|
|
assert handlers["remember"]({**fact, "scope": "project"}) == (
|
|
"remembered short-prs",
|
|
True,
|
|
)
|
|
note = (tmp_path / FOLDER / "projects" / "demo" / "short-prs.md").read_text()
|
|
assert split_frontmatter(note)[0]["provenance"]["project"] == "demo"
|
|
assert said == ["Remembered: Small PRs"]
|
|
assert "short-prs" in (k.index("x", "demo") or "")
|
|
general = k.handlers("y", project=None, provenance={}, say=said.append)
|
|
assert general["remember"]({**fact, "scope": "project"})[1] is False
|