Skip to main content

opi_agent/
hooks.rs

1//! Hook trait for agent loop customization (S8.2).
2
3use std::future::Future;
4use std::pin::Pin;
5
6use tokio_util::sync::CancellationToken;
7
8use crate::loop_types::AgentError;
9use crate::message::AgentMessage;
10
11/// Context provided before a tool call is executed.
12pub struct BeforeToolCallContext {
13    pub tool_call_id: String,
14    pub tool_name: String,
15    pub args: serde_json::Value,
16    pub messages: Vec<AgentMessage>,
17}
18
19/// Result of the before_tool_call hook.
20#[non_exhaustive]
21pub enum BeforeToolCallResult {
22    /// Allow the tool call to proceed.
23    Allow,
24    /// Reject the tool call with an error message.
25    Deny { reason: String },
26}
27
28/// Context provided after a tool call has been executed.
29pub struct AfterToolCallContext {
30    pub tool_call_id: String,
31    pub tool_name: String,
32    pub result: crate::tool::ToolResult,
33}
34
35/// Result of the after_tool_call hook.
36#[non_exhaustive]
37pub enum AfterToolCallResult {
38    /// Keep the original tool result unchanged.
39    Keep,
40    /// Replace the tool result entirely (field replacement, no deep merge).
41    Replace(crate::tool::ToolResult),
42}
43
44/// Context provided to the should_stop_after_turn hook.
45pub struct ShouldStopAfterTurnContext {
46    pub messages: Vec<AgentMessage>,
47    pub tool_results: Vec<opi_ai::message::ToolResultMessage>,
48}
49
50/// Context provided to the prepare_next_turn hook.
51pub struct PrepareNextTurnContext {
52    pub messages: Vec<AgentMessage>,
53    pub turn: u32,
54}
55
56/// Hook trait for customizing agent loop behavior.
57///
58/// Default implementations are no-ops or basic conversions.
59pub trait AgentHooks: Send + Sync {
60    /// Convert agent messages to provider-facing messages.
61    fn convert_to_llm(
62        &self,
63        messages: &[AgentMessage],
64    ) -> Result<Vec<opi_ai::message::Message>, AgentError>;
65
66    /// Transform messages before conversion to LLM format.
67    fn transform_context(
68        &self,
69        messages: Vec<AgentMessage>,
70        _signal: CancellationToken,
71    ) -> Pin<Box<dyn Future<Output = Result<Vec<AgentMessage>, AgentError>> + Send>> {
72        Box::pin(async { Ok(messages) })
73    }
74
75    /// Decide whether the loop should stop after this turn.
76    fn should_stop_after_turn(
77        &self,
78        _ctx: ShouldStopAfterTurnContext,
79    ) -> Pin<Box<dyn Future<Output = bool> + Send>> {
80        Box::pin(async { false })
81    }
82
83    /// Hook called before a tool is executed.
84    fn before_tool_call(
85        &self,
86        _ctx: BeforeToolCallContext,
87    ) -> Pin<Box<dyn Future<Output = BeforeToolCallResult> + Send>> {
88        Box::pin(async { BeforeToolCallResult::Allow })
89    }
90
91    /// Hook called after a tool has been executed.
92    fn after_tool_call(
93        &self,
94        _ctx: AfterToolCallContext,
95    ) -> Pin<Box<dyn Future<Output = AfterToolCallResult> + Send>> {
96        Box::pin(async { AfterToolCallResult::Keep })
97    }
98
99    /// Prepare context before the next turn begins.
100    fn prepare_next_turn(
101        &self,
102        _ctx: PrepareNextTurnContext,
103    ) -> Pin<Box<dyn Future<Output = Option<crate::loop_types::AgentLoopTurnUpdate>> + Send>> {
104        Box::pin(async { None })
105    }
106}