M7.1: extract nisse-core
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m42s
CI / e2e (pull_request) Successful in 4m0s

What isn't the dev agent's moves to nisse-core, unchanged in behaviour,
with its tests: canonical, render, render_anthropic, llm, catalog, probe,
context, views, state, reconcile, worker, cancel, dump, transport, matrix,
execution, testing, vault, memory, oplog, envfile and gitea (whole).

- nisse_core.state.StateStore holds the tables every agent has; an
  agent's subclass adds its tables (version, migrations), roles and
  views. nisse_dev.state.StateStore adds the build VM blocks (v5).
- views.project takes what each role is shown, rather than a fixed table.
- Vault takes the agent's name (agent=), for provenance.
- A test that nisse_core never imports an agent.
- The loop, config, git, delegation, expert, tools and evals stay in
  nisse-dev until nisse-ops has a loop to share.

Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on
the agent host and Terraform; Architecture Part L names the package.

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:
dongarandClaude Opus 5.5 committed 2026-10-13 18:37:01 +00:00
1 parent 4bbcb09c8e
commit 154a0c3fa8
119 files changed
+1086 -912

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
import json
from itertools import pairwise
from nisse_core.canonical import (
AssistantText,
Compaction,
GateOpened,
StoredEvent,
ToolCall,
ToolResult,
UserMessage,
)
from nisse_core.render import OpenAIChatRenderer, effective_events
def stored(events: list) -> list[StoredEvent]:
return [StoredEvent(id=i + 1, ts="t", event=e) for i, e in enumerate(events)]
def test_renders_mixed_transcript_in_order() -> None:
events = stored(
[
UserMessage(text="run the tests", source="m:1"),
ToolCall(call_id="call-9", tool="run_tests", arguments={"path": "x"}),
ToolResult(call_id="call-9", result="1 passed", success=True),
AssistantText(text="all green", model="local/qwen"),
]
)
body = OpenAIChatRenderer().render(events, tools=None, model="local/qwen")
assert body["model"] == "local/qwen"
roles = [m["role"] for m in body["messages"]]
assert roles == ["user", "assistant", "tool", "assistant"]
call = body["messages"][1]["tool_calls"][0]
assert call["id"] == "call-9"
assert call["function"]["name"] == "run_tests"
assert json.loads(call["function"]["arguments"]) == {"path": "x"}
assert body["messages"][2]["tool_call_id"] == "call-9"
assert "tools" not in body
def test_system_prompt_and_tools_pass_through() -> None:
tools = [{"type": "function", "function": {"name": "run_tests"}}]
body = OpenAIChatRenderer().render(
stored([UserMessage(text="hi", source="m:1")]),
tools=tools,
model="local/qwen",
system="you are nisse-dev",
)
assert body["messages"][0] == {"role": "system", "content": "you are nisse-dev"}
assert body["tools"] == tools
def test_compaction_replaces_its_range_only() -> None:
events = stored(
[
UserMessage(text="old-1", source="m:1"),
AssistantText(text="old-2", model="q"),
UserMessage(text="kept", source="m:3"),
]
)
events.append(
StoredEvent(
id=4,
ts="t",
event=Compaction(summary="the old stuff", model="q", replaces=[1, 2]),
)
)
remaining = effective_events(events)
assert [e.id for e in remaining] == [3, 4]
body = OpenAIChatRenderer().render(events, tools=None, model="q")
contents = [m["content"] for m in body["messages"]]
# Both render as user messages, so they share one: roles must alternate.
assert contents == ["kept\n\n[summary of earlier conversation]\nthe old stuff"]
def test_tool_call_without_a_result_gets_a_placeholder() -> None:
# Providers reject a dangling tool_use; this happens after a crash (L8)
# or with turn-ending gate tools like submit_plan.
events = stored(
[
UserMessage(text="go", source="m:1"),
ToolCall(call_id="call-x", tool="submit_plan", arguments={}),
]
)
body = OpenAIChatRenderer().render(events, tools=None, model="q")
assert [m["role"] for m in body["messages"]] == ["user", "assistant", "tool"]
assert body["messages"][2]["tool_call_id"] == "call-x"
assert body["messages"][2]["content"] == "[no result recorded]"
def test_answered_tool_call_gets_no_placeholder() -> None:
events = stored(
[
ToolCall(call_id="call-x", tool="run_command", arguments={}),
ToolResult(call_id="call-x", result="ok", success=True),
]
)
body = OpenAIChatRenderer().render(events, tools=None, model="q")
assert [m["role"] for m in body["messages"]] == ["assistant", "tool"]
assert body["messages"][1]["content"] == "ok"
def test_gate_event_renders_as_context() -> None:
events = stored([GateOpened(gate="arch", reason="plan submitted (rev-1)")])
body = OpenAIChatRenderer().render(events, tools=None, model="q")
assert body["messages"][0]["role"] == "user"
assert "arch" in body["messages"][0]["content"]
def _roles(body: dict) -> list[str]:
return [m["role"] for m in body["messages"]]
def test_one_turn_is_one_assistant_message() -> None:
# Text and two parallel tool calls from one model turn: the OpenAI format
# carries them as a single assistant message with a tool_calls list.
events = stored(
[
UserMessage(text="go", source="m:1"),
AssistantText(text="Looking around first.", model="q"),
ToolCall(call_id="a", tool="list_dir", arguments={"path": "."}),
ToolCall(call_id="b", tool="read_file", arguments={"path": "README.md"}),
ToolResult(call_id="a", result="README.md", success=True),
ToolResult(call_id="b", result="hello", success=True),
]
)
body = OpenAIChatRenderer().render(events, tools=None, model="q")
assert _roles(body) == ["user", "assistant", "tool", "tool"]
turn = body["messages"][1]
assert turn["content"] == "Looking around first."
assert [c["id"] for c in turn["tool_calls"]] == ["a", "b"]
def test_roles_never_repeat_for_strict_templates() -> None:
"""Mistral's chat template raises on two user or two assistant messages
in a row; every mix of events must render without one."""
events = stored(
[
UserMessage(text="one", source="m:1"),
UserMessage(text="two", source="m:2"),
AssistantText(text="thinking", model="q"),
ToolCall(call_id="a", tool="list_dir", arguments={}),
ToolResult(call_id="a", result="x", success=True),
AssistantText(text="more", model="q"),
AssistantText(text="and more", model="q"),
ToolCall(call_id="b", tool="finish", arguments={}),
]
)
roles = _roles(OpenAIChatRenderer().render(events, tools=None, model="q"))
for first, second in pairwise(roles):
assert not (first == second and first in ("user", "assistant")), roles