Rename the delegate and consultant roles to helper and expert

Architecture L9's role names, for the two roles with a direct
counterpart: the delegation role is now 'helper' and the ask_expert role
'expert', in RoleMap, configuration (roles.helper, roles.expert),
recorded events and tests. 'planner' and 'executor' stay distinct for
now; they become 'driver' when the new loop merges them (Development
Plan M2.3).

Configurations must rename their keys: nisse-dev.example.yaml and
tests/integration/nisse-test.yaml are updated here.

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:
dongarandClaude Opus 5.5 committed 2026-10-01 23:04:37 +00:00
1 parent e6aaa4783d
commit 3cf4cefacd
27 files changed
+68 -89

No files matched your search

+2 -2
View File
@@ -37,7 +37,7 @@ from nisse_dev.worker import SerialWorker
# manager allows for a stop, and a hard exit mid-work is already safe.
SHUTDOWN_WAIT_S = 10.0
TOOL_DRIVING_ROLES = ("planner", "executor", "delegate")
TOOL_DRIVING_ROLES = ("planner", "executor", "helper")
def discover_models(config: AgentConfig, oplog: OpLog) -> ModelCatalog:
@@ -220,7 +220,7 @@ def build_agent(
expert = ExpertDesk(
normalized, store, config.roles, budget=tuning.expert.consult_budget
)
delegate_model = config.roles.resolve("delegate")
delegate_model = config.roles.resolve("helper")
delegation = Delegation(
select_client(delegate_model, normalized, passthrough, catalog),
store,
+1 -1
View File
@@ -174,7 +174,7 @@ def run_task(
meter = MeteringClient(client, facts)
trims_before = getattr(client, "trimmed_requests", 0)
model = facts.alias
roles = RoleMap(executor=model, planner=model, delegate=model, consultant=model)
roles = RoleMap(executor=model, planner=model, helper=model, expert=model)
executor = LLMExecutor(
meter,
store,
+5 -5
View File
@@ -36,8 +36,8 @@ REQUIRED_KEYS = (
"llm.token",
"roles.executor",
"roles.planner",
"roles.delegate",
"roles.consultant",
"roles.helper",
"roles.expert",
"matrix.homeserver",
"matrix.user_id",
"matrix.token",
@@ -312,8 +312,8 @@ class AgentConfig:
roles=RoleMap(
executor=str(values["roles.executor"]),
planner=str(values["roles.planner"]),
delegate=str(values["roles.delegate"]),
consultant=str(values["roles.consultant"]),
helper=str(values["roles.helper"]),
expert=str(values["roles.expert"]),
),
matrix=MatrixConfig(
homeserver=str(values["matrix.homeserver"]).rstrip("/"),
@@ -371,5 +371,5 @@ class AgentConfig:
"repo": self.gitea_repo,
"planner": self.roles.planner,
"executor": self.roles.executor,
"delegate": self.roles.delegate,
"helper": self.roles.helper,
}
+1 -3
View File
@@ -184,9 +184,7 @@ class Delegation:
# a reason, and the next step is to talk about that reason.
self._cancel = cancel or Cancel()
def run(
self, *, brief: str, branch: str, role: str = "delegate"
) -> DelegationResult:
def run(self, *, brief: str, branch: str, role: str = "helper") -> DelegationResult:
started_at = self._now()
base_branch = self._git.current_branch()
# Recorded before the branch exists, so a crash anywhere after this
+1 -1
View File
@@ -165,7 +165,7 @@ class LLMExecutor:
# that it is circling and should get on with the work or fail.
return ToolOutcome(str(exc), success=False)
except LLMError as exc:
return ToolOutcome(f"the consultant did not answer: {exc}", success=False)
return ToolOutcome(f"the expert did not answer: {exc}", success=False)
def _scope(self) -> tuple[str | None, int]:
"""The approved plan, and where this milestone's events begin.
+5 -6
View File
@@ -17,7 +17,7 @@ memory: it survives a restart, and it is observable from outside the model,
which is the same reasoning L9 applies to escalation triggers.
Consults are recorded as ``delegation_started`` / ``delegation_completed`` with
role ``consultant``. That is deliberate — L9 wants a "consult/delegation eval
role ``expert``. That is deliberate — L9 wants a "consult/delegation eval
set", and reusing the composite events gets consult evaluation for free rather
than inventing a parallel record.
"""
@@ -35,7 +35,7 @@ from nisse_dev.canonical import (
from nisse_dev.llm import LLMClient, LLMError, RoleMap
from nisse_dev.state import StateStore
CONSULTANT_ROLE = "consultant"
EXPERT_ROLE = "expert"
DEFAULT_BUDGET = 3
SYSTEM_PROMPT = """You are a consultant answering one self-contained question.
@@ -111,8 +111,7 @@ class ExpertDesk:
return sum(
1
for e in self._store.read_events(after=since_event_id)
if isinstance(e.event, DelegationStarted)
and e.event.role == CONSULTANT_ROLE
if isinstance(e.event, DelegationStarted) and e.event.role == EXPERT_ROLE
)
def ask(self, question: str, *, since_event_id: int = 0) -> ExpertAnswer:
@@ -122,11 +121,11 @@ class ExpertDesk:
f"consult budget of {self._budget} is spent for this task "
f"({used} used); escalate or ask the operator instead"
)
model = self._roles.resolve(CONSULTANT_ROLE)
model = self._roles.resolve(EXPERT_ROLE)
self._store.append_event(
DelegationStarted(
brief=question,
role=CONSULTANT_ROLE,
role=EXPERT_ROLE,
model=model,
caps={"consults": self._budget, "used": used},
)
+3 -3
View File
@@ -109,10 +109,10 @@ class LLMClient(Protocol):
class RoleMap:
executor: str
planner: str
delegate: str
consultant: str
helper: str
expert: str
ROLES: ClassVar[tuple[str, ...]] = ("executor", "planner", "delegate", "consultant")
ROLES: ClassVar[tuple[str, ...]] = ("executor", "planner", "helper", "expert")
def resolve(self, role: str) -> str:
if role not in self.ROLES: