技能概览
先看适用场景、限制条件和安装路径,再决定是否继续深入。
适用场景: self-declaring modules. 本地化技能摘要: The 400-line cap and # DOC conventions in a0p-module-doctrine are how individual modules behave; this skill is how the registries that hold them are built. Claude Code, Cursor, and Windsurf workflows.
核心价值
推荐说明: a0p-self-declaring-modules helps agents self-declaring modules. The 400-line cap and # DOC conventions in a0p-module-doctrine are how individual modules behave; this skill is how the registries that hold them are
适用 Agent 类型
适用场景: self-declaring modules.
↓ 赋予的主要能力 · A0p Self Declaring Modules
! 使用限制与门槛
- 限制说明: ├── init.py # Scanner + dispatcher. Discovery only — no business logic.
- 限制说明: Rule 2 — The scanner is deterministic and cache-stable sorted(glob) — discovery order must be byte-identical across runs
- 限制说明: Anything injected into prompt prefixes (manifest, tool list) must be cache-stable or you blow the prefix-cache savings
! 来源说明
此页面仍可作为安装与查阅参考。继续使用前,请结合上方适用场景、限制条件和上游仓库说明一起判断。
Self-Declaring Modules
The pattern a0p uses everywhere a registry exists: metadata + handler colocated in one file, scanner discovers them, registry is generated at boot. Never edit a central list.
This skill is the meta-pattern. The 400-line cap and # DOC conventions in a0p-module-doctrine are how individual modules behave; this skill is how the registries that hold them are built.
The Pattern
python/services/<registry_name>/
├── __init__.py # Scanner + dispatcher. Discovery only — no business logic.
├── tool_a.py # SCHEMA = {...} + async def handle(...)
├── tool_b.py # SCHEMA = {...} + async def handle(...)
└── tool_c.py # SCHEMA = {...} + async def handle(...)
Three rules. Violate any of them and the pattern degrades back into the central-dispatcher problem you were trying to escape.
Rule 1 — Schema and handler are colocated
Every module exports both:
python1# python/services/tools/image_generate.py 2SCHEMA = { 3 "type": "function", 4 "function": { 5 "name": "image_generate", 6 "description": "Generate an image with Imagen-3.", 7 "parameters": { ... }, 8 }, 9 "tier": "free", 10 "approval_scope": None, 11 "enabled": True, 12} 13 14async def handle(prompt: str, aspect_ratio: str = "1:1", **_) -> dict: 15 ...
If a future "schema in DB" overlay applies (operational toggles like enabled, tier, description_override), it merges on top of the file-declared SCHEMA — the file remains source of truth for shape.
Rule 2 — The scanner is deterministic and cache-stable
python1def _discover() -> dict[str, ModuleType]: 2 out = {} 3 for path in sorted(glob.glob(_PATTERN)): # sorted for stability 4 spec = importlib.util.spec_from_file_location(...) 5 mod = importlib.util.module_from_spec(spec) 6 spec.loader.exec_module(mod) 7 out[mod.SCHEMA["function"]["name"]] = mod 8 return out
sorted(glob)— discovery order must be byte-identical across runs. Anything injected into prompt prefixes (manifest, tool list) must be cache-stable or you blow the prefix-cache savings.- mtime fingerprint — cache the discovery output keyed by
tuple((path, mtime) for path in files). Re-scan only when the fingerprint changes. See_discover_a0_skillsand_discover_distillersintool_executor.pyfor the canonical implementations. - Lazy bodies, eager metadata — load every SCHEMA at boot (cheap, prompt prefix needs them); load handler bodies on demand if they're heavy (image-gen SDK imports, etc.).
Rule 3 — Registration is by file presence, not by central list
The registry is generated from the scanner output. There is no ALL_TOOLS = [...] list to edit. To add a tool, drop a file. To remove a tool, delete the file. To disable, set SCHEMA["enabled"] = False.
This is why the pattern needs the 400-line cap (see a0p-module-doctrine §Hard Rules) — once you have it, no module ever grows large enough to need to be split, because each one is one tool / one route / one skill / one distiller.
The 400-line cap counts N only. N is non-blank, non-comment code lines. Comments, docstrings, # DOC blocks, and the # N:M annotations themselves are M and do not count toward the cap. A 700-line file with # 380:320 is compliant; a 410-line file with # 410:0 is not. Document liberally; just keep the executable surface bounded.
Existing examples in this codebase
Read at least one before adding a new registry:
| Registry | Location | Scanner | Notes |
|---|---|---|---|
| Route modules | python/routes/*.py | collect_doc_meta(), collect_ui_meta() | Has 4-place registration in __init__.py — legacy pattern, predates full discovery. New registries should not require manual registration. |
| Distill skills | .agents/skills/distill-*/SKILL.md | _discover_distillers() in tool_executor.py | Pure-content; no handler. |
| a0-skills | .agents/skills/a0-*/SKILL.md | _discover_a0_skills() in tool_executor.py | Manifest cached into doctrine prefix. |
| ZFAE tools (legacy) | python/services/tool_executor.py (single file) | None — central registry | Anti-pattern. Currently being refactored to this skill. |
Operational metadata fields (recommended)
Every SCHEMA should declare these even when the value is the default — explicit > implicit, and the scanner can use them for filtering:
python1SCHEMA = { 2 "type": "function", 3 "function": { ... }, # The OpenAI-shape function declaration 4 5 "tier": "free", # free | seeker | operator | patron | admin 6 "approval_scope": None, # None or scope name from approval_scopes table 7 "enabled": True, # File-level enable; runtime DB overlay can flip 8 "category": "media", # For UI grouping in tools tab 9 "cost_hint": "high", # "free" | "low" | "medium" | "high" 10 "side_effects": ["filesystem"], # see § Side-effect taxonomy below 11 "version": 1, # Bump on breaking schema changes 12 13 "recommended_skills": [ # Skill slugs the model should load before/while using this tool. 14 "github-solution-finder", # Names match a0-skill discovery (.agents/skills/a0-<name>/SKILL.md) 15 ], # Surfaced to the model via the tool description tail. 16}
The scanner exposes a typed view (e.g. class ToolSpec) that other systems consume — never reach into raw SCHEMA dicts from outside the registry module.
recommended_skills — wiring tools to skills
A tool that performs a complex action almost always has a skill that documents how to do it well. Declare the skill name(s) in recommended_skills and the registry layer appends a one-line hint to the tool's description shown to the model:
Best used with skill(s): github-solution-finder. Call
skill_load(name)to fetch the body before executing.
This closes the loop between the two registries — the model sees the connection without you having to re-document it inside every prompt. When the model picks the tool, it already knows which skill to consult; when it loads the skill, the body teaches it how to use the tool well.
Examples of expected pairings:
image_generate→infographic-builder(when the goal is structured information design, not just a picture)web_search→deep-research(when the goal is multi-source synthesis, not a quick lookup)github_api→github-solution-finder(when looking for libraries, not a known repo)port_scan/http_fuzz/web_recon→ a futurepentest-reconskill that codifies safe scope, rate limits, evidence collectionexploit_run→ a futurepentest-exploitationskill with the rules-of-engagement template and report format
Side-effect taxonomy
side_effects is a list. Use these tags so downstream gating (approval scopes, tier limits, audit log severity) can reason about a tool without reading its code:
filesystem— reads or writes local filesnetwork— outbound HTTP / sockets to non-attacker-controlled targets (search, public APIs, your own GCP)billing— costs money per call (image gen, paid LLM, paid API)external_account— touches an account whose credentials we hold (Stripe writes, GitHub push, Gmail send)mutating_db— writes to our own Postgresirreversible— cannot be safely retried or rolled backsecurity_passive— pen-testing recon that does not generate traffic to the target (cert lookups, public DNS, leaked-credential search)security_active— pen-testing traffic to a target system (port scan, fuzzer, exploit run, brute force). Always setapproval_scopefor these. The scope name should encode the target class so the user knows what they're approving (e.g.pentest_active_scoped_targets).
A pen-testing tool with side_effects: ["security_active", "network"] and approval_scope: "pentest_active_scoped_targets" makes the gating obvious to every downstream system without anyone having to special-case offensive tooling.
When NOT to use this pattern
- Single-implementation utilities —
python/services/sigma.pyis one Σ store, not a registry. Don't fragment it. - Cross-cutting concerns — auth, logging, the doctrine prefix builder. These are not extensible by file-drop and shouldn't pretend to be.
- Anything called inside a hot loop — the discovery scan is mtime-cached but adds at least one
os.statper file. Don't put per-request work behind it.
Refactor checklist (when converting a central dispatcher to this pattern)
- Create
python/services/<name>/__init__.pywith scanner + dispatcher + cache. - Extract each tool/handler/etc. to its own file. Each file:
SCHEMA = {...}+async def handle(...). Keep under 400 lines (a0p-module-doctrine§Hard Rules). - Replace the central
TOOL_SCHEMAS_CHAT = [...]andif name == "x": ...dispatch with calls into the registry. - Verify boot:
python -c "from python.services.<name> import registry; print(len(registry()))". - Run the existing tests — they should pass without modification if the dispatcher contract is preserved (
registry(),dispatch(name, **kwargs)). - Add one new test:
test_every_tool_module_declares_schema_and_handle— parametrize over discovered modules, assert both attributes exist and SCHEMA validates. - Re-stamp annotations:
python scripts/annotate.py. - Confirm
tool_executor.py(or whatever the old central file was) is now a thin re-export shim or deleted entirely.
Anti-patterns to refuse
- Registry edits required to add a module — if a new file requires editing a central list, the pattern is broken.
- Schema in code, handler somewhere else — guarantees drift. Same file, always.
- Non-deterministic discovery order —
os.listdir()without sorting will silently corrupt prompt-prefix caches across runs on different filesystems. - One file declaring two registry entries — split it. The 400-line cap is your friend here, not your enemy.
- Hot-reload that re-discovers on every request — mtime cache or boot-time freeze. Pick one.