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

Plugin architecture overview

Architectural context every plugin author needs before reading the per-type guides.

The five layers

The Threadory platform is split into five horizontal layers. Plugins live in the middle.

┌────────────────────────────────────────────────────────┐
│ UI            apps/web (Next.js)                       │
├────────────────────────────────────────────────────────┤
│ Runtime       packages/runtime (Temporal + MCP loader) │
├────────────────────────────────────────────────────────┤
│ Plugins       Connectors · Skills · Domain Packs       │  <-- you write code here
├────────────────────────────────────────────────────────┤
│ Kernel        packages/kernel (domain-agnostic core)   │
├────────────────────────────────────────────────────────┤
│ Storage       Postgres · Qdrant · Redis · Temporal     │
└────────────────────────────────────────────────────────┘

Plugins extend the platform without touching the kernel. The kernel offers domain-neutral primitives (entities, retrieval, workflows, approvals, telemetry). Plugins add concrete vertical knowledge.

The three plugin types

Type What it does When to write one
Connector Talks to an external system: ingests data and/or performs actions New SaaS / API to integrate (GitLab, tl;dv, Jira)
Skill Does something with the knowledge graph — a workflow New automation (PR review, meeting digest, …)
Domain Pack Bundles vertical expertise (parsers, agents, glossary, knowledge files) New vertical (Python web stack, Odoo, …)

A single integration often spans more than one plugin. Example:

  • connector_gitlab ingests merge requests and posts comments.
  • skill_pr_review consumes those MRs and produces review findings.
  • pack_python_web provides Python-specific parsers and agents the skill calls into.

Capability-based contracts

Plugins describe what they can do in capability terms, not concrete API calls. Skills declare which capabilities they need; connectors declare which capabilities they provide; the runtime wires them up.

# A skill requires capabilities ...
requires:
  entity_kinds:
    - kind: pull_request
      capabilities: [read_diff, read_file_tree]

# ... and a connector provides them ...
provides:
  sources:
    - kind: pull_request
      capabilities: [read_diff, read_file_tree]

If no installed connector advertises the required capability, the skill is greyed out in the UI — no wiring failures at runtime.

Discovery via entry points

The runtime discovers installed plugins through Python's standard importlib.metadata entry points. Each plugin type has a dedicated group:

Plugin type Entry-point group
Connector knowledge_os.connectors
Skill knowledge_os.skills
Domain Pack knowledge_os.domain_packs

Example pyproject.toml:

[project.entry-points."knowledge_os.connectors"]
my_connector = "connector_my:MyConnector"

The kernel-side discovery helper is kernel.plugin_registry.discovery.discover_plugins:

from kernel.plugin_registry.discovery import discover_plugins

for plugin in discover_plugins(kind="connector"):
    print(plugin.id, plugin.entry_point)
    cls = plugin.load()

Schema-first manifests

Every plugin ships a manifest.yaml. The manifest is parsed with pydantic v2 models that have:

  • extra="forbid" — any unknown key is a hard error
  • frozen=True — manifests are immutable across the process lifetime
  • discriminated unions where multiple shapes are valid (e.g. auth methods, trigger types)

config_schema blocks are full JSON Schema draft-2020-12 documents and are validated by jsonschema with Draft202012Validator.check_schema.

A schema-first manifest gives you:

  • A typed config object the UI can render into a form.
  • Hard early failure on a malformed YAML — pydantic raises before the plugin is registered.
  • Version negotiation: kernel_version: ">=0.1.0,<2.0" lets the runtime refuse incompatible plugins.

The kernel purity rule

The kernel cannot mention any vertical or domain term — the strings odoo, gitlab, tldv, jira, MR, ticket may not appear inside packages/kernel/, packages/runtime/, or packages/shared_types/. Domain knowledge belongs in plugins (connector_<name>, pack_<vertical>).

If you need a new concept that feels generic enough for the kernel, lift it into shared_types/ — do not import from kernel.* inside a plugin.

What this means for plugin authors:

  • Plugins import from connector_base, skill_base, pack_base, shared_types, testing_kit.
  • Plugins never import from kernel.*. The SkillContext Protocol gives you everything you need.
  • If a kernel API you want isn't exposed on SkillContext, raise an issue rather than reaching across.

Minimum baseline

Every plugin module must satisfy:

  • Python 3.12+
  • from __future__ import annotations as the first import in every .py file
  • Type-annotated end-to-end; the base packages are checked under mypy --strict
  • Ruff-clean against the repository's ruff.toml (line-length 100, security rules)
  • Pydantic v2 for all schema models (model_validate, model_dump, frozen=True on boundaries)
  • pytest-asyncio for async tests (asyncio_mode = "auto")

Lifecycle

A registered plugin moves through these states (per project):

Discovered  →  Available  →  Enabled  →  Configured  →  Active
                                                        │
                                                        ↓
                                        Error  ←──  health_check fails
  • Discovered — entry point loaded, manifest parsed.
  • Available — kernel_version is satisfied, schema is valid.
  • Enabled — project admin turned it on.
  • Configured — credentials + config validated against config_schema.
  • Active — health_check() (Connector) / wiring (Skill) / setup() (Pack) succeeded.
  • Error — recoverable failure surfaced in UI; recovers when the next check passes.

State is stored per project — the same plugin can be active in project A and disabled in project B.

Next steps