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
151pub 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}