1use std::sync::Arc;
2
3use crate::llm::{LlmClient, ReasoningConfig};
4use crate::tool::{Tool, ToolPolicy, ToolRegistry};
5use crate::types::{
6 AgentConfig, AtomicU64SessionIdGenerator, ResponseFormat, RetryConfig, SessionIdGenerator,
7};
8
9use super::AgentRuntime;
10use super::approval::ApprovalHandler;
11use super::context::ContextWindowManager;
12use super::middleware::{Middleware, MiddlewareRef};
13use super::recovery::{StopOnError, ToolErrorRecovery};
14use super::session_store::{InMemorySessionStore, SessionStore};
15
16pub struct AgentBuilder {
17 client: Arc<dyn LlmClient>,
18 config: AgentConfig,
19 tools: ToolRegistry,
20 approval_handler: Option<Arc<dyn ApprovalHandler>>,
21 tool_policy: Option<Arc<dyn ToolPolicy>>,
22 middlewares: Vec<MiddlewareRef>,
23 context_manager: Option<ContextWindowManager>,
24 session_store: Option<Arc<dyn SessionStore>>,
25 error_recovery: Option<Arc<dyn ToolErrorRecovery>>,
26 event_bus_capacity: usize,
27 session_id_generator: Option<Arc<dyn SessionIdGenerator>>,
28}
29
30impl AgentBuilder {
31 pub fn new(client: Arc<dyn LlmClient>) -> Self {
32 Self {
33 client,
34 config: AgentConfig::default(),
35 tools: ToolRegistry::default(),
36 approval_handler: None,
37 tool_policy: None,
38 middlewares: Vec::new(),
39 context_manager: None,
40 session_store: None,
41 error_recovery: None,
42 event_bus_capacity: 2048,
43 session_id_generator: None,
44 }
45 }
46
47 pub fn event_bus_capacity(mut self, capacity: usize) -> Self {
48 self.event_bus_capacity = capacity;
49 self
50 }
51
52 pub fn session_id_generator(mut self, generator: Arc<dyn SessionIdGenerator>) -> Self {
53 self.session_id_generator = Some(generator);
54 self
55 }
56
57 pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
58 self.config.system_prompt = Some(system_prompt.into());
59 self
60 }
61
62 pub fn enable_thought(mut self, enable: bool) -> Self {
68 self.config.enable_thought = enable;
69 self
70 }
71
72 pub fn reasoning(mut self, config: ReasoningConfig) -> Self {
73 self.config.reasoning = Some(config);
74 self
75 }
76
77 pub fn enable_thinking(mut self, enable: bool) -> Self {
82 let mut config = self.config.reasoning.take().unwrap_or_default();
83 config.enabled = Some(enable);
84 self.config.reasoning = Some(config);
85 self
86 }
87
88 pub fn thinking_budget(mut self, budget: u64) -> Self {
89 let mut config = self.config.reasoning.take().unwrap_or_default();
90 config.budget_tokens = Some(budget);
91 self.config.reasoning = Some(config);
92 self
93 }
94
95 pub fn tool_timeout(mut self, timeout_ms: u64) -> Self {
96 self.config.tool.tool_timeout_ms = Some(timeout_ms);
97 self
98 }
99
100 pub fn max_tool_output_chars(mut self, max_chars: usize) -> Self {
101 self.config.tool.max_tool_output_chars = Some(max_chars);
102 self
103 }
104
105 pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
106 self.tools.register(tool);
107 self
108 }
109
110 pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
111 self.tools.register_arc(tool);
112 self
113 }
114
115 pub fn approval_handler(mut self, handler: Arc<dyn ApprovalHandler>) -> Self {
116 self.approval_handler = Some(handler);
117 self
118 }
119
120 pub fn tool_policy(mut self, policy: Arc<dyn ToolPolicy>) -> Self {
121 self.tool_policy = Some(policy);
122 self
123 }
124
125 pub fn middleware(mut self, mw: impl Middleware + 'static) -> Self {
126 self.middlewares.push(Arc::new(mw));
127 self
128 }
129
130 pub fn context_window(mut self, max_tokens: usize) -> Self {
131 self.context_manager = Some(ContextWindowManager::new(max_tokens));
132 self
133 }
134
135 pub fn context_window_manager(mut self, manager: ContextWindowManager) -> Self {
136 self.context_manager = Some(manager);
137 self
138 }
139
140 pub fn response_format(mut self, format: ResponseFormat) -> Self {
141 self.config.llm.response_format = Some(format);
142 self
143 }
144
145 pub fn llm_retry(mut self, retry: RetryConfig) -> Self {
146 self.config.llm.llm_retry = Some(retry);
147 self
148 }
149
150 pub fn session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
151 self.session_store = Some(store);
152 self
153 }
154
155 pub fn error_recovery(mut self, recovery: Arc<dyn ToolErrorRecovery>) -> Self {
156 self.error_recovery = Some(recovery);
157 self
158 }
159
160 pub fn max_sessions(mut self, max: usize) -> Self {
161 self.config.session.max_sessions = Some(max);
162 self
163 }
164
165 pub fn max_turns_per_session(mut self, max: usize) -> Self {
166 self.config.session.max_turns_per_session = Some(max);
167 self
168 }
169
170 pub fn execution_max_turns(mut self, max: u32) -> Self {
174 self.config.execution.max_turns = Some(max);
175 self
176 }
177
178 pub fn max_message_tokens(mut self, max: usize) -> Self {
179 self.config.session.max_message_tokens = Some(max);
180 self
181 }
182
183 pub fn tool_error_retry_prompt(mut self, prompt: impl Into<String>) -> Self {
184 self.config.tool.tool_error_retry_prompt = Some(prompt.into());
185 self
186 }
187
188 pub fn language(mut self, language: crate::types::Language) -> Self {
189 self.config.language = language;
190 self
191 }
192
193 pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
202 match value {
203 Some(v) => f(self, v),
204 None => self,
205 }
206 }
207
208 pub fn build(self) -> crate::types::AgentResult<AgentRuntime> {
209 self.config.validate()?;
210
211 tracing::info!(
212 tool_count = self.tools.len(),
213 middleware_count = self.middlewares.len(),
214 has_approval = self.approval_handler.is_some(),
215 has_context_window = self.context_manager.is_some(),
216 "building agent runtime"
217 );
218
219 let event_bus = super::runtime::EventBus::new(self.event_bus_capacity);
220
221 self.tools.inject_event_bus(&event_bus);
224
225 let session_store = self
226 .session_store
227 .unwrap_or_else(|| Arc::new(InMemorySessionStore::new()));
228 let error_recovery = self.error_recovery.unwrap_or_else(|| Arc::new(StopOnError));
229 let session_id_generator = self
230 .session_id_generator
231 .unwrap_or_else(|| Arc::new(AtomicU64SessionIdGenerator::default()));
232
233 let session_manager = super::runtime::SessionManager::new(
234 session_id_generator,
235 session_store,
236 self.config.session.clone(),
237 );
238
239 let llm_engine = super::runtime::LlmEngine::new(self.client.clone(), event_bus.clone());
240
241 let tool_engine = super::runtime::ToolEngine::new(
242 self.tools,
243 self.approval_handler,
244 self.tool_policy,
245 error_recovery,
246 event_bus.clone(),
247 );
248
249 let runner = Arc::new(super::runtime::RuntimeCore::new(
250 self.config,
251 llm_engine,
252 tool_engine,
253 session_manager,
254 event_bus,
255 self.context_manager,
256 self.middlewares,
257 ));
258
259 Ok(AgentRuntime { runner })
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::llm::StreamChunk;
267 use crate::types::{AgentResult, ChatMessage, ResponseFormat};
268 use async_trait::async_trait;
269 use futures_core::Stream;
270 use serde_json::Value;
271 use std::pin::Pin;
272
273 struct DummyClient;
274
275 #[async_trait]
276 impl LlmClient for DummyClient {
277 async fn chat(
278 &self,
279 _messages: &[ChatMessage],
280 _tools: &[Value],
281 _reasoning: Option<&ReasoningConfig>,
282 _response_format: Option<&ResponseFormat>,
283 ) -> AgentResult<Value> {
284 Ok(Value::Null)
285 }
286
287 async fn chat_stream(
288 &self,
289 _messages: &[ChatMessage],
290 _tools: &[Value],
291 _reasoning: Option<&ReasoningConfig>,
292 _response_format: Option<&ResponseFormat>,
293 ) -> AgentResult<Pin<Box<dyn Stream<Item = AgentResult<StreamChunk>> + Send>>> {
294 unimplemented!("not used in builder tests")
295 }
296
297 fn capabilities(&self) -> crate::llm::LlmCapabilities {
298 crate::llm::LlmCapabilities::default()
299 }
300 }
301
302 #[test]
303 fn execution_max_turns_writes_per_run_config() {
304 let client: Arc<dyn LlmClient> = Arc::new(DummyClient);
305 let builder = AgentBuilder::new(client).execution_max_turns(200);
306 assert_eq!(builder.config.execution.max_turns, Some(200));
308 }
309
310 #[test]
311 fn execution_max_turns_defaults_to_none() {
312 let client: Arc<dyn LlmClient> = Arc::new(DummyClient);
313 let builder = AgentBuilder::new(client);
314 assert_eq!(builder.config.execution.max_turns, None);
315 }
316}