pub struct HookRunner { /* private fields */ }Expand description
Runs every registered interceptor and configured hook against a tool call.
Implementations§
Source§impl HookRunner
impl HookRunner
pub fn new(workspace: impl Into<PathBuf>, hooks: Vec<HookSpec>) -> Self
Sourcepub fn with_interceptor(self, interceptor: impl Interceptor + 'static) -> Self
pub fn with_interceptor(self, interceptor: impl Interceptor + 'static) -> Self
Adds an in-process participant, after any already registered.
Appends rather than replaces, and the order of the calls is the order they are consulted in — see the module docs for where that sits relative to subprocess hooks, and why.
RuntimeBuilder::with_interceptor
is how a host normally reaches this; the constructor is here for a host
building a runner for a runtime of its own.
Sourcepub fn with_reporter(
self,
report: impl Fn(&str) + Send + Sync + 'static,
) -> Self
pub fn with_reporter( self, report: impl Fn(&str) + Send + Sync + 'static, ) -> Self
Redirects failure reports somewhere other than stderr.
A broken participant is an operator’s problem, not the model’s, so it is
said out loud by default — including when
OnFailure::Allow means the turn carries on,
which is the case where nothing else would ever mention it. A host that
owns its own logging replaces the destination; it cannot remove it.
Sourcepub async fn decide_async(&self, call: &HookCall) -> HookOutcome
pub async fn decide_async(&self, call: &HookCall) -> HookOutcome
Consults every applicable subprocess hook, in order, until one refuses.
Never fails: every way a hook can go wrong ends as a HookOutcome
carrying words, because an error here would reach the model as a bare
blocked call with the reason thrown away.
Consults everybody: in-process interceptors first, then subprocess hooks.
Both are awaited on the caller’s own runtime. A hook is a subprocess,
and since mentra 0.24 spawning and waiting for one is an async
primitive rather than blocking work, so the spawn_blocking this used
to route hooks through has gone with the thread it needed — and with it
the one thing that thread could not do: a turn cancelled mid-hook now
drops the future and kills the program, where a blocking wait abandoned
the thread to it. That holds on every runtime flavor, current_thread
included.
Sourcepub async fn review_async(
&self,
call: &HookCall,
output: Value,
is_error: bool,
) -> HookOutcome
pub async fn review_async( &self, call: &HookCall, output: Value, is_error: bool, ) -> HookOutcome
Consults everybody about a call that has already run.
The after-the-call twin of decide_async, down to
the order and the threading: only the request differs, and the answers
that request admits. output is the result as JSON — a text result is
a JSON string, a structured one is itself — and is_error is the
tool’s own verdict, which a replacement may leave alone or overturn.
Answers HookOutcome::Allow when nobody objected, which for a result
means keep.
Trait Implementations§
Source§impl Clone for HookRunner
impl Clone for HookRunner
Source§impl Debug for HookRunner
impl Debug for HookRunner
Source§impl ExecutionHookParticipant for HookRunner
One participant, both seams — which is how basis registers a workspace’s
chain and the shape upstream added in 0.26 for exactly this.
impl ExecutionHookParticipant for HookRunner
One participant, both seams — which is how basis registers a workspace’s chain and the shape upstream added in 0.26 for exactly this.
The two legacy impls below say the same things to mentra’s two older
registries, and they are kept because they are the only door a
mentra::test::MockRuntime has (its builder takes a PreExecutionHook and
a PostExecutionHook and nothing mixed — upstream gap), and because
HookRunner is public and a host on those seams may still be using them.
All four methods route through decide_async
and review_async, so there is one chain and
one set of semantics however a runner is installed.
What the mixed registration buys, and why basis’s own open uses it: one
ExecutionHookRegistration
rather than two independent guards, whose snapshot is taken once and
retained across both sides of a call — so a workspace cannot be consulted
before a tool and absent after it, which two separately dropped guards
permit. And a rewrite’s attribution survives: mentra threads the reason a
Modify carried into the refusal that a rejected rewrite earns (invalid
JSON, a schema violation, a parallel-lane category flip), where the legacy
seam drops it. Threading it into a policy or authorizer denial used to be
a separate upstream gap (mentra#57); mentra 0.27 closed it, so a refusal
of a rewritten write now speaks in the policy’s words and names the hook
ahead of them — "mixed execution hooks (execution hook '<chain>': hook '<name>': <reason>) rewrote this call; the rewritten call then failed: <policy's own words>". See basis/tests/hooks/guarded.rs for what a
caller actually reads once mentra’s own 200-byte cap on a ToolCompleted
summary is applied — the policy’s words are often the part that no longer
fits.
Source§fn before<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PreExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<BeforeDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn before<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PreExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<BeforeDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Never returns Err, for
pre_tool_execution’s reason: an
error would reach the model as a blocked call with the reason thrown
away.
Source§fn after<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PostExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<AfterDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn after<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PostExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<AfterDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
A refusal arrives as a Replace rather than an
AfterDecision::Deny, and deliberately: mentra prefixes a Deny with
its own “denied by execution hook” wording, and after a tool has run the
strongest thing left is what the model reads — which basis’s chain has
already worded, naming the participant that objected. Overwriting the
result with that text and is_error: true is the same answer
post_tool_execution gives, in the
same words.
fn name(&self) -> &str
Source§impl PostExecutionHook for HookRunner
impl PostExecutionHook for HookRunner
Source§fn post_tool_execution<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PostExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<ResultDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn post_tool_execution<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PostExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<ResultDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Never returns Err, for the reason
pre_tool_execution does not: an error here
fails the turn, and a guard’s opinion is worth more to whoever reads the
transcript than a stack of runtime errors is.
A refusal — a participant’s deny, or a broken one that denies on
failure — arrives as the reason in place of the output, is_error: true.
That is the strongest thing left after a tool has run: the side effects
are done, and AgentEvent::ToolExecutionFinished has already carried
the real result to every subscriber, so what a refusal can still govern
is what the model reads. A guard that broke while checking an output
for credentials has not established that there were none in it.
Source§impl PreExecutionHook for HookRunner
impl PreExecutionHook for HookRunner
Source§fn pre_tool_execution<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PreExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<HookDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn pre_tool_execution<'life0, 'life1, 'async_trait>(
&'life0 self,
context: &'life1 PreExecutionContext,
) -> Pin<Box<dyn Future<Output = Result<HookDecision, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Never returns Err.
mentra turns a hook error into a bare blocked-tool result, which throws
the reason away; every outcome here is a HookDecision instead, so
whatever happened reaches both the model and the audit trail as words.