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_gitlabingests merge requests and posts comments.skill_pr_reviewconsumes those MRs and produces review findings.pack_python_webprovides 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 errorfrozen=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.*. TheSkillContextProtocol 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 annotationsas the first import in every.pyfile- 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=Trueon boundaries) pytest-asynciofor 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_versionis 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¶
- Building a connector? Head to Connector authoring.
- Building a skill? See Skill authoring.
- Building a domain pack? See Domain Pack authoring.