1use crate::compiler::{CompiledConfig, CompiledToolKind};
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeMap;
14
15pub const AGENT_CARD_SCHEMA_VERSION: &str = "cellstate.a2a.agent_card.v1";
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct AgentCard {
24 pub name: String,
26 pub description: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub url: Option<String>,
31 pub version: String,
33 pub capabilities: AgentCapabilities,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub authentication: Option<AgentAuthentication>,
38 #[serde(default)]
40 pub default_input_modes: Vec<String>,
41 #[serde(default)]
43 pub default_output_modes: Vec<String>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub skills: Vec<AgentSkillRef>,
47 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
49 pub metadata: BTreeMap<String, serde_json::Value>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct AgentCapabilities {
56 #[serde(default)]
58 pub streaming: bool,
59 #[serde(default)]
61 pub state_transition_history: bool,
62 #[serde(default)]
64 pub push_notifications: bool,
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub tool_types: Vec<String>,
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
70 pub memory_features: Vec<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct AgentAuthentication {
76 pub schemes: Vec<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AgentSkillRef {
83 pub name: String,
85 pub description: String,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub tags: Vec<String>,
90}
91
92pub fn generate_agent_card(config: &CompiledConfig, service_url: Option<&str>) -> AgentCard {
96 let name = config
97 .pack_meta_project
98 .as_deref()
99 .unwrap_or("CELLSTATE Agent")
100 .to_string();
101
102 let description = config
103 .pack_meta_description
104 .as_deref()
105 .unwrap_or("CELLSTATE agent service with persistent, auditable memory")
106 .to_string();
107
108 let version = config
109 .pack_meta_version
110 .as_deref()
111 .unwrap_or("1.0")
112 .to_string();
113
114 let mut tool_types: Vec<String> = config
116 .tools
117 .iter()
118 .map(|t| match t.kind {
119 CompiledToolKind::Exec => "exec",
120 CompiledToolKind::WasmExec => "wasm-exec",
121 CompiledToolKind::Prompt => "prompt",
122 CompiledToolKind::Bash => "bash",
123 CompiledToolKind::Browser => "browser",
124 CompiledToolKind::Composio => "composio",
125 CompiledToolKind::ComposioGateway => "composio-gateway",
126 })
127 .map(String::from)
128 .collect();
129 tool_types.sort();
130 tool_types.dedup();
131
132 let mut memory_features = vec![
134 "event-sourced".to_string(),
135 "cryptographic-receipts".to_string(),
136 "context-assembly".to_string(),
137 ];
138 if !config.pack_injections.is_empty() {
139 memory_features.push("context-injection".to_string());
140 }
141
142 let capabilities = AgentCapabilities {
143 streaming: true,
144 state_transition_history: true, push_notifications: false,
146 tool_types,
147 memory_features,
148 };
149
150 let builtin_skills = vec![
152 AgentSkillRef {
153 name: "trajectories".to_string(),
154 description:
155 "Create, read, update, and delete trajectories (top-level task containers)"
156 .to_string(),
157 tags: vec!["memory".to_string(), "crud".to_string()],
158 },
159 AgentSkillRef {
160 name: "scopes".to_string(),
161 description: "Manage scopes (context windows within trajectories) with token budgets"
162 .to_string(),
163 tags: vec!["memory".to_string(), "crud".to_string()],
164 },
165 AgentSkillRef {
166 name: "turns".to_string(),
167 description: "Record ephemeral conversation turns within scopes".to_string(),
168 tags: vec!["memory".to_string(), "crud".to_string()],
169 },
170 AgentSkillRef {
171 name: "artifacts".to_string(),
172 description: "Store and retrieve persistent artifacts (code, decisions, outputs)"
173 .to_string(),
174 tags: vec!["memory".to_string(), "crud".to_string()],
175 },
176 AgentSkillRef {
177 name: "notes".to_string(),
178 description:
179 "Manage cross-trajectory knowledge notes (observations, conventions, strategies)"
180 .to_string(),
181 tags: vec!["memory".to_string(), "crud".to_string()],
182 },
183 AgentSkillRef {
184 name: "context-assembly".to_string(),
185 description: "Assemble token-budgeted context from memory for LLM consumption"
186 .to_string(),
187 tags: vec!["memory".to_string(), "retrieval".to_string()],
188 },
189 AgentSkillRef {
190 name: "search".to_string(),
191 description: "Full-text and semantic search across all memory entities".to_string(),
192 tags: vec!["memory".to_string(), "retrieval".to_string()],
193 },
194 AgentSkillRef {
195 name: "coordination".to_string(),
196 description: "Multi-agent coordination: delegations, handoffs, messages, and locks"
197 .to_string(),
198 tags: vec!["multi-agent".to_string()],
199 },
200 ];
201
202 let mut skills: Vec<AgentSkillRef> = config
204 .pack_agents
205 .iter()
206 .filter(|a| a.enabled)
207 .map(|agent| AgentSkillRef {
208 name: agent.name.clone(),
209 description: agent
210 .description
211 .clone()
212 .unwrap_or_else(|| format!("Agent: {}", agent.name)),
213 tags: agent.tags.clone(),
214 })
215 .collect();
216 skills.extend(builtin_skills);
217
218 let mut metadata = BTreeMap::new();
220 metadata.insert(
221 "platform".to_string(),
222 serde_json::Value::String("cellstate".to_string()),
223 );
224 metadata.insert(
225 "schema_version".to_string(),
226 serde_json::Value::String(AGENT_CARD_SCHEMA_VERSION.to_string()),
227 );
228 metadata.insert(
229 "protocols".to_string(),
230 serde_json::json!(["mcp", "ag-ui", "a2a", "a2ui"]),
231 );
232 if let Some(ref homepage) = config.pack_meta_homepage {
233 metadata.insert(
234 "homepage".to_string(),
235 serde_json::Value::String(homepage.clone()),
236 );
237 }
238
239 AgentCard {
240 name,
241 description,
242 url: service_url.map(String::from),
243 version,
244 capabilities,
245 authentication: Some(AgentAuthentication {
246 schemes: vec!["bearer".to_string(), "apiKey".to_string()],
247 }),
248 default_input_modes: vec!["text/plain".to_string(), "application/json".to_string()],
249 default_output_modes: vec!["text/plain".to_string(), "application/json".to_string()],
250 skills,
251 metadata,
252 }
253}
254
255pub fn to_agent_card_json(card: &AgentCard) -> Result<String, serde_json::Error> {
257 serde_json::to_string_pretty(card)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_generate_agent_card_defaults() {
266 let config = CompiledConfig::default();
267 let card = generate_agent_card(&config, Some("https://api.example.com"));
268
269 assert_eq!(card.name, "CELLSTATE Agent");
270 assert!(card.capabilities.streaming);
271 assert!(card.capabilities.state_transition_history);
272 assert_eq!(card.url, Some("https://api.example.com".to_string()));
273 assert_eq!(card.version, "1.0");
274 assert!(card.default_input_modes.contains(&"text/plain".to_string()));
275 }
276
277 #[test]
278 fn test_generate_agent_card_with_project() {
279 let config = CompiledConfig {
280 pack_meta_project: Some("My Agent Service".to_string()),
281 pack_meta_description: Some("An agent that does things".to_string()),
282 ..CompiledConfig::default()
283 };
284
285 let card = generate_agent_card(&config, None);
286 assert_eq!(card.name, "My Agent Service");
287 assert_eq!(card.description, "An agent that does things");
288 assert!(card.url.is_none());
289 }
290
291 #[test]
292 fn test_agent_card_json_serialization() {
293 let config = CompiledConfig::default();
294 let card = generate_agent_card(&config, None);
295 let json = to_agent_card_json(&card).expect("should serialize");
296 assert!(json.contains("\"name\""));
297 assert!(json.contains("\"capabilities\""));
298 assert!(json.contains("\"streaming\""));
299 }
300
301 #[test]
302 fn test_agent_card_includes_builtin_skills() {
303 let config = CompiledConfig::default();
304 let card = generate_agent_card(&config, None);
305 let skill_names: Vec<&str> = card.skills.iter().map(|s| s.name.as_str()).collect();
306 assert!(skill_names.contains(&"trajectories"));
307 assert!(skill_names.contains(&"context-assembly"));
308 assert!(skill_names.contains(&"search"));
309 assert!(skill_names.contains(&"coordination"));
310 assert!(card.skills.len() >= 8); }
312
313 #[test]
314 fn test_agent_card_metadata_includes_platform() {
315 let config = CompiledConfig::default();
316 let card = generate_agent_card(&config, None);
317 assert_eq!(
318 card.metadata.get("platform"),
319 Some(&serde_json::Value::String("cellstate".to_string()))
320 );
321 assert_eq!(
322 card.metadata.get("schema_version"),
323 Some(&serde_json::Value::String(
324 AGENT_CARD_SCHEMA_VERSION.to_string()
325 ))
326 );
327 }
328}