Define a clear, minimal plugin contract and loader API for all external/ built-in plugins. Motivation: plugins (taggers/scorers) are the public extension points of the system and must declare what data they need, how they initialize, and be loadable from arbitrary modules so users can provide custom implementations without modifying the project.

Rules and how to apply them:

Example usage (illustrative):

Plugin class (sketch)

class MyTagger(TaggerBase): @classmethod def required_response_metadata_fields(cls) -> list[str]: return [“logprobs”, “finish_reason”]

@classmethod
def include_text(cls) -> bool:
    return False  # avoids returning full text when not needed

@classmethod
def validate_contract(cls) -> None:
    # ensure no __init__ and required methods exist
    ...

def init(self, ctx: Context) -> None:
    # optional initialization using provided Context
    ...

def tag_batch(self, responses: list[Response]) -> list[dict[str, float]]:
    ...

Loader usage (sketch)

plugin_cls = load_plugin(name=settings.tagger) # accepts package name or module:path plugin_cls.validate_contract() model.set_requested_response_fields(plugin_cls.required_response_metadata_fields()) model.set_requested_context_fields(plugin_cls.required_context_metadata_fields()) plugin = plugin_cls(settings=settings, model=model, context_metadata=model.get_context_metadata()) plugin.init(Context(settings=settings, model=model))

Rationale: This rule prevents fragile implicit contracts, avoids unnecessary data transfers (performance), enables third-party plugin distribution without altering core code, and makes plugin initialization predictable and testable. Following it reduces bugs from mismatched expectations (missing fields, incorrect init semantics, hardcoded package names) and simplifies upgrading third-party libraries by keeping argument names and responsibilities explicit.