Skip to main content

HookBus

Struct HookBus 

Source
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

Source

pub fn new() -> HookBus

Create an empty HookBus with no handlers.

Source

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.

Source

pub fn is_worker_view(&self) -> bool

Whether this is a worker’s view (Self::worker_transforms).

Source

pub fn extension_generation(&self, extension_id: &str) -> u64

Current load generation of extension_id (0 until first unloaded).

Source

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.

Source

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.

Source

pub async fn clear_session_injection(&self, session_id: &str)

Drop the injection recorded for session_id (session teardown).

Source

pub async fn set_session_injection(&self, content: String)

👎Deprecated:

use set_session_injection_for

Compatibility shim: writes under the empty key.

Source

pub async fn session_injection(&self) -> Option<String>

👎Deprecated:

use session_injection_for

Compatibility shim: reads the empty key.

Source

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.

Source

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):

  • Modify rewrites the input and the chain continues; every later handler sees (and vets) the modified input, and the final input is returned as Modify unless something blocks or confirms.
  • Confirm is recorded and the chain continues, so a later Block wins 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 Modify after a pending Confirm is 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.

Source

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: only Continue is 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.

Source

pub fn notify(self: &Arc<HookBus>, 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).

Source

pub fn notify_with<F, I>(self: &Arc<HookBus>, kind: HookKind, events: F)
where F: Future<Output = I> + Send + 'static, I: IntoIterator<Item = HookEvent> + Send, <I as IntoIterator>::IntoIter: Send,

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

Source

pub async fn unsubscribe_all(&self, extension_id: &str)

Remove all handlers for a given extension ID.

Source

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.

Source

pub async fn is_empty(&self) -> bool

Check if any handlers are registered (for fast-path decisions).

Source

pub async fn subscriptions_for( &self, extension_id: &str, ) -> Vec<(HookKind, Option<String>)>

Return all (kind, tool_filter) pairs subscribed by the given extension id. Sorted by kind name, then by tool_filter (None first), for stable output.

Trait Implementations§

Source§

impl Debug for HookBus

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Default for HookBus

Source§

fn default() -> HookBus

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ThreadSafetyMarker for T
where T: Send,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more