open_agent/types/agent_options.rs
1/// Configuration options for an AI agent instance.
2///
3/// `AgentOptions` controls all aspects of agent behavior including model selection,
4/// conversation management, tool usage, and lifecycle hooks. This struct should be
5/// constructed using [`AgentOptions::builder()`] rather than direct instantiation
6/// to ensure required fields are validated.
7///
8/// # Architecture
9///
10/// The options are organized into several functional areas:
11///
12/// - **Model Configuration**: `model`, `base_url`, `api_key`, `headers`, `protocol`,
13/// `temperature`, `max_tokens`
14/// - **Conversation Control**: `system_prompt`, `max_turns`, `timeout`
15/// - **Tool Management**: `tools`, `auto_execute_tools`, `max_tool_iterations`
16/// - **Lifecycle Hooks**: `hooks` for monitoring and interception
17///
18/// # Thread Safety
19///
20/// Tools are wrapped in `Arc<Tool>` to allow efficient cloning and sharing across
21/// threads, as agents may need to be cloned for parallel processing.
22///
23/// # Examples
24///
25/// ```no_run
26/// use open_agent::AgentOptions;
27///
28/// let options = AgentOptions::builder()
29/// .model("qwen2.5-32b-instruct")
30/// .base_url("http://localhost:1234/v1")
31/// .system_prompt("You are a helpful coding assistant")
32/// .max_turns(5)
33/// .temperature(0.7)
34/// .build()
35/// .expect("Valid configuration");
36/// ```
37#[derive(Clone)]
38pub struct AgentOptions {
39 /// System prompt that defines the agent's behavior and personality.
40 ///
41 /// This is sent as the first message in the conversation to establish
42 /// context and instructions. Can be empty if no system-level guidance
43 /// is needed.
44 system_prompt: String,
45
46 /// Model identifier for the LLM to use (e.g., "qwen2.5-32b-instruct", "gpt-4").
47 ///
48 /// This must match a model available at the configured `base_url`.
49 /// Different models have varying capabilities for tool use, context
50 /// length, and response quality.
51 model: String,
52
53 /// API endpoint URL (e.g., "http://localhost:1234/v1").
54 ///
55 /// The wire format is chosen by `protocol`: `OpenAiChat` posts to
56 /// `{base_url}/chat/completions`, which local inference servers (LM Studio,
57 /// llama.cpp, vLLM) and most cloud providers support; `Anthropic` posts to
58 /// `{base_url}/messages`.
59 base_url: String,
60
61 /// API authentication key for the provider.
62 ///
63 /// Many local servers don't require authentication, so the default
64 /// "not-needed" is often sufficient. For cloud providers like OpenAI,
65 /// set this to your actual API key.
66 api_key: String,
67
68 /// Caller-supplied HTTP headers applied to every model request.
69 ///
70 /// These may override the protocol defaults, including authentication and content type.
71 /// Header names are matched case-insensitively when the builder replaces an earlier value.
72 headers: BTreeMap<String, String>,
73
74 /// Maximum number of conversation turns (user message + assistant response = 1 turn).
75 ///
76 /// This limits how long a conversation can continue. In auto-execution mode
77 /// with tools, this prevents infinite loops. Set to 1 for single-shot
78 /// interactions or higher for multi-turn conversations.
79 max_turns: u32,
80
81 /// Maximum tokens the model should generate in a single response.
82 ///
83 /// `None` uses the provider's default. Lower values constrain response
84 /// length, which can be useful for cost control or ensuring concise answers.
85 /// Note this is separate from the model's context window size.
86 max_tokens: Option<u32>,
87
88 /// Sampling temperature for response generation (typically 0.0 to 2.0).
89 ///
90 /// - 0.0: Deterministic, always picks most likely tokens
91 /// - 0.7: Balanced creativity and consistency
92 /// - 1.0+: More random and creative responses
93 ///
94 /// `None` omits the field from the wire request entirely, leaving the server to apply
95 /// its own default. That is the default, and it is load-bearing rather than tidy: a
96 /// growing number of models reject the parameter outright — Anthropic's range stops at
97 /// 1.0, and several reasoning models 400 on any value at all — so a client-invented
98 /// temperature turns a working request into a hard error the caller never asked for.
99 /// This mirrors `max_tokens`, which stopped being defaulted in 0.7.0 for the same
100 /// reason.
101 temperature: Option<f32>,
102
103 /// HTTP request timeout in seconds.
104 ///
105 /// Maximum time to wait for the API to respond. Applies per API call,
106 /// not to the entire conversation. Increase for slower models or when
107 /// expecting long responses.
108 timeout: u64,
109
110 /// Tools available for the agent to use during conversations.
111 ///
112 /// Tools are wrapped in `Arc` for efficient cloning. When the agent
113 /// receives a tool use request, it looks up the tool by name in this
114 /// vector. Empty by default.
115 tools: Vec<Arc<Tool>>,
116
117 /// Whether to automatically execute tools and continue the conversation.
118 ///
119 /// - `true`: SDK automatically executes tool calls and sends results back
120 /// to the model, continuing until no more tools are requested
121 /// - `false`: Tool calls are returned to the caller, who must manually
122 /// execute them and provide results
123 ///
124 /// Auto-execution is convenient but gives less control. Manual execution
125 /// allows for approval workflows and selective tool access.
126 auto_execute_tools: bool,
127
128 /// Maximum iterations of tool execution in automatic mode.
129 ///
130 /// Prevents infinite loops where the agent continuously requests tools.
131 /// Each tool execution attempt counts as one iteration. Only relevant
132 /// when `auto_execute_tools` is true.
133 max_tool_iterations: u32,
134
135 /// Whether the model's reasoning channel is surfaced to the caller.
136 ///
137 /// Reasoning models stream chain-of-thought separately from content
138 /// (`reasoning_content` on DeepSeek, `reasoning` on OpenRouter). It is never
139 /// merged into assistant text regardless of this setting; the flag only decides
140 /// whether it is buffered and emitted as `StreamEvent::Reasoning` or discarded
141 /// as it arrives. `false` by default, because a caller that does not want it
142 /// should not pay to buffer it.
143 include_reasoning: bool,
144
145 /// The wire protocol this endpoint speaks.
146 ///
147 /// Selects the request path, the auth header, the body shape and the streaming
148 /// vocabulary. Defaults to [`ApiProtocol::OpenAiChat`], which is what every endpoint the
149 /// SDK supported before 0.9.0 speaks, so an existing configuration keeps its behaviour.
150 protocol: ApiProtocol,
151
152 /// Lifecycle hooks for observing and intercepting agent operations.
153 ///
154 /// Hooks allow you to inject custom logic at various points:
155 /// - Before/after API requests
156 /// - Tool execution interception
157 /// - Response streaming callbacks
158 ///
159 /// Useful for logging, metrics, debugging, and implementing custom
160 /// authorization logic.
161 hooks: Hooks,
162}
163
164/// Custom Debug implementation to prevent sensitive data leakage.
165///
166/// We override the default Debug implementation because:
167/// 1. The `api_key` and `headers` fields may contain sensitive credentials that
168/// shouldn't appear in logs or error messages
169/// 2. The `tools` vector contains Arc-wrapped closures that don't debug nicely,
170/// so we show a count instead
171///
172/// This ensures that debug output is safe for logging while remaining useful
173/// for troubleshooting.
174impl std::fmt::Debug for AgentOptions {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("AgentOptions")
177 .field("system_prompt", &self.system_prompt)
178 .field("model", &self.model)
179 .field("base_url", &self.base_url)
180 // Mask API key to prevent credential leakage in logs
181 .field("api_key", &"***")
182 .field("max_turns", &self.max_turns)
183 .field("max_tokens", &self.max_tokens)
184 .field("temperature", &self.temperature)
185 .field("timeout", &self.timeout)
186 // Show tool count instead of trying to debug Arc<Tool> contents
187 .field("tools", &format!("{} tools", self.tools.len()))
188 .field("auto_execute_tools", &self.auto_execute_tools)
189 .field("max_tool_iterations", &self.max_tool_iterations)
190 .field("include_reasoning", &self.include_reasoning)
191 .field("protocol", &self.protocol)
192 .field("hooks", &self.hooks)
193 .finish()
194 }
195}
196
197/// Default values optimized for common single-turn use cases.
198///
199/// These defaults are chosen to:
200/// - Require explicit configuration of critical fields (model, base_url)
201/// - Provide safe, sensible defaults for optional fields
202/// - Work with local inference servers that don't need authentication
203impl Default for AgentOptions {
204 fn default() -> Self {
205 Self {
206 // Empty string forces users to explicitly set context
207 system_prompt: String::new(),
208 // Empty string forces users to explicitly choose a model
209 model: String::new(),
210 // Empty string forces users to explicitly configure the endpoint
211 base_url: String::new(),
212 // Most local servers (LM Studio, llama.cpp) don't require auth
213 api_key: "not-needed".to_string(),
214 // No additional request metadata unless the caller opts in
215 headers: BTreeMap::new(),
216 // Default to single-shot interaction; users opt into conversations
217 max_turns: 1,
218 // No client-imposed cap; the server decides how long a response may be.
219 // Callers who want a ceiling set one explicitly via `max_tokens()`.
220 max_tokens: None,
221 // Unset: the field is omitted and the server decides. A client-invented value
222 // is rejected outright by several current models.
223 temperature: None,
224 // 60 seconds handles most requests without timing out prematurely
225 timeout: 60,
226 // No tools by default; users explicitly add capabilities
227 tools: Vec::new(),
228 // Manual tool execution by default for safety and control
229 auto_execute_tools: false,
230 // 5 iterations prevent infinite loops while allowing multi-step workflows
231 max_tool_iterations: 5,
232 // Empty hooks for no-op behavior
233 hooks: Hooks::new(),
234 // Reasoning is dropped unless a caller explicitly asks for it
235 include_reasoning: false,
236 // The only protocol the SDK spoke before 0.9.0
237 protocol: ApiProtocol::OpenAiChat,
238 }
239 }
240}
241
242impl AgentOptions {
243 /// Creates a new builder for constructing [`AgentOptions`].
244 ///
245 /// The builder pattern is used because:
246 /// 1. Some fields are required (model, base_url) and need validation
247 /// 2. Many fields have sensible defaults that can be overridden
248 /// 3. The API is more discoverable and readable than struct initialization
249 ///
250 /// # Example
251 ///
252 /// ```no_run
253 /// use open_agent::AgentOptions;
254 ///
255 /// let options = AgentOptions::builder()
256 /// .model("qwen2.5-32b-instruct")
257 /// .base_url("http://localhost:1234/v1")
258 /// .build()
259 /// .expect("Valid configuration");
260 /// ```
261 pub fn builder() -> AgentOptionsBuilder {
262 AgentOptionsBuilder::default()
263 }
264
265 /// Returns the system prompt.
266 pub fn system_prompt(&self) -> &str {
267 &self.system_prompt
268 }
269
270 /// Returns the model identifier.
271 pub fn model(&self) -> &str {
272 &self.model
273 }
274
275 /// Returns the base URL.
276 pub fn base_url(&self) -> &str {
277 &self.base_url
278 }
279
280 /// Returns the API key.
281 pub fn api_key(&self) -> &str {
282 &self.api_key
283 }
284
285 /// Returns the caller-supplied HTTP headers.
286 pub fn headers(&self) -> &BTreeMap<String, String> {
287 &self.headers
288 }
289
290 /// Returns the maximum number of conversation turns.
291 pub fn max_turns(&self) -> u32 {
292 self.max_turns
293 }
294
295 /// Returns the maximum tokens setting.
296 pub fn max_tokens(&self) -> Option<u32> {
297 self.max_tokens
298 }
299
300 /// Returns the sampling temperature, or `None` when the server should choose.
301 pub fn temperature(&self) -> Option<f32> {
302 self.temperature
303 }
304
305 /// Returns the wire protocol this endpoint speaks.
306 pub fn protocol(&self) -> ApiProtocol {
307 self.protocol
308 }
309
310 /// Returns the HTTP timeout in seconds.
311 pub fn timeout(&self) -> u64 {
312 self.timeout
313 }
314
315 /// Returns a reference to the tools vector.
316 pub fn tools(&self) -> &[Arc<Tool>] {
317 &self.tools
318 }
319
320 /// Returns whether automatic tool execution is enabled.
321 pub fn auto_execute_tools(&self) -> bool {
322 self.auto_execute_tools
323 }
324
325 /// Returns the maximum tool execution iterations.
326 pub fn max_tool_iterations(&self) -> u32 {
327 self.max_tool_iterations
328 }
329
330 /// Returns a reference to the hooks configuration.
331 pub fn hooks(&self) -> &Hooks {
332 &self.hooks
333 }
334
335 /// Returns whether the reasoning channel is surfaced to the caller.
336 pub fn include_reasoning(&self) -> bool {
337 self.include_reasoning
338 }
339}