feat: adopt the AGENTS.md expert-skill contract (#404)

* feat: detect expert skills by AGENTS.md presence

* feat: let the orchestrator choose the expert dispatch tool

* refactor: drop per-skill expert dispatch classification

* chore: remove duplicate TestSkillManager test classes left by rebase

* fix: clear legacy actor fields when AGENTS.md declares the expert

* refactor: move the expert prompt into ActiveTeamMiddleware
This commit is contained in:
jfilipiuk
2026-08-05 11:50:32 +02:00
committed by Xi Zhang
parent bd2464423a
commit ab1a6b0062
15 changed files with 971 additions and 822 deletions
+17 -19
View File
@@ -50,7 +50,6 @@ class _FakeSkillInfo:
name: str
description: str = ""
role: str = ""
default_dispatch: str = ""
type: str = "expert"
tags: list[str] = field(default_factory=list)
source: str = "builtin"
@@ -58,6 +57,11 @@ class _FakeSkillInfo:
# ``list_dispatchable_experts``. Tests that specifically want to
# exercise the empty-body reject path pass ``body=""``.
body: str = "persona"
# Legacy-frontmatter expert by default: ``expert_prompt_body`` reads
# ``body`` for these. Set ``expert_source="agents_md"`` plus
# ``agents_body`` to fake an expert on the current contract.
expert_source: str = "frontmatter"
agents_body: str = ""
def _make_ctx(active_teams: list[str] | None = None) -> tuple[CommandContext, _FakeUI]:
@@ -83,7 +87,6 @@ class TestExpertsList:
_FakeSkillInfo(
name="idea-brainstorm",
role="Research idea brainstormer",
default_dispatch="sync",
),
],
):
@@ -110,7 +113,6 @@ class TestExpertsList:
_FakeSkillInfo(
name="idea-brainstorm",
role="Research idea brainstormer",
default_dispatch="sync",
),
],
):
@@ -136,31 +138,27 @@ class TestExpertToggle:
)
assert ctx.channel_runtime.active_teams == []
async def test_async_expert_refused_with_reason_when_async_unavailable(self):
"""When an installed expert declares ``default_dispatch: async`` but
async dispatch is unavailable, ``/expert`` must refuse with the specific
reason — not the empty-body / name-collision default — so the user
knows to enable ``enable_async_subagents`` or start langgraph dev.
Reviewer thread on PR #391."""
ctx, ui = _make_ctx()
async_expert = _FakeSkillInfo(
name="literature-review", default_dispatch="async"
)
async def test_async_outage_does_not_block_invite(self):
"""An async outage must not make an installed expert un-invitable.
The old per-skill classification refused here whenever
``enable_async_subagents`` was off or langgraph dev was unreachable.
Every expert now keeps its in-turn reach, so the outage degrades the
reach rather than removing the expert.
"""
ctx, _ui = _make_ctx()
with (
patch(
"EvoScientist.tools.skills_manager.list_expert_skills",
return_value=[async_expert],
return_value=[_FakeSkillInfo(name="literature-review")],
),
patch(
"EvoScientist.subagents.expert_container.is_async_dispatch_available",
"EvoScientist.langgraph_dev.manager.is_async_subagents_available",
return_value=False,
),
):
await ExpertCommand().execute(ctx, args=["literature-review"])
assert any("async dispatch is unavailable" in text for text, _ in ui.lines), (
f"expected honest async-unavailable message, got: {ui.lines}"
)
assert ctx.channel_runtime.active_teams == []
assert ctx.channel_runtime.active_teams == ["literature-review"]
async def test_invite_adds_to_active_teams(self):
ctx, ui = _make_ctx()