Hooks¶
HookEngine ¶
Manages global and per-room hook registration and execution.
has_off_lock_hooks ¶
Whether a needs_lock=False check applies to event (RFC §9.5.1).
has_sync_hooks ¶
Whether a SYNC hook of trigger applies to event in room_id.
has_hooks ¶
Whether a hook is registered — for trigger, or for any trigger at all.
O(1) either way: a set lookup, or the set's emptiness. The framework
skips the work only a hook would consume when nothing is listening —
the context built for BEFORE_DELIVER, the room history loaded for
the inbound pipeline — so it runs a handful of times per message,
never per hook. Global and room hooks alike are in the index; identity
hooks live in the framework's own registry, and a caller that needs
both asks both.
fail_closed_hook ¶
The name of the first SYNC hook on trigger that fails closed, or None.
For a caller that cannot run the pipeline at all (the room's context would not build): with such a hook registered, the payload must be withheld as the hook's own failure would withhold it.
run_sync_hooks
async
¶
run_sync_hooks(room_id, trigger, event, context, *, skip_event_filter=False, needs_lock=None, fold=None, fire_observers=True)
Run sync hooks sequentially. Stops on block, passes modified events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room ID to run hooks for. |
required |
trigger
|
HookTrigger
|
The hook trigger type. |
required |
event
|
RoomEvent | Any
|
The event to pass to hooks. For voice hooks, this may be a VoiceSession or str instead of RoomEvent. |
required |
context
|
RoomContext
|
The room context. |
required |
skip_event_filter
|
bool
|
If True, skip channel-based event filtering. Use this for voice hooks where event is not a RoomEvent. |
False
|
needs_lock
|
bool | None
|
|
None
|
fold
|
Callable[[Any, dict[str, Any]], Any] | None
|
For a trigger whose hooks may also rewrite the payload
through |
None
|
fire_observers
|
bool
|
|
True
|
run_async_hooks
async
¶
run_async_hooks(room_id, trigger, event, context, *, skip_event_filter=False, name_prefix=None, exclude_name_prefix=None)
Run async hooks concurrently. Errors are logged, never raised.
Finds hooks regardless of their declared execution mode so that
hooks registered with the default SYNC execution still fire
for triggers that are only invoked asynchronously (e.g.
AFTER_BROADCAST, lifecycle hooks, voice hooks).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room ID to run hooks for. |
required |
trigger
|
HookTrigger
|
The hook trigger type. |
required |
event
|
RoomEvent | Any
|
The event to pass to hooks. For voice hooks, this may be a VoiceSession or str instead of RoomEvent. |
required |
context
|
RoomContext
|
The room context. |
required |
skip_event_filter
|
bool
|
If True, skip channel-based event filtering. Use this for voice hooks where event is not a RoomEvent. |
False
|
name_prefix
|
str | None
|
Only run hooks whose name starts with this prefix. |
None
|
exclude_name_prefix
|
str | None
|
Skip hooks whose name starts with this prefix. |
None
|
run_observers
async
¶
Run only the ASYNC-registered hooks for trigger, fire-and-forget.
The counterpart of :meth:run_async_hooks, which deliberately ignores
the declared execution mode. Here the mode is the whole point: it is
what separates a hook that observes a call from one that serves it.
A refused tool call must still be observable — an audit trail that cannot see a denial cannot tell a denied agent from an idle one — but it must not reach a hook that would serve it, or the denial would merely hide the side effect instead of preventing it. Only a SYNC hook can serve a call (it is the one that returns a result), so dispatching the ASYNC hooks alone makes the distinction structural rather than a rule each hook author has to remember.
HookRegistration
dataclass
¶
HookRegistration(trigger, execution, fn, priority=0, name='', timeout=30.0, channel_types=None, channel_ids=None, directions=None, event_types=None, fail_closed=False, needs_lock=True)
A registered hook function.
Attributes:
| Name | Type | Description |
|---|---|---|
trigger |
HookTrigger
|
When the hook fires (BEFORE_BROADCAST, AFTER_BROADCAST, etc.) |
execution |
HookExecution
|
SYNC (can block/modify) or ASYNC (fire-and-forget) |
fn |
SyncHookFn | AsyncHookFn
|
The hook function |
priority |
int
|
Lower numbers run first (default: 0) |
name |
str
|
Optional name for logging and removal |
timeout |
float
|
Max execution time in seconds (default: 30.0) |
channel_types |
set[ChannelType] | None
|
Only run for events from these channel types (None = all) |
channel_ids |
set[str] | None
|
Only run for events from these channel IDs (None = all) |
directions |
set[ChannelDirection] | None
|
Only run for events with these directions (None = all) |
event_types |
set[EventType] | None
|
Only run for events of these types (None = all) |
fail_closed |
bool
|
SYNC only — a timeout, an exception or an unusable result blocks instead of allowing (RFC §9.3). For a content check that must never let an unchecked payload through. |
needs_lock |
bool
|
BEFORE_BROADCAST SYNC only — |