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