Skip to content

Commit 5f5adeb

Browse files
committed
docs: add clarifications for component hook payload fields and additional suggestions by maintainers
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
1 parent 2981c9f commit 5f5adeb

File tree

1 file changed

+77
-3
lines changed

1 file changed

+77
-3
lines changed

docs/dev/hook_system.md

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Hooks support two execution timing modes, configurable per-registration:
4444
- **Blocking** (default): The hook is awaited inline. Use for policy enforcement, payload transformation, and any hook that must complete before execution continues.
4545
- **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.
4646

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.
4848

4949
### Plugin Framework
5050

@@ -54,6 +54,34 @@ The hook system is backed by a lightweight plugin framework built as a Mellea de
5454
- Exposes a base class and decorator to implement concrete plugins and register hook functions
5555
- Implements a plugin manager that loads, registers, and governs the execution of plugins
5656

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 JSON or 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+
5785
## 2. Common Payload Fields
5886

5987
All hook payloads inherit these base fields:
@@ -193,6 +221,21 @@ Hooks around Component creation and execution. All Mellea primitives — Instruc
193221

194222
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"`.
195223

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):
225+
226+
| Field | Instruction | Message | Query | Transform | GenerativeSlot |
227+
|-------|:-----------:|:-------:|:-----:|:---------:|:--------------:|
228+
| `description` ||||||
229+
| `images` ||||||
230+
| `requirements` ||||||
231+
| `icl_examples` ||||||
232+
| `grounding_context` ||||||
233+
| `user_variables` ||||||
234+
| `prefix` ||||||
235+
| `template_id` ||||||
236+
237+
Plugins should check for `None`/empty values rather than assuming all fields are present for all component types.
238+
196239

197240
#### `component_pre_create`
198241

@@ -335,6 +378,10 @@ All component payloads include a `component_type: str` field (e.g., `"Instructio
335378

336379
Low-level hooks between the component abstraction and raw LLM API calls. These operate on the (Backend, Context) tuple — they do not require a session.
337380

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+
338385

339386
#### `generation_pre_call`
340387

@@ -732,7 +779,7 @@ Hooks around context changes and management. These operate on the Context direct
732779

733780
#### `context_update`
734781

735-
- **Trigger**: When data is added to context or context changes.
782+
- **Trigger**: When a component or CBlock is explicitly 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.
736783
- **Use Cases**:
737784
- Context audit trail
738785
- Memory management policies
@@ -754,7 +801,7 @@ Hooks around context changes and management. These operate on the Context direct
754801

755802
#### `context_prune`
756803

757-
- **Trigger**: When context trimming or pruning logic runs.
804+
- **Trigger**: When `view_for_generation` is called and context exceeds token limits, or when a dedicated prune API is invoked. This is the point where context is linearized and token budget enforcement becomes relevant.
758805
- **Use Cases**:
759806
- Token budget management
760807
- Recording pruning events
@@ -950,6 +997,12 @@ class ContextSnapshot:
950997

951998
Hooks can return different result types to control execution:
952999

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+
9531006
### Modify Payload
9541007

9551008
```python
@@ -1067,6 +1120,26 @@ m = mellea.start_session(
10671120
)
10681121
```
10691122

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`) is for scoped overrides — selectively enabling or disabling specific hooks for a particular session — not for 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:
1134+
1135+
```python
1136+
@hook("react_pre_reasoning", ReactReasoningPayload, ReactReasoningResult)
1137+
async def before_reasoning(self, payload, context):
1138+
...
1139+
```
1140+
1141+
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+
10701143
## 8. Example Implementations
10711144

10721145
### Content Policy Plugin
@@ -1356,6 +1429,7 @@ plugins:
13561429
- Hook payload contracts are versioned (e.g., `payload_version: "1.0"`)
13571430
- Breaking changes increment major version
13581431
- 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
13591433

13601434
### Default Behavior
13611435

0 commit comments

Comments
 (0)