You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/dev/hook_system.md
+77-3Lines changed: 77 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,7 +44,7 @@ Hooks support two execution timing modes, configurable per-registration:
44
44
-**Blocking** (default): The hook is awaited inline. Use for policy enforcement, payload transformation, andany hook that must complete before execution continues.
45
45
-**Fire-and-forget**: The hook is dispatched via `asyncio.create_task()`and runs in the background. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees.
46
46
47
-
Fire-and-forget hooks cannot modify payloads or block execution — their `PluginResult`is ignored. Any exceptions in fire-and-forget hooks are logged but do not propagate.
47
+
Fire-and-forget hooks cannot modify payloads or block execution — their `PluginResult`is ignored. Any exceptions in fire-and-forget hooks are logged but do not propagate. Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; blocking hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it.
48
48
49
49
### Plugin Framework
50
50
@@ -54,6 +54,34 @@ The hook system is backed by a lightweight plugin framework built as a Mellea de
54
54
- Exposes a base classand decorator to implement concrete plugins and register hook functions
55
55
- Implements a plugin manager that loads, registers, and governs the execution of plugins
56
56
57
+
### Hook Invocation Responsibilities
58
+
59
+
Hooks are called from Mellea's base classes (`Component.aact()`, `Backend.generate()`, `SamplingStrategy.run()`, etc.). This means hook invocation is a framework-level concern, and authors of new backends, sampling strategies, or components do not need to manually insert hook calls.
60
+
61
+
The calling convention is a single async call at each hook site:
62
+
63
+
```python
64
+
result = await plugin_manager.invoke_hook(hook_type, payload, context)
65
+
```
66
+
67
+
The caller (the base class method) is responsible for both invoking the hook and processing the result. Processing means checking the result for one of three possible outcomes:
68
+
69
+
1. **Continue with original payload**: — `PluginResult(continue_processing=True)`with no `modified_payload`. The caller proceeds unchanged.
70
+
2. **Continue with modified payload**: — `PluginResult(continue_processing=True, modified_payload=...)`. The caller uses the modified payload fields in place of the originals.
71
+
3. **Block execution** — `PluginResult(continue_processing=False, violation=...)`. The caller raises or returns early with structured error information.
72
+
73
+
Hooks cannot redirect control flow, jump to arbitrary code, or alter the calling method's logic beyond these outcomes. This is enforced by the `PluginResult` type.
74
+
75
+
### Payload Design Principles
76
+
77
+
Hook payloads follow five design principles:
78
+
79
+
1. **Strongly typed** — Each hook has a dedicated payload dataclass (not a generic dict). This enables IDE autocompletion, static analysis, and clear documentation of what each hook receives.
80
+
2. **Sufficient (maximize-at-boundary)** — Each payload includes everything available at that point in time. Post-hooks include the pre-hook fields plus results. This avoids forcing plugins to maintain their own state across pre/post pairs.
81
+
3. **Immutable context** — `PluginContext` fields are read-only; only the `payload`is mutable. This separates "what the plugin can observe"from"what the plugin can change."
82
+
4. **Serializable** — Payloads should be serializable for external (MCP-based) plugins that run out-of-process. All payload fields use types that can round-trip through JSONor similar formats.
83
+
5. **Versioned** — Payload schemas carry a `payload_version` so plugins can detect incompatible changes at registration time rather than at runtime.
84
+
57
85
## 2. Common Payload Fields
58
86
59
87
All hook payloads inherit these base fields:
@@ -193,6 +221,21 @@ Hooks around Component creation and execution. All Mellea primitives — Instruc
193
221
194
222
All component payloads include a `component_type: str` field (e.g., `"Instruction"`, `"Message"`, `"GenerativeSlot"`, `"Query"`, `"Transform"`) so plugins can filter by type. For example, a plugin targeting only generative slots would check `component_type =="GenerativeSlot"`.
195
223
224
+
Not all`ComponentPreCreatePayload` fields are populated for every component type. The table below shows which fields are available per type (`✓`= populated, `—`=`None`or empty):
Plugins should check for`None`/empty values rather than assuming all fields are present forall component types.
238
+
196
239
197
240
#### `component_pre_create`
198
241
@@ -335,6 +378,10 @@ All component payloads include a `component_type: str` field (e.g., `"Instructio
335
378
336
379
Low-level hooks between the component abstraction and raw LLMAPI calls. These operate on the (Backend, Context) tuple — they do not require a session.
337
380
381
+
>**Context Modification Sequencing**
382
+
>
383
+
> Modifications to `Context` at `component_pre_execute` are reflected in the subsequent `generation_pre_call`, because context linearization happens after the component-level hook. Modifications to `Context` after `generation_pre_call` (e.g., in`generation_post_call`) do not affect the current generation — the prompt has already been sent. This ordering is by design: `component_pre_execute`is the last point where context changes influence what the LLM sees.
384
+
338
385
339
386
#### `generation_pre_call`
340
387
@@ -732,7 +779,7 @@ Hooks around context changes and management. These operate on the Context direct
732
779
733
780
#### `context_update`
734
781
735
-
-**Trigger**: When data isadded to context or context changes.
782
+
-**Trigger**: When a component or CBlock isexplicitly appended to a session's context (e.g., after a successful generation or a user-initiated addition). Does not fire on internal framework reads or context linearization.
736
783
-**Use Cases**:
737
784
- Context audit trail
738
785
- Memory management policies
@@ -754,7 +801,7 @@ Hooks around context changes and management. These operate on the Context direct
754
801
755
802
#### `context_prune`
756
803
757
-
-**Trigger**: When context trimming orpruning logic runs.
804
+
-**Trigger**: When `view_for_generation`is called andcontext exceeds token limits, orwhen a dedicated prune APIis invoked. This is the point where context is linearized and token budget enforcement becomes relevant.
758
805
-**Use Cases**:
759
806
- Token budget management
760
807
- Recording pruning events
@@ -950,6 +997,12 @@ class ContextSnapshot:
950
997
951
998
Hooks can return different result types to control execution:
952
999
1000
+
1. **Continue (no-op)** — `PluginResult(continue_processing=True)`with no `modified_payload`. Execution proceeds with the original payload unchanged.
1001
+
2. **Continue with modification** — `PluginResult(continue_processing=True, modified_payload=...)`. Execution proceeds with the modified payload fields in place of the originals.
1002
+
3. **Block execution** — `PluginResult(continue_processing=False, violation=...)`. Execution halts with structured error information via `PluginViolation`.
1003
+
1004
+
These three outcomes are exhaustive. Hooks cannot redirect control flow, throw arbitrary exceptions, or alter the calling method's logic beyond these outcomes. This is enforced by the `PluginResult` type — there is no escape hatch. The `violation` field provides structured error information but does not influence which code path runs next.
1005
+
953
1006
### Modify Payload
954
1007
955
1008
```python
@@ -1067,6 +1120,26 @@ m = mellea.start_session(
1067
1120
)
1068
1121
```
1069
1122
1123
+
### Global PluginManager
1124
+
1125
+
The hook system uses a **singleton PluginManager** that is initialized once (typically at application startup via YAML config) and shared globally. Session-level configuration (e.g., `hooks_enabled`) isfor scoped overrides — selectively enabling or disabling specific hooks for a particular session — notfor owning or replacing the plugin manager.
1126
+
1127
+
For the functional (non-session) path (e.g., calling `instruct()`or`generate()` directly without a `MelleaSession`), the PluginManager is accessed directly. Hooks still fire at the same points in the execution lifecycle; the only difference is that session-scoped overrides do not apply.
1128
+
1129
+
### Custom Hook Types
1130
+
1131
+
The plugin framework supports custom hook types for domain-specific extension points beyond the built-in lifecycle hooks. This is particularly relevant for agentic patterns (ReAct, tool-use loops, etc.) where the execution flow is application-defined.
1132
+
1133
+
Custom hooks are registered using the `@hook` decorator:
Custom hooks follow the same calling convention, payload chaining, and result semantics as built-in hooks. The plugin manager discovers them via the decorator metadata at registration time. As agentic patterns stabilize in Mellea, frequently-used custom hooks may be promoted to built-in hooks.
1142
+
1070
1143
## 8. Example Implementations
1071
1144
1072
1145
### Content Policy Plugin
@@ -1356,6 +1429,7 @@ plugins:
1356
1429
- Hook payload contracts are versioned (e.g., `payload_version: "1.0"`)
1357
1430
- Breaking changes increment major version
1358
1431
- Deprecated fields marked and maintained for one major version
1432
+
- Hook payload versions are independent of Mellea release versions. Payload versions change only when the payload schema changes, which may or may not coincide with a Mellea release
0 commit comments