Перейти к содержанию

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 unique ids within each group.
  • graph_builders[].triggers_on is non-empty.
  • config_schema.type must be "object"; the schema is validated with Draft202012Validator.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_includes is an AND over the project's tech-stack tags.
  • tech_stack_excludes blacklists tags.
  • project_kind is an allowlist (empty = match anything).
  • optional: false means the runtime requires this pack for matching projects.
  • optional: true means 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), duplicate ids collapse to the most-derived spec — child wins.
  • glossary entries 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.yaml loads without errors.
  • setup / teardown are no-ops or wire the expected state.
  • For each parser: applies_to returns the expected boolean and parse returns the expected entities for a small fixture.
  • For each agent: feed a fixture and assert structured output (use testing_kit.FakeLLMRouter if the agent calls into LLMs).
  • For each glossary: load_glossary produces 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.yaml loads with pack_base.load_manifest.
  • [ ] All class: paths in provides resolve at import time.
  • [ ] applies_to.project_attributes describes 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.* — only pack_base, shared_types, and your own modules.
  • [ ] Entry point registered under knowledge_os.domain_packs.
  • [ ] setup and teardown are idempotent and async.