types — Plugin Type Hierarchy
On this page
Overview
Four canonical plugin archetypes for a RAG ecosystem, each extending BasePlugin
with a PLUGIN_TYPE class marker (used by PluginRegistry's type index) and, in some cases, a
narrower execute() signature or extra convenience methods.
RetrievalPlugin
class RetrievalPlugin(BasePlugin):
PLUGIN_TYPE = "retrieval"
@abstractmethod
async def execute(self, input_data: Dict[str, Any], context: ExecutionContext) -> List[Dict[str, Any]]:
"""Return ranked list of retrieved documents."""
async def retrieve(self, query: str, top_k: int = 5, context: ExecutionContext = None) -> List[Dict[str, Any]]:
ctx = context or ExecutionContext(query=query)
return await self.safe_execute({"query": query, "top_k": top_k}, ctx)
Output contract (documented in the class docstring, not enforced at runtime): each returned dict
should contain at least content: str, score: float (0–1 relevance), and source: str.
retrieve() is a convenience wrapper — it builds a minimal ExecutionContext if none is given and
calls safe_execute() (so it still goes through validation, timeout, retries-if-configured-by-caller,
and caching).
Example
from zerda.plugins import RetrievalPlugin, PluginMetadata, SchemaProperty, ExecutionContext
class InMemorySearch(RetrievalPlugin):
METADATA = PluginMetadata(
name="inmem_search", version="1.0.0", author="docs",
description="Searches an in-memory list of documents.",
input_schema=[SchemaProperty(name="query", type="string", description="Search text")],
)
def __init__(self, docs, config=None):
super().__init__(config)
self._docs = docs
async def initialize(self) -> bool:
return True
async def execute(self, input_data, context: ExecutionContext):
q = input_data["query"].lower()
return [
{"content": d, "score": 1.0 if q in d.lower() else 0.0, "source": "memory"}
for d in self._docs if q in d.lower()
]
async def cleanup(self) -> None:
pass
plugin = InMemorySearch(["The sky is blue.", "RAG stands for Retrieval-Augmented Generation."])
await plugin.initialize()
results = await plugin.retrieve("RAG", top_k=3)
ToolPlugin
class ToolPlugin(BasePlugin):
PLUGIN_TYPE = "tool"
@abstractmethod
async def execute(self, input_data: Dict[str, Any], context: ExecutionContext) -> Dict[str, Any]:
"""Run the tool and return a structured result."""
The simplest of the four types — adds only the PLUGIN_TYPE marker and a narrowed return-type
annotation on execute(). Per its docstring, this is "the primary target for LLM function-calling,"
i.e. the type you'd typically expose via PluginManager.tool_descriptors() /
anthropic_tools().
ActionPlugin
class ActionPlugin(BasePlugin):
PLUGIN_TYPE = "action"
REQUIRES_CONFIRMATION: bool = True
async def can_execute(self, input_data, context) -> bool:
return True # override for pre-flight checks
async def safe_execute(self, input_data, context) -> Any:
if not await self.can_execute(input_data, context):
raise PluginPermissionError(f"[{self.name}] Pre-flight check failed; execution blocked.")
return await super().safe_execute(input_data, context)
@abstractmethod
async def execute(self, input_data, context) -> Dict[str, Any]:
"""Execute the action. Return dict with at least "success": bool and "message": str."""
The only type in this package that overrides safe_execute() rather than only execute(). It
inserts a can_execute() pre-flight gate before delegating to BasePlugin.safe_execute(). This means
the pre-flight check runs before caching/timeout logic — a rejected can_execute() never touches the
cache or the timeout wrapper.
REQUIRES_CONFIRMATION is a class attribute intended as a signal to calling code (e.g. an application's
UI layer) that this action should be confirmed before running; the framework itself does not read or
enforce this flag anywhere in plugin_manager.py or elsewhere in the package — enforcing it is left
to the application.
Raises
PluginPermissionError— ifcan_execute()returnsFalse.- Everything
BasePlugin.safe_execute()can raise (see base_plugin).
Example
from zerda.plugins import ActionPlugin, PluginMetadata, SchemaProperty, ExecutionContext
class SendEmailPlugin(ActionPlugin):
METADATA = PluginMetadata(
name="send_email", version="1.0.0", author="docs",
description="Sends an email.",
input_schema=[SchemaProperty(name="to", type="string", description="Recipient")],
permissions=["network_access"],
)
async def can_execute(self, input_data, context) -> bool:
return "@" in input_data.get("to", "")
async def initialize(self) -> bool:
return True
async def execute(self, input_data, context: ExecutionContext) -> dict:
# ... actually send the email ...
return {"success": True, "message": f"Sent to {input_data['to']}"}
async def cleanup(self) -> None:
pass
ProcessingPlugin
class ProcessingPlugin(BasePlugin):
PLUGIN_TYPE = "processing"
@abstractmethod
async def execute(self, input_data: Dict[str, Any], context: ExecutionContext) -> List[Any]:
"""Process and return transformed items."""
async def process(self, items: List[Any], context: ExecutionContext = None, **kwargs) -> List[Any]:
ctx = context or ExecutionContext()
return await self.safe_execute({"items": items, **kwargs}, ctx)
Input contract: {"items": List[Any], ...extra}. Output contract: List[Any] — same or
transformed items. Like RetrievalPlugin.retrieve(), process() is a thin convenience wrapper around
safe_execute().
Example
from zerda.plugins import ProcessingPlugin, PluginMetadata, ExecutionContext
class UppercasePlugin(ProcessingPlugin):
METADATA = PluginMetadata(
name="uppercase", version="1.0.0", author="docs",
description="Uppercases a list of strings.",
)
async def initialize(self) -> bool:
return True
async def execute(self, input_data, context: ExecutionContext) -> list:
return [s.upper() for s in input_data["items"]]
async def cleanup(self) -> None:
pass
plugin = UppercasePlugin()
await plugin.initialize()
out = await plugin.process(["hello", "world"]) # ["HELLO", "WORLD"]
PLUGIN_TYPE_MAP
PLUGIN_TYPE_MAP = {
"retrieval": RetrievalPlugin,
"tool": ToolPlugin,
"action": ActionPlugin,
"processing": ProcessingPlugin,
}
A plain dict constant exported from plugins/types/__init__ (and re-exported from the top-level
plugins package). Maps each PLUGIN_TYPE string to its corresponding abstract base class. Not
referenced elsewhere within this source tree's other modules — provided as a lookup table for
application code that needs to resolve a type name back to a class (e.g. for validating a manifest
before dynamic subclassing).
Choosing a type
Related APIs
BasePlugin— the shared parent of all four typesPluginRegistry.list_by_type— filter the registry byPLUGIN_TYPEPluginManager.select— accepts aplugin_typefilter