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
@@ -17,7 +17,7 @@ silently ignored would look exactly like a knob that has no effect.
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field, fields, replace
|
||||
from dataclasses import dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -32,8 +32,7 @@ REQUIRED_KEYS = (
|
||||
"state_db",
|
||||
"llm.base_url",
|
||||
"llm.token",
|
||||
"roles.executor",
|
||||
"roles.planner",
|
||||
"roles.driver",
|
||||
"roles.helper",
|
||||
"roles.expert",
|
||||
"matrix.homeserver",
|
||||
@@ -46,12 +45,16 @@ REQUIRED_KEYS = (
|
||||
"gitea.operators",
|
||||
)
|
||||
OPTIONAL_KEYS = (
|
||||
"roles.driver",
|
||||
"roles.classifier",
|
||||
"llm.max_output_tokens",
|
||||
"llm.request_timeout_s",
|
||||
"loop",
|
||||
)
|
||||
# Settings the old loop had (until M2.8), and what to do instead.
|
||||
RETIRED_KEYS = {
|
||||
"loop": "the loop is the only one now; remove it",
|
||||
"roles.planner": "use roles.driver",
|
||||
"roles.executor": "the driver does the work now; remove it",
|
||||
}
|
||||
# A project entry (D8), under `projects: {name: {...}}`.
|
||||
PROJECT_REQUIRED = ("repo", "workspace", "build_host.address", "build_host.key")
|
||||
PROJECT_OPTIONAL = (
|
||||
@@ -85,7 +88,7 @@ class ConfigError(RuntimeError):
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutorTuning:
|
||||
class DriverTuning:
|
||||
max_turns: int = 40
|
||||
failure_limit: int = 2
|
||||
|
||||
@@ -125,7 +128,7 @@ class LoopTuning:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Tuning:
|
||||
executor: ExecutorTuning = field(default_factory=ExecutorTuning)
|
||||
driver: DriverTuning = field(default_factory=DriverTuning)
|
||||
expert: ExpertTuning = field(default_factory=ExpertTuning)
|
||||
delegation: DelegationTuning = field(default_factory=DelegationTuning)
|
||||
tools: ToolsTuning = field(default_factory=ToolsTuning)
|
||||
@@ -148,6 +151,9 @@ def _parse_tuning(raw: Any, errors: list[str]) -> Tuning:
|
||||
sections: dict[str, Any] = {}
|
||||
for name, section in raw.items():
|
||||
cls = _TUNING_SECTIONS.get(name)
|
||||
if name == "executor": # until M2.8
|
||||
errors.append("tuning.executor: retired — it is tuning.driver now")
|
||||
continue
|
||||
if cls is None:
|
||||
errors.append(f"tuning.{name}: unknown section")
|
||||
continue
|
||||
@@ -474,60 +480,8 @@ class AgentConfig:
|
||||
max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS
|
||||
request_timeout_s: float = DEFAULT_REQUEST_TIMEOUT_S
|
||||
tuning: Tuning = field(default_factory=Tuning)
|
||||
# Which loop runs: "old" (control.py) until the switch-over (M2.8), or
|
||||
# "new" (nisse_dev.loop).
|
||||
loop: str = "old"
|
||||
config_file: Path | None = None
|
||||
|
||||
# -- one project, for the old loop ----------------------------------------
|
||||
# control.py (until M2.8) knows one project; these read it, and refuse a
|
||||
# configuration with more.
|
||||
|
||||
def only_project(self) -> Project:
|
||||
if len(self.projects) != 1:
|
||||
names = ", ".join(p.name for p in self.projects)
|
||||
raise ConfigError(
|
||||
f"cannot start: the old loop runs exactly one project, but "
|
||||
f"{len(self.projects)} are configured ({names}); use `loop: new`"
|
||||
)
|
||||
return self.projects[0]
|
||||
|
||||
def with_project(self, **changes: Any) -> "AgentConfig":
|
||||
"""This configuration with its one project changed (for tests)."""
|
||||
return replace(self, projects=(replace(self.only_project(), **changes),))
|
||||
|
||||
@property
|
||||
def project(self) -> str:
|
||||
return self.only_project().name
|
||||
|
||||
@property
|
||||
def gitea_repo(self) -> str:
|
||||
return self.only_project().repo
|
||||
|
||||
@property
|
||||
def workspace(self) -> Path:
|
||||
return self.only_project().workspace
|
||||
|
||||
@property
|
||||
def build_host(self) -> BuildHost:
|
||||
return self.only_project().build_host
|
||||
|
||||
@property
|
||||
def base_branch(self) -> str:
|
||||
return self.only_project().base_branch
|
||||
|
||||
@property
|
||||
def push_key(self) -> Path | None:
|
||||
return self.only_project().push_key
|
||||
|
||||
@property
|
||||
def push_user(self) -> str | None:
|
||||
return self.only_project().push_user
|
||||
|
||||
@property
|
||||
def push_token(self) -> str | None:
|
||||
return self.only_project().push_token
|
||||
|
||||
@classmethod
|
||||
def load(cls, env: Mapping[str, str] = os.environ) -> "AgentConfig":
|
||||
"""The file named by NISSE_CONFIG, or ./nisse-dev.yaml."""
|
||||
@@ -573,7 +527,10 @@ class AgentConfig:
|
||||
|
||||
known = set(REQUIRED_KEYS) | set(OPTIONAL_KEYS)
|
||||
for key in sorted(set(flat) - known):
|
||||
errors.append(f"{key}: unknown setting")
|
||||
if key in RETIRED_KEYS:
|
||||
errors.append(f"{key}: retired — {RETIRED_KEYS[key]}")
|
||||
else:
|
||||
errors.append(f"{key}: unknown setting")
|
||||
values = {
|
||||
key: _resolve(value, key, env, errors)
|
||||
for key, value in flat.items()
|
||||
@@ -607,9 +564,6 @@ class AgentConfig:
|
||||
raise ValueError
|
||||
except (TypeError, ValueError):
|
||||
errors.append("llm.request_timeout_s: must be a positive number of seconds")
|
||||
loop = str(values.get("loop") or "old")
|
||||
if loop not in ("old", "new"):
|
||||
errors.append("loop: must be old or new")
|
||||
if errors:
|
||||
raise ConfigError("cannot start: " + "; ".join(errors))
|
||||
|
||||
@@ -618,11 +572,9 @@ class AgentConfig:
|
||||
llm_base_url=str(values["llm.base_url"]),
|
||||
llm_token=str(values["llm.token"]),
|
||||
roles=RoleMap(
|
||||
executor=str(values["roles.executor"]),
|
||||
planner=str(values["roles.planner"]),
|
||||
driver=str(values["roles.driver"]),
|
||||
helper=str(values["roles.helper"]),
|
||||
expert=str(values["roles.expert"]),
|
||||
driver=str(values.get("roles.driver") or ""),
|
||||
classifier=str(values.get("roles.classifier") or ""),
|
||||
),
|
||||
matrix=MatrixConfig(
|
||||
@@ -640,7 +592,6 @@ class AgentConfig:
|
||||
max_output_tokens=max_output,
|
||||
request_timeout_s=request_timeout,
|
||||
tuning=tuning,
|
||||
loop=loop,
|
||||
config_file=config_file,
|
||||
)
|
||||
config._refuse_secrets_inside_the_clone(env)
|
||||
@@ -685,7 +636,6 @@ class AgentConfig:
|
||||
f"{p.name} ({p.repo}, {p.build_host.address}:{p.workspace})"
|
||||
for p in self.projects
|
||||
),
|
||||
"planner": self.roles.planner,
|
||||
"executor": self.roles.executor,
|
||||
"driver": self.roles.driver,
|
||||
"helper": self.roles.helper,
|
||||
}
|
||||
Reference in new issue
Block a user