agents_runtime/agent/
builder.rs

1//! Fluent builder API for constructing Deep Agents
2//!
3//! This module provides the ConfigurableAgentBuilder that offers a fluent interface
4//! for building Deep Agents, mirroring the Python SDK's ergonomic construction patterns.
5
6use super::api::{create_async_deep_agent_from_config, create_deep_agent_from_config};
7use super::config::{DeepAgentConfig, SubAgentConfig, SummarizationConfig};
8use super::runtime::DeepAgent;
9use crate::middleware::HitlPolicy;
10use crate::planner::LlmBackedPlanner;
11use crate::providers::{
12    AnthropicConfig, AnthropicMessagesModel, GeminiChatModel, GeminiConfig, OpenAiChatModel,
13    OpenAiConfig,
14};
15use agents_core::agent::PlannerHandle;
16use agents_core::llm::LanguageModel;
17use agents_core::persistence::Checkpointer;
18use agents_core::tools::ToolBox;
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21
22/// Builder API to assemble a DeepAgent in a single fluent flow, mirroring the Python
23/// `create_configurable_agent` experience. Prefer this for ergonomic construction.
24pub struct ConfigurableAgentBuilder {
25    instructions: String,
26    planner: Option<Arc<dyn PlannerHandle>>,
27    tools: Vec<ToolBox>,
28    subagents: Vec<SubAgentConfig>,
29    summarization: Option<SummarizationConfig>,
30    tool_interrupts: HashMap<String, HitlPolicy>,
31    builtin_tools: Option<HashSet<String>>,
32    auto_general_purpose: bool,
33    enable_prompt_caching: bool,
34    checkpointer: Option<Arc<dyn Checkpointer>>,
35    event_dispatcher: Option<Arc<agents_core::events::EventDispatcher>>,
36    enable_pii_sanitization: bool,
37}
38
39impl ConfigurableAgentBuilder {
40    pub fn new(instructions: impl Into<String>) -> Self {
41        Self {
42            instructions: instructions.into(),
43            planner: None,
44            tools: Vec::new(),
45            subagents: Vec::new(),
46            summarization: None,
47            tool_interrupts: HashMap::new(),
48            builtin_tools: None,
49            auto_general_purpose: true,
50            enable_prompt_caching: false,
51            checkpointer: None,
52            event_dispatcher: None,
53            enable_pii_sanitization: true, // Enabled by default for security
54        }
55    }
56
57    /// Set the language model for the agent (mirrors Python's `model` parameter)
58    pub fn with_model(mut self, model: Arc<dyn LanguageModel>) -> Self {
59        let planner: Arc<dyn PlannerHandle> = Arc::new(LlmBackedPlanner::new(model));
60        self.planner = Some(planner);
61        self
62    }
63
64    /// Low-level planner API (for advanced use cases)
65    pub fn with_planner(mut self, planner: Arc<dyn PlannerHandle>) -> Self {
66        self.planner = Some(planner);
67        self
68    }
69
70    /// Convenience method for OpenAI models (equivalent to model=OpenAiChatModel)
71    pub fn with_openai_chat(self, config: OpenAiConfig) -> anyhow::Result<Self> {
72        let model = Arc::new(OpenAiChatModel::new(config)?);
73        Ok(self.with_model(model))
74    }
75
76    /// Convenience method for Anthropic models (equivalent to model=AnthropicMessagesModel)  
77    pub fn with_anthropic_messages(self, config: AnthropicConfig) -> anyhow::Result<Self> {
78        let model = Arc::new(AnthropicMessagesModel::new(config)?);
79        Ok(self.with_model(model))
80    }
81
82    /// Convenience method for Gemini models (equivalent to model=GeminiChatModel)
83    pub fn with_gemini_chat(self, config: GeminiConfig) -> anyhow::Result<Self> {
84        let model = Arc::new(GeminiChatModel::new(config)?);
85        Ok(self.with_model(model))
86    }
87
88    /// Add a tool to the agent
89    pub fn with_tool(mut self, tool: ToolBox) -> Self {
90        self.tools.push(tool);
91        self
92    }
93
94    /// Add multiple tools
95    pub fn with_tools<I>(mut self, tools: I) -> Self
96    where
97        I: IntoIterator<Item = ToolBox>,
98    {
99        self.tools.extend(tools);
100        self
101    }
102
103    pub fn with_subagent_config<I>(mut self, cfgs: I) -> Self
104    where
105        I: IntoIterator<Item = SubAgentConfig>,
106    {
107        self.subagents.extend(cfgs);
108        self
109    }
110
111    /// Convenience method: automatically create subagents from a list of tools.
112    /// Each tool becomes a specialized subagent with that single tool.
113    pub fn with_subagent_tools<I>(mut self, tools: I) -> Self
114    where
115        I: IntoIterator<Item = ToolBox>,
116    {
117        for tool in tools {
118            let tool_name = tool.schema().name.clone();
119            let subagent_config = SubAgentConfig::new(
120                format!("{}-agent", tool_name),
121                format!("Specialized agent for {} operations", tool_name),
122                format!(
123                    "You are a specialized agent. Use the {} tool to complete tasks efficiently.",
124                    tool_name
125                ),
126            )
127            .with_tools(vec![tool]);
128            self.subagents.push(subagent_config);
129        }
130        self
131    }
132
133    pub fn with_summarization(mut self, config: SummarizationConfig) -> Self {
134        self.summarization = Some(config);
135        self
136    }
137
138    pub fn with_tool_interrupt(mut self, tool_name: impl Into<String>, policy: HitlPolicy) -> Self {
139        self.tool_interrupts.insert(tool_name.into(), policy);
140        self
141    }
142
143    pub fn with_builtin_tools<I, S>(mut self, names: I) -> Self
144    where
145        I: IntoIterator<Item = S>,
146        S: Into<String>,
147    {
148        self.builtin_tools = Some(names.into_iter().map(|s| s.into()).collect());
149        self
150    }
151
152    pub fn with_auto_general_purpose(mut self, enabled: bool) -> Self {
153        self.auto_general_purpose = enabled;
154        self
155    }
156
157    pub fn with_prompt_caching(mut self, enabled: bool) -> Self {
158        self.enable_prompt_caching = enabled;
159        self
160    }
161
162    pub fn with_checkpointer(mut self, checkpointer: Arc<dyn Checkpointer>) -> Self {
163        self.checkpointer = Some(checkpointer);
164        self
165    }
166
167    /// Add a single event broadcaster to the agent
168    ///
169    /// Example:
170    /// ```ignore
171    /// builder.with_event_broadcaster(console_broadcaster)
172    /// ```
173    pub fn with_event_broadcaster(
174        mut self,
175        broadcaster: Arc<dyn agents_core::events::EventBroadcaster>,
176    ) -> Self {
177        // Create dispatcher if it doesn't exist
178        if self.event_dispatcher.is_none() {
179            self.event_dispatcher = Some(Arc::new(agents_core::events::EventDispatcher::new()));
180        }
181
182        // Add broadcaster to the dispatcher (uses interior mutability)
183        if let Some(dispatcher) = &self.event_dispatcher {
184            dispatcher.add_broadcaster(broadcaster);
185        }
186
187        self
188    }
189
190    /// Add multiple event broadcasters at once (cleaner API)
191    ///
192    /// Example:
193    /// ```ignore
194    /// builder.with_event_broadcasters(vec![
195    ///     console_broadcaster,
196    ///     whatsapp_broadcaster,
197    ///     dynamodb_broadcaster,
198    /// ])
199    /// ```
200    pub fn with_event_broadcasters(
201        mut self,
202        broadcasters: Vec<Arc<dyn agents_core::events::EventBroadcaster>>,
203    ) -> Self {
204        // Create dispatcher if it doesn't exist
205        if self.event_dispatcher.is_none() {
206            self.event_dispatcher = Some(Arc::new(agents_core::events::EventDispatcher::new()));
207        }
208
209        // Add all broadcasters
210        if let Some(dispatcher) = &self.event_dispatcher {
211            for broadcaster in broadcasters {
212                dispatcher.add_broadcaster(broadcaster);
213            }
214        }
215
216        self
217    }
218
219    /// Set the event dispatcher directly (replaces any existing dispatcher)
220    pub fn with_event_dispatcher(
221        mut self,
222        dispatcher: Arc<agents_core::events::EventDispatcher>,
223    ) -> Self {
224        self.event_dispatcher = Some(dispatcher);
225        self
226    }
227
228    /// Enable or disable PII sanitization in event data.
229    ///
230    /// **Enabled by default for security.**
231    ///
232    /// When enabled (default):
233    /// - Message previews are truncated to 100 characters
234    /// - Sensitive fields (passwords, tokens, api_keys, etc.) are redacted
235    /// - PII patterns (emails, phones, credit cards) are removed
236    ///
237    /// Disable only if you need raw data and have other security measures in place.
238    ///
239    /// # Example
240    ///
241    /// ```ignore
242    /// // Keep default (enabled)
243    /// let agent = DeepAgentBuilder::new("instructions")
244    ///     .with_model(model)
245    ///     .build()?;
246    ///
247    /// // Explicitly disable (not recommended for production)
248    /// let agent = DeepAgentBuilder::new("instructions")
249    ///     .with_model(model)
250    ///     .with_pii_sanitization(false)
251    ///     .build()?;
252    /// ```
253    pub fn with_pii_sanitization(mut self, enabled: bool) -> Self {
254        self.enable_pii_sanitization = enabled;
255        self
256    }
257
258    pub fn build(self) -> anyhow::Result<DeepAgent> {
259        self.finalize(create_deep_agent_from_config)
260    }
261
262    /// Build an agent using the async constructor alias. This mirrors the Python
263    /// async_create_deep_agent entry point, while reusing the same runtime internals.
264    pub fn build_async(self) -> anyhow::Result<DeepAgent> {
265        self.finalize(create_async_deep_agent_from_config)
266    }
267
268    fn finalize(self, ctor: fn(DeepAgentConfig) -> DeepAgent) -> anyhow::Result<DeepAgent> {
269        let Self {
270            instructions,
271            planner,
272            tools,
273            subagents,
274            summarization,
275            tool_interrupts,
276            builtin_tools,
277            auto_general_purpose,
278            enable_prompt_caching,
279            checkpointer,
280            event_dispatcher,
281            enable_pii_sanitization,
282        } = self;
283
284        let planner = planner
285            .ok_or_else(|| anyhow::anyhow!("model must be set (use with_model or with_*_chat)"))?;
286
287        let mut cfg = DeepAgentConfig::new(instructions, planner)
288            .with_auto_general_purpose(auto_general_purpose)
289            .with_prompt_caching(enable_prompt_caching)
290            .with_pii_sanitization(enable_pii_sanitization);
291
292        if let Some(ckpt) = checkpointer {
293            cfg = cfg.with_checkpointer(ckpt);
294        }
295        if let Some(dispatcher) = event_dispatcher {
296            cfg = cfg.with_event_dispatcher(dispatcher);
297        }
298        if let Some(sum) = summarization {
299            cfg = cfg.with_summarization(sum);
300        }
301        if let Some(selected) = builtin_tools {
302            cfg = cfg.with_builtin_tools(selected);
303        }
304        for (name, policy) in tool_interrupts {
305            cfg = cfg.with_tool_interrupt(name, policy);
306        }
307        for tool in tools {
308            cfg = cfg.with_tool(tool);
309        }
310        for sub_cfg in subagents {
311            cfg = cfg.with_subagent_config(sub_cfg);
312        }
313
314        Ok(ctor(cfg))
315    }
316}