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 index = self.connections.iter().position(|c| c == connection)?;
56        Some(self.connections.remove(index))
57    }
58
59    /// Serializes this preset to a pretty-printed JSON string.
60    pub fn to_json(&self) -> Result<String, AgentError> {
61        let json = serde_json::to_string_pretty(self)
62            .map_err(|e| AgentError::SerializationError(e.to_string()))?;
63        Ok(json)
64    }
65
66    /// Deserializes a preset from a JSON string.
67    pub fn from_json(json_str: &str) -> Result<Self, AgentError> {
68        let preset: PresetSpec = serde_json::from_str(json_str)
69            .map_err(|e| AgentError::SerializationError(e.to_string()))?;
70        Ok(preset)
71    }
72}
73
74/// The runtime specification of an agent instance.
75///
76/// Contains all the information needed to instantiate and configure an agent,
77/// including its ID, definition reference, ports, and configuration values.
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct AgentSpec {
80    /// Unique identifier for this agent instance.
81    #[serde(skip_serializing_if = "String::is_empty", default)]
82    pub id: String,
83
84    /// Name of the AgentDefinition this agent is based on.
85    #[serde(skip_serializing_if = "String::is_empty", default)]
86    pub def_name: String,
87
88    /// List of input port names (overrides definition if set).
89    #[serde(skip_serializing_if = "Option::is_none", default)]
90    pub inputs: Option<Vec<String>>,
91
92    /// List of output port names (overrides definition if set).
93    #[serde(skip_serializing_if = "Option::is_none", default)]
94    pub outputs: Option<Vec<String>>,
95
96    /// Configuration values for this agent instance.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub configs: Option<AgentConfigs>,
99
100    /// Configuration specifications (metadata about configs).
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub config_specs: Option<AgentConfigSpecs>,
103
104    /// Whether this agent is disabled (will not be started).
105    #[serde(default, skip_serializing_if = "<&bool>::not")]
106    pub disabled: bool,
107
108    /// Extension fields for custom data.
109    #[serde(flatten)]
110    pub extensions: FnvIndexMap<String, serde_json::Value>,
111}
112
113impl AgentSpec {
114    /// Updates this agent spec from a JSON value.
115    ///
116    /// Known fields (id, def_name, inputs, outputs, configs, disabled) are parsed
117    /// and applied. Unknown fields are stored in the extensions map.
118    pub fn update(&mut self, value: &Value) -> Result<(), AgentError> {
119        let update_map = value
120            .as_object()
121            .ok_or_else(|| AgentError::SerializationError("Expected JSON object".to_string()))?;
122
123        for (k, v) in update_map {
124            match k.as_str() {
125                "id" => {
126                    if let Some(id_str) = v.as_str() {
127                        self.id = id_str.to_string();
128                    }
129                }
130                "def_name" => {
131                    if let Some(def_name_str) = v.as_str() {
132                        self.def_name = def_name_str.to_string();
133                    }
134                }
135                "inputs" => {
136                    if let Some(inputs_array) = v.as_array() {
137                        self.inputs = Some(
138                            inputs_array
139                                .iter()
140                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
141                                .collect(),
142                        );
143                    }
144                }
145                "outputs" => {
146                    if let Some(outputs_array) = v.as_array() {
147                        self.outputs = Some(
148                            outputs_array
149                                .iter()
150                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
151                                .collect(),
152                        );
153                    }
154                }
155                "configs" => {
156                    let configs: AgentConfigs = serde_json::from_value(v.clone())
157                        .map_err(|e| AgentError::SerializationError(e.to_string()))?;
158                    self.configs = Some(configs);
159                }
160                "disabled" => {
161                    if let Some(disabled_bool) = v.as_bool() {
162                        self.disabled = disabled_bool;
163                    }
164                }
165                _ => {
166                    // Update extensions: null removes the key
167                    if v.is_null() {
168                        self.extensions.shift_remove(k);
169                    } else {
170                        self.extensions.insert(k.clone(), v.clone());
171                    }
172                }
173            }
174        }
175
176        Ok(())
177    }
178}
179
180/// A connection between two agent ports.
181///
182/// Defines a directed edge in the agent graph, connecting an output port
183/// of a source agent to an input port of a target agent.
184#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
185pub struct ConnectionSpec {
186    /// ID of the source agent.
187    pub source: String,
188
189    /// Output port name on the source agent.
190    pub source_handle: String,
191
192    /// ID of the target agent.
193    pub target: String,
194
195    /// Input port name on the target agent.
196    pub target_handle: String,
197}