Roles, capability profiles and help on the new loop (M2.6)
- roles.driver (unset: the planner's model) and roles.classifier (unset: the expert's); the mapping is pinned when an item opens, and the driver and the go-ahead reader run on the item's own (L9). - profiles: in configuration (tools, strengths, weaknesses), used in briefings: the driver's instructions say what its helper and expert are good at, the helper's brief who reads its handback; never a model name. - delegate(task) and ask_expert(question) for the driver. A helper works on a branch off the milestone's; on a handback its work comes back as uncommitted changes, otherwise it is discarded, so harness branches never hold commits only the clone has. ask_expert keeps its budget. - Triggers: failure_limit failed commands in a row (Driver's new "stuck" outcome) or the turn cap, mid-milestone, call a helper in once per milestone; if it fails too, the item waits on the operator. A delegation cut off by a crash is discarded at boot. - Evaluation capture: delegations and ask_expert as before, and the reader's verdicts as classifier pairs. Tests: help tests (merge-back, discard, needs a milestone, the stuck trigger and its once-per-milestone limit, the expert and its budget, pinning, briefings by profile, boot discarding a cut-off delegation), config tests for roles and profiles, and the walk delegating with a scripted helper that sometimes fails, with a delegations_settle property. The walk found helper commits stranded on a merged PR's branch, fixed by returning a helper's work uncommitted. 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:
1 parent
04c352b30c
commit
59fd00ce8c
19 files changed
+890
-38
No files matched your search
@@ -46,6 +46,8 @@ REQUIRED_KEYS = (
|
||||
"gitea.operators",
|
||||
)
|
||||
OPTIONAL_KEYS = (
|
||||
"roles.driver",
|
||||
"roles.classifier",
|
||||
"llm.max_output_tokens",
|
||||
"llm.request_timeout_s",
|
||||
"loop",
|
||||
@@ -379,6 +381,48 @@ class BuildHost:
|
||||
port: int = 22
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Profile:
|
||||
"""A model's capability profile (L9), from benchmarks: what one role is
|
||||
told about another, never the model's name."""
|
||||
|
||||
tools: str = "" # how reliably it keeps the tool-call contract
|
||||
strengths: str = ""
|
||||
weaknesses: str = ""
|
||||
|
||||
def describe(self) -> str:
|
||||
parts = [
|
||||
f"tool calls: {self.tools}" if self.tools else "",
|
||||
f"strong at {self.strengths}" if self.strengths else "",
|
||||
f"weak at {self.weaknesses}" if self.weaknesses else "",
|
||||
]
|
||||
return "; ".join(p for p in parts if p) or "no profile recorded"
|
||||
|
||||
|
||||
def _parse_profiles(raw: Any, errors: list[str]) -> dict[str, "Profile"]:
|
||||
"""`profiles:`, model alias -> {tools, strengths, weaknesses}."""
|
||||
if raw is None:
|
||||
return {}
|
||||
if not isinstance(raw, dict):
|
||||
errors.append("profiles: must be a mapping of model alias to profile")
|
||||
return {}
|
||||
profiles: dict[str, Profile] = {}
|
||||
for model, spec in raw.items():
|
||||
where = f"profiles.{model}"
|
||||
if not isinstance(spec, dict):
|
||||
errors.append(f"{where}: must be a mapping")
|
||||
continue
|
||||
unknown = set(spec) - {"tools", "strengths", "weaknesses"}
|
||||
if unknown:
|
||||
errors.append(f"{where}.{min(unknown)}: unknown setting")
|
||||
continue
|
||||
if not all(isinstance(v, str) for v in spec.values()):
|
||||
errors.append(f"{where}: values are text")
|
||||
continue
|
||||
profiles[str(model)] = Profile(**{k: v.strip() for k, v in spec.items()})
|
||||
return profiles
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Secret:
|
||||
value: str = field(repr=False)
|
||||
@@ -426,6 +470,7 @@ class AgentConfig:
|
||||
gitea_token: str = field(repr=False)
|
||||
gitea_operators: frozenset[str]
|
||||
projects: tuple[Project, ...]
|
||||
profiles: Mapping[str, Profile] = field(default_factory=dict)
|
||||
max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS
|
||||
request_timeout_s: float = DEFAULT_REQUEST_TIMEOUT_S
|
||||
tuning: Tuning = field(default_factory=Tuning)
|
||||
@@ -520,7 +565,10 @@ class AgentConfig:
|
||||
) -> "AgentConfig":
|
||||
tuning = _parse_tuning(tree.get("tuning"), errors)
|
||||
projects = _parse_projects(tree.get("projects"), env, errors)
|
||||
flat = _flatten({k: v for k, v in tree.items() if k != "projects"})
|
||||
profiles = _parse_profiles(tree.get("profiles"), errors)
|
||||
flat = _flatten(
|
||||
{k: v for k, v in tree.items() if k not in ("projects", "profiles")}
|
||||
)
|
||||
flat.pop("tuning", None)
|
||||
|
||||
known = set(REQUIRED_KEYS) | set(OPTIONAL_KEYS)
|
||||
@@ -574,6 +622,8 @@ class AgentConfig:
|
||||
planner=str(values["roles.planner"]),
|
||||
helper=str(values["roles.helper"]),
|
||||
expert=str(values["roles.expert"]),
|
||||
driver=str(values.get("roles.driver") or ""),
|
||||
classifier=str(values.get("roles.classifier") or ""),
|
||||
),
|
||||
matrix=MatrixConfig(
|
||||
homeserver=str(values["matrix.homeserver"]).rstrip("/"),
|
||||
@@ -586,6 +636,7 @@ class AgentConfig:
|
||||
gitea_token=str(values["gitea.token"]),
|
||||
gitea_operators=_people(values["gitea.operators"]),
|
||||
projects=projects,
|
||||
profiles=profiles,
|
||||
max_output_tokens=max_output,
|
||||
request_timeout_s=request_timeout,
|
||||
tuning=tuning,
|
||||
|
||||
Reference in new issue
Block a user