Skip to main content

agentoven_core/
agent.rs

1//! Agent β€” the primary resource in AgentOven.
2//!
3//! An Agent is a versioned, deployable AI unit registered in the control plane.
4//! When baked (deployed), it automatically gets an A2A Agent Card for discovery.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::ingredient::Ingredient;
11
12/// Agent execution mode.
13#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "lowercase")]
15pub enum AgentMode {
16    /// AgentOven runs the agentic loop (built-in executor).
17    #[default]
18    Managed,
19    /// Agent is external, proxied via A2A endpoint.
20    External,
21    /// Unknown or unset mode (deserialized from empty string or unrecognized value).
22    #[serde(other)]
23    Unknown,
24}
25
26impl std::fmt::Display for AgentMode {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            AgentMode::Managed => write!(f, "managed"),
30            AgentMode::External => write!(f, "external"),
31            AgentMode::Unknown => write!(f, "unknown"),
32        }
33    }
34}
35
36/// A guardrail applied to an agent's input or output.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Guardrail {
39    /// Guardrail kind (e.g., "content-filter", "pii-detector", "token-budget").
40    pub kind: String,
41    /// When to apply: "pre" (before LLM) or "post" (after LLM).
42    #[serde(default = "default_guardrail_stage")]
43    pub stage: String,
44    /// Kind-specific configuration.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub config: Option<serde_json::Value>,
47}
48
49fn default_guardrail_stage() -> String {
50    "pre".into()
51}
52
53/// An Agent registered in AgentOven β€” a versioned, deployable AI unit.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Agent {
56    /// Unique identifier.
57    pub id: String,
58
59    /// Human-readable name (must be unique within a kitchen/workspace).
60    pub name: String,
61
62    /// Semantic version (e.g., "1.0.0").
63    pub version: String,
64
65    /// Description of what this agent does.
66    pub description: String,
67
68    /// The framework used to build this agent.
69    pub framework: AgentFramework,
70
71    /// Agent mode: managed (AgentOven executor) or external (A2A proxy).
72    #[serde(default)]
73    pub mode: AgentMode,
74
75    /// Primary model provider name.
76    #[serde(default)]
77    pub model_provider: String,
78
79    /// Primary model name.
80    #[serde(default)]
81    pub model_name: String,
82
83    /// Backup model provider for failover.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub backup_provider: Option<String>,
86
87    /// Backup model name for failover.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub backup_model: Option<String>,
90
91    /// System prompt / instructions.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub system_prompt: Option<String>,
94
95    /// Maximum turns for managed agentic loop.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub max_turns: Option<u32>,
98
99    /// Agent skills/capabilities.
100    #[serde(default)]
101    pub skills: Vec<String>,
102
103    /// Guardrails applied to this agent.
104    #[serde(default)]
105    pub guardrails: Vec<Guardrail>,
106
107    /// A2A endpoint for external agents.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub a2a_endpoint: Option<String>,
110
111    /// Ingredients: models, tools, prompts, and data sources.
112    #[serde(default)]
113    pub ingredients: Vec<Ingredient>,
114
115    /// Tags for categorization and search.
116    #[serde(default)]
117    pub tags: Vec<String>,
118
119    /// Current deployment status.
120    pub status: AgentStatus,
121
122    /// The kitchen (workspace) this agent belongs to.
123    /// Accepts both "kitchen" and "kitchen_id" from JSON.
124    #[serde(default, alias = "kitchen", skip_serializing_if = "Option::is_none")]
125    pub kitchen_id: Option<String>,
126
127    /// Resolved configuration (populated by the control plane after baking).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub resolved_config: Option<serde_json::Value>,
130
131    /// Who created this agent.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub created_by: Option<String>,
134
135    /// When this version was created.
136    pub created_at: DateTime<Utc>,
137
138    /// When this version was last updated.
139    pub updated_at: DateTime<Utc>,
140
141    /// Optional metadata.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub metadata: Option<serde_json::Value>,
144}
145
146impl Agent {
147    /// Start building a new agent.
148    pub fn builder(name: impl Into<String>) -> AgentBuilder {
149        AgentBuilder::new(name)
150    }
151
152    /// Get the fully qualified name: "name@version".
153    pub fn qualified_name(&self) -> String {
154        format!("{}@{}", self.name, self.version)
155    }
156}
157
158/// Builder for creating agents with a fluent API.
159#[derive(Debug)]
160pub struct AgentBuilder {
161    name: String,
162    version: String,
163    description: String,
164    framework: AgentFramework,
165    mode: AgentMode,
166    model_provider: String,
167    model_name: String,
168    backup_provider: Option<String>,
169    backup_model: Option<String>,
170    system_prompt: Option<String>,
171    max_turns: Option<u32>,
172    skills: Vec<String>,
173    guardrails: Vec<Guardrail>,
174    a2a_endpoint: Option<String>,
175    ingredients: Vec<Ingredient>,
176    tags: Vec<String>,
177    kitchen_id: Option<String>,
178    metadata: Option<serde_json::Value>,
179}
180
181impl AgentBuilder {
182    pub fn new(name: impl Into<String>) -> Self {
183        Self {
184            name: name.into(),
185            version: "0.1.0".into(),
186            description: String::new(),
187            framework: AgentFramework::Custom,
188            mode: AgentMode::default(),
189            model_provider: String::new(),
190            model_name: String::new(),
191            backup_provider: None,
192            backup_model: None,
193            system_prompt: None,
194            max_turns: None,
195            skills: Vec::new(),
196            guardrails: Vec::new(),
197            a2a_endpoint: None,
198            ingredients: Vec::new(),
199            tags: Vec::new(),
200            kitchen_id: None,
201            metadata: None,
202        }
203    }
204
205    pub fn version(mut self, version: impl Into<String>) -> Self {
206        self.version = version.into();
207        self
208    }
209
210    pub fn description(mut self, desc: impl Into<String>) -> Self {
211        self.description = desc.into();
212        self
213    }
214
215    pub fn framework(mut self, framework: AgentFramework) -> Self {
216        self.framework = framework;
217        self
218    }
219
220    pub fn ingredient(mut self, ingredient: Ingredient) -> Self {
221        self.ingredients.push(ingredient);
222        self
223    }
224
225    pub fn tag(mut self, tag: impl Into<String>) -> Self {
226        self.tags.push(tag.into());
227        self
228    }
229
230    pub fn kitchen(mut self, kitchen_id: impl Into<String>) -> Self {
231        self.kitchen_id = Some(kitchen_id.into());
232        self
233    }
234
235    pub fn mode(mut self, mode: AgentMode) -> Self {
236        self.mode = mode;
237        self
238    }
239
240    pub fn model_provider(mut self, provider: impl Into<String>) -> Self {
241        self.model_provider = provider.into();
242        self
243    }
244
245    pub fn model_name(mut self, name: impl Into<String>) -> Self {
246        self.model_name = name.into();
247        self
248    }
249
250    pub fn backup_provider(mut self, provider: impl Into<String>) -> Self {
251        self.backup_provider = Some(provider.into());
252        self
253    }
254
255    pub fn backup_model(mut self, model: impl Into<String>) -> Self {
256        self.backup_model = Some(model.into());
257        self
258    }
259
260    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
261        self.system_prompt = Some(prompt.into());
262        self
263    }
264
265    pub fn max_turns(mut self, turns: u32) -> Self {
266        self.max_turns = Some(turns);
267        self
268    }
269
270    pub fn skill(mut self, s: impl Into<String>) -> Self {
271        self.skills.push(s.into());
272        self
273    }
274
275    pub fn guardrail(mut self, g: Guardrail) -> Self {
276        self.guardrails.push(g);
277        self
278    }
279
280    pub fn a2a_endpoint(mut self, endpoint: impl Into<String>) -> Self {
281        self.a2a_endpoint = Some(endpoint.into());
282        self
283    }
284
285    pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
286        self.metadata = Some(metadata);
287        self
288    }
289
290    pub fn build(self) -> Agent {
291        let now = Utc::now();
292        Agent {
293            id: Uuid::new_v4().to_string(),
294            name: self.name,
295            version: self.version,
296            description: self.description,
297            framework: self.framework,
298            mode: self.mode,
299            model_provider: self.model_provider,
300            model_name: self.model_name,
301            backup_provider: self.backup_provider,
302            backup_model: self.backup_model,
303            system_prompt: self.system_prompt,
304            max_turns: self.max_turns,
305            skills: self.skills,
306            guardrails: self.guardrails,
307            a2a_endpoint: self.a2a_endpoint,
308            ingredients: self.ingredients,
309            tags: self.tags,
310            status: AgentStatus::Draft,
311            kitchen_id: self.kitchen_id,
312            resolved_config: None,
313            created_by: None,
314            created_at: now,
315            updated_at: now,
316            metadata: self.metadata,
317        }
318    }
319}
320
321/// The framework used to build an agent.
322#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
323#[serde(rename_all = "lowercase")]
324pub enum AgentFramework {
325    /// LangGraph / LangChain agents.
326    #[serde(alias = "lang-graph", alias = "langgraph")]
327    Langchain,
328    /// CrewAI agents.
329    #[serde(alias = "crew-ai")]
330    Crewai,
331    /// OpenAI SDK agents.
332    #[serde(alias = "openai-sdk", alias = "open-ai-sdk")]
333    Openai,
334    /// AutoGen agents.
335    #[serde(alias = "auto-gen")]
336    Autogen,
337    /// Managed by AgentOven runtime.
338    Managed,
339    /// Custom / unknown framework.
340    #[default]
341    #[serde(other)]
342    Custom,
343}
344
345/// Deployment status of an agent.
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
347#[serde(rename_all = "lowercase")]
348pub enum AgentStatus {
349    /// Agent is in draft β€” not yet deployed.
350    Draft,
351    /// Agent is being baked (deploying).
352    Baking,
353    /// Agent is deployed and serving.
354    Ready,
355    /// Agent is paused/disabled.
356    Cooled,
357    /// Agent deployment failed.
358    Burnt,
359    /// Agent has been retired.
360    Retired,
361}
362
363impl std::fmt::Display for AgentStatus {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        match self {
366            AgentStatus::Draft => write!(f, "🟑 draft"),
367            AgentStatus::Baking => write!(f, "πŸ”₯ baking"),
368            AgentStatus::Ready => write!(f, "🟒 ready"),
369            AgentStatus::Cooled => write!(f, "⏸️  cooled"),
370            AgentStatus::Burnt => write!(f, "πŸ”΄ burnt"),
371            AgentStatus::Retired => write!(f, "⚫ retired"),
372        }
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::ingredient::Ingredient;
380
381    #[test]
382    fn test_agent_builder() {
383        let agent = Agent::builder("summarizer")
384            .version("1.0.0")
385            .description("Summarizes documents")
386            .framework(AgentFramework::Langchain)
387            .ingredient(Ingredient::model("gpt-4o").provider("azure-openai").build())
388            .tag("nlp")
389            .tag("summarization")
390            .build();
391
392        assert_eq!(agent.name, "summarizer");
393        assert_eq!(agent.version, "1.0.0");
394        assert_eq!(agent.qualified_name(), "summarizer@1.0.0");
395        assert_eq!(agent.status, AgentStatus::Draft);
396        assert_eq!(agent.ingredients.len(), 1);
397        assert_eq!(agent.tags, vec!["nlp", "summarization"]);
398    }
399}