cellstate_pipeline/pack/
a2a.rs

1//! A2A (Agent-to-Agent) Agent Card generation.
2//!
3//! Generates A2A Agent Cards from compiled Pack configuration for
4//! agent discovery in the Google A2A protocol ecosystem (150+ orgs).
5//!
6//! Agent Cards are served at `/.well-known/agent.json` and describe
7//! an agent's capabilities, authentication, and supported I/O modes.
8//!
9//! Re-export path: cellstate_pipeline::pack::a2a::*
10
11use crate::compiler::{CompiledConfig, CompiledToolKind};
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeMap;
14
15/// CELLSTATE's version stamp for emitted Agent Card payloads.
16pub const AGENT_CARD_SCHEMA_VERSION: &str = "cellstate.a2a.agent_card.v1";
17
18/// A2A Agent Card — the discovery document for an agent service.
19///
20/// Served at `/.well-known/agent.json` per the A2A protocol spec.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct AgentCard {
24    /// Agent service name.
25    pub name: String,
26    /// Human-readable description of the agent's capabilities.
27    pub description: String,
28    /// Service URL endpoint.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub url: Option<String>,
31    /// Agent version.
32    pub version: String,
33    /// Agent capabilities.
34    pub capabilities: AgentCapabilities,
35    /// Authentication configuration.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub authentication: Option<AgentAuthentication>,
38    /// Default input content types accepted.
39    #[serde(default)]
40    pub default_input_modes: Vec<String>,
41    /// Default output content types produced.
42    #[serde(default)]
43    pub default_output_modes: Vec<String>,
44    /// Individual skills/agents available in this service.
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub skills: Vec<AgentSkillRef>,
47    /// Additional metadata.
48    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
49    pub metadata: BTreeMap<String, serde_json::Value>,
50}
51
52/// Agent capabilities block.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct AgentCapabilities {
56    /// Whether the agent supports streaming responses.
57    #[serde(default)]
58    pub streaming: bool,
59    /// Whether the agent maintains state transition history.
60    #[serde(default)]
61    pub state_transition_history: bool,
62    /// Whether the agent supports push notifications.
63    #[serde(default)]
64    pub push_notifications: bool,
65    /// Tool types available.
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub tool_types: Vec<String>,
68    /// Memory features available.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub memory_features: Vec<String>,
71}
72
73/// Authentication schemes supported.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct AgentAuthentication {
76    /// Supported auth schemes (e.g., "bearer", "apiKey").
77    pub schemes: Vec<String>,
78}
79
80/// Reference to an individual skill within the agent service.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AgentSkillRef {
83    /// Skill name.
84    pub name: String,
85    /// Skill description.
86    pub description: String,
87    /// Tags for categorization.
88    #[serde(default, skip_serializing_if = "Vec::is_empty")]
89    pub tags: Vec<String>,
90}
91
92/// Generate an A2A Agent Card from a compiled CELLSTATE config.
93///
94/// Maps CELLSTATE pack agents, tools, and memory features to A2A discovery format.
95pub 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    // Derive tool types from config
115    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    // Derive memory features
133    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, // CELLSTATE's Event DAG
145        push_notifications: false,
146        tool_types,
147        memory_features,
148    };
149
150    // Built-in CELLSTATE API skills — these are always available regardless of pack config.
151    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    // Map pack agents to skill refs
203    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    // Add CELLSTATE-specific metadata
219    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
255/// Serialize an Agent Card to canonical JSON.
256pub 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); // at least the 8 built-in skills
311    }
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}