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

+2 -2
View File
@@ -1,6 +1,6 @@
"""The executor benchmark: canned milestones, run repeatedly per model.
"""The driver benchmark: canned milestones, run repeatedly per model.
Choosing an executor model, and later tuning its knobs, should rest on
Choosing a driver model, and later tuning its knobs, should rest on
measured success rates, not on one run or on reputation. Each task is a fixed
approved plan over a seed project, with a hidden check the model never sees;
success means the check passes, not that the model said it finished. Runs use
+8 -3
View File
@@ -74,12 +74,17 @@ class Row:
def tuning_label(tuning: dict[str, Any]) -> str:
"""The settings in a run's tuning that differ from today's defaults, as
"executor.max_turns=24"; empty when there are none. Relative to today's
defaults so that rows at the current settings stay unlabelled."""
"driver.max_turns=24"; empty when there are none. Relative to today's
defaults so that rows at the current settings stay unlabelled. Runs from
before M2.8 called the driver's section ``executor``."""
defaults = asdict(Tuning())
renamed = {
("driver" if section == "executor" else section): values
for section, values in tuning.items()
}
return ", ".join(
f"{section}.{key}={value}"
for section, values in sorted(tuning.items())
for section, values in sorted(renamed.items())
for key, value in sorted((values or {}).items())
if defaults.get(section, {}).get(key) != value
)
+63 -23
View File
@@ -1,4 +1,4 @@
"""Running one task once with one executor model, and recording it."""
"""Running one task once with one driver model, and recording it."""
import io
import shlex
@@ -14,7 +14,7 @@ from typing import Any
from nisse_dev.bench.tasks import Task
from nisse_dev.canonical import (
CanonicalEvent,
PlanSubmitted,
Fact,
StoredEvent,
ToolCall,
ToolResult,
@@ -23,11 +23,37 @@ from nisse_dev.canonical import (
from nisse_dev.catalog import ModelFacts
from nisse_dev.config import Tuning
from nisse_dev.execution import SSHHost
from nisse_dev.executor import LLMExecutor
from nisse_dev.llm import RoleMap
from nisse_dev.loop.agent import SYSTEM
from nisse_dev.loop.driver import Driver, RunOutcome
from nisse_dev.loop.tools import (
ASK_HUMAN,
CLOSE_ITEM,
FINISH_MILESTONE,
START_MILESTONE,
)
from nisse_dev.oplog import OpLog
from nisse_dev.state import StateStore
from nisse_dev.tools import Workspace
from nisse_dev.tools import EXECUTION_TOOLS, Workspace
# What the driver is offered once an item is agreed, less what the benchmark
# turns off: delegation and ask_expert (an escalation is the model's failure,
# not a frontier model's success) and review, which has no PR here.
BENCH_TOOLS = [
ASK_HUMAN,
START_MILESTONE,
FINISH_MILESTONE,
CLOSE_ITEM,
*EXECUTION_TOOLS,
]
def outcome_of(run: RunOutcome) -> str:
"""A driver run as the benchmark records it: ``completed`` when the
driver says the work is done, ``escalated`` when it asks the operator,
otherwise how the run ended (capped, stuck, failed)."""
if run.kind == "wait":
return "escalated" if run.tool == "ask_human" else "completed"
return run.kind
class MeteringClient:
@@ -158,11 +184,13 @@ def run_task(
commit: str = "",
workdir: Path | None = None,
) -> RunRecord:
"""Run one task once: seed a workspace, hand the executor the approved
plan, then judge the result with the hidden check.
"""Run one task once: seed a workspace, hand the driver an agreed item
whose restatement is the task's plan, then judge the result with the
hidden check.
Delegation and ask_expert are off: an escalation is a failure of the
model under test, not a frontier model's success."""
It is the loop's own driver, system prompt and tools, so what is measured
is what the agent would do, without the harness around it: the milestone
is started but no branch is cut, and finishing it ends the run."""
with tempfile.TemporaryDirectory(dir=workdir) as tmp:
root = Path(tmp)
# The workspace is on the build VM, like the agent's own (L13).
@@ -174,39 +202,51 @@ def run_task(
)
task.seed_workspace(workspace)
store = StateStore(root / "state.db", oplog=OpLog("nisse-bench", io.StringIO()))
# One work item per run, as the agent's own tasks are (L1).
# One work item per run, as the agent's own are (L1).
store.open_work_item(
"bench", title=task.name, position="EXECUTING", cause="benchmark run"
"bench", title=task.name, position="RUNNING", cause="benchmark run"
)
store.transcript("bench", "planner").append(
PlanSubmitted(plan=task.plan, model="benchmark")
store.transcript("bench", "orchestrator").append(
Fact(text=f"The operator agreed to this restatement:\n\n{task.plan}")
)
store.transcript("bench", "operator").append(
UserMessage(text="start milestone 1", source="benchmark")
UserMessage(text="go", source="benchmark")
)
meter = MeteringClient(client, facts)
trims_before = getattr(client, "trimmed_requests", 0)
model = facts.alias
roles = RoleMap(executor=model, planner=model, helper=model, expert=model)
executor = LLMExecutor(
driver = Driver(
meter,
roles,
workspace,
max_turns=tuning.executor.max_turns,
failure_limit=tuning.executor.failure_limit,
model,
max_turns=tuning.driver.max_turns,
failure_limit=tuning.driver.failure_limit,
)
system = SYSTEM.format(
repo=f"bench/{task.name}",
issue=1,
title=task.title,
labels="",
helpers="",
workspace=workspace.describe(),
)
started = time.monotonic()
try:
outcome = executor.run(store.transcript("bench", "executor"), 1)
status, detail = outcome.status, outcome.note
run = driver.run(
store.transcript("bench", "driver"),
system=system,
tools=BENCH_TOOLS,
workspace=workspace,
handlers={"start_milestone": lambda args: ("started", True)},
)
status, detail = outcome_of(run), run.note
except Exception as exc: # noqa: BLE001 -- recorded, the benchmark goes on
status, detail = "error", repr(exc)
wall = time.monotonic() - started
success, check_output = task.check_workspace(workspace)
host.execute(f"rm -rf -- {shlex.quote(str(workspace.root))}", timeout_s=60)
failed, invented = _count(store.transcript("bench", "executor").read())
failed, invented = _count(store.transcript("bench", "driver").read())
store.close()
return RunRecord(
+3 -3
View File
@@ -3,11 +3,11 @@
Each task is a directory under ``tasks/``:
- ``task.yaml``: ``title``, ``plan`` (the approved milestone plan, written the
way the planner is asked to write them), and ``check`` (a shell command run
way the driver restates them), and ``check`` (a shell command run
in the workspace afterwards).
- ``seed/``: the project the executor starts from.
- ``seed/``: the project the driver starts from.
- ``hidden/``: files the check needs, copied in only after the run, into
``.bench_check/``. The executor never sees them.
``.bench_check/``. The driver never sees them.
- ``solution/``: a reference solution, overlaid on the seed by the tests to
prove each task can be solved and that its check tells the difference.
"""
@@ -9,4 +9,4 @@ plan: |
3. Fix `total_value` so that each item contributes its unit price times its
quantity.
4. Run `python -m pytest -q` until every test passes.
5. Call finish.
5. Call finish_milestone, with final yes.
@@ -37,4 +37,4 @@ plan: |
then the same with `list --overdue`, which must print
`[!] 1 Pay rent (due 2026-10-01)`. Delete /tmp/tracker-check.json
before and after.
9. Call finish.
9. Call finish_milestone, with final yes.
@@ -9,4 +9,4 @@ plan: |
(NNN mod 25) / 100. Correct only the failing region's entry; leave every
other line as it is.
3. Run `python -m pytest -q` until every test passes.
4. Call finish.
4. Call finish_milestone, with final yes.
@@ -13,4 +13,4 @@ plan: |
using pytest's capsys fixture.
4. Run `python -m pytest -q` until every test passes, and check that
`python -m greeter --shout Bob` prints `HELLO, BOB!`.
5. Call finish.
5. Call finish_milestone, with final yes.
@@ -15,4 +15,4 @@ plan: |
are different functions and keep their names. Change no behaviour.
4. Update the tests under `tests/` to use the new name, and run
`python -m pytest -q` until every test passes.
5. Call finish.
5. Call finish_milestone, with final yes.
@@ -12,4 +12,4 @@ plan: |
invalid input for each of the eight rules.
4. Run `python -m pytest -q test_durations.py`, and fix `durations.py`
until every test passes.
5. Call finish.
5. Call finish_milestone, with final yes.
@@ -18,4 +18,4 @@ plan: |
`invoicing/currency.py` have up to six decimal places (JPY is 0.006712),
and no step may round a rate or fraction to fewer places than that.
4. Run `python -m pytest -q` until every test passes.
5. Call finish.
5. Call finish_milestone, with final yes.
@@ -10,4 +10,4 @@ plan: |
"a--b" -> "a-b"; "" -> "".
3. Create `test_slugify.py` with pytest tests covering the four examples.
4. Run `python -m pytest -q test_slugify.py`, and fix anything until it passes.
5. Call finish.
5. Call finish_milestone, with final yes.