1use std::sync::Arc;
2
3use crate::llm::{ReasoningConfig, StreamClient};
4use crate::tool::{Tool, ToolPolicy, ToolRegistry};
5use crate::types::{
6 AgentConfig, AtomicU64SessionIdGenerator, ConvertToLlmFn, ResponseFormat, RetryConfig,
7 SessionIdGenerator,
8};
9
10use super::AgentRuntime;
11use super::approval::ApprovalHandler;
12use super::context::ContextWindowManager;
13use super::middleware::{Middleware, MiddlewareRef};
14use super::react_loop_guard::ReactLoopGuard;
15use super::recovery::{StopOnError, ToolErrorRecovery};
16use super::session_store::{InMemorySessionStore, SessionStore};
17
18pub struct AgentBuilder {
19 client: Arc<dyn StreamClient>,
20 config: AgentConfig,
21 tools: ToolRegistry,
22 approval_handler: Option<Arc<dyn ApprovalHandler>>,
23 tool_policy: Option<Arc<dyn ToolPolicy>>,
24 middlewares: Vec<MiddlewareRef>,
25 context_manager: Option<ContextWindowManager>,
26 session_store: Option<Arc<dyn SessionStore>>,
27 error_recovery: Option<Arc<dyn ToolErrorRecovery>>,
28 event_bus_capacity: usize,
29 session_id_generator: Option<Arc<dyn SessionIdGenerator>>,
30 convert_to_llm: Option<ConvertToLlmFn>,
31 guard: Option<Arc<dyn ReactLoopGuard>>,
32}
33
34impl AgentBuilder {
35 pub fn new(client: Arc<dyn StreamClient>) -> Self {
36 Self {
37 client,
38 config: AgentConfig::default(),
39 tools: ToolRegistry::default(),
40 approval_handler: None,
41 tool_policy: None,
42 middlewares: Vec::new(),
43 context_manager: None,
44 session_store: None,
45 error_recovery: None,
46 event_bus_capacity: 2048,
47 session_id_generator: None,
48 convert_to_llm: None,
49 guard: None,
50 }
51 }
52
53 pub fn event_bus_capacity(mut self, capacity: usize) -> Self {
54 self.event_bus_capacity = capacity;
55 self
56 }
57
58 pub fn session_id_generator(mut self, generator: Arc<dyn SessionIdGenerator>) -> Self {
59 self.session_id_generator = Some(generator);
60 self
61 }
62
63 pub fn convert_to_llm(mut self, cb: ConvertToLlmFn) -> Self {
70 self.convert_to_llm = Some(cb);
71 self
72 }
73
74 pub fn guard(mut self, guard: impl ReactLoopGuard + 'static) -> Self {
75 self.guard = Some(Arc::new(guard));
76 self
77 }
78
79 pub fn guard_dyn(mut self, guard: Arc<dyn ReactLoopGuard>) -> Self {
84 self.guard = Some(guard);
85 self
86 }
87
88 pub fn get_guard(&self) -> Option<&Arc<dyn ReactLoopGuard>> {
92 self.guard.as_ref()
93 }
94
95 pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
96 self.config.system_prompt = Some(system_prompt.into());
97 self
98 }
99
100 pub fn enable_thought(mut self, enable: bool) -> Self {
106 self.config.enable_thought = enable;
107 self
108 }
109
110 pub fn reasoning(mut self, config: ReasoningConfig) -> Self {
111 self.config.reasoning = Some(config);
112 self
113 }
114
115 pub fn enable_thinking(mut self, enable: bool) -> Self {
120 let mut config = self.config.reasoning.take().unwrap_or_default();
121 config.enabled = Some(enable);
122 self.config.reasoning = Some(config);
123 self
124 }
125
126 pub fn thinking_budget(mut self, budget: u64) -> Self {
127 let mut config = self.config.reasoning.take().unwrap_or_default();
128 config.budget_tokens = Some(budget);
129 self.config.reasoning = Some(config);
130 self
131 }
132
133 pub fn tool_timeout(mut self, timeout_ms: u64) -> Self {
134 self.config.tool.tool_timeout_ms = Some(timeout_ms);
135 self
136 }
137
138 pub fn max_tool_output_chars(mut self, max_chars: usize) -> Self {
139 self.config.tool.max_tool_output_chars = Some(max_chars);
140 self
141 }
142
143 pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
144 self.tools.register(tool);
145 self
146 }
147
148 pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
149 self.tools.register_arc(tool);
150 self
151 }
152
153 pub fn approval_handler(mut self, handler: Arc<dyn ApprovalHandler>) -> Self {
154 self.approval_handler = Some(handler);
155 self
156 }
157
158 pub fn tool_policy(mut self, policy: Arc<dyn ToolPolicy>) -> Self {
159 self.tool_policy = Some(policy);
160 self
161 }
162
163 pub fn middleware(mut self, mw: impl Middleware + 'static) -> Self {
164 self.middlewares.push(Arc::new(mw));
165 self
166 }
167
168 pub fn context_window(mut self, max_tokens: usize) -> Self {
169 self.context_manager = Some(ContextWindowManager::new(max_tokens));
170 self
171 }
172
173 pub fn context_window_manager(mut self, manager: ContextWindowManager) -> Self {
174 self.context_manager = Some(manager);
175 self
176 }
177
178 pub fn response_format(mut self, format: ResponseFormat) -> Self {
179 self.config.llm.response_format = Some(format);
180 self
181 }
182
183 pub fn llm_retry(mut self, retry: RetryConfig) -> Self {
184 self.config.llm.llm_retry = Some(retry);
185 self
186 }
187
188 pub fn session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
189 self.session_store = Some(store);
190 self
191 }
192
193 pub fn error_recovery(mut self, recovery: Arc<dyn ToolErrorRecovery>) -> Self {
194 self.error_recovery = Some(recovery);
195 self
196 }
197
198 pub fn max_sessions(mut self, max: usize) -> Self {
199 self.config.session.max_sessions = Some(max);
200 self
201 }
202
203 pub fn max_turns_per_session(mut self, max: usize) -> Self {
204 self.config.session.max_turns_per_session = Some(max);
205 self
206 }
207
208 pub fn execution_max_turns(mut self, max: u32) -> Self {
212 self.config.execution.max_turns = Some(max);
213 self
214 }
215
216 pub fn max_message_tokens(mut self, max: usize) -> Self {
217 self.config.session.max_message_tokens = Some(max);
218 self
219 }
220
221 pub fn tool_error_retry_prompt(mut self, prompt: impl Into<String>) -> Self {
222 self.config.tool.tool_error_retry_prompt = Some(prompt.into());
223 self
224 }
225
226 pub fn language(mut self, language: crate::types::Language) -> Self {
227 self.config.language = language;
228 self
229 }
230
231 pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
240 match value {
241 Some(v) => f(self, v),
242 None => self,
243 }
244 }
245
246 pub fn build(self) -> crate::types::AgentResult<AgentRuntime> {
247 self.config.validate()?;
248
249 tracing::info!(
250 tool_count = self.tools.len(),
251 middleware_count = self.middlewares.len(),
252 has_approval = self.approval_handler.is_some(),
253 has_context_window = self.context_manager.is_some(),
254 "building agent runtime"
255 );
256
257 let event_bus = super::runtime::EventBus::new(self.event_bus_capacity);
258
259 let session_store = self
260 .session_store
261 .unwrap_or_else(|| Arc::new(InMemorySessionStore::new()));
262 let error_recovery = self.error_recovery.unwrap_or_else(|| Arc::new(StopOnError));
263 let session_id_generator = self
264 .session_id_generator
265 .unwrap_or_else(|| Arc::new(AtomicU64SessionIdGenerator::default()));
266
267 let session_manager = super::runtime::SessionManager::new(
268 session_id_generator,
269 session_store,
270 self.config.session.clone(),
271 );
272
273 let llm_engine = super::runtime::LlmEngine::new(self.client.clone(), event_bus.clone());
274
275 let tool_engine = super::runtime::ToolEngine::new(
276 self.tools,
277 self.approval_handler,
278 self.tool_policy,
279 error_recovery,
280 event_bus.clone(),
281 );
282
283 let runner = Arc::new(super::runtime::RuntimeCore::new(
284 self.config,
285 llm_engine,
286 tool_engine,
287 session_manager,
288 event_bus,
289 self.context_manager,
290 self.middlewares,
291 self.convert_to_llm,
292 self.guard,
293 ));
294
295 Ok(AgentRuntime { runner })
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::engine::DenyAllApprovalHandler;
303 use crate::llm::{LlmClient, ReasoningEffort, StreamChunk};
304 use crate::tool::{Content, ToolContext};
305 use crate::types::{
306 AgentError, AgentResult, ApprovalRequest, ChatMessage, Language, ResponseFormat,
307 };
308 use async_trait::async_trait;
309 use futures_core::Stream;
310 use serde_json::Value;
311 use std::pin::Pin;
312
313 struct DummyClient;
314
315 #[async_trait]
316 impl LlmClient for DummyClient {
317 async fn chat(
318 &self,
319 _messages: &[ChatMessage],
320 _tools: &[Value],
321 _reasoning: Option<&ReasoningConfig>,
322 _response_format: Option<&ResponseFormat>,
323 ) -> AgentResult<Value> {
324 Ok(Value::Null)
325 }
326
327 async fn chat_stream(
328 &self,
329 _messages: &[ChatMessage],
330 _tools: &[Value],
331 _reasoning: Option<&ReasoningConfig>,
332 _response_format: Option<&ResponseFormat>,
333 ) -> AgentResult<Pin<Box<dyn Stream<Item = AgentResult<StreamChunk>> + Send>>> {
334 unimplemented!("not used in builder tests")
335 }
336
337 fn capabilities(&self) -> crate::llm::LlmCapabilities {
338 crate::llm::LlmCapabilities::default()
339 }
340 }
341
342 #[test]
343 fn execution_max_turns_writes_per_run_config() {
344 let client = crate::llm::adapt(Arc::new(DummyClient));
345 let builder = AgentBuilder::new(client).execution_max_turns(200);
346 assert_eq!(builder.config.execution.max_turns, Some(200));
348 }
349
350 #[test]
351 fn execution_max_turns_defaults_to_none() {
352 let client = crate::llm::adapt(Arc::new(DummyClient));
353 let builder = AgentBuilder::new(client);
354 assert_eq!(builder.config.execution.max_turns, None);
355 }
356
357 fn b() -> AgentBuilder {
358 AgentBuilder::new(crate::llm::adapt(Arc::new(DummyClient)))
359 }
360
361 struct NoopTool;
362
363 #[async_trait]
364 impl Tool for NoopTool {
365 fn name(&self) -> &'static str {
366 "noop"
367 }
368 fn description(&self) -> &'static str {
369 "noop tool"
370 }
371 fn schema(&self) -> Value {
372 serde_json::json!({"type": "object"})
373 }
374 async fn call(&self, _args: &Value, _ctx: &ToolContext) -> AgentResult<Vec<Content>> {
375 Ok(vec![Content::text("ok")])
376 }
377 }
378
379 struct AutoApprovePolicy;
380
381 #[async_trait]
382 impl ToolPolicy for AutoApprovePolicy {
383 async fn evaluate_approval(
384 &self,
385 _tool_name: &str,
386 _args: &Value,
387 ) -> Option<ApprovalRequest> {
388 None
389 }
390 }
391
392 struct NoopMiddleware;
393
394 impl Middleware for NoopMiddleware {}
395
396 #[test]
397 fn system_prompt_sets_config() {
398 assert_eq!(
399 b().system_prompt("be helpful")
400 .config
401 .system_prompt
402 .as_deref(),
403 Some("be helpful")
404 );
405 }
406
407 #[test]
408 fn enable_thought_sets_config() {
409 assert!(b().enable_thought(true).config.enable_thought);
410 }
411
412 #[test]
413 fn reasoning_sets_config() {
414 let rc = ReasoningConfig {
415 enabled: Some(true),
416 budget_tokens: Some(64),
417 effort: Some(ReasoningEffort::Medium),
418 };
419 let builder = b().reasoning(rc);
420 let got = builder.config.reasoning.as_ref().unwrap();
421 assert_eq!(got.enabled, Some(true));
422 assert_eq!(got.budget_tokens, Some(64));
423 assert!(matches!(got.effort.as_ref(), Some(ReasoningEffort::Medium)));
424 }
425
426 #[test]
427 fn enable_thinking_and_budget_set_reasoning() {
428 let builder = b().enable_thinking(true).thinking_budget(128);
429 let got = builder.config.reasoning.as_ref().unwrap();
430 assert_eq!(got.enabled, Some(true));
431 assert_eq!(got.budget_tokens, Some(128));
432 }
433
434 #[test]
435 fn tool_limits_set_config() {
436 let builder = b().tool_timeout(5_000).max_tool_output_chars(1_024);
437 assert_eq!(builder.config.tool.tool_timeout_ms, Some(5_000));
438 assert_eq!(builder.config.tool.max_tool_output_chars, Some(1_024));
439 }
440
441 #[test]
442 fn register_tool_adds_to_registry() {
443 assert_eq!(b().register_tool(NoopTool).tools.len(), 1);
444 }
445
446 #[test]
447 fn approval_handler_and_tool_policy_are_set() {
448 let builder = b()
449 .approval_handler(Arc::new(DenyAllApprovalHandler))
450 .tool_policy(Arc::new(AutoApprovePolicy));
451 assert!(builder.approval_handler.is_some());
452 assert!(builder.tool_policy.is_some());
453 }
454
455 #[test]
456 fn middleware_and_context_window_are_set() {
457 let builder = b().middleware(NoopMiddleware).context_window(8_000);
458 assert_eq!(builder.middlewares.len(), 1);
459 assert!(builder.context_manager.is_some());
460 }
461
462 #[test]
463 fn response_format_and_retry_set_config() {
464 let builder = b()
465 .response_format(ResponseFormat::JsonObject)
466 .llm_retry(RetryConfig::default().max_retries(5));
467 assert!(builder.config.llm.response_format.is_some());
468 assert_eq!(
469 builder.config.llm.llm_retry.as_ref().unwrap().max_retries,
470 5
471 );
472 }
473
474 #[test]
475 fn session_store_and_error_recovery_are_set() {
476 let builder = b()
477 .session_store(Arc::new(InMemorySessionStore::new()))
478 .error_recovery(Arc::new(StopOnError));
479 assert!(builder.session_store.is_some());
480 assert!(builder.error_recovery.is_some());
481 }
482
483 #[test]
484 fn session_limits_set_config() {
485 let builder = b()
486 .max_sessions(10)
487 .max_turns_per_session(20)
488 .max_message_tokens(30);
489 assert_eq!(builder.config.session.max_sessions, Some(10));
490 assert_eq!(builder.config.session.max_turns_per_session, Some(20));
491 assert_eq!(builder.config.session.max_message_tokens, Some(30));
492 }
493
494 #[test]
495 fn tool_error_retry_prompt_and_language_set_config() {
496 let builder = b()
497 .tool_error_retry_prompt("try again")
498 .language(Language::Zh);
499 assert_eq!(
500 builder.config.tool.tool_error_retry_prompt.as_deref(),
501 Some("try again")
502 );
503 assert_eq!(builder.config.language, Language::Zh);
504 }
505
506 #[test]
507 fn apply_if_applies_when_some_and_skips_when_none() {
508 let applied = b().apply_if(Some(3_000_u64), |b, t| b.tool_timeout(t));
509 assert_eq!(applied.config.tool.tool_timeout_ms, Some(3_000));
510
511 let skipped = b().apply_if(None, |b, t| b.tool_timeout(t));
512 assert_eq!(skipped.config.tool.tool_timeout_ms, None);
513 }
514
515 #[test]
516 fn build_ok_with_defaults() {
517 assert!(b().build().is_ok());
518 }
519
520 #[test]
521 fn build_err_on_invalid_config() {
522 assert!(matches!(
523 b().execution_max_turns(0).build(),
524 Err(AgentError::ConfigError(_))
525 ));
526 }
527}