M2.8: switch over to the new loop
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:
1 parent
09232dcbdd
commit
6ae2375194
89 files changed
+951
-8592
No files matched your search
@@ -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 == ""
|
||||
Reference in new issue
Block a user