Skip to main content

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:
  1. Dispatches a typed payload describing the event to all registered plugins
  2. Runs plugins in priority order, grouped by execution mode
  3. Returns a result — continue unchanged, continue with a modified payload, or block execution entirely
Plugins are organized into six hook categories:
  • 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, generation_error)
  • 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.
Every hook function receives two arguments:
  • 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.).
A hook returns either 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

Blocking execution

Use block() 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.
The caller catches the violation as a PluginViolationError with .reason, .code, .hook_type, and .plugin_name attributes.

Modifying payloads

Payloads are frozen Pydantic models. Direct mutation raises FrozenModelError. Instead, use the modify() helper:
For more control, use 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 a Plugin 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

Key syntax:
  • Inherit from Plugin and set name and priority as class keyword arguments.
  • Decorate methods with @hook(HookType.XXX). The self parameter gives access to shared state.
  • The class priority is 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):
  1. PluginSet(priority=N) override — applies to all items in the set
  2. @hook(priority=M) on the method — overrides the class default
  3. Plugin(priority=N) class keyword — default for all methods
  4. 50 — the global default if nothing else is set

Registering a plugin

See the full PII redaction example.

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.
Remove with unregister():

Session scope

Pass plugins to start_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:
All three forms support 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:
See the scoped plugins example and the session-scoped example.

PluginSets — composing plugins

A PluginSet 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

A 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):
See the full PluginSet composition example.

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

generation_error

Fires: When the LLM backend raises an exception during output materialization, just before the exception is re-raised. Payload fields: exception, model_output Writable fields: (observe-only) Use cases:
  • Error telemetry and alerting on backend failures
  • Logging structured error information for debugging

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), is_control_flow Writable fields: model_tool_call Use cases:
  • Tool authorization (allow-listing)
  • Argument validation and sanitization
The payload includes an is_control_flow field that is True for framework control-flow tools (e.g. the ReAct loop’s final_answer). Allowlist plugins should check this field to avoid blocking internal tools. See Control-flow tools for the recommended pattern.

tool_post_invoke

Fires: After tool execution completes. Payload fields: model_tool_call, tool_output, tool_message, execution_time_ms, success, error, is_control_flow 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.
Only SEQUENTIAL and TRANSFORM modes can modify payloads. AUDIT, CONCURRENT, and FIRE_AND_FORGET modes have their modifications silently discarded regardless of the policy table.

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 via asyncio.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

In SEQUENTIAL 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:
  1. Plugin A caps max_tokens to 256
  2. Plugin B (seeing the capped value) adds a temperature default
  3. 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 a PluginViolationError to the caller.
See the execution modes example.

Tool hooks — securing tool calls

The tool_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. The is_control_flow guard ensures framework tools like final_answer are not blocked:

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 a PluginSet for clean per-session registration:
See the full tool hooks example.

Control-flow tools

Mellea’s frameworks use internal tools for control flow. For example, the ReAct loop uses a final_answer tool to signal that the agent has finished reasoning. These tools flow through the same invocation path as user-defined tools — hooks always fire for them — but the payload carries an is_control_flow flag so each plugin can decide its own policy. The recommended pattern for allowlist plugins is to skip control-flow tools explicitly:
Logging and telemetry plugins typically do not check this flag — they observe all tool calls including control-flow tools:

Querying the registry

Use is_internal_tool() to check whether a tool name is a known control-flow tool:

Patterns and best practices

Observability stack

Combine session tracing, component latency, and generation logging, all using FIRE_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 in AUDIT 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:
See the full testing example.

Idempotent lifecycle hooks

If you use the advanced MelleaPlugin 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:

See also: Glossary, Tools and Agents, Safety Guardrails, OpenTelemetry Tracing