Skip to main content

machi_agent/
builder.rs

1//! Agent builder.
2
3use std::sync::Arc;
4
5use machi_tools::{SharedTool, ToolRegistry};
6use machi_types::{ErrorCode, MachiError};
7
8use crate::definition::{AgentDefinition, CompletionRequirement, Instructions};
9use crate::instance::Agent;
10
11/// Builds a validated [`Agent`].
12#[derive(Default)]
13pub struct AgentBuilder {
14    definition: Option<AgentDefinition>,
15    tools: Vec<SharedTool>,
16}
17
18impl std::fmt::Debug for AgentBuilder {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        f.debug_struct("AgentBuilder")
21            .field("definition", &self.definition)
22            .field("tools", &self.tools.len())
23            .finish()
24    }
25}
26
27impl AgentBuilder {
28    /// Empty builder.
29    #[must_use]
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Start from a definition.
35    #[must_use]
36    pub fn from_definition(definition: AgentDefinition) -> Self {
37        Self {
38            definition: Some(definition),
39            tools: Vec::new(),
40        }
41    }
42
43    /// Programmatic minimal definition.
44    #[must_use]
45    pub fn named(name: impl Into<String>) -> Self {
46        Self {
47            definition: Some(AgentDefinition::new(name)),
48            tools: Vec::new(),
49        }
50    }
51
52    /// Set instructions.
53    #[must_use]
54    pub fn instructions(mut self, instructions: impl Into<Instructions>) -> Self {
55        if let Some(def) = &mut self.definition {
56            def.instructions = instructions.into();
57        }
58        self
59    }
60
61    /// Set model.
62    #[must_use]
63    pub fn model(mut self, model: impl Into<String>) -> Self {
64        if let Some(def) = &mut self.definition {
65            def.model = model.into();
66        }
67        self
68    }
69
70    /// Set description.
71    #[must_use]
72    pub fn description(mut self, description: impl Into<String>) -> Self {
73        if let Some(def) = &mut self.definition {
74            def.description = description.into();
75        }
76        self
77    }
78
79    /// Set max steps.
80    #[must_use]
81    pub fn max_steps(mut self, max_steps: usize) -> Self {
82        if let Some(def) = &mut self.definition {
83            def.max_steps = max_steps;
84        }
85        self
86    }
87
88    /// Attach tools available to the agent (filtered by definition policy).
89    #[must_use]
90    pub fn tools(mut self, tools: Vec<SharedTool>) -> Self {
91        self.tools = tools;
92        self
93    }
94
95    /// Require a named tool call before the turn may complete.
96    #[must_use]
97    pub fn completion(mut self, requirement: CompletionRequirement) -> Self {
98        if let Some(def) = &mut self.definition {
99            def.completion = Some(requirement);
100        }
101        self
102    }
103
104    /// Require structured JSON output matching a JSON Schema object.
105    #[must_use]
106    pub fn output_schema(mut self, schema: serde_json::Value) -> Self {
107        if let Some(def) = &mut self.definition {
108            def.output_schema = Some(schema);
109        }
110        self
111    }
112
113    /// Build the agent instance.
114    ///
115    /// # Errors
116    ///
117    /// Returns validation or build errors.
118    pub fn build(self) -> Result<Agent, MachiError> {
119        let definition = self.definition.ok_or_else(|| {
120            MachiError::new(ErrorCode::AgentBuild, "agent definition is required")
121        })?;
122        definition.validate()?;
123
124        // Definition-level allowed_tools / denylist applied at resolution (W5.4).
125        let filtered: Vec<SharedTool> = self
126            .tools
127            .into_iter()
128            .filter(|t| definition.tools.admits(t.name()))
129            .collect();
130
131        let system_prompt = definition.instructions.resolve();
132        let tools = Arc::new(ToolRegistry::from_tools(filtered));
133        Ok(Agent::new(definition, system_prompt, tools))
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn rejects_empty_name() {
143        let err = AgentBuilder::named("  ").build().expect_err("empty");
144        assert_eq!(err.code(), ErrorCode::AgentInvalidDefinition);
145    }
146
147    #[test]
148    fn builds_minimal() {
149        let agent = AgentBuilder::named("assistant")
150            .instructions("You are helpful.")
151            .model("mock")
152            .build()
153            .expect("build");
154        assert_eq!(agent.name(), "assistant");
155        assert_eq!(agent.system_prompt(), "You are helpful.");
156    }
157
158    #[test]
159    fn allowed_tools_filtered_at_build() {
160        use std::sync::Arc;
161
162        use machi_tools::CalcTool;
163
164        let mut def = AgentDefinition::new("x");
165        def.tools = crate::definition::ToolPolicy::Allowlist(vec!["calc".into()]);
166        def.model = "m".into();
167        // Two calc instances under different names aren't available; policy drops non-calc.
168        // Empty allowlist of "other" leaves no tools.
169        let agent_empty = AgentBuilder::from_definition({
170            let mut d = def.clone();
171            d.tools = crate::definition::ToolPolicy::Allowlist(vec!["other".into()]);
172            d
173        })
174        .tools(vec![Arc::new(CalcTool)])
175        .build()
176        .expect("build");
177        assert!(agent_empty.tools().is_empty());
178
179        let agent = AgentBuilder::from_definition(def)
180            .tools(vec![Arc::new(CalcTool)])
181            .build()
182            .expect("build");
183        assert_eq!(agent.tools().names(), vec!["calc".to_owned()]);
184    }
185}