Skip to main content

echo_agent/agent/subagent/
builder.rs

1//! Subagent builder — ergonomic chainable configuration
2
3use std::path::PathBuf;
4
5use super::types::{ExecutionMode, SubagentDefinition, SubagentKind};
6
7/// Builder for creating [`SubagentDefinition`] instances with a fluent API.
8///
9/// # Example
10///
11/// ```rust
12/// use echo_agent::agent::subagent::SubagentBuilder;
13///
14/// let def = SubagentBuilder::new("researcher")
15///     .description("Researches topics thoroughly")
16///     .fork_mode()
17///     .model("qwen3")
18///     .system_prompt("You are a research specialist...")
19///     .tools(vec!["search", "read_file"])
20///     .inherit_history(10)
21///     .timeout(120)
22///     .tag("research")
23///     .can_delegate()
24///     .build();
25///
26/// assert_eq!(def.name, "researcher");
27/// ```
28pub struct SubagentBuilder {
29    definition: SubagentDefinition,
30}
31
32impl SubagentBuilder {
33    /// Start building a subagent with the given name.
34    pub fn new(name: impl Into<String>) -> Self {
35        Self {
36            definition: SubagentDefinition {
37                name: name.into(),
38                description: String::new(),
39                kind: SubagentKind::BuiltIn,
40                execution_mode: ExecutionMode::Sync,
41                model: None,
42                system_prompt: None,
43                tool_filter: None,
44                max_iterations: None,
45                token_limit: None,
46                inherit_history: None,
47                inherit_memory: false,
48                timeout_secs: 0,
49                can_delegate: false,
50                tags: Vec::new(),
51            },
52        }
53    }
54
55    /// Set a human-readable description (shown to the LLM in tool descriptions).
56    pub fn description(mut self, desc: impl Into<String>) -> Self {
57        self.definition.description = desc.into();
58        self
59    }
60
61    /// Set the agent source kind.
62    pub fn kind(mut self, kind: SubagentKind) -> Self {
63        self.definition.kind = kind;
64        self
65    }
66
67    /// Load from a custom `.md` definition file.
68    pub fn custom(mut self, path: impl Into<PathBuf>) -> Self {
69        self.definition.kind = SubagentKind::Custom { path: path.into() };
70        self
71    }
72
73    /// Load from a plugin source.
74    pub fn plugin(mut self, source: impl Into<String>) -> Self {
75        self.definition.kind = SubagentKind::Plugin {
76            source: source.into(),
77        };
78        self
79    }
80
81    /// Set execution mode to Sync (default).
82    pub fn sync_mode(mut self) -> Self {
83        self.definition.execution_mode = ExecutionMode::Sync;
84        self
85    }
86
87    /// Set execution mode to Fork (inherits context, runs independently).
88    pub fn fork_mode(mut self) -> Self {
89        self.definition.execution_mode = ExecutionMode::Fork;
90        self.definition.inherit_history = Some(10);
91        self.definition.inherit_memory = true;
92        self
93    }
94
95    /// Set execution mode to Teammate (parallel, mailbox-based).
96    pub fn teammate_mode(mut self) -> Self {
97        self.definition.execution_mode = ExecutionMode::Teammate;
98        self
99    }
100
101    /// Override the model for this subagent.
102    pub fn model(mut self, model: impl Into<String>) -> Self {
103        self.definition.model = Some(model.into());
104        self
105    }
106
107    /// Override the system prompt.
108    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
109        self.definition.system_prompt = Some(prompt.into());
110        self
111    }
112
113    /// Restrict available tools by name.
114    pub fn tools(mut self, tools: Vec<impl Into<String>>) -> Self {
115        self.definition.tool_filter = Some(tools.into_iter().map(Into::into).collect());
116        self
117    }
118
119    /// Set max iterations.
120    pub fn max_iterations(mut self, max: usize) -> Self {
121        self.definition.max_iterations = Some(max);
122        self
123    }
124
125    /// Set token limit.
126    pub fn token_limit(mut self, limit: usize) -> Self {
127        self.definition.token_limit = Some(limit);
128        self
129    }
130
131    /// Set number of recent messages to inherit from parent (Fork mode).
132    pub fn inherit_history(mut self, count: usize) -> Self {
133        self.definition.inherit_history = Some(count);
134        self
135    }
136
137    /// Enable memory inheritance.
138    pub fn inherit_memory(mut self) -> Self {
139        self.definition.inherit_memory = true;
140        self
141    }
142
143    /// Set timeout in seconds (0 = no timeout).
144    pub fn timeout(mut self, secs: u64) -> Self {
145        self.definition.timeout_secs = secs;
146        self
147    }
148
149    /// Allow this subagent to delegate to further subagents.
150    pub fn can_delegate(mut self) -> Self {
151        self.definition.can_delegate = true;
152        self
153    }
154
155    /// Add a tag for discovery/filtering.
156    pub fn tag(mut self, tag: impl Into<String>) -> Self {
157        self.definition.tags.push(tag.into());
158        self
159    }
160
161    /// Add multiple tags.
162    pub fn tags(mut self, tags: Vec<impl Into<String>>) -> Self {
163        self.definition
164            .tags
165            .extend(tags.into_iter().map(Into::into));
166        self
167    }
168
169    /// Build the definition.
170    pub fn build(self) -> SubagentDefinition {
171        // Auto-fill description if empty
172        let mut def = self.definition;
173        if def.description.is_empty() {
174            def.description = format!("Subagent '{}'", def.name);
175        }
176        def
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_builder_sync() {
186        let def = SubagentBuilder::new("worker")
187            .description("Does work")
188            .sync_mode()
189            .timeout(60)
190            .build();
191
192        assert_eq!(def.name, "worker");
193        assert_eq!(def.execution_mode, ExecutionMode::Sync);
194        assert_eq!(def.timeout_secs, 60);
195        assert!(def.inherit_history.is_none());
196    }
197
198    #[test]
199    fn test_builder_fork() {
200        let def = SubagentBuilder::new("researcher")
201            .description("Researches")
202            .fork_mode()
203            .model("qwen3")
204            .system_prompt("You are a researcher")
205            .tools(vec!["search", "read"])
206            .inherit_history(10)
207            .timeout(120)
208            .tag("research")
209            .can_delegate()
210            .build();
211
212        assert_eq!(def.execution_mode, ExecutionMode::Fork);
213        assert_eq!(def.model.as_deref(), Some("qwen3"));
214        assert_eq!(def.inherit_history, Some(10));
215        assert!(def.can_delegate);
216        assert_eq!(def.tags, vec!["research"]);
217        assert_eq!(
218            def.tool_filter.as_deref(),
219            Some(["search".to_string(), "read".to_string()].as_slice())
220        );
221    }
222
223    #[test]
224    fn test_builder_teammate() {
225        let def = SubagentBuilder::new("tm").teammate_mode().build();
226
227        assert_eq!(def.execution_mode, ExecutionMode::Teammate);
228        assert!(def.inherit_history.is_none());
229    }
230
231    #[test]
232    fn test_builder_auto_description() {
233        let def = SubagentBuilder::new("auto").build();
234        assert!(def.description.contains("auto"));
235    }
236
237    #[test]
238    fn test_builder_custom_kind() {
239        let def = SubagentBuilder::new("custom")
240            .custom("/path/to/agent.md")
241            .build();
242        assert!(matches!(def.kind, SubagentKind::Custom { .. }));
243    }
244
245    #[test]
246    fn test_builder_plugin_kind() {
247        let def = SubagentBuilder::new("plugin")
248            .plugin("remote://registry")
249            .build();
250        assert!(matches!(def.kind, SubagentKind::Plugin { .. }));
251    }
252}