Overview — what are plugins?
Plugins let you intercept and modify Mellea’s execution at well-defined points without changing library code. Whether you need to enforce token budgets, redact PII, log every generation call, or block unsafe tool invocations, plugins give you fine-grained control over the entire pipeline. When an event fires during Mellea’s execution (e.g., “about to call the LLM” or “tool invocation requested”), the plugin system:- Dispatches a typed payload describing the event to all registered plugins
- Runs plugins in priority order, grouped by execution mode
- Returns a result — continue unchanged, continue with a modified payload, or block execution entirely
- Session Lifecycle — session init, reset, and cleanup (
session_pre_init,session_post_init,session_reset,session_cleanup) - Component Lifecycle — before and after component execution (
component_pre_execute,component_post_success,component_post_error) - Generation Pipeline — before and after LLM calls (
generation_pre_call,generation_post_call) - Validation — before and after requirement checks (
validation_pre_check,validation_post_check) - Sampling Pipeline — sampling loop events (
sampling_loop_start,sampling_iteration,sampling_repair,sampling_loop_end) - Tool Execution — before and after tool invocations (
tool_pre_invoke,tool_post_invoke)
Plugins require the hooks extra:
pip install 'mellea[hooks]'Quick start — your first plugin in 5 minutes
Here is a complete, working plugin in under 20 lines of user code. It logs a one-line summary before every LLM call.payload— A frozen, typed object containing all the data relevant to this hook point. You can read any field but cannot mutate it directly.ctx— Read-only context with session metadata (backend name, model ID, etc.).
None (continue unchanged) or a PluginResult (to modify the payload or block execution). The quickstart hook returns None implicitly, as it observes without interfering.
See the full example.
Standalone function hooks
The@hook decorator turns any async function into a plugin. This is the simplest and most common way to extend Mellea.
Anatomy of a hook function
hook_type— Which event to listen for (e.g.,HookType.GENERATION_PRE_CALL).mode— How the hook executes. Default:PluginMode.SEQUENTIAL. See Execution Modes Deep Dive.priority— Lower numbers run first. Default:50.
Execution modes at a glance
| Mode | Serial/Parallel | Can Block | Can Modify | Errors Propagated |
|---|---|---|---|---|
SEQUENTIAL | Serial | Yes | Yes | Yes |
TRANSFORM | Serial | No | Yes | Yes |
AUDIT | Serial | No | No | Yes |
CONCURRENT | Parallel | Yes | No | Yes |
FIRE_AND_FORGET | Background | No | No | No |
Blocking execution
Useblock() to reject an operation. The caller receives a PluginViolationError.
block() accepts:
reason(required) — Human-readable explanation.code— Machine-readable error code for programmatic handling.details— A dict with additional structured data.
PluginViolationError with .reason, .code, .hook_type, and .plugin_name attributes.
Modifying payloads
Payloads are frozen Pydantic models. Direct mutation raisesFrozenModelError. Instead, use the modify() helper:
model_copy(update={...}) directly and return a PluginResult:
Payload policies
Each hook type declares which fields are writable. Changes to non-writable fields are silently discarded by the framework. For example,generation_pre_call allows modifying model_options, format, and tool_calls, but not action or context. This ensures plugins cannot make changes the framework hasn’t sanctioned.
See the full policy table in the Hook Types Reference.
See the standalone hooks example and the payload modification example.
Class-based plugins
When you need shared state across multiple hooks (e.g., a redaction counter, a rate limiter’s token bucket), group them in aPlugin subclass.
When to use a class vs. standalone functions
- Standalone functions — Best for single-concern hooks that don’t share state.
- Class-based plugins — Best when multiple hooks operate on a shared concern and need access to the same instance state.
Defining a plugin
- Inherit from
Pluginand setnameandpriorityas class keyword arguments. - Decorate methods with
@hook(HookType.XXX). Theselfparameter gives access to shared state. - The class
priorityis the default for all methods. Override per-method with@hook(HookType.XXX, priority=M).
Priority resolution
Priority is resolved in this order (highest precedence first):PluginSet(priority=N)override — applies to all items in the set@hook(priority=M)on the method — overrides the class defaultPlugin(priority=N)class keyword — default for all methods50— the global default if nothing else is set
Registering a plugin
Registration and scoping
Plugins can be activated at three levels. Each level determines when hooks fire and when they are cleaned up.Global scope
Register at module level, fires for every session and every functional API call.unregister():
Session scope
Pass plugins tostart_session(), fires only within that session.
With-block scope
Activate plugins for a specific block of code with guaranteed cleanup.plugin_scope():
Plugin instance as context manager:
PluginSet as context manager:
async with for async code:
Nesting
Scopes stack cleanly. Each exit deregisters only its own plugins.Cleanup guarantee
Plugins are always deregistered on scope exit, even if the block raises an exception. There is no resource leak on error.Re-entrant restriction
The same instance cannot be active in two overlapping scopes. Create separate instances if you need parallel or nested activation:PluginSets — composing plugins
APluginSet groups related hooks and plugins into a reusable, named bundle. Use it to organize plugins by concern (security, observability, compliance) and register or scope them as a unit.
Creating a pluginset
PluginSet accepts any mix of standalone @hook functions, Plugin instances, or nested PluginSets.
Registering
Priority override
PluginSet(priority=N) overrides the priority of all contained items, including nested sets:
Real-world pattern
Register observability globally (fires everywhere) and security per-session (fires only where needed):Hook types reference
This section is a comprehensive reference for every implemented hook type. For each hook, you’ll find when it fires, what payload fields are available, which fields are writable, and typical use cases.Session lifecycle
session_pre_init
Fires: Immediately when start_session() is called, before backend initialization.
Payload fields: backend_name, model_id, model_options, context_type
Writable fields: model_id, model_options
Use cases:
- Enforcing model usage restrictions
- Injecting default model options
session_post_init
Fires: After the session is fully initialized, before any operations.
Payload fields: session (the MelleaSession instance)
Writable fields: (observe-only)
Use cases:
- Initializing telemetry for the session
- Logging session configuration
session_reset
Fires: When session.reset() is called to clear context.
Payload fields: previous_context
Writable fields: (observe-only)
Use cases:
- Preserving audit trails before reset
- Resetting plugin-specific state
session_cleanup
Fires: When the session closes (via close(), cleanup(), or context manager exit).
Payload fields: context, interaction_count
Writable fields: (observe-only)
Use cases:
- Flushing telemetry buffers
- Aggregating session metrics
Component lifecycle
component_pre_execute
Fires: Before any component is executed via aact(). This is the primary interception point for all generation requests.
Payload fields: component_type, action, context_view, requirements, model_options, format, strategy, tool_calls_enabled
Writable fields: requirements, model_options, format, strategy, tool_calls_enabled
Use cases:
- Policy enforcement on generation requests
- Injecting or modifying model options
- Content filtering and authorization checks
- Routing to different sampling strategies
component_post_success
Fires: After successful component execution.
Payload fields: component_type, action, result, context_before, context_after, generate_log, sampling_results, latency_ms
Writable fields: (observe-only)
Use cases:
- Latency and metrics collection
- Audit logging
component_post_error
Fires: When component execution fails with an exception.
Payload fields: component_type, action, error, error_type, stack_trace, context, model_options
Writable fields: (observe-only)
Use cases:
- Error logging and alerting
- Failure analysis
Generation pipeline
generation_pre_call
Fires: Just before the backend transmits data to the LLM API.
Payload fields: action, context, model_options, format, tool_calls
Writable fields: model_options, format, tool_calls
Use cases:
- Token budget enforcement
- Prompt injection detection
- Last-mile model option adjustments
generation_post_call
Fires: After the LLM response is fully materialized (model_output.value is available).
Payload fields: prompt, model_output, latency_ms
Writable fields: (observe-only)
Use cases:
- Output logging and inspection
- Response caching
- Quality metrics and hallucination detection
Validation
validation_pre_check
Fires: Before running requirement validation.
Payload fields: requirements, target, context, model_options
Writable fields: requirements, model_options
Use cases:
- Injecting additional requirements
- Overriding validation model options
validation_post_check
Fires: After all validations complete.
Payload fields: requirements, results, all_validations_passed, passed_count, failed_count, generate_logs
Writable fields: results, all_validations_passed
Use cases:
- Logging validation outcomes
- Overriding validation results
- Triggering alerts on failures
Sampling pipeline
sampling_loop_start
Fires: When a sampling strategy begins execution.
Payload fields: strategy_name, action, context, requirements, loop_budget
Writable fields: loop_budget
Use cases:
- Dynamically adjusting the iteration budget
- Logging sampling configuration
sampling_iteration
Fires: After each sampling attempt.
Payload fields: iteration, action, result, validation_results, all_validations_passed, valid_count, total_count
Writable fields: (observe-only)
Use cases:
- Iteration-level metrics
- Debugging sampling behavior
sampling_repair
Fires: When repair is invoked after a validation failure.
Payload fields: repair_type, failed_action, failed_result, failed_validations, repair_action, repair_context, repair_iteration
Writable fields: (observe-only)
Use cases:
- Analyzing failure patterns
- Logging repair events
sampling_loop_end
Fires: When sampling completes (success or failure).
Payload fields: success, iterations_used, final_result, final_action, final_context, failure_reason, all_results, all_validations
Writable fields: (observe-only)
Use cases:
- Sampling effectiveness metrics
- Cost tracking
Tool execution
tool_pre_invoke
Fires: Before invoking a tool from LLM output.
Payload fields: model_tool_call (contains name, args, callable)
Writable fields: model_tool_call
Use cases:
- Tool authorization (allow-listing)
- Argument validation and sanitization
tool_post_invoke
Fires: After tool execution completes.
Payload fields: model_tool_call, tool_output, tool_message, execution_time_ms, success, error
Writable fields: tool_output
Use cases:
- Audit logging of tool calls
- Output transformation
- Error handling
Hook payload policy table
This table summarizes which fields are writable for each hook type. Changes to non-writable fields are silently discarded.| Hook Point | Writable Fields |
|---|---|
session_pre_init | model_id, model_options |
session_post_init | (observe-only) |
session_reset | (observe-only) |
session_cleanup | (observe-only) |
component_pre_execute | requirements, model_options, format, strategy, tool_calls_enabled |
component_post_success | (observe-only) |
component_post_error | (observe-only) |
generation_pre_call | model_options, format, tool_calls |
generation_post_call | (observe-only) |
validation_pre_check | requirements, model_options |
validation_post_check | results, all_validations_passed |
sampling_loop_start | loop_budget |
sampling_iteration | (observe-only) |
sampling_repair | (observe-only) |
sampling_loop_end | (observe-only) |
tool_pre_invoke | model_tool_call |
tool_post_invoke | tool_output |
Execution modes deep dive
All hooks for a given hook type are sorted by priority, then dispatched in groups by execution mode. The execution order is always: SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET.SEQUENTIAL (default)
Serial, chained execution. Each hook receives the payload from the prior hook. Can both block and modify. This is the default mode, use it when you need full control over the pipeline.TRANSFORM
Serial, chained execution after all SEQUENTIAL hooks. Can modify but cannot block (block() calls are suppressed with a warning). Use for data transformation (PII redaction, prompt rewriting) where you want to guarantee the pipeline continues.
AUDIT
Awaited inline after TRANSFORM. Observe-only: payload modifications are discarded and violations are logged but do not block. Use for shadow policies, canary deployments, and gradual policy rollout.CONCURRENT
Dispatched in parallel after AUDIT. Can block (fail-fast on first blocking result) but cannot modify; modifications are discarded to avoid non-deterministic last-writer-wins races. Use for independent validation checks that benefit from parallel execution.FIRE_AND_FORGET
Dispatched viaasyncio.create_task() after all other phases. Receives a copy-on-write snapshot of the payload. Cannot modify or block. Exceptions are logged but never propagated. Use for telemetry, async logging, and side-effects that must not slow down the pipeline.
The FIRE_AND_FORGET log output may appear after the main result is printed. This is expected behavior, as these hooks run in the background.
Chaining
InSEQUENTIAL and TRANSFORM modes, when multiple plugins modify the same payload, modifications are composed. Plugin B sees the output of Plugin A (after policy filtering). This enables pipelines like:
- Plugin A caps
max_tokensto 256 - Plugin B (seeing the capped value) adds a
temperaturedefault - The final payload has both modifications applied
Error handling
SEQUENTIAL,TRANSFORM,AUDIT,CONCURRENT,FIRE_AND_FORGET— exceptions are logged and swallowed. They never affect the pipeline.block()— this is intentional control flow, not an error. It raises aPluginViolationErrorto the caller.
Tool hooks — securing tool calls
Thetool_pre_invoke and tool_post_invoke hooks give you fine-grained control over tool-call governance. See the MCP integration guide for tool calling basics.
Tool allow-listing
Block any tool not on an explicit approved list:Argument validation
Inspect and reject unsafe arguments before invocation:Argument sanitization
Auto-fix arguments instead of blocking (a repair pattern):Audit logging
Fire-and-forget logging of every tool call for audit trails:Composing tool hooks
Group tool security hooks into aPluginSet for clean per-session registration:
Patterns and best practices
Observability stack
Combine session tracing, component latency, and generation logging, all usingFIRE_AND_FORGET or AUDIT mode so they never slow down the pipeline:
Layered security
Stack enforcement across scopes:- Global: Token budget enforcement (
SEQUENTIAL) - Session-scoped: Content policy for sensitive sessions
- With-block: Feature flags for specific operations
Input/output guardrails
Block PII on input (component_pre_execute) and redact PII from output (generation_post_call) using a class-based plugin with shared state:
Graceful degradation with AUDIT mode
Deploy a new policy inAUDIT mode first, where violations are logged but do not block. Monitor the logs. When you’re confident, promote to SEQUENTIAL:
Testing plugins
You can unit-test hook functions without running a full Mellea session. Construct a payload mock, call the function directly, and assert the result:Idempotent lifecycle hooks
If you use the advancedMelleaPlugin base class (which provides initialize() and shutdown() callbacks), make them idempotent, as they may be called once per @hook method on your plugin.
API reference
All public symbols are available from a single import:| Symbol | Description |
|---|---|
@hook(hook_type, *, mode, priority) | Decorator that marks an async function as a hook handler |
Plugin | Base class for multi-hook plugins with shared state. Set name and priority via class keywords |
PluginSet(name, items, *, priority) | Groups hooks, plugins, and nested sets into a reusable bundle |
register(items, *, session_id) | Register hooks/plugins. session_id=None for global scope |
unregister(items) | Remove globally-registered items |
plugin_scope(*items) | Context manager that registers on enter, deregisters on exit |
block(reason, *, code, details) | Create a blocking PluginResult |
modify(payload, **field_updates) | Create a modifying PluginResult via model_copy |
HookType | Enum with all 17 hook types |
PluginMode | Enum: SEQUENTIAL, TRANSFORM, AUDIT, CONCURRENT, FIRE_AND_FORGET |
PluginResult | Typed result with continue_processing, modified_payload, and violation |
PluginViolationError | Exception with .reason, .code, .hook_type, .plugin_name |
See also: Glossary, Tools and Agents, Security and Taint Tracking, OpenTelemetry Tracing