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
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
"""Configuration any agent builds on: references resolved from the
|
|
environment, people lists, tuning sections of the agent's own, the LLM's
|
|
numbers, and the vault; every problem collected, none raised one by one."""
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from nisse_core.config import (
|
|
flatten,
|
|
llm_numbers,
|
|
parse_tuning,
|
|
parse_vault,
|
|
people,
|
|
resolve,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Review:
|
|
max_rounds: int = 3
|
|
wait_s: float = 60.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Tuning:
|
|
review: Review = field(default_factory=Review)
|
|
|
|
|
|
def test_an_agents_own_tuning_sections() -> None:
|
|
errors: list[str] = []
|
|
tuning = parse_tuning({"review": {"max_rounds": 2}}, Tuning, errors)
|
|
assert tuning == Tuning(Review(max_rounds=2)) and errors == []
|
|
assert parse_tuning(None, Tuning, errors) == Tuning()
|
|
parse_tuning(
|
|
{"review": {"max_rounds": 0, "x": 1}, "old": {}, "new": {}},
|
|
Tuning,
|
|
errors,
|
|
retired={"old": "use review"},
|
|
)
|
|
assert errors == [
|
|
"tuning.review.max_rounds: must be a positive number",
|
|
"tuning.review.x: unknown setting",
|
|
"tuning.old: retired — use review",
|
|
"tuning.new: unknown section",
|
|
]
|
|
|
|
|
|
def test_references_and_people() -> None:
|
|
errors: list[str] = []
|
|
env = {"TOKEN": "s3cret"}
|
|
assert resolve("${TOKEN}", "gitea.token", env, errors) == "s3cret"
|
|
resolve("${NOPE}", "llm.token", env, errors)
|
|
assert errors == ["llm.token: references ${NOPE}, which is unset or empty"]
|
|
assert people("don, ,ann,") == {"don", "ann"}
|
|
assert flatten({"llm": {"base_url": "u"}, "tuning": {"a": {}}}) == {
|
|
"llm.base_url": "u",
|
|
"tuning": {"a": {}},
|
|
}
|
|
|
|
|
|
def test_llm_numbers_and_the_vault(tmp_path: Path) -> None:
|
|
errors: list[str] = []
|
|
assert llm_numbers({"llm.max_output_tokens": 100}, errors)[0] == 100
|
|
llm_numbers({"llm.request_timeout_s": -1}, errors)
|
|
assert errors == ["llm.request_timeout_s: must be a positive number of seconds"]
|
|
vault = parse_vault({"root": str(tmp_path), "folders": {"notes": "ro"}}, [])
|
|
assert vault is not None and vault.folders == {"notes": "ro"}
|
|
bad: list[str] = []
|
|
assert parse_vault({"root": "/v", "folders": {"../x": "rw"}}, bad) is None
|
|
assert bad == ["vault.folders.../x: a folder inside the vault"]
|