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}