Skip to main content

lean_ctx/core/a2a/
agent_card.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct AgentCard {
5    pub name: String,
6    pub description: String,
7    pub url: Option<String>,
8    pub version: String,
9    pub protocol_version: String,
10    pub capabilities: AgentCapabilities,
11    pub skills: Vec<AgentSkill>,
12    pub authentication: AuthenticationInfo,
13    pub default_input_modes: Vec<String>,
14    pub default_output_modes: Vec<String>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AgentCapabilities {
19    pub streaming: bool,
20    pub push_notifications: bool,
21    pub state_transition_history: bool,
22    pub tools: Vec<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AgentSkill {
27    pub id: String,
28    pub name: String,
29    pub description: String,
30    pub tags: Vec<String>,
31    pub examples: Vec<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AuthenticationInfo {
36    pub schemes: Vec<String>,
37}
38
39pub fn generate_agent_card(tools: &[String], version: &str, port: Option<u16>) -> AgentCard {
40    let url = port.map(|p| format!("http://127.0.0.1:{p}"));
41
42    AgentCard {
43        name: "lean-ctx".to_string(),
44        description: "Context Engineering Infrastructure Layer — intelligent compression, \
45                       code knowledge graph, structured agent memory, and multi-agent coordination \
46                       via MCP"
47            .to_string(),
48        url,
49        version: version.to_string(),
50        protocol_version: "0.1.0".to_string(),
51        capabilities: AgentCapabilities {
52            streaming: false,
53            push_notifications: false,
54            state_transition_history: true,
55            tools: tools.to_vec(),
56        },
57        skills: build_skills(),
58        authentication: AuthenticationInfo {
59            schemes: vec!["none".to_string()],
60        },
61        default_input_modes: vec!["text/plain".to_string(), "application/json".to_string()],
62        default_output_modes: vec!["text/plain".to_string(), "application/json".to_string()],
63    }
64}
65
66fn build_skills() -> Vec<AgentSkill> {
67    vec![
68        AgentSkill {
69            id: "context-compression".to_string(),
70            name: "Context Compression".to_string(),
71            description: "Intelligent file and shell output compression with 8 modes, \
72                           neural token pipeline, and LITM-aware positioning"
73                .to_string(),
74            tags: vec![
75                "compression".to_string(),
76                "tokens".to_string(),
77                "optimization".to_string(),
78            ],
79            examples: vec![
80                "Read a file with automatic mode selection".to_string(),
81                "Compress shell output preserving key information".to_string(),
82            ],
83        },
84        AgentSkill {
85            id: "code-knowledge-graph".to_string(),
86            name: "Code Knowledge Graph".to_string(),
87            description: "Property graph with tree-sitter deep queries, import resolution, \
88                           impact analysis, and architecture detection"
89                .to_string(),
90            tags: vec![
91                "graph".to_string(),
92                "analysis".to_string(),
93                "architecture".to_string(),
94            ],
95            examples: vec![
96                "What breaks if I change function X?".to_string(),
97                "Show me the architecture clusters".to_string(),
98            ],
99        },
100        AgentSkill {
101            id: "agent-memory".to_string(),
102            name: "Structured Agent Memory".to_string(),
103            description: "Episodic, procedural, and semantic memory with cross-session \
104                           persistence, embedding-based recall, and lifecycle management"
105                .to_string(),
106            tags: vec![
107                "memory".to_string(),
108                "knowledge".to_string(),
109                "persistence".to_string(),
110            ],
111            examples: vec![
112                "Remember a project pattern for future sessions".to_string(),
113                "Recall what happened last time I deployed".to_string(),
114            ],
115        },
116        AgentSkill {
117            id: "multi-agent-coordination".to_string(),
118            name: "Multi-Agent Coordination".to_string(),
119            description: "Agent registry, task delegation, context sharing with privacy \
120                           controls, and cost attribution"
121                .to_string(),
122            tags: vec![
123                "agents".to_string(),
124                "coordination".to_string(),
125                "delegation".to_string(),
126            ],
127            examples: vec![
128                "Delegate a sub-task to another agent".to_string(),
129                "Share context with team agents".to_string(),
130            ],
131        },
132        AgentSkill {
133            id: "semantic-search".to_string(),
134            name: "Semantic Code Search".to_string(),
135            description: "Hybrid BM25 + dense embedding search with tree-sitter AST-aware \
136                           chunking and reciprocal rank fusion"
137                .to_string(),
138            tags: vec![
139                "search".to_string(),
140                "embeddings".to_string(),
141                "code".to_string(),
142            ],
143            examples: vec![
144                "Find code related to authentication handling".to_string(),
145                "Search for error recovery patterns".to_string(),
146            ],
147        },
148    ]
149}
150
151/// Build an agent card from the current runtime state for HTTP endpoints.
152pub fn build_agent_card(project_root: &str) -> serde_json::Value {
153    let version = env!("CARGO_PKG_VERSION");
154    let card = generate_agent_card(&default_tool_list(), version, None);
155
156    serde_json::json!({
157        "name": card.name,
158        "description": card.description,
159        "url": card.url,
160        "version": card.version,
161        "protocolVersion": card.protocol_version,
162        "provider": {
163            "organization": "LeanCTX",
164            "url": "https://leanctx.com"
165        },
166        "documentationUrl": "https://leanctx.com/docs",
167        "capabilities": {
168            "streaming": card.capabilities.streaming,
169            "pushNotifications": card.capabilities.push_notifications,
170            "stateTransitionHistory": card.capabilities.state_transition_history,
171            "tools": card.capabilities.tools,
172        },
173        "skills": card.skills.iter().map(|s| serde_json::json!({
174            "id": s.id,
175            "name": s.name,
176            "description": s.description,
177            "tags": s.tags,
178            "examples": s.examples,
179            "inputModes": ["text/plain", "application/json"],
180            "outputModes": ["text/plain", "application/json"],
181        })).collect::<Vec<_>>(),
182        "authentication": {
183            "schemes": card.authentication.schemes,
184        },
185        "defaultInputModes": card.default_input_modes,
186        "defaultOutputModes": card.default_output_modes,
187        "supportsAuthenticatedExtendedCard": false,
188        "projectRoot": project_root,
189    })
190}
191
192fn default_tool_list() -> Vec<String> {
193    vec![
194        "ctx_read",
195        "ctx_shell",
196        "ctx_search",
197        "ctx_tree",
198        "ctx_session",
199        "ctx_knowledge",
200        "ctx_agent",
201        "ctx_handoff",
202        "ctx_pack",
203    ]
204    .into_iter()
205    .map(String::from)
206    .collect()
207}
208
209pub fn save_agent_card(card: &AgentCard) -> std::io::Result<()> {
210    let dir = crate::core::data_dir::lean_ctx_data_dir()
211        .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, format!("data dir: {e}")))?;
212    std::fs::create_dir_all(&dir)?;
213
214    let well_known = dir.join(".well-known");
215    std::fs::create_dir_all(&well_known)?;
216
217    let json = serde_json::to_string_pretty(card).map_err(std::io::Error::other)?;
218    std::fs::write(well_known.join("agent.json"), json)?;
219    Ok(())
220}
221
222pub fn load_agent_card() -> Option<AgentCard> {
223    let path = crate::core::data_dir::lean_ctx_data_dir()
224        .ok()?
225        .join(".well-known")
226        .join("agent.json");
227    let content = std::fs::read_to_string(path).ok()?;
228    serde_json::from_str(&content).ok()
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn generates_valid_card() {
237        let tools = vec!["ctx_read".to_string(), "ctx_shell".to_string()];
238        let card = generate_agent_card(&tools, "3.0.0", Some(3344));
239
240        assert_eq!(card.name, "lean-ctx");
241        assert_eq!(card.capabilities.tools.len(), 2);
242        assert_eq!(card.skills.len(), 5);
243        assert_eq!(card.url, Some("http://127.0.0.1:3344".to_string()));
244    }
245
246    #[test]
247    fn card_serializes_to_valid_json() {
248        let card = generate_agent_card(&["ctx_read".to_string()], "3.0.0", None);
249        let json = serde_json::to_string_pretty(&card).unwrap();
250        assert!(json.contains("lean-ctx"));
251        assert!(json.contains("context-compression"));
252
253        let parsed: AgentCard = serde_json::from_str(&json).unwrap();
254        assert_eq!(parsed.name, card.name);
255    }
256}