M7.2: share the loop's parts
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
This commit is contained in:
1 parent
90afc4f8dd
commit
f32ccf9881
34 files changed
+899
-485
No files matched your search
@@ -0,0 +1,83 @@
|
||||
"""A driver run on its own (L2), as any agent uses it: the agent says which
|
||||
tools wait, and execution goes to whatever executor it gives (L13)."""
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from nisse_core.log.canonical import CanonicalEvent, ToolCall, ToolResult
|
||||
from nisse_core.loop.driver import Driver, ToolOutcome
|
||||
from nisse_core.loop.tools import tool
|
||||
from nisse_core.oplog import OpLog
|
||||
from nisse_core.state.store import StateStore
|
||||
|
||||
|
||||
class Scripted:
|
||||
def __init__(self, *turns: list[CanonicalEvent]) -> None:
|
||||
self._turns = list(turns)
|
||||
|
||||
def complete(self, events, tools, model, system=None) -> list[CanonicalEvent]:
|
||||
return self._turns.pop(0)
|
||||
|
||||
|
||||
def call(name: str, **arguments: str) -> ToolCall:
|
||||
return ToolCall(call_id=f"c-{name}", tool=name, arguments=arguments)
|
||||
|
||||
|
||||
class Reader:
|
||||
"""Not a workspace: reads from a dict, as a review agent's might."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def dispatch(self, tool: str, arguments: dict[str, Any]) -> ToolOutcome:
|
||||
self.calls.append((tool, arguments))
|
||||
return ToolOutcome({"a.py": "x = 1"}.get(arguments["path"], "none"), True)
|
||||
|
||||
|
||||
READ = tool("read_file", "Read a file.", path="Its path.")
|
||||
POST = tool("post_review", "Post the review. Ends your turn.", body="The review.")
|
||||
ASK = tool("ask_human", "Ask the operator.", question="The question.")
|
||||
|
||||
|
||||
def transcript(tmp_path: Path) -> Any:
|
||||
store = StateStore(tmp_path / "s.db", oplog=OpLog("t", io.StringIO()))
|
||||
return store.transcript("x", "driver")
|
||||
|
||||
|
||||
def results(log: Any) -> list[str]:
|
||||
return [e.event.result for e in log.read() if isinstance(e.event, ToolResult)]
|
||||
|
||||
|
||||
def test_the_agent_says_which_tools_wait(tmp_path: Path) -> None:
|
||||
model = Scripted(
|
||||
[call("read_file", path="a.py")], [call("post_review", body="looks fine")]
|
||||
)
|
||||
reader = Reader()
|
||||
log = transcript(tmp_path)
|
||||
ran = Driver(model, "m").run(
|
||||
log,
|
||||
system="s",
|
||||
tools=[READ, POST],
|
||||
waiting={"post_review"},
|
||||
workspace=reader,
|
||||
)
|
||||
assert (ran.kind, ran.tool, ran.arguments) == (
|
||||
"wait",
|
||||
"post_review",
|
||||
{"body": "looks fine"},
|
||||
)
|
||||
assert reader.calls == [("read_file", {"path": "a.py"})]
|
||||
assert results(log) == ["x = 1", "posted; your turn has ended"]
|
||||
|
||||
|
||||
def test_a_tool_not_named_waiting_doesnt_end_the_run(tmp_path: Path) -> None:
|
||||
# ask_human waits in nisse-dev; here no handler answers it and it isn't
|
||||
# waiting, so it is no tool at all, and the run goes on.
|
||||
model = Scripted([call("ask_human", question="?")], [call("post_review", body="")])
|
||||
log = transcript(tmp_path)
|
||||
ran = Driver(model, "m").run(
|
||||
log, system="s", tools=[ASK, POST], waiting={"post_review"}, workspace=None
|
||||
)
|
||||
assert ran.tool == "post_review"
|
||||
assert results(log)[0].startswith("no such tool: ask_human")
|
||||
Reference in new issue
Block a user