Skip to main content

agent_base/types/
config.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
5pub enum Language {
6    #[default]
7    En,
8    Zh,
9}
10
11impl std::fmt::Display for Language {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            Language::En => write!(f, "en"),
15            Language::Zh => write!(f, "zh"),
16        }
17    }
18}
19
20#[derive(Clone, Debug)]
21pub struct RetryConfig {
22    pub max_retries: u32,
23    pub initial_backoff_ms: u64,
24    pub max_backoff_ms: u64,
25    pub backoff_multiplier: f64,
26    pub jitter: bool,
27}
28
29impl Default for RetryConfig {
30    fn default() -> Self {
31        Self {
32            max_retries: 3,
33            initial_backoff_ms: 500,
34            max_backoff_ms: 10_000,
35            backoff_multiplier: 2.0,
36            jitter: true,
37        }
38    }
39}
40
41impl RetryConfig {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    pub fn max_retries(mut self, n: u32) -> Self {
47        self.max_retries = n;
48        self
49    }
50
51    pub fn initial_backoff_ms(mut self, ms: u64) -> Self {
52        self.initial_backoff_ms = ms;
53        self
54    }
55
56    pub fn max_backoff_ms(mut self, ms: u64) -> Self {
57        self.max_backoff_ms = ms;
58        self
59    }
60
61    pub fn no_jitter(mut self) -> Self {
62        self.jitter = false;
63        self
64    }
65}
66
67#[derive(Clone, Debug)]
68pub enum ResponseFormat {
69    JsonObject,
70    JsonSchema { name: String, schema: Value },
71}
72
73impl ResponseFormat {
74    pub fn to_api_value(&self) -> Value {
75        match self {
76            ResponseFormat::JsonObject => {
77                serde_json::json!({ "type": "json_object" })
78            }
79            ResponseFormat::JsonSchema { name, schema } => {
80                serde_json::json!({
81                    "type": "json_schema",
82                    "json_schema": {
83                        "name": name,
84                        "schema": schema,
85                    }
86                })
87            }
88        }
89    }
90}
91
92use crate::llm::ReasoningConfig;
93
94/// Safety configuration for agent runtime guardrails.
95///
96/// These limits are hard constraints enforced by code, not prompt —
97/// the model cannot bypass them regardless of its capability.
98#[derive(Clone, Debug)]
99pub struct SafetyConfig {
100    /// Maximum number of tool calls allowed per turn.
101    /// When exceeded, tool calls are discarded and the LLM is forced to summarize.
102    /// Default: 128.
103    pub max_tool_calls_per_turn: usize,
104
105    /// Maximum consecutive failures for the same tool before stopping retries.
106    /// Default: 3.
107    pub max_consecutive_failures: usize,
108}
109
110impl Default for SafetyConfig {
111    fn default() -> Self {
112        Self {
113            max_tool_calls_per_turn: 128,
114            max_consecutive_failures: 3,
115        }
116    }
117}
118
119#[derive(Clone, Debug, Default)]
120pub struct AgentConfig {
121    pub system_prompt: Option<String>,
122    /// Controls whether to include the reasoning content in LLM responses.
123    ///
124    /// Distinction from `reasoning.enabled`:
125    /// - `enable_thought`: controls whether the `reasoning_content` field is forwarded
126    ///   to consumers (i.e., "show the thinking process")
127    /// - `reasoning.enabled`: controls whether the model's extended thinking / reasoning
128    ///   mode is enabled (i.e., "let the model think deeply")
129    ///
130    /// Both are usually kept in sync, but can be controlled independently. For example,
131    /// to enable deep thinking without showing the process, set `enable_thought = false`
132    /// and `reasoning.enabled = true`.
133    pub enable_thought: bool,
134    /// Reasoning/thinking configuration that controls LLM reasoning behavior.
135    ///
136    /// - `enabled`: whether to enable extended thinking mode (equivalent to builder's `enable_thinking`)
137    /// - `budget_tokens`: thinking token budget cap
138    /// - `effort`: reasoning intensity/depth (semantics vary by provider)
139    pub reasoning: Option<ReasoningConfig>,
140    pub language: Language,
141    pub execution: ExecutionConfig,
142    pub llm: LlmConfig,
143    pub tool: ToolConfig,
144    pub session: SessionConfig,
145    pub safety: SafetyConfig,
146}
147
148impl AgentConfig {
149    /// Validate the configuration, returning an error for invalid values.
150    pub fn validate(&self) -> crate::types::AgentResult<()> {
151        use crate::types::AgentError;
152
153        if let Some(max_turns) = self.execution.max_turns
154            && max_turns == 0
155        {
156            return Err(AgentError::config_error(
157                "execution.max_turns must be > 0".to_string(),
158            ));
159        }
160
161        if let Some(max_sessions) = self.session.max_sessions
162            && max_sessions == 0
163        {
164            return Err(AgentError::config_error(
165                "session.max_sessions must be > 0".to_string(),
166            ));
167        }
168
169        if let Some(tool_timeout_ms) = self.tool.tool_timeout_ms
170            && tool_timeout_ms == 0
171        {
172            return Err(AgentError::config_error(
173                "tool.tool_timeout_ms must be > 0".to_string(),
174            ));
175        }
176
177        if self.safety.max_tool_calls_per_turn == 0 {
178            return Err(AgentError::config_error(
179                "safety.max_tool_calls_per_turn must be > 0".to_string(),
180            ));
181        }
182
183        Ok(())
184    }
185}
186
187#[derive(Clone, Debug, Default)]
188pub struct ExecutionConfig {
189    pub max_turns: Option<u32>,
190    pub approval_timeout_ms: Option<u64>,
191    pub fail_on_persist_error: bool,
192}
193
194#[derive(Clone, Debug, Default)]
195pub struct LlmConfig {
196    pub response_format: Option<ResponseFormat>,
197    pub llm_retry: Option<RetryConfig>,
198}
199
200/// Default tool timeout: 10 minutes.
201/// This is a safety net against hung tools; tools should override
202/// `timeout_ms()` with a value appropriate for their workload.
203pub const DEFAULT_TOOL_TIMEOUT_MS: u64 = 600_000;
204
205#[derive(Clone, Debug)]
206pub struct ToolConfig {
207    /// Default timeout for tools that don't override `timeout_ms()`.
208    /// Defaults to `DEFAULT_TOOL_TIMEOUT_MS` (10 minutes).
209    pub default_tool_timeout_ms: u64,
210    pub tool_timeout_ms: Option<u64>,
211    pub max_tool_output_chars: Option<usize>,
212    pub tool_error_retry_prompt: Option<String>,
213}
214
215impl Default for ToolConfig {
216    fn default() -> Self {
217        Self {
218            default_tool_timeout_ms: DEFAULT_TOOL_TIMEOUT_MS,
219            tool_timeout_ms: None,
220            max_tool_output_chars: None,
221            tool_error_retry_prompt: None,
222        }
223    }
224}
225
226#[derive(Clone, Debug, Default)]
227pub struct SessionConfig {
228    /// 最大 session 数量,超过时 LRU 逐出整个 session(从内存卸载,数据保留)
229    /// None = 不限制
230    pub max_sessions: Option<usize>,
231
232    /// 单个 session 最大保留轮数,超过从前面截掉最旧轮次
233    /// None = 不限制
234    pub max_turns_per_session: Option<usize>,
235
236    /// 单条消息 token 上限(安全阀),超过不存入 session 历史
237    /// 阈值应设很高(如 100k),只拦异常情况
238    /// None = 不限制
239    pub max_message_tokens: Option<usize>,
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::types::AgentError;
246
247    #[test]
248    fn language_display_and_default() {
249        assert_eq!(Language::default(), Language::En);
250        assert_eq!(Language::En.to_string(), "en");
251        assert_eq!(Language::Zh.to_string(), "zh");
252    }
253
254    #[test]
255    fn retry_config_defaults_and_builders() {
256        let cfg = RetryConfig::default();
257        assert_eq!(cfg.max_retries, 3);
258        assert_eq!(cfg.initial_backoff_ms, 500);
259        assert_eq!(cfg.max_backoff_ms, 10_000);
260        assert_eq!(cfg.backoff_multiplier, 2.0);
261        assert!(cfg.jitter);
262
263        let cfg = RetryConfig::new()
264            .max_retries(7)
265            .initial_backoff_ms(100)
266            .max_backoff_ms(20_000)
267            .no_jitter();
268        assert_eq!(cfg.max_retries, 7);
269        assert_eq!(cfg.initial_backoff_ms, 100);
270        assert_eq!(cfg.max_backoff_ms, 20_000);
271        assert!(!cfg.jitter);
272    }
273
274    #[test]
275    fn response_format_to_api_value() {
276        let v = ResponseFormat::JsonObject.to_api_value();
277        assert_eq!(v["type"], "json_object");
278
279        let v = ResponseFormat::JsonSchema {
280            name: "event".to_string(),
281            schema: serde_json::json!({"type": "object"}),
282        }
283        .to_api_value();
284        assert_eq!(v["type"], "json_schema");
285        assert_eq!(v["json_schema"]["name"], "event");
286        assert_eq!(v["json_schema"]["schema"]["type"], "object");
287    }
288
289    #[test]
290    fn safety_config_defaults() {
291        let cfg = SafetyConfig::default();
292        assert_eq!(cfg.max_tool_calls_per_turn, 128);
293        assert_eq!(cfg.max_consecutive_failures, 3);
294    }
295
296    #[test]
297    fn validate_accepts_default() {
298        assert!(AgentConfig::default().validate().is_ok());
299    }
300
301    #[test]
302    fn validate_rejects_zero_max_turns() {
303        let mut cfg = AgentConfig::default();
304        cfg.execution.max_turns = Some(0);
305        let err = cfg.validate().unwrap_err();
306        assert!(matches!(err, AgentError::ConfigError(_)));
307        assert!(err.to_string().contains("max_turns"));
308    }
309
310    #[test]
311    fn validate_rejects_zero_max_sessions() {
312        let mut cfg = AgentConfig::default();
313        cfg.session.max_sessions = Some(0);
314        assert!(matches!(cfg.validate(), Err(AgentError::ConfigError(_))));
315    }
316
317    #[test]
318    fn validate_rejects_zero_tool_timeout() {
319        let mut cfg = AgentConfig::default();
320        cfg.tool.tool_timeout_ms = Some(0);
321        assert!(matches!(cfg.validate(), Err(AgentError::ConfigError(_))));
322    }
323
324    #[test]
325    fn validate_rejects_zero_max_tool_calls() {
326        let mut cfg = AgentConfig::default();
327        cfg.safety.max_tool_calls_per_turn = 0;
328        assert!(matches!(cfg.validate(), Err(AgentError::ConfigError(_))));
329    }
330}