Skip to main content

modular_agent_core/
spec.rs

1use std::ops::Not;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::FnvIndexMap;
7use crate::config::AgentConfigs;
8use crate::definition::AgentConfigSpecs;
9use crate::error::AgentError;
10
11/// A map of preset names to their specifications.
12pub type PresetSpecs = FnvIndexMap<String, PresetSpec>;
13
14/// The serializable specification of a preset (workflow).
15///
16/// A preset defines a complete workflow configuration including all agents
17/// and their connections. This struct is designed for JSON serialization
18/// and can be loaded from or saved to preset files.
19#[derive(Clone, Debug, Default, Deserialize, Serialize)]
20pub struct PresetSpec {
21    /// List of agent specifications in this preset.
22    pub agents: Vec<AgentSpec>,
23
24    /// List of connections between agents.
25    pub connections: Vec<ConnectionSpec>,
26
27    /// Extension fields for custom data.
28    ///
29    /// Any JSON fields not matching defined fields are captured here.
30    #[serde(flatten)]
31    pub extensions: FnvIndexMap<String, Value>,
32}
33
34impl PresetSpec {
35    /// Adds an agent to this preset.
36    pub fn add_agent(&mut self, agent: AgentSpec) {
37        self.agents.push(agent);
38    }
39
40    /// Removes an agent from this preset by its ID.
41    pub fn remove_agent(&mut self, agent_id: &str) {
42        self.agents.retain(|agent| agent.id != agent_id);
43    }
44
45    /// Adds a connection to this preset.
46    pub fn add_connection(&mut self, connection: ConnectionSpec) {
47        self.connections.push(connection);
48    }
49
50    /// Removes a connection from this preset.
51    ///
52    /// Returns `Some(ConnectionSpec)` if the connection was found and removed,
53    /// or `None` if it was not found.
54    pub fn remove_connection(&mut self, connection: &ConnectionSpec) -> Option<ConnectionSpec> {
55        let Some(index) = self.connections.iter().position(|c| c == connection) else {
56            return None;
57        };
58        Some(self.connections.remove(index))
59    }
60
61    /// Serializes this preset to a pretty-printed JSON string.
62    pub fn to_json(&self) -> Result<String, AgentError> {
63        let json = serde_json::to_string_pretty(self)
64            .map_err(|e| AgentError::SerializationError(e.to_string()))?;
65        Ok(json)
66    }
67
68    /// Deserializes a preset from a JSON string.
69    pub fn from_json(json_str: &str) -> Result<Self, AgentError> {
70        let preset: PresetSpec = serde_json::from_str(json_str)
71            .map_err(|e| AgentError::SerializationError(e.to_string()))?;
72        Ok(preset)
73    }
74}
75
76/// The runtime specification of an agent instance.
77///
78/// Contains all the information needed to instantiate and configure an agent,
79/// including its ID, definition reference, ports, and configuration values.
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct AgentSpec {
82    /// Unique identifier for this agent instance.
83    #[serde(skip_serializing_if = "String::is_empty", default)]
84    pub id: String,
85
86    /// Name of the AgentDefinition this agent is based on.
87    #[serde(skip_serializing_if = "String::is_empty", default)]
88    pub def_name: String,
89
90    /// List of input port names (overrides definition if set).
91    #[serde(skip_serializing_if = "Option::is_none", default)]
92    pub inputs: Option<Vec<String>>,
93
94    /// List of output port names (overrides definition if set).
95    #[serde(skip_serializing_if = "Option::is_none", default)]
96    pub outputs: Option<Vec<String>>,
97
98    /// Configuration values for this agent instance.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub configs: Option<AgentConfigs>,
101
102    /// Configuration specifications (metadata about configs).
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub config_specs: Option<AgentConfigSpecs>,
105
106    /// Whether this agent is disabled (will not be started).
107    #[serde(default, skip_serializing_if = "<&bool>::not")]
108    pub disabled: bool,
109
110    /// Extension fields for custom data.
111    #[serde(flatten)]
112    pub extensions: FnvIndexMap<String, serde_json::Value>,
113}
114
115impl AgentSpec {
116    /// Updates this agent spec from a JSON value.
117    ///
118    /// Known fields (id, def_name, inputs, outputs, configs, disabled) are parsed
119    /// and applied. Unknown fields are stored in the extensions map.
120    pub fn update(&mut self, value: &Value) -> Result<(), AgentError> {
121        let update_map = value
122            .as_object()
123            .ok_or_else(|| AgentError::SerializationError("Expected JSON object".to_string()))?;
124
125        for (k, v) in update_map {
126            match k.as_str() {
127                "id" => {
128                    if let Some(id_str) = v.as_str() {
129                        self.id = id_str.to_string();
130                    }
131                }
132                "def_name" => {
133                    if let Some(def_name_str) = v.as_str() {
134                        self.def_name = def_name_str.to_string();
135                    }
136                }
137                "inputs" => {
138                    if let Some(inputs_array) = v.as_array() {
139                        self.inputs = Some(
140                            inputs_array
141                                .iter()
142                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
143                                .collect(),
144                        );
145                    }
146                }
147                "outputs" => {
148                    if let Some(outputs_array) = v.as_array() {
149                        self.outputs = Some(
150                            outputs_array
151                                .iter()
152                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
153                                .collect(),
154                        );
155                    }
156                }
157                "configs" => {
158                    let configs: AgentConfigs = serde_json::from_value(v.clone())
159                        .map_err(|e| AgentError::SerializationError(e.to_string()))?;
160                    self.configs = Some(configs);
161                }
162                "disabled" => {
163                    if let Some(disabled_bool) = v.as_bool() {
164                        self.disabled = disabled_bool;
165                    }
166                }
167                _ => {
168                    // Update extensions
169                    self.extensions.insert(k.clone(), v.clone());
170                }
171            }
172        }
173
174        Ok(())
175    }
176}
177
178/// A connection between two agent ports.
179///
180/// Defines a directed edge in the agent graph, connecting an output port
181/// of a source agent to an input port of a target agent.
182#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
183pub struct ConnectionSpec {
184    /// ID of the source agent.
185    pub source: String,
186
187    /// Output port name on the source agent.
188    pub source_handle: String,
189
190    /// ID of the target agent.
191    pub target: String,
192
193    /// Input port name on the target agent.
194    pub target_handle: String,
195}