Skip to main content

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