"""`SpecResolver`:内置 + 用户 skill 的统一只读解析入口(Prompt 外置方案A · 步4)。 内置 Agent(`SPECS`,纯内存)与用户 Skill(`SkillRegistry`,DB)二者都产出 `AgentSpec`。 resolver 提供统一 `.get(name)`: - **内置优先、零 DB**:先查内置 `SPECS`(纯内存查表);命中内置 name 时**绝不触发**任何 `SkillRegistry` 调用(评审 HIGH / 不变量 #3:四审 prompt 不被用户 skill 偷换的安全边界, 其守卫前移在 `SkillRegistry` 入库期,故读路径对内置永远是确定性纯内存)。 - 未命中内置才查 `SkillRegistry`;都无 → `AppError(NOT_FOUND)`。 `build` 是**纯合并**,不做保留名冲突校验(守卫前移至 `SkillRegistry.load`)。 `output_schema_for`:内置 → `SCHEMA_CATALOG[name]` 真类;纯用户 skill → None。 name 语义锁死为**精确字符串相等**(无大小写折叠、无连字符归一、无模糊匹配)。 不可变:`build` 用 `dict(SPECS)` 复制一份内置快照,resolver 不改入参。 """ from __future__ import annotations from pydantic import BaseModel from ww_agents import SCHEMA_CATALOG, SPECS, AgentSpec from ww_skills.skill_registry import SkillRegistryLike class SpecResolver: """内置(SPECS,纯内存)+ 用户 skill(SkillRegistry,DB)的统一只读解析入口。""" def __init__(self, builtin: dict[str, AgentSpec], skills: SkillRegistryLike) -> None: self._builtin = builtin self._skills = skills @classmethod def build(cls, skills: SkillRegistryLike) -> SpecResolver: """纯合并:拷贝内置 SPECS 快照 + 持有 SkillRegistry(不做冲突校验,守卫已前移)。""" return cls(builtin=dict(SPECS), skills=skills) def get(self, name: str) -> AgentSpec: """先查内置(纯内存、零 DB);未命中才查 SkillRegistry;都无 → NOT_FOUND。 命中内置 name 时**绝不触发** SkillRegistry 调用(确定性 + 安全边界)。 """ builtin = self._builtin.get(name) if builtin is not None: return builtin return self._skills.get(name) def output_schema_for(self, name: str) -> type[BaseModel] | None: """命中内置 → SCHEMA_CATALOG[name];纯用户 skill / 未知 → None。 精确字符串相等命中——拼错近似 name(如 `character_gen` vs `character-gen`)→ None 不报错(预期行为,非 bug)。 """ if name in self._builtin: return SCHEMA_CATALOG[name] return None def names(self) -> list[str]: """内置 + 用户 skill 的全部 name(去重、排序)。""" return sorted(set(self._builtin) | set(self._skills.names())) def list_scope(self, scope: str) -> list[AgentSpec]: """按 scope 过滤内置 + 用户 skill,按 name 排序。""" merged = [s for s in self._builtin.values() if s.scope == scope] merged.extend(self._skills.list_scope(scope)) return sorted(merged, key=lambda s: s.name)