Chapter 5: Plugin Development¶
Learning Objectives¶
By the end of this chapter you will be able to:
- Name the 10 FCC plugin types and describe the role of each.
- Implement a custom plugin with the correct abstract base class.
- Register plugins via Python entry points for automatic discovery.
- Test plugins in isolation and in integration with the framework.
- 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:
- Load persona plugins first (they define the entities other plugins reference).
- Load workflow and scenario plugins second (they reference personas).
- Load scorer and validator plugins third (they evaluate workflow outputs).
- 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:
- Entry points. Any installed package with
fcc.plugins.*entry points is discovered automatically. - Configuration. The
fcc.yamlfile can list additional plugin modules to load. - 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¶
- Test plugins in isolation. Each plugin should have unit tests that verify its behavior without loading the full framework.
- Test plugin discovery. An integration test should verify that the plugin is found by the PluginRegistry.
- Test cross-plugin interactions. If your plugin references personas or workflows from other plugins, test that the references resolve correctly.
- 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 6: Event Bus and Observability -- subscriber plugins in depth
- FCC Guidebook, Chapter 6 -- full plugin reference
- Notebook 08: Plugin Development -- interactive plugin building
- Book 1, Chapter 1: What Is FCC? -- plugin subsystem overview
← Chapter 4: Simulation and Traces | Next: Chapter 6 -- Event Bus and Observability →