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