Shared parts, not a shared scheduler (agreed): nisse-review will have its own small scheduler; a common one waits until two exist to compare. - nisse_core.loop.driver: the driver run, moved; the agent names its waiting tools per run (nisse-dev's are WAITING_TOOLS), and execution goes to any Executor (ToolOutcome moves with it). - nisse_core.loop.tools: the tool helper and the tools any agent offers (notes, memory, memory_done, ask_expert); nisse_core.loop.expert. - nisse_core.knowledge.handlers: Knowledge, the vault and memory as control tools, the index and prompt text, out of LoopAgent. - nisse_core.config: file reading, references, people, tuning sections of the agent's own, LLM numbers, the vault. - New core tests: a driver run with a non-workspace executor and the agent's own waiting set; Knowledge alone; the shared config pieces. Docs: the Development Plan marks M7.2 done; Architecture Part L and the nisse-core README name the new parts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
70 lines
2.9 KiB
Python
70 lines
2.9 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_core.loop.expert import DEFAULT_BUDGET
|
|
from nisse_core.loop.tools import tool
|
|
from nisse_core.models.llm import LLMClient, RoleMap
|
|
from nisse_dev.config import Profile
|
|
from nisse_dev.delegation import Caps
|
|
|
|
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.",
|
|
)
|
|
|
|
|
|
@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")
|
|
)
|