M2.8: switch over to the new loop
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m16s
CI / e2e (pull_request) Successful in 10m51s

nisse-dev now runs only the conversational loop. The gated state machine
it was built alongside is gone:

- Removed control.py, planner.py, executor.py, approvals.py, gates.py
  (command_form moves to loop/reader.py), publish.py and poller.py, the
  old assembly in agent.py (its shared helpers move to loop/build.py),
  the store's state-machine view (get_state, transition), their tests,
  the old end-to-end and live planning/milestone/gate tests, and
  specs/orchestrator.qnt with its scenarios.
- Roles are driver, helper, expert and classifier; driver is required.
  loop:, roles.planner, roles.executor and tuning.executor are refused
  with what to use instead; tuning.driver takes the turn cap and failure
  limit.
- The benchmark and the capability matrix measure the driver: the
  loop's own system prompt and tools, the task's plan as the agreed
  restatement. Task plans end with finish_milestone, so earlier runs
  show as (old); stored runs' executor tuning reads as the driver's.
- A delegation on the loop records what its helper's alias resolved to
  (L9), as the old path's did.
- The dump shows a work item's revision and what it waits for.
- Behaviour the old tests covered and the loop shares stays covered:
  driver runs that fail on a model call, stop mid-turn, reset the
  failure count; go-aheads ignoring case and punctuation; a request too
  big to fit is never sent; the assembly tests, now on build_loop_agent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-08 03:12:58 +00:00
1 parent 09232dcbdd
commit 6ae2375194
89 files changed
+951 -8592

No files matched your search

+44 -10
View File
@@ -1,4 +1,4 @@
"""The executor benchmark."""
"""The driver benchmark."""
import json
import shutil
@@ -19,7 +19,7 @@ from nisse_dev.bench.runner import MeteringClient, run_task
from nisse_dev.bench.tasks import CHECK_DIR, load_tasks
from nisse_dev.canonical import AssistantText, ToolCall
from nisse_dev.catalog import ModelFacts
from nisse_dev.config import ExecutorTuning, Tuning
from nisse_dev.config import DriverTuning, Tuning
from nisse_dev.execution import SSHHost
# The build VM for every test here: the session's throwaway sshd.
@@ -79,7 +79,7 @@ def test_the_seed_fails_its_check_and_the_solution_passes(task, tmp_path: Path)
def test_the_hidden_check_is_not_in_the_seed(task, tmp_path: Path) -> None:
task.seed_into(tmp_path)
assert not (tmp_path / CHECK_DIR).exists()
assert "Call finish" in task.plan # plans end the way the executor must
assert "Call finish_milestone" in task.plan # as the driver must end
def test_long_output_really_is_long(tmp_path: Path) -> None:
@@ -192,7 +192,7 @@ SOLVE_SLUGIFY = [
)
],
[call("run_command", "r", command="python -c 'import slugify'")],
[call("finish", "f", summary="added slugify")],
[call("finish_milestone", "f", summary="added slugify", final="yes")],
]
@@ -208,7 +208,7 @@ def test_a_solved_task_is_a_success(tmp_path: Path) -> None:
assert record.tokens_in == 3000
assert record.task_hash == task.content_hash
assert record.model_info["supports_tool_choice"] is True
assert record.tuning["executor"]["max_turns"] == 40
assert record.tuning["driver"]["max_turns"] == 40
def test_saying_finished_is_not_success(tmp_path: Path) -> None:
@@ -223,7 +223,7 @@ def test_saying_finished_is_not_success(tmp_path: Path) -> None:
content="def slugify(t):\n return t\n",
)
],
[call("finish", "f", summary="done")],
[call("finish_milestone", "f", summary="done", final="yes")],
]
record = run_task(task, FACTS, Scripted(wrong), Tuning(), host=HOST)
assert record.outcome == "completed"
@@ -239,7 +239,7 @@ def test_misbehaviour_is_counted(tmp_path: Path) -> None:
]
# A higher failure limit, so the invented tool and the failed command
# don't escalate before the prose turn is reached.
tuning = Tuning(executor=ExecutorTuning(failure_limit=5))
tuning = Tuning(driver=DriverTuning(failure_limit=5))
record = run_task(task, FACTS, Scripted(turns), tuning, host=HOST)
assert record.invented_tools == 1
assert record.failed_commands == 1
@@ -247,6 +247,31 @@ def test_misbehaviour_is_counted(tmp_path: Path) -> None:
assert not record.success
def test_asking_the_operator_is_an_escalation(tmp_path: Path) -> None:
[task] = load_tasks(["write-function"])
asks = [[call("ask_human", "q", question="which file?")]]
record = run_task(task, FACTS, Scripted(asks), Tuning(), host=HOST)
assert (record.outcome, record.success) == ("escalated", False)
def test_the_driver_gets_the_loops_own_prompt_and_tools(tmp_path: Path) -> None:
from nisse_dev.loop.agent import SYSTEM
seen: list[tuple[str, set[str]]] = []
class Spy(Scripted):
def complete(self, events, tools, model, system=None): # type: ignore[no-untyped-def]
seen.append((system or "", {t["function"]["name"] for t in tools}))
return super().complete(events, tools, model, system)
[task] = load_tasks(["write-function"])
run_task(task, FACTS, Spy(list(SOLVE_SLUGIFY)), Tuning(), host=HOST)
system, tools = seen[0]
assert system.startswith(SYSTEM.split("{")[0])
assert {"start_milestone", "finish_milestone", "run_command"} <= tools
assert not {"delegate", "ask_expert", "restate"} & tools
def test_a_fallback_answer_can_never_be_a_success(tmp_path: Path) -> None:
[task] = load_tasks(["write-function"])
client = Scripted(list(SOLVE_SLUGIFY), answered="claude-sonnet-5")
@@ -383,11 +408,11 @@ def test_results_stored_before_waits_were_recorded_still_load() -> None:
def test_runs_with_different_tuning_are_kept_apart() -> None:
current = {t.name: t.content_hash for t in TASKS}
capped = record_json()
capped["tuning"]["executor"]["max_turns"] = 7
capped["tuning"]["driver"]["max_turns"] = 7
rows = summarise([record_json(), capped], current)
assert sorted(row.tuning for row in rows) == ["", "executor.max_turns=7"]
assert sorted(row.tuning for row in rows) == ["", "driver.max_turns=7"]
table = render(rows)
assert "executor.max_turns=7" in table
assert "driver.max_turns=7" in table
assert "tuning = settings that differ" in table
@@ -395,3 +420,12 @@ def test_a_report_at_default_tuning_has_no_tuning_column() -> None:
current = {t.name: t.content_hash for t in TASKS}
table = render(summarise([record_json()], current))
assert "tuning" not in table
def test_runs_from_before_the_driver_read_as_its_defaults() -> None:
# Stored before M2.8, the driver's section was called executor.
current = {t.name: t.content_hash for t in TASKS}
old = record_json()
old["tuning"]["executor"] = old["tuning"].pop("driver")
[row] = summarise([old], current)
assert row.tuning == ""