feat: add agent registry manifests and coverage

Introduce a manifest-backed agent registry surface and align graph tests with the new runtime prompt and tool indexing behavior.
This commit is contained in:
2026-04-02 14:34:26 +08:00
parent e9ba8597e9
commit 4251a79062
12 changed files with 1111 additions and 423 deletions

View File

@@ -0,0 +1,11 @@
"""Registry manifest models and validation helpers."""
from app.agents.registry.indexes import RegistryIndexes, build_registry_indexes
from app.agents.registry.loader import RegistryBundle, load_builtin_registry_bundle
__all__ = [
"RegistryBundle",
"RegistryIndexes",
"build_registry_indexes",
"load_builtin_registry_bundle",
]

View File

@@ -0,0 +1,114 @@
from app.agents.prompts import SUB_COMMANDER_PROMPTS_BY_KEY
from app.agents.registry.models import (
AgentManifest,
CapabilityManifest,
SpecialistTemplateManifest,
SubCommanderManifest,
)
from app.agents.state import AgentRole
from app.agents.tools import SUB_COMMANDER_TOOLSETS
TOP_LEVEL_AGENT_DEFAULT_SUB_COMMANDERS: dict[str, tuple[str, ...]] = {
AgentRole.MASTER.value: (),
AgentRole.SCHEDULE_PLANNER.value: (
"schedule_analysis",
"schedule_planning",
),
AgentRole.EXECUTOR.value: (
"executor_tasks",
"executor_forum",
),
AgentRole.LIBRARIAN.value: (
"librarian_retrieval",
"librarian_graph",
),
AgentRole.ANALYST.value: (
"analyst_progress",
"analyst_insights",
),
}
TOP_LEVEL_AGENT_DISPLAY_NAMES: dict[str, str] = {
AgentRole.MASTER.value: "Master",
AgentRole.SCHEDULE_PLANNER.value: "Schedule Planner",
AgentRole.EXECUTOR.value: "Executor",
AgentRole.LIBRARIAN.value: "Librarian",
AgentRole.ANALYST.value: "Analyst",
}
TOP_LEVEL_AGENT_ROUTING_HINTS: dict[str, tuple[str, ...]] = {
AgentRole.MASTER.value: (
"Route user requests to the most suitable top-level runtime agent or answer directly.",
),
AgentRole.SCHEDULE_PLANNER.value: (
"Handle planning-oriented requests using schedule analysis and schedule planning sub-commanders.",
),
AgentRole.EXECUTOR.value: (
"Handle execution-oriented requests using task and forum sub-commanders.",
),
AgentRole.LIBRARIAN.value: (
"Handle knowledge retrieval and graph-context requests using librarian sub-commanders.",
),
AgentRole.ANALYST.value: (
"Handle reporting and insight requests using analyst sub-commanders.",
),
}
SUB_COMMANDER_PARENT_AGENT_IDS: dict[str, str] = {
"schedule_analysis": AgentRole.SCHEDULE_PLANNER.value,
"schedule_planning": AgentRole.SCHEDULE_PLANNER.value,
"executor_tasks": AgentRole.EXECUTOR.value,
"executor_forum": AgentRole.EXECUTOR.value,
"librarian_retrieval": AgentRole.LIBRARIAN.value,
"librarian_graph": AgentRole.LIBRARIAN.value,
"analyst_progress": AgentRole.ANALYST.value,
"analyst_insights": AgentRole.ANALYST.value,
}
BUILTIN_AGENT_MANIFESTS: tuple[AgentManifest, ...] = tuple(
AgentManifest(
agent_id=role.value,
display_name=TOP_LEVEL_AGENT_DISPLAY_NAMES[role.value],
role_value=role.value,
system_prompt_key=role.value,
routing_hints=list(TOP_LEVEL_AGENT_ROUTING_HINTS[role.value]),
default_sub_commanders=list(TOP_LEVEL_AGENT_DEFAULT_SUB_COMMANDERS[role.value]),
skill_context_key=role.value.replace("agent_", ""),
)
for role in AgentRole
)
_capability_tool_names = tuple(
dict.fromkeys(
tool.name
for tools in SUB_COMMANDER_TOOLSETS.values()
for tool in tools
)
)
BUILTIN_CAPABILITY_MANIFESTS: tuple[CapabilityManifest, ...] = tuple(
CapabilityManifest(
capability_id=tool_name,
tool_name=tool_name,
)
for tool_name in _capability_tool_names
)
BUILTIN_SUB_COMMANDER_MANIFESTS: tuple[SubCommanderManifest, ...] = tuple(
SubCommanderManifest(
sub_commander_id=sub_commander_id,
parent_agent_id=SUB_COMMANDER_PARENT_AGENT_IDS[sub_commander_id],
prompt_text=SUB_COMMANDER_PROMPTS_BY_KEY[sub_commander_id],
capability_ids=list(
dict.fromkeys(tool.name for tool in tools)
),
)
for sub_commander_id, tools in SUB_COMMANDER_TOOLSETS.items()
)
BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS: tuple[SpecialistTemplateManifest, ...] = ()

View File

@@ -0,0 +1,76 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from types import MappingProxyType
from app.agents.registry.loader import RegistryBundle
from app.agents.registry.models import (
AgentManifest,
CapabilityManifest,
SpecialistTemplateManifest,
SubCommanderManifest,
)
@dataclass(frozen=True)
class RegistryIndexes:
agent_by_id: Mapping[str, AgentManifest]
sub_commander_by_id: Mapping[str, SubCommanderManifest]
capability_by_id: Mapping[str, CapabilityManifest]
specialist_template_by_id: Mapping[str, SpecialistTemplateManifest]
agent_prompt_key_by_id: Mapping[str, str]
sub_commander_prompt_key_by_id: Mapping[str, str]
skill_context_key_by_agent_id: Mapping[str, str]
capability_id_by_tool_name: Mapping[str, str]
capability_ids_by_sub_commander_id: Mapping[str, tuple[str, ...]]
def summarize_registry_indexes(indexes: RegistryIndexes) -> dict[str, int]:
return {
"agent_count": len(indexes.agent_by_id),
"sub_commander_count": len(indexes.sub_commander_by_id),
"capability_count": len(indexes.capability_by_id),
"specialist_template_count": len(indexes.specialist_template_by_id),
}
def build_registry_indexes(bundle: RegistryBundle) -> RegistryIndexes:
agent_by_id = {agent.agent_id: agent for agent in bundle.agents}
sub_commander_by_id = {
sub_commander.sub_commander_id: sub_commander
for sub_commander in bundle.sub_commanders
}
capability_by_id = {
capability.capability_id: capability for capability in bundle.capabilities
}
specialist_template_by_id = {
template.template_id: template for template in bundle.specialist_templates
}
return RegistryIndexes(
agent_by_id=MappingProxyType(agent_by_id),
sub_commander_by_id=MappingProxyType(sub_commander_by_id),
capability_by_id=MappingProxyType(capability_by_id),
specialist_template_by_id=MappingProxyType(specialist_template_by_id),
agent_prompt_key_by_id=MappingProxyType({
agent.agent_id: agent.system_prompt_key for agent in bundle.agents
}),
sub_commander_prompt_key_by_id=MappingProxyType({
sub_commander.sub_commander_id: sub_commander.sub_commander_id
for sub_commander in bundle.sub_commanders
}),
skill_context_key_by_agent_id=MappingProxyType({
agent.agent_id: agent.skill_context_key
for agent in bundle.agents
if agent.skill_context_key is not None
}),
capability_id_by_tool_name=MappingProxyType({
capability.tool_name: capability.capability_id
for capability in bundle.capabilities
}),
capability_ids_by_sub_commander_id=MappingProxyType({
sub_commander.sub_commander_id: tuple(sub_commander.capability_ids)
for sub_commander in bundle.sub_commanders
}),
)

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from dataclasses import dataclass
from app.agents.registry.builtins import (
BUILTIN_AGENT_MANIFESTS,
BUILTIN_CAPABILITY_MANIFESTS,
BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS,
BUILTIN_SUB_COMMANDER_MANIFESTS,
)
from app.agents.registry.models import (
AgentManifest,
CapabilityManifest,
SpecialistTemplateManifest,
SubCommanderManifest,
)
@dataclass(frozen=True)
class RegistryBundle:
agents: tuple[AgentManifest, ...]
sub_commanders: tuple[SubCommanderManifest, ...]
capabilities: tuple[CapabilityManifest, ...]
specialist_templates: tuple[SpecialistTemplateManifest, ...]
def load_builtin_registry_bundle() -> RegistryBundle:
return RegistryBundle(
agents=BUILTIN_AGENT_MANIFESTS,
sub_commanders=BUILTIN_SUB_COMMANDER_MANIFESTS,
capabilities=BUILTIN_CAPABILITY_MANIFESTS,
specialist_templates=BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS,
)

View File

@@ -0,0 +1,32 @@
from pydantic import BaseModel
class AgentManifest(BaseModel):
agent_id: str
display_name: str
role_value: str
system_prompt_key: str
routing_hints: list[str]
default_sub_commanders: list[str]
skill_context_key: str | None = None
continuity_policy: str | None = None
clarification_policy: str | None = None
class SubCommanderManifest(BaseModel):
sub_commander_id: str
parent_agent_id: str
prompt_text: str
capability_ids: list[str]
class CapabilityManifest(BaseModel):
capability_id: str
tool_name: str
class SpecialistTemplateManifest(BaseModel):
template_id: str
display_name: str
description: str
allowed_capability_ids: list[str] | None = None

View File

@@ -0,0 +1,55 @@
from collections.abc import Iterable
from app.agents.registry.models import (
AgentManifest,
CapabilityManifest,
SpecialistTemplateManifest,
SubCommanderManifest,
)
def _validate_unique_ids(values: Iterable[str], label: str) -> set[str]:
unique_values: set[str] = set()
for value in values:
if value in unique_values:
raise ValueError(f"duplicate {label}: {value}")
unique_values.add(value)
return unique_values
def validate_registry_bundle(
*,
agents: list[AgentManifest],
sub_commanders: list[SubCommanderManifest],
capabilities: list[CapabilityManifest],
specialist_templates: list[SpecialistTemplateManifest],
) -> None:
agent_ids = _validate_unique_ids((agent.agent_id for agent in agents), "agent id")
_validate_unique_ids(
(sub_commander.sub_commander_id for sub_commander in sub_commanders),
"sub commander id",
)
capability_ids = _validate_unique_ids(
(capability.capability_id for capability in capabilities),
"capability id",
)
_validate_unique_ids(
(specialist_template.template_id for specialist_template in specialist_templates),
"template id",
)
for sub_commander in sub_commanders:
if sub_commander.parent_agent_id not in agent_ids:
raise ValueError(f"unknown parent agent id: {sub_commander.parent_agent_id}")
for capability_id in sub_commander.capability_ids:
if capability_id not in capability_ids:
raise ValueError(f"unknown capability id: {capability_id}")
for specialist_template in specialist_templates:
if specialist_template.allowed_capability_ids is None:
continue
for capability_id in specialist_template.allowed_capability_ids:
if capability_id not in capability_ids:
raise ValueError(f"unknown capability id: {capability_id}")