Authoring a Domain Pack¶
How to build a Domain Pack plugin against
pack_base— bundle vertical expertise that skills can call into.
A Domain Pack is not a workflow. It is a bundle of vertical-specific components that hook into
the platform when a project's attributes match the pack's predicate. Skills opt in to a pack via
optional.domain_packs and call its agents through ctx.agents.run(...).
What a pack can ship¶
| Component | Role |
|---|---|
| Parsers | Extract structure from source files (AST, manifest, schema) |
| Specialist Agents | Domain-specific LLM agents skills can call by id |
| Graph Builders | Materialize structural sub-graphs on entity events |
| Knowledge Bundles | Statically curated knowledge (version patterns, anti-patterns, best practices) |
| Glossary | Domain terms auto-injected into embeddings and prompt context |
Package skeleton¶
packages/pack_<vertical>/
├── pyproject.toml
├── src/pack_<vertical>/
│ ├── __init__.py
│ ├── manifest.yaml
│ ├── pack.py # implements DomainPack
│ ├── parsers.py
│ ├── agents.py
│ ├── graph.py
│ ├── knowledge/
│ │ └── v1/ # KnowledgeBundle on-disk content
│ └── glossary.yaml
└── tests/
└── test_pack.py
pyproject.toml:
[project]
name = "pack_python_web"
version = "0.1.0"
description = "Python web stack domain pack."
requires-python = ">=3.12"
dependencies = ["pack_base"]
[project.entry-points."knowledge_os.domain_packs"]
pack_python_web = "pack_python_web:PythonWebPack"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/pack_python_web"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
The DomainPack Protocol and friends¶
Five Protocols live in pack_base.protocol. All are typing-only — no runtime checks.
class Parser(Protocol):
id: str
def applies_to(self, *, path: Path, content: bytes | None = None) -> bool: ...
async def parse(self, *, path: Path, content: bytes) -> list[NormalizedEntity]: ...
class SpecialistAgent(Protocol):
id: str
capabilities: tuple[str, ...]
async def run(self, *, input: Mapping[str, Any]) -> Mapping[str, Any]: ...
class GraphBuilder(Protocol):
id: str
triggers_on: tuple[str, ...]
async def build(self, *, entity: Entity, context: Mapping[str, Any]) -> None: ...
class KnowledgeBundle(Protocol):
id: str
version_filter: str | None
path: Path
async def load(self) -> list[NormalizedEntity]: ...
class DomainPack(Protocol):
manifest: DomainPackManifest
parsers: tuple[Parser, ...]
agents: tuple[SpecialistAgent, ...]
graph_builders: tuple[GraphBuilder, ...]
knowledge_bundles: tuple[KnowledgeBundle, ...]
async def setup(
self, *,
project_attributes: Mapping[str, Any],
config: Mapping[str, Any],
) -> None: ...
async def teardown(self) -> None: ...
A concrete pack exposes its components as immutable tuples of instances. The runtime
iterates them during setup to register parsers, agents, builders, and bundles into per-project
registries.
The manifest¶
A pack manifest has two top-level keys: plugin: and domain_pack:.
plugin:
id: pack_python_web
type: domain_pack
version: "0.1.0"
kernel_version: ">=0.1.0,<2.0"
display:
name: Python Web
description: Parsers, agents, and glossary for Python web stacks.
category: domain_pack
tags: [python, fastapi, django]
author:
name: Threadory
domain_pack:
applies_to:
project_attributes:
tech_stack_includes: [python]
tech_stack_excludes: []
project_kind: [service, api]
optional: false
version_aware: false
extends: null
provides:
parsers:
- id: python_module
applies_to: { file_extensions: [.py] }
class: pack_python_web.parsers.PythonModuleParser
agents:
- id: python_web_expert
capabilities: [review_code, detect_antipatterns]
class: pack_python_web.agents.PythonWebExpertAgent
graph_builders:
- id: python_import_graph
triggers_on: [code_file:updated]
class: pack_python_web.graph.PythonImportGraphBuilder
knowledge_bundles:
- id: python_web_v1_patterns
path: knowledge/v1/
glossary:
- file: glossary.yaml
config_schema:
type: object
properties:
strict_mode:
type: boolean
default: false
title: Strict mode
description: Treat warnings as errors.
additionalProperties: false
Rules DomainPackManifest enforces at load time:
provides.<group>items must have uniqueids within each group.graph_builders[].triggers_onis non-empty.config_schema.typemust be"object"; the schema is validated withDraft202012Validator.check_schema.
applies_to — when does this pack activate?¶
applies_to.project_attributes is matched against project.attributes at activation time:
def matches(self, project_attributes: Mapping[str, Any]) -> bool:
# All `tech_stack_includes` tags must appear in project_attributes["tech_stack"].
# None of `tech_stack_excludes` may appear.
# If `project_kind` is non-empty, project_attributes["project_kind"] must match.
tech_stack_includesis an AND over the project's tech-stack tags.tech_stack_excludesblacklists tags.project_kindis an allowlist (empty = match anything).optional: falsemeans the runtime requires this pack for matching projects.optional: truemeans the pack is suggested — the UI offers it but doesn't force it.
pack_generic_web ships with an empty project_attributes: {} so it matches every project as a
no-op fallback. See packages/pack_generic_web/src/pack_generic_web/manifest.yaml for the minimal
manifest shape.
extends — pack inheritance¶
Set extends: <pack_id> to inherit from another pack. The runtime resolves the chain with
pack_base.resolve_pack_chain and merges the provides blocks with merge_provides. Rules:
- The chain is resolved oldest-ancestor-first → most-derived-last.
- Within each group (
parsers,agents,graph_builders,knowledge_bundles), duplicateids collapse to the most-derived spec — child wins. glossaryentries are concatenated in chain order (no dedupe — terms within a single glossary must already be unique, see below).- Cycles raise
PackInheritanceError. - Missing ancestors raise
PackInheritanceError.
Example:
# pack_helpdesk (base)
domain_pack:
provides:
agents:
- { id: ticket_triager, capabilities: [classify], class: pack_helpdesk.agents.TicketTriager }
# pack_python_helpdesk (child)
domain_pack:
extends: pack_helpdesk
provides:
agents:
- { id: ticket_triager, capabilities: [classify, suggest_module], class: pack_python_helpdesk.agents.PyTriager }
- { id: stack_trace_analyzer, capabilities: [parse_traceback], class: pack_python_helpdesk.agents.PyStackTrace }
After merging, the project sees two agents — ticket_triager (the Python-specific override) and
stack_trace_analyzer — because the child's ticket_triager shadowed the base's.
Programmatic use:
from pack_base import resolve_pack_chain, merge_provides
chain = resolve_pack_chain("pack_python_helpdesk", registry)
provides = merge_provides(chain)
# provides.agents now contains the overridden ticket_triager + the new stack_trace_analyzer.
version_aware¶
Set version_aware: true if your pack reads project.attributes to pick a versioned knowledge
bundle (e.g. selecting python_v3_11_patterns vs python_v3_12_patterns based on a project's
declared Python version). The runtime hands the full project_attributes mapping to setup.
Glossary loader¶
Glossary YAML supports either a flat list:
- term: dependency injection
definition: Wiring concrete implementations into a class at runtime, not import time.
aliases: [DI, IoC]
- term: ASGI
definition: Asynchronous Server Gateway Interface — Python web server contract.
…or a mapping with an entries: key (lets you add document-level metadata in the future):
entries:
- { term: ASGI, definition: ..., aliases: [] }
Load it with pack_base.load_glossary:
from pathlib import Path
from pack_base import load_glossary
entries = load_glossary(Path(__file__).parent / "glossary.yaml")
load_glossary enforces case-insensitive uniqueness on term. Duplicates raise
GlossaryLoadError. Each entry is a frozen GlossaryEntry (term, definition, aliases: list[str]).
The runtime injects glossary entries into embedding context and prompt templates automatically once the pack is active.
Worked example — pack_python_web¶
# src/pack_python_web/pack.py
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from pack_base import DomainPackManifest, load_manifest
from pack_base.protocol import GraphBuilder, KnowledgeBundle, Parser, SpecialistAgent
from .agents import PythonWebExpertAgent
from .graph import PythonImportGraphBuilder
from .knowledge import PythonWebV1Bundle
from .parsers import PythonModuleParser
class PythonWebPack:
"""Domain pack for Python web stacks (FastAPI, Django, Flask, ...)."""
def __init__(self) -> None:
manifest_path = Path(__file__).parent / "manifest.yaml"
self.manifest: DomainPackManifest = load_manifest(manifest_path)
self.parsers: tuple[Parser, ...] = (PythonModuleParser(),)
self.agents: tuple[SpecialistAgent, ...] = (PythonWebExpertAgent(),)
self.graph_builders: tuple[GraphBuilder, ...] = (PythonImportGraphBuilder(),)
self.knowledge_bundles: tuple[KnowledgeBundle, ...] = (
PythonWebV1Bundle(path=Path(__file__).parent / "knowledge" / "v1"),
)
async def setup(
self,
*,
project_attributes: Mapping[str, Any],
config: Mapping[str, Any],
) -> None:
# Wire framework-specific knobs based on project_attributes.
# E.g. activate a Django-specific sub-parser if "django" is in tech_stack.
...
async def teardown(self) -> None:
...
A minimal Parser:
# src/pack_python_web/parsers.py
from __future__ import annotations
from pathlib import Path
from shared_types import NormalizedEntity
class PythonModuleParser:
id = "python_module"
def applies_to(self, *, path: Path, content: bytes | None = None) -> bool:
return path.suffix == ".py"
async def parse(self, *, path: Path, content: bytes) -> list[NormalizedEntity]:
# Parse imports, classes, functions — return entities (kind=code_symbol, etc.)
return []
A minimal SpecialistAgent:
# src/pack_python_web/agents.py
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
class PythonWebExpertAgent:
id = "python_web_expert"
capabilities: tuple[str, ...] = ("review_code", "detect_antipatterns")
async def run(self, *, input: Mapping[str, Any]) -> Mapping[str, Any]:
# Compose a prompt, call the LLM router, parse the structured response.
return {"findings": []}
Skills call this agent through ctx.agents.run(agent="python_web_expert", input=...). The runtime
routes the call to this pack's agent because the pack is active for the project.
The no-op default — pack_generic_web¶
packages/pack_generic_web/ is the trivial reference pack: empty provides, empty glossary,
matches every project (applies_to.project_attributes: {}). It is what runs when no vertical pack
matches. Use it as a copy-paste starting point:
plugin:
id: pack_generic_web
type: domain_pack
version: "0.1.0"
kernel_version: ">=0.0.1"
display:
name: Generic Web
description: No-op default domain pack. Active when no vertical pack matches.
category: domain_pack
author:
name: Threadory
domain_pack:
applies_to:
project_attributes: {}
optional: false
extends: null
provides:
parsers: []
agents: []
graph_builders: []
knowledge_bundles: []
glossary: []
config_schema:
type: object
properties: {}
additionalProperties: false
Testing¶
Pack tests typically cover:
manifest.yamlloads without errors.setup/teardownare no-ops or wire the expected state.- For each parser:
applies_toreturns the expected boolean andparsereturns the expected entities for a small fixture. - For each agent: feed a fixture and assert structured output (use
testing_kit.FakeLLMRouterif the agent calls into LLMs). - For each glossary:
load_glossaryproduces the expected entries.
Example smoke test:
from pack_python_web import PythonWebPack
def test_manifest_loads() -> None:
pack = PythonWebPack()
assert pack.manifest.plugin.id == "pack_python_web"
assert pack.manifest.plugin.type == "domain_pack"
async def test_setup_is_idempotent() -> None:
pack = PythonWebPack()
await pack.setup(project_attributes={"tech_stack": ["python"]}, config={})
await pack.setup(project_attributes={"tech_stack": ["python"]}, config={})
await pack.teardown()
def test_inheritance_chain_with_registry() -> None:
from pack_base import resolve_pack_chain, merge_provides
from pack_python_web import PythonWebPack
registry = {"pack_python_web": PythonWebPack().manifest}
chain = resolve_pack_chain("pack_python_web", registry)
merged = merge_provides(chain)
assert len(merged.agents) == 1
assert merged.agents[0].id == "python_web_expert"
See Testing for the full pattern.
Pack checklist¶
- [ ]
manifest.yamlloads withpack_base.load_manifest. - [ ] All
class:paths inprovidesresolve at import time. - [ ]
applies_to.project_attributesdescribes the target vertical (use[]-empty fields for "match anything in this slot"). - [ ] If
extends:is set, the parent pack is in the same deployment and the chain has no cycles. - [ ] Glossary terms are unique (case-insensitive).
- [ ] No imports from
kernel.*— onlypack_base,shared_types, and your own modules. - [ ] Entry point registered under
knowledge_os.domain_packs. - [ ]
setupandteardownare idempotent and async.