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
77 lines
3.2 KiB
Python
77 lines
3.2 KiB
Python
"""Help for the driver (L9): a helper for a sub-task (delegation), an expert
|
|
for one question, both by role, briefed from capability profiles.
|
|
|
|
The driver asks when it chooses to (its instructions treat asking as
|
|
normal), and the harness asks for it on observable triggers: commands failing
|
|
in a row, or the turn cap. Delegation runs on a branch off the milestone's,
|
|
merged back on a handback and discarded otherwise (L9: never build on a
|
|
half-edited tree).
|
|
"""
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from dataclasses import dataclass, field
|
|
|
|
from nisse_dev.config import Profile
|
|
from nisse_dev.delegation import Caps
|
|
from nisse_dev.expert import DEFAULT_BUDGET
|
|
from nisse_dev.llm import LLMClient, RoleMap
|
|
from nisse_dev.loop.tools import _tool
|
|
|
|
DELEGATE = _tool(
|
|
"delegate",
|
|
"Hand a sub-task of the current milestone to a helper, who works on the "
|
|
"milestone's branch with the same tools and reports back what changed, "
|
|
"what is now true and what next. Use it when the sub-task is beyond what "
|
|
"you can do reliably, or you are going round in circles. Needs a "
|
|
"milestone started.",
|
|
task="The sub-task, self-contained: what to do, where, and how to know it's done.",
|
|
)
|
|
ASK_EXPERT = _tool(
|
|
"ask_expert",
|
|
"Ask an expert one self-contained question: a design trade-off, how a "
|
|
"library behaves. It can't see the repository. The budget per item is "
|
|
"small.",
|
|
question="The question, with whatever context it needs.",
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Helpers:
|
|
client: LLMClient # for the expert
|
|
roles: RoleMap # the configured mapping; each item pins its own (L9)
|
|
# The helper's client by model (vendor passthrough where it fits, L9);
|
|
# unset, ``client``.
|
|
client_for: Callable[[str], LLMClient] | None = None
|
|
# What an alias resolves to, recorded with each delegation: eval history
|
|
# keyed on an alias alone averages over whatever it pointed at (L9).
|
|
backing: Callable[[str], str] | None = None
|
|
caps: Caps = field(default_factory=Caps)
|
|
expert_budget: int = DEFAULT_BUDGET
|
|
profiles: Mapping[str, Profile] = field(default_factory=dict)
|
|
|
|
def delegate_client(self, model: str) -> LLMClient:
|
|
return self.client_for(model) if self.client_for else self.client
|
|
|
|
def profile(self, model: str) -> str:
|
|
"""What one role is told about another: its capability profile,
|
|
never its name (L9)."""
|
|
found = self.profiles.get(model)
|
|
return found.describe() if found else "no profile recorded"
|
|
|
|
def briefing(self, roles: RoleMap) -> str:
|
|
"""The driver's instructions on asking for help."""
|
|
return (
|
|
"Asking for help is normal, not a failure. delegate hands a "
|
|
"sub-task to a helper ("
|
|
+ self.profile(roles.resolve("helper"))
|
|
+ "); ask_expert asks one question of an expert ("
|
|
+ self.profile(roles.resolve("expert"))
|
|
+ "). If commands keep failing, a helper is called in for you."
|
|
)
|
|
|
|
def reader(self, roles: RoleMap) -> str:
|
|
"""Who acts on a helper's handback: the driver, by its profile."""
|
|
return "the driver, a model with this profile: " + self.profile(
|
|
roles.resolve("driver")
|
|
)
|