Files
nisse/nisse-dev/src/nisse_dev/loop/help.py
T
dongarandClaude Opus 5.5 6ae2375194
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m16s
CI / e2e (pull_request) Successful in 10m51s
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
2026-10-08 03:12:58 +00:00

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")
)