Skip to content

Chapter 5: Plugin Development

Learning Objectives

By the end of this chapter you will be able to:

  1. Name the 10 FCC plugin types and describe the role of each.
  2. Implement a custom plugin with the correct abstract base class.
  3. Register plugins via Python entry points for automatic discovery.
  4. Test plugins in isolation and in integration with the framework.
  5. Use cross-plugin orchestration to coordinate multiple plugins.

The mindmap below enumerates the ten plugin types FCC supports, each paired with its abstract base class, so developers can pick the right extension point at a glance.

mindmap
  root((10 Plugin Types))
    Personas
      Contribute YAML definitions
      PersonaPlugin ABC
    Engines
      Custom simulation engines
      EnginePlugin ABC
    Templates
      Jinja2 template dirs
      TemplatePlugin ABC
    Scorers
      Quality scoring functions
      ScorerPlugin ABC
    Validators
      Validation rules
      ValidatorPlugin ABC
    Providers
      AI model providers
      AIProviderPlugin ABC
    Governance
      Rules and policies
      GovernancePlugin ABC
    Scenarios
      Pre-built scenarios
      ScenarioPlugin ABC
    Workflows
      Custom workflow graphs
      WorkflowPlugin ABC
    Subscribers
      Event bus subscribers
      EventSubscriberPlugin ABC

Newer plugin categories such as vocabulary_providers extend the list beyond the ten shown here; the mindmap captures the stable core while leaving room for plugin types introduced in later minor releases.

The Plugin Architecture

FCC's plugin system is the primary mechanism for extending the framework without modifying its source code. There are 10 plugin types, each corresponding to an entry-point group and an abstract base class:

# Plugin Type Entry Point Group Purpose
1 Personas fcc.plugins.personas Contribute new persona YAML definitions
2 Engines fcc.plugins.engines Provide custom simulation engines
3 Templates fcc.plugins.templates Add Jinja2 templates for documentation
4 Scorers fcc.plugins.scorers Define custom quality scoring functions
5 Validators fcc.plugins.validators Add custom validation rules
6 Providers fcc.plugins.providers Integrate new AI model providers
7 Governance fcc.plugins.governance Contribute governance rules and policies
8 Scenarios fcc.plugins.scenarios Add pre-built scenarios
9 Workflows fcc.plugins.workflows Contribute custom workflow graphs
10 Subscribers fcc.plugins.subscribers Register event bus subscribers

Plugins are discovered at runtime via Python's importlib.metadata.entry_points() mechanism. This means any installed Python package that declares the right entry points becomes an FCC plugin automatically.

Building Your First Plugin: A Custom Scorer

Let us build a scorer plugin that evaluates code outputs for test coverage mentions. This is a practical, focused example that covers the full plugin lifecycle.

Step 1: Implement the Plugin

"""fcc_coverage_scorer -- A custom scorer plugin for FCC."""
from dataclasses import dataclass
from fcc.collaboration.scoring import ScorerPlugin, ScoreResult


@dataclass(frozen=True)
class CoverageScorer(ScorerPlugin):
    """Scores artifacts based on whether they mention test coverage."""

    name: str = "coverage_scorer"
    description: str = "Checks that code artifacts mention test coverage"

    def score(self, artifact: str, context: dict) -> ScoreResult:
        """Score the artifact for test coverage mentions."""
        coverage_keywords = [
            "test coverage", "pytest", "unittest",
            "coverage report", "assert", "test_"
        ]
        matches = sum(1 for kw in coverage_keywords if kw in artifact.lower())
        score = min(matches / 3.0, 1.0)  # Normalize to 0-1

        return ScoreResult(
            scorer_name=self.name,
            score=score,
            passed=score >= 0.5,
            details={
                "keywords_found": matches,
                "threshold": 0.5,
            },
            message=(
                f"Found {matches} coverage keywords. "
                f"Score: {score:.2f} ({'PASS' if score >= 0.5 else 'FAIL'})"
            ),
        )

Step 2: Register via Entry Points

In your plugin package's pyproject.toml:

[project]
name = "fcc-coverage-scorer"
version = "0.1.0"

[project.entry-points."fcc.plugins.scorers"]
coverage_scorer = "fcc_coverage_scorer:CoverageScorer"

When the package is installed, FCC's plugin discovery system finds the entry point and registers the scorer.

Step 3: Test the Plugin

"""Tests for the coverage scorer plugin."""
import pytest
from fcc_coverage_scorer import CoverageScorer


@pytest.fixture
def scorer():
    return CoverageScorer()


def test_scores_high_for_test_code(scorer):
    artifact = """
    def test_data_pipeline():
        assert pipeline.run() == expected
        # Test coverage: 95%
    """
    result = scorer.score(artifact, {})
    assert result.passed
    assert result.score >= 0.5


def test_scores_low_for_no_tests(scorer):
    artifact = "This is a design document with no code."
    result = scorer.score(artifact, {})
    assert not result.passed
    assert result.score < 0.5


def test_score_result_has_details(scorer):
    result = scorer.score("pytest assert test_func", {})
    assert "keywords_found" in result.details

Step 4: Integration Test

Verify the plugin is discovered by the framework:

def test_plugin_discovered():
    from fcc.plugins.registry import PluginRegistry

    registry = PluginRegistry()
    registry.discover_all()

    scorer = registry.get_scorer("coverage_scorer")
    assert scorer is not None
    assert scorer.name == "coverage_scorer"

Plugin Types in Depth

Persona Plugins

Persona plugins contribute YAML files with persona definitions. They are the simplest plugin type -- no code is required, just data:

from fcc.plugins.base import PersonaPlugin
from pathlib import Path

class MyPersonaPlugin(PersonaPlugin):
    name = "my_domain_personas"

    def get_persona_paths(self) -> list[Path]:
        return [Path(__file__).parent / "data" / "personas.yaml"]

The PersonaRegistry merges plugin-contributed personas with the core catalog and local project personas.

Engine Plugins

Engine plugins provide alternative simulation backends. You might implement an engine that calls a local LLM, uses a different API protocol, or applies domain-specific output post-processing:

from fcc.plugins.base import EnginePlugin
from fcc.simulation.engine import SimulationResult

class LocalLLMEngine(EnginePlugin):
    name = "local_llm"

    def run(self, scenario, config) -> SimulationResult:
        # Call your local LLM inference server
        ...

Subscriber Plugins

Subscriber plugins register event bus listeners. This is the 10th plugin type, added to enable third-party packages to react to FCC events without modifying the core:

from fcc.messaging.plugin_bridge import EventSubscriberPlugin
from fcc.messaging.events import Event, EventType

class MetricsCollector(EventSubscriberPlugin):
    name = "metrics_collector"
    event_types = [EventType.NODE_COMPLETED, EventType.GATE_EVALUATED]

    def handle(self, event: Event) -> None:
        # Send metrics to your monitoring system
        ...

Cross-Plugin Orchestration

When multiple plugins interact, the cross-plugin orchestration system manages dependencies and execution order. For example:

  • A persona plugin contributes new personas.
  • A workflow plugin contributes a graph that references those personas.
  • A scorer plugin contributes quality gates for the workflow's outputs.
  • A subscriber plugin logs all events from the workflow.

The orchestration system resolves these dependencies at startup:

  1. Load persona plugins first (they define the entities other plugins reference).
  2. Load workflow and scenario plugins second (they reference personas).
  3. Load scorer and validator plugins third (they evaluate workflow outputs).
  4. Load subscriber plugins last (they observe everything).

Dependency resolution is automatic based on the plugin type hierarchy. Circular dependencies between plugins are detected and reported as errors.

Plugin Interaction Matrix

The PluginRegistry maintains an interaction matrix that tracks which plugins reference each other. You can query it:

registry = PluginRegistry()
registry.discover_all()

# What does the coverage_scorer interact with?
interactions = registry.get_interactions("coverage_scorer")
for interaction in interactions:
    print(f"{interaction.source} -> {interaction.target}: {interaction.type}")

Plugin Discovery and Registration

Plugins are discovered in three ways, checked in order:

  1. Entry points. Any installed package with fcc.plugins.* entry points is discovered automatically.
  2. Configuration. The fcc.yaml file can list additional plugin modules to load.
  3. Programmatic. The PluginRegistry.register() method allows manual registration for testing and prototyping.

For production use, entry points are the recommended approach because they integrate with pip's dependency management and do not require configuration changes.

Testing Best Practices

  1. Test plugins in isolation. Each plugin should have unit tests that verify its behavior without loading the full framework.
  2. Test plugin discovery. An integration test should verify that the plugin is found by the PluginRegistry.
  3. Test cross-plugin interactions. If your plugin references personas or workflows from other plugins, test that the references resolve correctly.
  4. Use mock mode. Plugin integration tests should run in mock mode to avoid API costs and ensure determinism.

Key Takeaways

  • FCC has 10 plugin types, each with an ABC and entry-point group.
  • Plugins are discovered automatically via Python entry points.
  • Cross-plugin orchestration resolves dependencies based on plugin type hierarchy.
  • Test plugins at three levels: isolation, discovery, and integration.
  • Entry points are the recommended registration mechanism for production.

Cross-References


← Chapter 4: Simulation and Traces | Next: Chapter 6 -- Event Bus and Observability →