Skip to main content

agent_base/types/
config.rs

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