Skip to main content

systemprompt_models/a2a/
agent_card.rs

1use super::security::{OAuth2Flow, OAuth2Flows, SecurityScheme};
2use super::transport::ProtocolBinding;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6pub const ARTIFACT_RENDERING_URI: &str = "https://systemprompt.io/extensions/artifact-rendering/v1";
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "camelCase")]
10pub struct AgentInterface {
11    pub url: String,
12    pub protocol_binding: ProtocolBinding,
13    #[serde(default = "default_protocol_version")]
14    pub protocol_version: String,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub struct AgentProvider {
19    pub organization: String,
20    pub url: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "camelCase")]
25pub struct AgentCapabilities {
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub streaming: Option<bool>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub push_notifications: Option<bool>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub state_transition_history: Option<bool>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub extensions: Option<Vec<AgentExtension>>,
34}
35
36impl Default for AgentCapabilities {
37    fn default() -> Self {
38        Self {
39            streaming: Some(true),
40            push_notifications: Some(true),
41            state_transition_history: Some(true),
42            extensions: None,
43        }
44    }
45}
46
47impl AgentCapabilities {
48    pub const fn normalize(mut self) -> Self {
49        if self.streaming.is_none() {
50            self.streaming = Some(true);
51        }
52        if self.push_notifications.is_none() {
53            self.push_notifications = Some(false);
54        }
55        if self.state_transition_history.is_none() {
56            self.state_transition_history = Some(true);
57        }
58        self
59    }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63pub struct AgentExtension {
64    pub uri: String,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub description: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub required: Option<bool>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub params: Option<serde_json::Value>,
71}
72
73impl AgentExtension {
74    pub fn mcp_tools_extension() -> Self {
75        Self {
76            uri: "systemprompt:mcp-tools".to_string(),
77            description: Some("MCP tool execution capabilities".to_string()),
78            required: Some(false),
79            params: Some(serde_json::json!({
80                "supported_protocols": ["mcp-1.0"]
81            })),
82        }
83    }
84
85    pub fn mcp_tools_extension_with_servers(servers: &[serde_json::Value]) -> Self {
86        Self {
87            uri: "systemprompt:mcp-tools".to_string(),
88            description: Some("MCP tool execution capabilities with server endpoints".to_string()),
89            required: Some(false),
90            params: Some(serde_json::json!({
91                "supported_protocols": ["mcp-1.0"],
92                "servers": servers
93            })),
94        }
95    }
96
97    pub fn opencode_integration_extension() -> Self {
98        Self {
99            uri: "systemprompt:opencode-integration".to_string(),
100            description: Some("OpenCode AI reasoning integration".to_string()),
101            required: Some(false),
102            params: Some(serde_json::json!({
103                "reasoning_model": "claude-3-5-sonnet",
104                "execution_mode": "structured_planning"
105            })),
106        }
107    }
108
109    pub fn artifact_rendering_extension() -> Self {
110        Self {
111            uri: ARTIFACT_RENDERING_URI.to_string(),
112            description: Some(
113                "MCP tool results rendered as typed artifacts with UI hints".to_string(),
114            ),
115            required: Some(false),
116            params: Some(serde_json::json!({
117                "supported_types": ["table", "form", "chart", "tree", "code", "json", "markdown"],
118                "version": "1.0.0"
119            })),
120        }
121    }
122
123    pub fn agent_identity(agent_name: &str) -> Self {
124        Self {
125            uri: "systemprompt:agent-identity".to_string(),
126            description: Some("systemprompt.io platform agent name".to_string()),
127            required: Some(true),
128            params: Some(serde_json::json!({
129                "name": agent_name
130            })),
131        }
132    }
133
134    pub fn system_instructions(system_prompt: &str) -> Self {
135        Self {
136            uri: "systemprompt:system-instructions".to_string(),
137            description: Some("Agent system prompt and behavioral guidelines".to_string()),
138            required: Some(true),
139            params: Some(serde_json::json!({
140                "systemPrompt": system_prompt,
141                "format": "text/plain"
142            })),
143        }
144    }
145
146    pub fn system_instructions_opt(system_prompt: Option<&str>) -> Option<Self> {
147        system_prompt.map(Self::system_instructions)
148    }
149
150    pub fn service_status(
151        status: &str,
152        port: Option<u16>,
153        pid: Option<u32>,
154        default: bool,
155    ) -> Self {
156        let mut params = serde_json::json!({
157            "status": status,
158            "default": default
159        });
160
161        if let Some(p) = port {
162            params["port"] = serde_json::json!(p);
163        }
164        if let Some(p) = pid {
165            params["pid"] = serde_json::json!(p);
166        }
167
168        Self {
169            uri: "systemprompt:service-status".to_string(),
170            description: Some("Runtime service status from orchestrator".to_string()),
171            required: Some(true),
172            params: Some(params),
173        }
174    }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "camelCase")]
179pub struct AgentSkill {
180    // JSON: A2A spec skill identifier in published agent card (may belong to another agent)
181    pub id: String,
182    pub name: String,
183    pub description: String,
184    pub tags: Vec<String>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub examples: Option<Vec<String>>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub input_modes: Option<Vec<String>>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub output_modes: Option<Vec<String>>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
193}
194
195impl AgentSkill {
196    pub const fn from_mcp_server(
197        server_name: String,
198        display_name: String,
199        description: String,
200        tags: Vec<String>,
201    ) -> Self {
202        Self {
203            id: server_name,
204            name: display_name,
205            description,
206            tags,
207            examples: None,
208            input_modes: None,
209            output_modes: None,
210            security: None,
211        }
212    }
213
214    pub fn mcp_server_name(&self) -> &str {
215        &self.id
216    }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
220pub struct AgentCardSignature {
221    pub protected: String,
222    pub signature: String,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub header: Option<serde_json::Value>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
228#[serde(rename_all = "camelCase")]
229pub struct AgentCard {
230    pub name: String,
231    pub description: String,
232    pub supported_interfaces: Vec<AgentInterface>,
233    pub version: String,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub icon_url: Option<String>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub provider: Option<AgentProvider>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub documentation_url: Option<String>,
240    pub capabilities: AgentCapabilities,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub security_schemes: Option<HashMap<String, SecurityScheme>>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
245    pub default_input_modes: Vec<String>,
246    pub default_output_modes: Vec<String>,
247    #[serde(default)]
248    pub skills: Vec<AgentSkill>,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub supports_authenticated_extended_card: Option<bool>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub signatures: Option<Vec<AgentCardSignature>>,
253}
254
255fn default_protocol_version() -> String {
256    "1.0.0".to_string()
257}
258
259impl AgentCard {
260    pub fn builder(
261        name: String,
262        description: String,
263        url: String,
264        version: String,
265    ) -> AgentCardBuilder {
266        AgentCardBuilder::new(name, description, url, version)
267    }
268
269    pub fn url(&self) -> Option<&str> {
270        self.supported_interfaces.first().map(|i| i.url.as_str())
271    }
272
273    pub fn has_mcp_extension(&self) -> bool {
274        self.capabilities
275            .extensions
276            .as_ref()
277            .is_some_and(|exts| exts.iter().any(|ext| ext.uri == "systemprompt:mcp-tools"))
278    }
279
280    pub fn ensure_mcp_extension(&mut self) {
281        if self.has_mcp_extension() {
282            return;
283        }
284
285        self.capabilities
286            .extensions
287            .get_or_insert_with(Vec::new)
288            .push(AgentExtension::mcp_tools_extension());
289    }
290}
291
292#[derive(Debug)]
293pub struct AgentCardBuilder {
294    agent_card: AgentCard,
295}
296
297impl AgentCardBuilder {
298    pub fn new(name: String, description: String, url: String, version: String) -> Self {
299        Self {
300            agent_card: AgentCard {
301                name,
302                description,
303                supported_interfaces: vec![AgentInterface {
304                    url,
305                    protocol_binding: ProtocolBinding::JsonRpc,
306                    protocol_version: "1.0.0".to_string(),
307                }],
308                version,
309                icon_url: None,
310                provider: None,
311                documentation_url: None,
312                capabilities: AgentCapabilities::default(),
313                security_schemes: None,
314                security: None,
315                default_input_modes: vec!["text/plain".to_string()],
316                default_output_modes: vec!["text/plain".to_string()],
317                skills: Vec::new(),
318                supports_authenticated_extended_card: Some(false),
319                signatures: None,
320            },
321        }
322    }
323
324    pub fn with_mcp_skills(
325        mut self,
326        mcp_servers: Vec<(String, String, String, Vec<String>)>,
327    ) -> Self {
328        for (server_name, display_name, description, tags) in mcp_servers {
329            let skill = AgentSkill::from_mcp_server(server_name, display_name, description, tags);
330            self.agent_card.skills.push(skill);
331        }
332
333        let mcp_extension = AgentExtension::mcp_tools_extension();
334        let opencode_extension = AgentExtension::opencode_integration_extension();
335        let artifact_rendering = AgentExtension::artifact_rendering_extension();
336
337        self.agent_card.capabilities.extensions =
338            Some(vec![mcp_extension, opencode_extension, artifact_rendering]);
339
340        self
341    }
342
343    pub const fn with_streaming(mut self) -> Self {
344        self.agent_card.capabilities.streaming = Some(true);
345        self
346    }
347
348    pub const fn with_push_notifications(mut self) -> Self {
349        self.agent_card.capabilities.push_notifications = Some(true);
350        self
351    }
352
353    pub fn with_provider(mut self, organization: String, url: String) -> Self {
354        self.agent_card.provider = Some(AgentProvider { organization, url });
355        self
356    }
357
358    pub fn with_oauth2_security(
359        mut self,
360        authorization_url: String,
361        token_url: String,
362        scopes: HashMap<String, String>,
363    ) -> Self {
364        let oauth2_flows = OAuth2Flows {
365            authorization_code: Some(OAuth2Flow {
366                authorization_url: Some(authorization_url),
367                token_url: Some(token_url),
368                refresh_url: None,
369                scopes,
370            }),
371            implicit: None,
372            password: None,
373            client_credentials: None,
374        };
375
376        let oauth2_scheme = SecurityScheme::OAuth2 {
377            flows: Box::new(oauth2_flows),
378            description: Some("OAuth 2.0 authorization code flow for secure access".to_string()),
379        };
380
381        self.agent_card
382            .security_schemes
383            .get_or_insert_with(HashMap::new)
384            .insert("oauth2".to_string(), oauth2_scheme);
385
386        let mut authentication_requirement = HashMap::new();
387        authentication_requirement.insert(
388            "oauth2".to_string(),
389            vec!["admin".to_string(), "user".to_string()],
390        );
391
392        self.agent_card
393            .security
394            .get_or_insert_with(Vec::new)
395            .push(authentication_requirement);
396
397        self
398    }
399
400    pub fn build(self) -> AgentCard {
401        self.agent_card
402    }
403}