Skip to main content

lc_a2a/protocol/
card.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::model::{default_input_modes, default_output_modes, default_protocol_version};
5
6/// A2A v1.0.1 protocol version (the first stable line; Agent Cards restructured
7/// to `supportedInterfaces[]` — incompatible with v0.3's single-declaration shape).
8pub const A2A_VERSION_V101: &str = "1.0.1";
9
10/// Transport binding of one agent interface (v1.0.1: three bindings,
11/// cumulative — a card may advertise several).
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "lowercase")]
14pub enum A2ATransport {
15    /// JSON-RPC 2.0 over HTTP.
16    JsonRpc,
17    /// Plain HTTP+JSON.
18    HttpJson,
19    /// gRPC (see `a2a.proto`; the规范源 for all bindings).
20    Grpc,
21}
22
23/// One interface of an agent (v1.0.1 `supportedInterfaces[]` entry).
24///
25/// v1.0.1 breaks with v0.3: instead of a single protocol declaration on the
26/// card, each interface carries its own `protocolVersion`, transport binding,
27/// URL, and (enterprise multi-tenancy) optional tenant.
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub struct AgentInterface {
30    /// Protocol version spoken on THIS interface (e.g. "1.0.1").
31    #[serde(rename = "protocolVersion")]
32    pub protocol_version: String,
33    /// Transport binding of this interface.
34    pub transport: A2ATransport,
35    /// Endpoint URL for this interface.
36    pub url: String,
37    /// Enterprise multi-tenancy: tenant this interface serves (optional).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub tenant: Option<String>,
40}
41
42impl AgentInterface {
43    /// Creates an interface entry.
44    pub fn new(
45        protocol_version: impl Into<String>,
46        transport: A2ATransport,
47        url: impl Into<String>,
48    ) -> Self {
49        Self {
50            protocol_version: protocol_version.into(),
51            transport,
52            url: url.into(),
53            tenant: None,
54        }
55    }
56
57    /// Sets the tenant (enterprise multi-tenancy).
58    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
59        self.tenant = Some(tenant.into());
60        self
61    }
62}
63
64/// A skill that an agent can perform.
65///
66/// Aligned with the structured skill objects required by the A2A v0.3
67/// Agent Card specification (a flat string list is not sufficient).
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AgentSkill {
70    /// Stable identifier for the skill.
71    pub id: String,
72    /// Human-readable skill name.
73    pub name: String,
74    /// Description of what the skill does.
75    pub description: String,
76}
77
78impl AgentSkill {
79    /// Create a new skill.
80    pub fn new(
81        id: impl Into<String>,
82        name: impl Into<String>,
83        description: impl Into<String>,
84    ) -> Self {
85        Self {
86            id: id.into(),
87            name: name.into(),
88            description: description.into(),
89        }
90    }
91}
92
93/// Agent metadata card, served at `/.well-known/agent-card.json`.
94///
95/// Describes an agent's identity, endpoint, and capabilities so that
96/// other agents can discover and interact with it. Aligned with the
97/// A2A v0.3 Agent Card specification.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct AgentCard {
100    /// Human-readable agent name.
101    pub name: String,
102    /// Description of what the agent does.
103    pub description: String,
104    /// Base URL where the agent accepts A2A requests.
105    pub url: String,
106    /// Structured list of skills the agent can perform.
107    #[serde(default)]
108    pub skills: Vec<AgentSkill>,
109    /// A2A protocol version supported by this agent.
110    #[serde(default = "default_protocol_version", rename = "protocolVersion")]
111    pub protocol_version: String,
112    /// Security schemes the agent supports (e.g. `{"bearerAuth": {...}}`).
113    #[serde(skip_serializing_if = "Option::is_none", rename = "securitySchemes")]
114    pub security_schemes: Option<Value>,
115    /// Interfaces the agent exposes (e.g. `{"sse": true}`).
116    ///
117    /// Deprecated by v1.0.1's `supported_interfaces` (typed); kept for
118    /// v0.3-era readers. New cards should populate `supported_interfaces`.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub interfaces: Option<Value>,
121    /// v1.0.1 typed interface list: each entry carries its own
122    /// `protocolVersion`, transport binding, URL and optional tenant.
123    ///
124    /// A v1.0.1-compliant card MUST have at least one interface here; the
125    /// legacy top-level `protocol_version`/`url` remain for v0.3 readers.
126    #[serde(
127        default,
128        skip_serializing_if = "Vec::is_empty",
129        rename = "supportedInterfaces"
130    )]
131    pub supported_interfaces: Vec<AgentInterface>,
132    /// Provider/organization name (optional).
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub provider: Option<String>,
135    /// Documentation URL (optional).
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub documentation_url: Option<String>,
138    /// Authentication schemes supported (optional).
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub authentication: Option<Vec<String>>,
141    /// Default input modes (e.g. ["text", "image"]).
142    #[serde(default = "default_input_modes")]
143    pub default_input_modes: Vec<String>,
144    /// Default output modes (e.g. ["text"]).
145    #[serde(default = "default_output_modes")]
146    pub default_output_modes: Vec<String>,
147    /// Digital signature over the canonical card content (P1-3 / P2-5).
148    ///
149    /// When present, clients SHOULD verify it against the agent's public key
150    /// before trusting the card (see `lc_a2a::security`).
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub signature: Option<String>,
153    /// Data classification this agent deals with (e.g. "public", "internal",
154    /// "confidential"). Used for data-boundary / federation policy (P2-7/P2-8).
155    #[serde(skip_serializing_if = "Option::is_none", rename = "dataClass")]
156    pub data_class: Option<String>,
157    /// Jurisdiction(s) this agent operates under (e.g. "US", "EU"). Used for
158    /// compliance-aware routing in federations (P2-8).
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub jurisdiction: Option<String>,
161    /// Optional protocol capabilities the agent can negotiate (P2-8).
162    ///
163    /// Backward-compatible extension points, e.g. `"tasks/runWorkflow"`,
164    /// `"streaming-sse"`, `"input-required-resume"`. Unknown entries are
165    /// ignored by clients that do not understand them.
166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
167    pub capabilities: Vec<String>,
168}
169
170impl AgentCard {
171    /// Create a new agent card.
172    pub fn new(
173        name: impl Into<String>,
174        description: impl Into<String>,
175        url: impl Into<String>,
176    ) -> Self {
177        Self {
178            name: name.into(),
179            description: description.into(),
180            url: url.into(),
181            skills: Vec::new(),
182            protocol_version: default_protocol_version(),
183            security_schemes: None,
184            interfaces: None,
185            supported_interfaces: Vec::new(),
186            provider: None,
187            documentation_url: None,
188            authentication: None,
189            default_input_modes: default_input_modes(),
190            default_output_modes: default_output_modes(),
191            signature: None,
192            data_class: None,
193            jurisdiction: None,
194            capabilities: Vec::new(),
195        }
196    }
197
198    /// Add a skill.
199    pub fn with_skill(mut self, skill: AgentSkill) -> Self {
200        self.skills.push(skill);
201        self
202    }
203
204    /// Set the A2A protocol version this agent supports.
205    pub fn with_protocol_version(mut self, version: impl Into<String>) -> Self {
206        self.protocol_version = version.into();
207        self
208    }
209
210    /// Set the security schemes advertised on the card.
211    pub fn with_security_schemes(mut self, schemes: Value) -> Self {
212        self.security_schemes = Some(schemes);
213        self
214    }
215
216    /// Set the interfaces advertised on the card.
217    pub fn with_interfaces(mut self, interfaces: Value) -> Self {
218        self.interfaces = Some(interfaces);
219        self
220    }
221
222    /// Advertises a typed v1.0.1 interface.
223    pub fn with_supported_interface(mut self, interface: AgentInterface) -> Self {
224        self.supported_interfaces.push(interface);
225        self
226    }
227
228    /// v1.0.1 negotiation: picks the protocol version of the first supported
229    /// interface whose transport matches `transport` and whose
230    /// `protocol_version` is in `client_versions` (order = card priority).
231    /// Errors when nothing matches.
232    pub fn negotiate(
233        &self,
234        transport: A2ATransport,
235        client_versions: &[&str],
236    ) -> Result<AgentInterface, String> {
237        self.supported_interfaces
238            .iter()
239            .find(|i| {
240                i.transport == transport && client_versions.contains(&i.protocol_version.as_str())
241            })
242            .cloned()
243            .ok_or_else(|| {
244                format!(
245                    "no mutually supported interface: transport={transport:?} client_versions={client_versions:?}"
246                )
247            })
248    }
249
250    /// Whether the card is v1.0.1-shaped (has at least one typed interface).
251    pub fn is_v101(&self) -> bool {
252        !self.supported_interfaces.is_empty()
253    }
254
255    /// Set the provider/organization name.
256    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
257        self.provider = Some(provider.into());
258        self
259    }
260
261    /// Set the documentation URL.
262    pub fn with_documentation_url(mut self, url: impl Into<String>) -> Self {
263        self.documentation_url = Some(url.into());
264        self
265    }
266
267    /// Set the authentication schemes.
268    pub fn with_authentication(mut self, schemes: Vec<String>) -> Self {
269        self.authentication = Some(schemes);
270        self
271    }
272
273    /// Set the digital signature over the card content (P1-3).
274    pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
275        self.signature = Some(signature.into());
276        self
277    }
278
279    /// Set the data classification of this agent (P2-8).
280    pub fn with_data_class(mut self, class: impl Into<String>) -> Self {
281        self.data_class = Some(class.into());
282        self
283    }
284
285    /// Set the jurisdiction(s) this agent operates under (P2-8).
286    pub fn with_jurisdiction(mut self, jurisdiction: impl Into<String>) -> Self {
287        self.jurisdiction = Some(jurisdiction.into());
288        self
289    }
290
291    /// Advertise an optional protocol capability (P2-8).
292    pub fn with_capability(mut self, capability: impl Into<String>) -> Self {
293        self.capabilities.push(capability.into());
294        self
295    }
296}