Skip to main content

autumn_plugin_agent/
hooks.rs

1//! Lifecycle hooks: code that runs at fixed points of the agent loop.
2//!
3//! An [`AgentHooks`] implementation sees every model call, every tool call,
4//! and the final outcome. Use hooks for audit logs, metrics, secret
5//! redaction, progress reports, or custom guards. Every method has a no-op
6//! default, so an implementation overrides only what it needs.
7//!
8//! Hooks run in registration order. A [`HookAction::Block`] from any
9//! `before_tool` hook stops that call; the model sees the reason as the tool
10//! result.
11
12use futures::future::BoxFuture;
13
14use crate::agent::AgentOutcome;
15use crate::client::{ChatRequest, ChatResponse, TokenUsage};
16use crate::ids::{RunId, SessionId};
17use crate::tools::{ToolCall, ToolContext};
18
19/// Read-only facts about the run a hook fires in.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct RunInfo {
22    /// The run's id.
23    pub run_id: RunId,
24    /// The session the run belongs to, if any.
25    pub session_id: Option<SessionId>,
26    /// Tool-execution rounds finished so far.
27    pub steps_used: u32,
28    /// The run's step budget.
29    pub max_steps: u32,
30    /// Provider-reported tokens spent so far.
31    pub usage: TokenUsage,
32}
33
34/// What a `before_tool` hook wants the loop to do with a call.
35// `Modify` carries a `serde_json::Value`, which has no `Eq` impl.
36#[allow(clippy::derive_partial_eq_without_eq)]
37#[derive(Debug, Clone, PartialEq)]
38pub enum HookAction {
39    /// Run the call as the model asked.
40    Continue,
41    /// Run the call with these arguments instead.
42    Modify(serde_json::Value),
43    /// Do not run the call. The model sees the reason as an error result.
44    Block(String),
45}
46
47/// The result of one tool call, as hooks see it.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ToolOutput {
50    /// The text the model receives as the tool result.
51    pub content: String,
52    /// `true` when the call failed, was blocked, or was denied.
53    pub is_error: bool,
54}
55
56/// Code that runs at fixed points of the agent loop.
57///
58/// ```rust
59/// use autumn_plugin_agent::hooks::{AgentHooks, HookAction, RunInfo};
60/// use autumn_plugin_agent::tools::{ToolCall, ToolContext};
61/// use futures::future::BoxFuture;
62///
63/// /// Never let the agent email anyone outside the company.
64/// #[derive(Debug)]
65/// struct InternalMailOnly;
66///
67/// impl AgentHooks for InternalMailOnly {
68///     fn before_tool<'a>(
69///         &'a self,
70///         call: &'a ToolCall,
71///         _ctx: &'a ToolContext,
72///     ) -> BoxFuture<'a, HookAction> {
73///         Box::pin(async move {
74///             let to = call.arguments["to"].as_str().unwrap_or_default();
75///             if call.name == "send_email" && !to.ends_with("@example.com") {
76///                 HookAction::Block("external recipients are not allowed".into())
77///             } else {
78///                 HookAction::Continue
79///             }
80///         })
81///     }
82/// }
83/// ```
84pub trait AgentHooks: Send + Sync + std::fmt::Debug {
85    /// Runs before each model call. May edit the request (for example, to
86    /// redact text).
87    fn before_model<'a>(
88        &'a self,
89        _request: &'a mut ChatRequest,
90        _info: &'a RunInfo,
91    ) -> BoxFuture<'a, ()> {
92        Box::pin(std::future::ready(()))
93    }
94
95    /// Runs after each model call with the raw response.
96    fn after_model<'a>(
97        &'a self,
98        _response: &'a ChatResponse,
99        _info: &'a RunInfo,
100    ) -> BoxFuture<'a, ()> {
101        Box::pin(std::future::ready(()))
102    }
103
104    /// Runs before each tool call that the policy allowed.
105    fn before_tool<'a>(
106        &'a self,
107        _call: &'a ToolCall,
108        _ctx: &'a ToolContext,
109    ) -> BoxFuture<'a, HookAction> {
110        Box::pin(std::future::ready(HookAction::Continue))
111    }
112
113    /// Runs after each tool call, including blocked and denied ones.
114    fn after_tool<'a>(
115        &'a self,
116        _call: &'a ToolCall,
117        _output: &'a ToolOutput,
118        _ctx: &'a ToolContext,
119    ) -> BoxFuture<'a, ()> {
120        Box::pin(std::future::ready(()))
121    }
122
123    /// Runs once when the run returns an outcome (including a pause for
124    /// approval).
125    fn on_outcome<'a>(
126        &'a self,
127        _outcome: &'a AgentOutcome,
128        _info: &'a RunInfo,
129    ) -> BoxFuture<'a, ()> {
130        Box::pin(std::future::ready(()))
131    }
132}