pub struct HookBus { /* private fields */ }Expand description
The central hook dispatcher.
Thread-safe: uses RwLock so multiple concurrent emitters can read
the handler list, and registration takes a write lock only briefly.
Implementations§
Source§impl HookBus
impl HookBus
Sourcepub fn worker_transforms(host: &Arc<HookBus>) -> Arc<HookBus> ⓘ
pub fn worker_transforms(host: &Arc<HookBus>) -> Arc<HookBus> ⓘ
The hook bus a subagent worker runs with: a read-only view of
host’s after_tool_call subscriptions from extensions that hold
tools.transform_output, so output transforms (compression,
redaction) see worker tool results too. Nothing else fires in a
worker: no before_tool_call (no confirm prompt, block or modify),
no message or session hooks, no observe-only after_tool_call.
Live, not a snapshot: every Self::emit reads host’s current
handler list, so an extension loaded, reloaded or unloaded while a
worker runs applies from that worker’s next tool call (a snapshot
would keep calling a reloaded extension’s stopped instance).
Dispatch is otherwise Self::emit’s: chain order, the permission
matrix, the 5 s timeout, first replace wins. Handlers are called
through ExtensionHandler::handle_no_spawn: a worker never starts
or restarts an extension process, and one that is not running
passes the output through.
Sourcepub fn is_worker_view(&self) -> bool
pub fn is_worker_view(&self) -> bool
Whether this is a worker’s view (Self::worker_transforms).
Sourcepub fn extension_generation(&self, extension_id: &str) -> u64
pub fn extension_generation(&self, extension_id: &str) -> u64
Current load generation of extension_id (0 until first unloaded).
Sourcepub async fn set_session_injection_for(&self, session_id: &str, content: String)
pub async fn set_session_injection_for(&self, session_id: &str, content: String)
Record context injected by an on_session_start handler for
session_id.
Called once per session, by whoever emits the session-start event, after extensions have loaded. Later calls for the same id replace the stored value.
Sourcepub async fn session_injection_for(&self, session_id: &str) -> Option<String>
pub async fn session_injection_for(&self, session_id: &str) -> Option<String>
Context injected at session start for session_id, if any. Read on
every turn while composing the system prompt.
Sourcepub async fn clear_session_injection(&self, session_id: &str)
pub async fn clear_session_injection(&self, session_id: &str)
Drop the injection recorded for session_id (session teardown).
Sourcepub async fn set_session_injection(&self, content: String)
👎Deprecated: use set_session_injection_for
pub async fn set_session_injection(&self, content: String)
use set_session_injection_for
Compatibility shim: writes under the empty key.
Sourcepub async fn session_injection(&self) -> Option<String>
👎Deprecated: use session_injection_for
pub async fn session_injection(&self) -> Option<String>
use session_injection_for
Compatibility shim: reads the empty key.
Sourcepub async fn subscribe(
&self,
kind: HookKind,
handler: Arc<dyn ExtensionHandler>,
tool_filter: Option<String>,
matcher: Option<HookMatcher>,
permissions: PermissionSet,
) -> Result<(), String>
pub async fn subscribe( &self, kind: HookKind, handler: Arc<dyn ExtensionHandler>, tool_filter: Option<String>, matcher: Option<HookMatcher>, permissions: PermissionSet, ) -> Result<(), String>
Register a handler for a specific hook kind.
Returns an error if the handler’s permissions don’t allow subscribing to this hook kind.
Sourcepub async fn emit(&self, event: &HookEvent) -> HookResult
pub async fn emit(&self, event: &HookEvent) -> HookResult
Emit a hook event to all registered handlers.
Returns the first Block result if any handler blocks, otherwise
returns Continue. Handlers are called in registration order.
For before_tool_call only Block ends the chain (#148 I1):
Modifyrewrites the input and the chain continues; every later handler sees (and vets) the modified input, and the final input is returned asModifyunless something blocks or confirms.Confirmis recorded and the chain continues, so a laterBlockwins even when the confirming extension is already latched. Confirms are merged (all sources, all messages) and returned last, carrying any input modified BEFORE the first Confirm.- A
Modifyafter a pendingConfirmis ignored (logged at warn): the user confirms the input as it stood when they were asked.
If no handlers are registered for this hook, returns immediately (the no-extensions fast path).
On a worker view (Self::worker_transforms) only the host’s
after_tool_call transform handlers run, each through
handle_no_spawn; every other kind returns Continue at once.
Sourcepub async fn emit_concurrent(&self, event: &HookEvent) -> HookResult
pub async fn emit_concurrent(&self, event: &HookEvent) -> HookResult
Emit a hook event to all registered handlers concurrently.
All handlers race under a single shared timeout (per_handler_timeout).
Results are collected and the first Block wins; injections are merged.
When to use this over emit():
Only safe for hook kinds whose handlers are order-independent — i.e. where no handler’s result depends on another’s execution. Currently that applies to:
on_session_end: onlyContinueis a valid result; handlers are fire-and-forget notification calls (deck, d20, jawz-widget, synaps-tasks all write to their own stores independently).
The notify-only kinds do not come through here: notify
delivers to each handler on its own.
Do NOT use for before_tool_call / after_tool_call / before_message
hooks where Block / Modify / Replace / Inject semantics require a
defined winner when two handlers disagree — join_all completion order is
nondeterministic, so the “winner” would be unstable. Those hooks must use
the sequential emit, which enforces first-wins ordering.
With N extensions and a 5 s per-handler timeout, serial emit takes up to N×5 s; concurrent emit collapses that to a single 5 s window regardless of N — critical for teardown budgets.
Sourcepub fn notify(self: &Arc<Self>, event: HookEvent)
pub fn notify(self: &Arc<Self>, event: HookEvent)
Deliver a notify-only event (HookKind::is_notification) without
waiting for anyone. Returns at once and cannot fail: the caller’s
work is already done and no handler can delay, fail or change it.
The handlers run concurrently on one detached task, each under
[HANDLER_TIMEOUT], so the task ends within that time of the event
being ready and holds nothing of the caller’s. Every result, error,
timeout and panic is ignored, and costs the other handlers nothing
(Self::notify_with). Delivery is best effort and unordered; the
event is dropped when
- nobody subscribes to the kind (then nothing is spawned at all),
- this is a worker’s view (
Self::worker_transforms), - [
NOTIFY_MAX_IN_FLIGHT] deliveries are still in flight (logged), - there is no Tokio runtime to deliver on, or it shuts down first.
A handler that is stopped is not started again by a late delivery
(ProcessExtension::shutdown is terminal).
Sourcepub fn notify_with<F, I>(self: &Arc<Self>, kind: HookKind, events: F)
pub fn notify_with<F, I>(self: &Arc<Self>, kind: HookKind, events: F)
Self::notify for events that cost something to build (a file
lookup): events runs on the detached task, inside the same in-flight
limit, and is not polled at all when the delivery would be dropped.
The build is not under the handler timeout, so it must end on its own.
What it yields is None, Some(event) or a Vec of events. Several
are one delivery, on the one task: every handler is sent them in that
order, the next once it has answered the one before. So a caller with
many events to send takes one in-flight slot, not one each, and
cannot have its later events dropped because its earlier ones are
still in flight. An event of another kind than kind is not sent.
Each handler gets the sequence on its own. One that does not answer
an event within [HANDLER_TIMEOUT] is sent no more of this delivery
(logged once, with the number not sent): the request that timed out
is still in its input, and each further one would keep it busy, and
its other hooks waiting, that much longer. Nobody else’s events are
held up or dropped by it, and the task ends within one timeout of
the slowest answer. A handler that panics on an event loses that
event and nothing more: it is sent the next, and the other handlers
are not touched. (That is where a panic unwinds. The release profile
aborts on any panic, here as anywhere.)
Sourcepub async fn unsubscribe_all(&self, extension_id: &str)
pub async fn unsubscribe_all(&self, extension_id: &str)
Remove all handlers for a given extension ID.
Sourcepub async fn handler_count(&self) -> usize
pub async fn handler_count(&self) -> usize
Number of registered handlers across all hooks. On a worker view: the host transform handlers it would dispatch.
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for HookBus
impl !RefUnwindSafe for HookBus
impl !UnwindSafe for HookBus
impl Forget for HookBus
impl Move for HookBus
impl Send for HookBus
impl Sync for HookBus
impl Unpin for HookBus
impl UnsafeUnpin for HookBus
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.