Skip to content

Chapter 6: Plugin Architecture

Overview

The FCC framework is designed to be extended without modifying its core. The plugin system provides a structured way for third-party packages to contribute personas, engines, templates, scorers, validators, AI providers, governance rules, scenarios, workflows, and event subscribers.

Every plugin type has an abstract base class (ABC), a dedicated entry-point group, and a place in the PluginRegistry discovery pipeline. This chapter explains how plugins are discovered, loaded, validated, and used.

The mind map below groups the ten FCC plugin types into four practical clusters — Content, Engine, Quality, and System — giving a quick map of where third-party extensions attach.

mindmap
  root((10 Plugin Types))
    Content Plugins
      Personas
        Contribute YAML definitions
      Scenarios
        Add simulation scenarios
      Workflows
        Custom workflow graphs
      Templates
        Jinja2 template directories
    Engine Plugins
      Engines
        Custom simulation engines
      Providers
        AI provider clients
    Quality Plugins
      Scorers
        Quality scoring functions
      Validators
        Validation rules
    System Plugins
      Governance
        Tags and quality gates
      Subscribers
        Event bus subscribers

The subsequent table restates these groups as their concrete entry-point names and ABC classes so you can match the map to the actual pyproject.toml wiring.

The 10 Plugin Types

Each plugin type maps to an entry-point group and an ABC defined in src/fcc/plugins/base.py:

# Plugin Type Entry-Point Group ABC Purpose
1 Personas fcc.plugins.personas PersonaPlugin Contribute persona YAML definitions
2 Engines fcc.plugins.engines EnginePlugin Contribute simulation engine classes
3 Templates fcc.plugins.templates TemplatePlugin Contribute Jinja2 template directories
4 Scorers fcc.plugins.scorers ScorerPlugin Contribute quality scoring functions
5 Validators fcc.plugins.validators ValidatorPlugin Contribute validation rules
6 Providers fcc.plugins.providers AIProviderPlugin Contribute AI provider client implementations
7 Governance fcc.plugins.governance GovernancePlugin Contribute tags and quality gates
8 Scenarios fcc.plugins.scenarios ScenarioPlugin Contribute scenario JSON files
9 Workflows fcc.plugins.workflows WorkflowPlugin Contribute workflow graph definitions
10 Subscribers fcc.plugins.subscribers EventSubscriberPlugin Contribute event bus subscribers

Entry-Point Discovery

Plugins are discovered via Python's standard importlib.metadata.entry_points() mechanism. A package registers a plugin by declaring an entry point in its pyproject.toml:

[project.entry-points."fcc.plugins.personas"]
my_personas = "my_package.fcc_plugin:MyPersonaPlugin"

When PluginRegistry.discover() runs, it iterates over all 10 entry-point groups, loads each entry point, instantiates the class, reads its plugin_meta(), and registers the result:

from fcc.plugins.registry import PluginRegistry

registry = PluginRegistry()
result = registry.discover()

print(result.discovered)  # total entry points found
print(result.loaded)      # successfully loaded
print(result.errors)      # loading failures

The discovery process is resilient: a failing plugin is logged and skipped, not propagated. This ensures that a broken third-party package does not prevent the rest of the framework from operating.

Plugin Lifecycle

A plugin moves through four stages:

1. Discovery

PluginRegistry.discover() scans all entry-point groups and finds callable entry points.

2. Instantiation

Each entry point is loaded (ep.load()) and instantiated (plugin_cls()). The instance must have a plugin_meta() method.

3. Registration

The PluginMeta returned by plugin_meta() is used to register the plugin. If a plugin ID conflicts with an already-registered plugin, a PluginConflictError is raised.

4. Validation

After discovery, registry.validate(plugin_id) checks that the instance is an instance of the expected ABC for its declared plugin type. registry.validate_all() validates every loaded plugin and returns a dict of {plugin_id: [errors]}.

errors = registry.validate_all()
for plugin_id, issues in errors.items():
    if issues:
        print(f"{plugin_id}: {issues}")

PluginMeta Metadata

Every plugin exposes a PluginMeta frozen dataclass:

from fcc.plugins.base import PluginMeta, PluginType

@dataclass(frozen=True)
class PluginMeta:
    id: str                      # unique identifier
    name: str                    # human-readable name
    version: str                 # semver string
    plugin_type: PluginType      # enum value
    description: str = ""        # optional description
    author: str = ""             # optional author
    source_package: str = ""     # pip package name
    tags: tuple[str, ...] = ()   # classification tags

The id must be globally unique across all loaded plugins. The plugin_type determines which ABC the instance is validated against.

Cross-Plugin Orchestration

When multiple plugins contribute personas that need to interact, the CrossPluginOrchestrator in src/fcc/plugins/orchestration.py manages the relationships.

Plugin Dependencies

PluginDependency models a dependency between two plugins:

from fcc.plugins.orchestration import PluginDependency, CrossPluginOrchestrator

dep = PluginDependency(
    source_plugin="ml_lifecycle",
    target_plugin="data_engineering",
    dependency_type="provides_metadata",
    description="ML lifecycle personas consume data lineage metadata",
)

orchestrator = CrossPluginOrchestrator(dependencies=[dep])
deps = orchestrator.resolve_dependencies("ml_lifecycle")
reverse = orchestrator.reverse_dependencies("data_engineering")

Plugin Interactions

PluginInteraction models a persona-to-persona interaction that crosses plugin boundaries:

from fcc.plugins.orchestration import PluginInteraction

interaction = PluginInteraction(
    source_persona="DE_PIPE",
    source_plugin="data_engineering",
    target_persona="ML_TRAIN",
    target_plugin="ml_lifecycle",
    interaction_type="upstream",
    description="Pipeline output feeds model training",
)

orchestrator.add_interaction(interaction)
matrix = orchestrator.get_interaction_matrix()

Ecosystem Health

The orchestrator aggregates per-plugin health checks into an EcosystemHealthReport:

from fcc.plugins.orchestration import PluginHealthStatus

statuses = [
    PluginHealthStatus(plugin_id="ml_lifecycle", healthy=True, persona_count=9),
    PluginHealthStatus(plugin_id="data_engineering", healthy=True, persona_count=6),
]

report = orchestrator.check_health(statuses)
print(report.total_plugins)      # 2
print(report.healthy_plugins)    # 2
print(report.total_personas)     # 15

Writing a Custom Plugin

Here is a minimal example of a persona plugin:

from pathlib import Path
from fcc.plugins.base import PersonaPlugin, PluginMeta, PluginType


class MyPersonaPlugin(PersonaPlugin):
    """Contributes custom personas to the FCC registry."""

    def plugin_meta(self) -> PluginMeta:
        return PluginMeta(
            id="my-personas",
            name="My Custom Personas",
            version="1.0.0",
            plugin_type=PluginType.PERSONAS,
            description="Domain-specific personas for my project",
            author="My Team",
            source_package="my-fcc-plugin",
        )

    def get_persona_paths(self) -> list[Path]:
        """Return paths to YAML files with persona definitions."""
        return [Path(__file__).parent / "data" / "personas.yaml"]

    def get_dimension_paths(self) -> list[Path]:
        """Optional: return paths to dimension profile YAMLs."""
        return [Path(__file__).parent / "data" / "dimensions.yaml"]

    def get_cross_reference_paths(self) -> list[Path]:
        """Optional: return paths to cross-reference YAMLs."""
        return []

Register it in pyproject.toml:

[project.entry-points."fcc.plugins.personas"]
my_personas = "my_package.plugin:MyPersonaPlugin"

After installing the package, PluginRegistry.discover() will find and load the plugin automatically.

Plugin Testing

Test plugins by instantiating them directly, without entry-point discovery:

def test_my_plugin():
    plugin = MyPersonaPlugin()
    meta = plugin.plugin_meta()
    assert meta.id == "my-personas"
    assert meta.plugin_type == PluginType.PERSONAS

    paths = plugin.get_persona_paths()
    assert len(paths) >= 1
    for p in paths:
        assert p.exists(), f"Persona YAML not found: {p}"

Accessing Plugins at Runtime

After discovery, use the registry to access plugin instances:

from fcc.plugins.base import PluginType

# All persona plugins
persona_plugins = registry.get_plugins(PluginType.PERSONAS)
for plugin in persona_plugins:
    for path in plugin.get_persona_paths():
        print(f"Loading personas from: {path}")

# Specific plugin by ID
my_plugin = registry.get_plugin("my-personas")

# Metadata for all plugins
all_meta = registry.get_all_meta()

# Count by type
counts = registry.plugins_by_type()
# {"fcc.plugins.personas": 3, "fcc.plugins.engines": 1, ...}

Key Takeaways

  • 10 plugin types cover every extension point: personas, engines, templates, scorers, validators, providers, governance, scenarios, workflows, and subscribers.
  • Plugins are discovered via importlib.metadata.entry_points() and registered in PluginRegistry.
  • Each plugin exposes PluginMeta metadata and implements the ABC for its type.
  • CrossPluginOrchestrator manages inter-plugin dependencies, persona interactions, and ecosystem health.
  • Writing a plugin requires an ABC implementation, a PluginMeta, and a pyproject.toml entry point.
  • Discovery is resilient: broken plugins are logged, not propagated.

Previous: Chapter 5 -- The Workflow System | Next: Chapter 7 -- Event Bus and Observability

Try this in Notebook 06