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

+1 -1
View File
@@ -263,7 +263,7 @@ def test_a_tool_less_model_warns_but_still_boots(tmp_path: Path, monkeypatch) ->
logged = stream.getvalue()
assert "model_tools_unsupported" in logged
assert '"role": "executor"' in logged
assert '"role": "consultant"' not in logged # ask_expert drives no tools
assert '"role": "expert"' not in logged # ask_expert drives no tools
def test_the_resolved_model_is_pinned_at_task_start(
+1 -1
View File
@@ -20,7 +20,7 @@ from nisse_dev.planner import LLMPlanningAgent
from nisse_dev.state import StateStore
from nisse_dev.transport import InMemoryTransport
ROLES = RoleMap(executor="x", planner="y", delegate="y", consultant="y")
ROLES = RoleMap(executor="x", planner="y", helper="y", expert="y")
class FakeGitea:
+1 -1
View File
@@ -27,7 +27,7 @@ ALL_EVENTS = [
GateOpened(gate="arch", reason="plan submitted (rev-1)"),
DelegationStarted(
brief="fix the flaky test",
role="delegate",
role="helper",
model="claude-opus-5",
caps={"turns": 20, "tokens": 100000},
),
+3 -3
View File
@@ -130,13 +130,13 @@ def test_validate_names_every_bad_role_at_once() -> None:
{
"executor": "local-fast",
"planner": "clause-opus-5",
"delegate": "remote-architecture",
"consultant": "local-fast",
"helper": "remote-architecture",
"expert": "local-fast",
}
)
message = str(caught.value)
assert "planner='clause-opus-5'" in message
assert "delegate='remote-architecture'" in message
assert "helper='remote-architecture'" in message
assert "executor" not in message # the good ones are not noise
+4 -4
View File
@@ -16,8 +16,8 @@ COMPLETE: dict[str, Any] = {
"roles": {
"executor": "claude-haiku-4-5",
"planner": "claude-opus-5",
"delegate": "claude-opus-5",
"consultant": "claude-haiku-4-5",
"helper": "claude-opus-5",
"expert": "claude-haiku-4-5",
},
"matrix": {
"homeserver": "https://matrix.test/",
@@ -210,8 +210,8 @@ llm: {base_url: "https://litellm.test", token: "${TEST_LLM_TOKEN}"}
roles:
executor: local-fast
planner: claude-opus-5
delegate: claude-opus-5
consultant: claude-haiku-4-5
helper: claude-opus-5
expert: claude-haiku-4-5
matrix:
homeserver: https://matrix.test
user_id: "@nisse-dev-testing:test"
+2 -2
View File
@@ -262,7 +262,7 @@ def test_the_executor_sees_only_its_milestone(tmp_path: Path) -> None:
seen["system"] = system
return [AssistantText(text="done", model=model)]
roles = RoleMap(executor="m", planner="m", delegate="m", consultant="m")
roles = RoleMap(executor="m", planner="m", helper="m", expert="m")
LLMExecutor(Client(), store, roles, Workspace()).run(1)
texts = [getattr(e, "text", None) for e in seen["events"]]
@@ -286,7 +286,7 @@ def test_a_request_that_cannot_fit_stops_the_milestone(tmp_path: Path) -> None:
inner = RecordingClient()
client = BudgetedClient(inner, lambda m: (4096, 1024))
roles = RoleMap(executor="m", planner="m", delegate="m", consultant="m")
roles = RoleMap(executor="m", planner="m", helper="m", expert="m")
outcome = LLMExecutor(client, store, roles, Workspace()).run(1)
assert outcome.status == "escalate"
+8 -24
View File
@@ -385,9 +385,7 @@ def test_clean_cut_after_a_tool_result_is_resumable(
repo.checkout_new_branch("d")
(repo.root / "partial.py").write_text("x")
repo.commit_all("partial work")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolCall(call_id="c", tool="write_file", arguments={}))
store.append_event(ToolResult(call_id="c", result="wrote", success=True))
intent = pending_delegation_intent(store)
@@ -400,9 +398,7 @@ def test_dangling_tool_call_gates(
) -> None:
# The effect of the call is unknown and not verifiable by inspection.
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolCall(call_id="c", tool="run_command", arguments={}))
intent = pending_delegation_intent(store)
@@ -412,9 +408,7 @@ def test_dangling_tool_call_gates(
def test_missing_branch_means_nothing_started(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolResult(call_id="c", result="ok", success=True))
intent = pending_delegation_intent(store, branch="never-created")
@@ -425,9 +419,7 @@ def test_a_finished_delegation_settles_rather_than_resumes(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(
DelegationCompleted(
handback="done",
@@ -448,9 +440,7 @@ def test_resume_continues_the_same_delegation(
# Interrupt after one turn, then resume: no second delegation_started,
# because L7 defines a session as the delegation, not the process.
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolResult(call_id="c", result="wrote a.py", success=True))
intent = pending_delegation_intent(store)
@@ -484,9 +474,7 @@ def test_recover_resumes_a_clean_cut_with_a_notice(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolResult(call_id="c", result="wrote a.py", success=True))
pending_delegation_intent(store)
notices: list[str] = []
@@ -505,9 +493,7 @@ def test_recover_settles_a_finished_delegation_without_resuming(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(
DelegationCompleted(
handback="done",
@@ -539,9 +525,7 @@ def test_recover_leaves_an_unclean_cut_for_the_boot_gate(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(ToolCall(call_id="c", tool="run_command", arguments={}))
pending_delegation_intent(store)
+6 -8
View File
@@ -24,7 +24,7 @@ def record_pair(
*,
brief: str = "add greet",
model: str = "claude-haiku-4-5",
role: str = "delegate",
role: str = "helper",
status: str = "handback",
) -> int:
store.append_event(
@@ -56,15 +56,13 @@ def test_unpaired_start_is_skipped(store: StateStore) -> None:
# A crash mid-delegation leaves a start with no completion; a half-formed
# record would pollute the set L9 exists to make trustworthy.
store.append_event(
DelegationStarted(brief="died", role="delegate", model="m", caps={})
DelegationStarted(brief="died", role="helper", model="m", caps={})
)
assert collect(store) == []
def test_unrelated_events_between_the_pair_are_ignored(store: StateStore) -> None:
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
)
store.append_event(DelegationStarted(brief="b", role="helper", model="m", caps={}))
store.append_event(AssistantText(text="working", model="m"))
store.append_event(
DelegationCompleted(
@@ -97,10 +95,10 @@ def test_a_verdict_can_be_revised(store: StateStore) -> None:
def test_consults_and_delegations_share_the_set(store: StateStore) -> None:
record_pair(store, role="delegate")
record_pair(store, role="consultant", brief="which approach?")
record_pair(store, role="helper")
record_pair(store, role="expert", brief="which approach?")
roles = [r.role for r in collect(store)]
assert roles == ["delegate", "consultant"]
assert roles == ["helper", "expert"]
def test_collect_can_start_after_a_marker(store: StateStore) -> None:
+2 -2
View File
@@ -20,8 +20,8 @@ from nisse_dev.tools import Workspace
ROLES = RoleMap(
executor="llama3.2",
planner="claude-haiku-4-5",
delegate="claude-haiku-4-5",
consultant="claude-haiku-4-5",
helper="claude-haiku-4-5",
expert="claude-haiku-4-5",
)
+3 -3
View File
@@ -25,8 +25,8 @@ from nisse_dev.state import StateStore
ROLES = RoleMap(
executor="llama3.2",
planner="haiku",
delegate="haiku",
consultant="claude-haiku-4-5",
helper="haiku",
expert="claude-haiku-4-5",
)
GOOD = '{"answer": "Use SQLite; it is enough here.", "confidence": "high", "caveats": "Assumes one writer."}'
@@ -137,7 +137,7 @@ def test_consults_are_recorded_as_composite_events(store: StateStore) -> None:
events = [e.event for e in store.read_events()]
started = next(e for e in events if isinstance(e, DelegationStarted))
completed = next(e for e in events if isinstance(e, DelegationCompleted))
assert started.role == "consultant"
assert started.role == "expert"
assert started.brief == "q"
assert completed.exit_status == "answered"
+4 -4
View File
@@ -184,8 +184,8 @@ def test_malformed_response_raises_llm_error() -> None:
ROLES = RoleMap(
executor="local/qwen",
planner="anthropic/claude-opus-5",
delegate="anthropic/claude-opus-5",
consultant="anthropic/claude-opus-5",
helper="anthropic/claude-opus-5",
expert="anthropic/claude-opus-5",
)
@@ -195,8 +195,8 @@ def test_role_map_resolves_and_pins() -> None:
assert roles.pinned() == {
"executor": "local/qwen",
"planner": "anthropic/claude-opus-5",
"delegate": "anthropic/claude-opus-5",
"consultant": "anthropic/claude-opus-5",
"helper": "anthropic/claude-opus-5",
"expert": "anthropic/claude-opus-5",
}
+2 -2
View File
@@ -29,8 +29,8 @@ from nisse_dev.transport import InMemoryTransport
ROLES = RoleMap(
executor="local/qwen",
planner="anthropic/claude-opus-5",
delegate="anthropic/claude-opus-5",
consultant="anthropic/claude-opus-5",
helper="anthropic/claude-opus-5",
expert="anthropic/claude-opus-5",
)
+2 -2
View File
@@ -27,8 +27,8 @@ from nisse_dev.transport import InMemoryTransport
ROLES = RoleMap(
executor="llama3.2",
planner="haiku",
delegate="haiku",
consultant="haiku",
helper="haiku",
expert="haiku",
)
+1 -1
View File
@@ -15,7 +15,7 @@ from nisse_dev.publish import PublishResult
from nisse_dev.state import StateStore
from nisse_dev.transport import InMemoryTransport
ROLES = RoleMap(executor="x", planner="y", delegate="y", consultant="y")
ROLES = RoleMap(executor="x", planner="y", helper="y", expert="y")
class ScriptedClient:
+2 -2
View File
@@ -31,8 +31,8 @@ from nisse_dev.worker import SerialWorker
ROLES = RoleMap(
executor="llama3.2",
planner="claude-haiku-4-5",
delegate="claude-haiku-4-5",
consultant="claude-haiku-4-5",
helper="claude-haiku-4-5",
expert="claude-haiku-4-5",
)