Skip to main content

oxi_agent/
config.rs

1/// Agent configuration
2use oxi_ai::CompactionStrategy;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6fn default_context_window() -> usize {
7    128_000
8}
9
10/// Hook context for `shouldStopAfterTurn`.
11#[derive(Debug, Clone)]
12pub struct ShouldStopAfterTurnContext {
13    /// The assistant message that completed the turn.
14    pub message: oxi_ai::AssistantMessage,
15    /// Tool result messages from this turn.
16    pub tool_results: Vec<oxi_ai::ToolResultMessage>,
17    /// Current iteration number.
18    pub iteration: usize,
19}
20
21/// Result of `beforeToolCall` hook.
22#[derive(Debug, Clone, Default)]
23pub struct BeforeToolCallResult {
24    /// If `true`, the tool call is blocked and an error result is returned.
25    pub block: bool,
26    /// Human-readable reason for blocking.
27    pub reason: Option<String>,
28}
29
30/// Result of `afterToolCall` hook.
31#[derive(Debug, Clone, Default)]
32pub struct AfterToolCallResult {
33    /// Override content for the tool result.
34    pub content: Option<String>,
35    /// Override error status.
36    pub is_error: Option<bool>,
37    /// Signal that the agent should stop after this batch.
38    pub terminate: Option<bool>,
39}
40
41/// Hook context for `beforeToolCall`.
42#[derive(Debug, Clone)]
43pub struct BeforeToolCallContext {
44    /// The tool call being made.
45    pub tool_call_id: String,
46    /// Tool name.
47    pub tool_name: String,
48    /// Validated arguments.
49    pub args: serde_json::Value,
50}
51
52/// Hook context for `afterToolCall`.
53#[derive(Debug, Clone)]
54pub struct AfterToolCallContext {
55    /// The tool call that was made.
56    pub tool_call_id: String,
57    /// Tool name.
58    pub tool_name: String,
59    /// The tool result content.
60    pub result: String,
61    /// Whether the result is an error.
62    pub is_error: bool,
63}
64
65/// Callback hooks for the agent loop.
66///
67/// These mirror pi-mono's `AgentLoopConfig` hooks, allowing callers to
68/// inject custom logic at key points in the agentic loop.
69#[derive(Default)]
70#[allow(clippy::type_complexity)]
71pub struct AgentHooks {
72    /// Called after each turn completes. Return `true` to stop the agent loop.
73    ///
74    /// Wrapped in `Arc` so the hook can be invoked multiple times without
75    /// being consumed (unlike `Box<dyn Fn>` which requires `take()`).
76    pub should_stop_after_turn:
77        Option<Arc<dyn Fn(&ShouldStopAfterTurnContext) -> bool + Send + Sync>>,
78
79    /// Called before a tool is executed. Return a `BeforeToolCallResult` with
80    /// `block: true` to prevent execution.
81    #[allow(clippy::type_complexity)]
82    pub before_tool_call:
83        Option<Box<dyn Fn(&BeforeToolCallContext) -> BeforeToolCallResult + Send + Sync>>,
84
85    /// Called after a tool execution completes. Can override the result.
86    #[allow(clippy::type_complexity)]
87    pub after_tool_call:
88        Option<Box<dyn Fn(&AfterToolCallContext) -> AfterToolCallResult + Send + Sync>>,
89
90    /// Returns steering messages to inject mid-run. Called after each turn
91    /// (unless stopped).
92    #[allow(clippy::type_complexity)]
93    pub get_steering_messages: Option<Box<dyn Fn() -> Vec<String> + Send + Sync>>,
94
95    /// Returns follow-up messages to process after the agent would stop.
96    /// Called when the agent has no more tool calls and no steering messages.
97    #[allow(clippy::type_complexity)]
98    pub get_follow_up_messages: Option<Box<dyn Fn() -> Vec<String> + Send + Sync>>,
99
100    /// Tool execution mode.
101    pub tool_execution: ToolExecutionMode,
102}
103
104/// How tool calls are executed within a single assistant turn.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum ToolExecutionMode {
107    /// Execute tool calls sequentially, one at a time.
108    Sequential,
109    /// Execute tool calls concurrently (in parallel).
110    #[default]
111    Parallel,
112}
113
114/// Agent runtime configuration
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AgentConfig {
117    /// Agent name
118    pub name: String,
119    /// Agent description
120    pub description: Option<String>,
121    /// Model ID to use
122    pub model_id: String,
123    /// System prompt
124    pub system_prompt: Option<String>,
125    /// Maximum number of agent iterations
126    pub max_iterations: usize,
127    /// Timeout in seconds for the entire agent run
128    pub timeout_seconds: u64,
129    /// Temperature for generation (0.0 to 1.0)
130    pub temperature: Option<f64>,
131    /// Maximum tokens to generate
132    pub max_tokens: Option<usize>,
133    /// Compaction strategy for long conversations
134    #[serde(default)]
135    pub compaction_strategy: CompactionStrategy,
136    /// Custom instruction passed to the compactor
137    #[serde(default)]
138    pub compaction_instruction: Option<String>,
139    /// Model context window size (used for threshold-based compaction)
140    #[serde(default = "default_context_window")]
141    pub context_window: usize,
142    /// API key override for the provider.
143    ///
144    /// When set, this is injected into [`oxi_ai::StreamOptions`] so the
145    /// provider uses it instead of an environment variable.
146    #[serde(default)]
147    pub api_key: Option<String>,
148    /// Working directory for file tools. Defaults to current directory if None.
149    #[serde(default)]
150    pub workspace_dir: Option<std::path::PathBuf>,
151    /// Output mode for agent responses.
152    ///
153    /// When set, the agent extracts structured output from the final response.
154    /// See [`OutputMode`] for available modes.
155    ///
156    /// [`OutputMode`]: crate::structured_output::OutputMode
157    #[serde(default)]
158    pub output_mode: Option<String>,
159}
160
161impl Default for AgentConfig {
162    fn default() -> Self {
163        Self {
164            name: "oxi-agent".to_string(),
165            description: None,
166            model_id: "claude-sonnet-4-20250514".to_string(),
167            system_prompt: None,
168            max_iterations: 10,
169            timeout_seconds: 300,
170            temperature: None,
171            max_tokens: None,
172            compaction_strategy: CompactionStrategy::default(),
173            compaction_instruction: None,
174            context_window: 128_000,
175            api_key: None,
176            workspace_dir: None,
177            output_mode: None,
178        }
179    }
180}
181
182impl AgentConfig {
183    /// Create a new config with the given model ID.
184    pub fn new(model_id: impl Into<String>) -> Self {
185        Self {
186            model_id: model_id.into(),
187            ..Default::default()
188        }
189    }
190
191    /// Set the agent name.
192    pub fn with_name(mut self, name: impl Into<String>) -> Self {
193        self.name = name.into();
194        self
195    }
196
197    /// Set the system prompt.
198    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
199        self.system_prompt = Some(prompt.into());
200        self
201    }
202
203    /// Set the maximum number of agent loop iterations.
204    pub fn with_max_iterations(mut self, max: usize) -> Self {
205        self.max_iterations = max;
206        self
207    }
208
209    /// Set the timeout in seconds for the entire agent run.
210    pub fn with_timeout(mut self, seconds: u64) -> Self {
211        self.timeout_seconds = seconds;
212        self
213    }
214
215    /// Set the compaction strategy for long conversations.
216    pub fn with_compaction_strategy(mut self, strategy: CompactionStrategy) -> Self {
217        self.compaction_strategy = strategy;
218        self
219    }
220
221    /// Set a custom instruction passed to the compactor.
222    pub fn with_compaction_instruction(mut self, instruction: impl Into<String>) -> Self {
223        self.compaction_instruction = Some(instruction.into());
224        self
225    }
226}