Skip to main content

Plugin registry

PluginRegistry is the central object of the system. It holds the set of loaded plugins, checks that their manifests are compatible with the core version, computes the initialisation order with respect to dependencies, and exposes plugins by the kind + name pair.

Obtaining a registry

The recommended way is to construct a PluginRegistry and feed it a discovery root via registry.discover(path). The call walks the given directory recursively, finds folders with a dagstack.toml, validates the manifests, and loads the plugins into the registry.

from dagstack.plugin_system import PluginRegistry

registry = PluginRegistry()
registry.discover("plugins/")

Direct construction (PluginRegistry()) is the supported entry point — discover is the method that walks the directory and validates manifests. There is no convenience function that builds the registry for you.

Lifecycle

Every loaded plugin moves through three phases:

  1. Registrationdiscover reads the manifest, instantiates the plugin, and adds it to the registry. Only manifest validation and module import run at this stage.
  2. Initialisationsetup_all() computes the initialisation order via topological sort of the declared dependencies (topo_sort) and calls setup(context) on every plugin.
  3. Teardownteardown_all() calls teardown() in reverse order. Errors in individual plugins do not abort the loop; they are aggregated into TeardownErrors.
import asyncio
import logging
from dagstack.plugin_system import PluginContext, PluginRegistry


async def main() -> None:
registry = PluginRegistry()
registry.discover("plugins/")

ctx = PluginContext(
config={},
logger=logging.getLogger("app"),
registry=registry,
)
await registry.setup_all(ctx)
try:
...
finally:
await registry.teardown_all()


asyncio.run(main())

Retrieving a plugin

Access to a plugin is by the kind + name pair. The method returns a proxy object that applies the dispatch rules and resource injection.

llm = registry.get_plugin("llm", name="openai_compatible")
response = llm.complete(prompt="Explain dagstack in one paragraph.")

If the plugin is not found, KindUnknown (the kind is not registered) or AmbiguousPlugin (several plugins share the same kind+name) is raised.

Registry errors

All errors inherit from PluginRegistryError:

ClassWhen it is raised
ManifestInvalidThe manifest does not match the schema (missing required field, wrong type).
AmbiguousPluginTwo plugins are registered with the same kind+name pair.
VersionIncompatibleThe plugin declares a core_version incompatible with the installed core.
KindUnknownThe plugin kind is not registered in the registry.
RuntimeNotSupportedThe plugin supports none of the available runtimes (in_process, mcp_stdio, mcp_http).
DependencyCycleThe topological sort detected a cycle in the declared inter-plugin dependencies.
TeardownErrorsAggregated error raised when teardown_all() caught exceptions in several plugins.
from dagstack.plugin_system import PluginRegistry
from dagstack.plugin_system import ManifestInvalid, PluginRegistryError

registry = PluginRegistry()
try:
registry.discover("plugins/")
except ManifestInvalid as exc:
print(f"Invalid manifest: {exc}")
raise
except PluginRegistryError:
raise

Invariants

The registry upholds the following invariants on top of the plugin lifecycle:

  • Teardown order is the reverse of setup. If setup_all() initialised plugins in the order A → B → C, then teardown_all() calls teardown in the order C → B → A.
  • Continue-on-failure during setup (ADR-0002 §Partial-failure). If a single plugin's setup raises, the registry logs the failure, skips plugins that depend on it, and proceeds with the rest. Failed plugins are absent from _setup_done; the host can inspect them via the setup_all log output. setup_all does not propagate the first failure outwards.
  • Stable get_plugin results. The same (kind, name) pair always returns the same plugin instance for the lifetime of the registry.

:::info Phase 2 — explicit unavailable_plugins() A direct way to list plugins that failed setup (and the dependents that were skipped because of them) lands in Phase 2 alongside the live config-watch integration. In 0.1.0-rc.2 the host inspects the setup_all logs to discover the same information. :::