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