Skip to main content

rullama_a2a/
convert.rs

1//! Conversions between hand-written serde types and proto-generated types.
2//!
3//! These are gated behind the `grpc` feature and enable the gRPC service layer
4//! to use the same `A2aHandler` trait as JSON-RPC and REST.
5
6#[cfg(feature = "grpc")]
7mod grpc_convert {
8    use std::collections::HashMap;
9
10    use crate::agent_card::*;
11    use crate::proto::lf_a2a_v1 as pb;
12    use crate::push_notification::{AuthenticationInfo, TaskPushNotificationConfig};
13    use crate::task::{Task, TaskState, TaskStatus};
14    use crate::types::{Artifact, Message, Part, Role};
15
16    // ===================================================================
17    // Helpers: HashMap<String, serde_json::Value> <-> prost_types::Struct
18    // ===================================================================
19
20    pub(crate) fn hashmap_to_struct(m: HashMap<String, serde_json::Value>) -> prost_types::Struct {
21        prost_types::Struct {
22            fields: m
23                .into_iter()
24                .map(|(k, v)| (k, json_to_prost_value(v)))
25                .collect(),
26        }
27    }
28
29    pub(crate) fn struct_to_hashmap(s: prost_types::Struct) -> HashMap<String, serde_json::Value> {
30        s.fields
31            .into_iter()
32            .map(|(k, v)| (k, prost_value_to_json(v)))
33            .collect()
34    }
35
36    fn json_to_prost_value(v: serde_json::Value) -> prost_types::Value {
37        use prost_types::value::Kind;
38        let kind = match v {
39            serde_json::Value::Null => Kind::NullValue(0),
40            serde_json::Value::Bool(b) => Kind::BoolValue(b),
41            serde_json::Value::Number(n) => Kind::NumberValue(n.as_f64().unwrap_or(0.0)),
42            serde_json::Value::String(s) => Kind::StringValue(s),
43            serde_json::Value::Array(arr) => Kind::ListValue(prost_types::ListValue {
44                values: arr.into_iter().map(json_to_prost_value).collect(),
45            }),
46            serde_json::Value::Object(obj) => Kind::StructValue(prost_types::Struct {
47                fields: obj
48                    .into_iter()
49                    .map(|(k, v)| (k, json_to_prost_value(v)))
50                    .collect(),
51            }),
52        };
53        prost_types::Value { kind: Some(kind) }
54    }
55
56    fn prost_value_to_json(v: prost_types::Value) -> serde_json::Value {
57        use prost_types::value::Kind;
58        match v.kind {
59            Some(Kind::NullValue(_)) | None => serde_json::Value::Null,
60            Some(Kind::BoolValue(b)) => serde_json::Value::Bool(b),
61            Some(Kind::NumberValue(n)) => serde_json::Number::from_f64(n)
62                .map(serde_json::Value::Number)
63                .unwrap_or(serde_json::Value::Null),
64            Some(Kind::StringValue(s)) => serde_json::Value::String(s),
65            Some(Kind::ListValue(l)) => {
66                serde_json::Value::Array(l.values.into_iter().map(prost_value_to_json).collect())
67            }
68            Some(Kind::StructValue(s)) => {
69                let map: serde_json::Map<String, serde_json::Value> = s
70                    .fields
71                    .into_iter()
72                    .map(|(k, v)| (k, prost_value_to_json(v)))
73                    .collect();
74                serde_json::Value::Object(map)
75            }
76        }
77    }
78
79    fn opt_hashmap_to_struct(
80        m: Option<HashMap<String, serde_json::Value>>,
81    ) -> Option<prost_types::Struct> {
82        m.map(hashmap_to_struct)
83    }
84
85    fn opt_struct_to_hashmap(
86        s: Option<prost_types::Struct>,
87    ) -> Option<HashMap<String, serde_json::Value>> {
88        s.map(struct_to_hashmap)
89    }
90
91    fn opt_empty(s: &str) -> Option<String> {
92        if s.is_empty() {
93            None
94        } else {
95            Some(s.to_string())
96        }
97    }
98
99    // ===================================================================
100    // Role
101    // ===================================================================
102
103    impl From<Role> for i32 {
104        fn from(r: Role) -> i32 {
105            match r {
106                Role::User => pb::Role::User as i32,
107                Role::Agent => pb::Role::Agent as i32,
108                Role::Unspecified => pb::Role::Unspecified as i32,
109            }
110        }
111    }
112
113    impl From<i32> for Role {
114        fn from(v: i32) -> Self {
115            match v {
116                1 => Role::User,
117                2 => Role::Agent,
118                _ => Role::Unspecified,
119            }
120        }
121    }
122
123    // ===================================================================
124    // TaskState
125    // ===================================================================
126
127    impl From<TaskState> for i32 {
128        fn from(s: TaskState) -> i32 {
129            match s {
130                TaskState::Unspecified => 0,
131                TaskState::Submitted => 1,
132                TaskState::Working => 2,
133                TaskState::Completed => 3,
134                TaskState::Failed => 4,
135                TaskState::Canceled => 5,
136                TaskState::InputRequired => 6,
137                TaskState::Rejected => 7,
138                TaskState::AuthRequired => 8,
139            }
140        }
141    }
142
143    impl From<i32> for TaskState {
144        fn from(v: i32) -> Self {
145            match v {
146                1 => TaskState::Submitted,
147                2 => TaskState::Working,
148                3 => TaskState::Completed,
149                4 => TaskState::Failed,
150                5 => TaskState::Canceled,
151                6 => TaskState::InputRequired,
152                7 => TaskState::Rejected,
153                8 => TaskState::AuthRequired,
154                _ => TaskState::Unspecified,
155            }
156        }
157    }
158
159    // ===================================================================
160    // Part (flat struct with optional fields)
161    // ===================================================================
162
163    impl From<Part> for pb::Part {
164        fn from(p: Part) -> pb::Part {
165            // Map the flat Part struct to the proto Part with oneof content.
166            let content = if let Some(text) = p.text {
167                Some(pb::part::Content::Text(text))
168            } else if let Some(url) = p.url {
169                Some(pb::part::Content::Url(url))
170            } else if let Some(raw) = p.raw {
171                Some(pb::part::Content::Raw(raw.into_bytes()))
172            } else {
173                p.data
174                    .map(|data| pb::part::Content::Data(json_to_prost_value(data)))
175            };
176
177            pb::Part {
178                content,
179                metadata: opt_hashmap_to_struct(p.metadata),
180                filename: p.filename.unwrap_or_default(),
181                media_type: p.media_type.unwrap_or_default(),
182            }
183        }
184    }
185
186    impl From<pb::Part> for Part {
187        fn from(p: pb::Part) -> Part {
188            let metadata = opt_struct_to_hashmap(p.metadata);
189            let media_type = opt_empty(&p.media_type);
190            let filename = opt_empty(&p.filename);
191
192            match p.content {
193                Some(pb::part::Content::Text(t)) => Part {
194                    text: Some(t),
195                    raw: None,
196                    url: None,
197                    data: None,
198                    media_type,
199                    filename,
200                    metadata,
201                },
202                Some(pb::part::Content::Url(u)) => Part {
203                    text: None,
204                    raw: None,
205                    url: Some(u),
206                    data: None,
207                    media_type,
208                    filename,
209                    metadata,
210                },
211                Some(pb::part::Content::Raw(b)) => Part {
212                    text: None,
213                    raw: Some(String::from_utf8_lossy(&b).to_string()),
214                    url: None,
215                    data: None,
216                    media_type,
217                    filename,
218                    metadata,
219                },
220                Some(pb::part::Content::Data(v)) => Part {
221                    text: None,
222                    raw: None,
223                    url: None,
224                    data: Some(prost_value_to_json(v)),
225                    media_type,
226                    filename,
227                    metadata,
228                },
229                None => Part {
230                    text: Some(String::new()),
231                    raw: None,
232                    url: None,
233                    data: None,
234                    media_type,
235                    filename,
236                    metadata,
237                },
238            }
239        }
240    }
241
242    // ===================================================================
243    // Message (no more `kind` field)
244    // ===================================================================
245
246    impl From<Message> for pb::Message {
247        fn from(m: Message) -> pb::Message {
248            pb::Message {
249                message_id: m.message_id,
250                context_id: m.context_id.unwrap_or_default(),
251                task_id: m.task_id.unwrap_or_default(),
252                role: i32::from(m.role),
253                parts: m.parts.into_iter().map(Into::into).collect(),
254                metadata: opt_hashmap_to_struct(m.metadata),
255                extensions: m.extensions.unwrap_or_default(),
256                reference_task_ids: m.reference_task_ids.unwrap_or_default(),
257            }
258        }
259    }
260
261    impl From<pb::Message> for Message {
262        fn from(m: pb::Message) -> Message {
263            Message {
264                message_id: m.message_id,
265                role: Role::from(m.role),
266                parts: m.parts.into_iter().map(Into::into).collect(),
267                context_id: opt_empty(&m.context_id),
268                task_id: opt_empty(&m.task_id),
269                reference_task_ids: if m.reference_task_ids.is_empty() {
270                    None
271                } else {
272                    Some(m.reference_task_ids)
273                },
274                metadata: opt_struct_to_hashmap(m.metadata),
275                extensions: if m.extensions.is_empty() {
276                    None
277                } else {
278                    Some(m.extensions)
279                },
280            }
281        }
282    }
283
284    // ===================================================================
285    // Artifact
286    // ===================================================================
287
288    impl From<Artifact> for pb::Artifact {
289        fn from(a: Artifact) -> pb::Artifact {
290            pb::Artifact {
291                artifact_id: a.artifact_id,
292                name: a.name.unwrap_or_default(),
293                description: a.description.unwrap_or_default(),
294                parts: a.parts.into_iter().map(Into::into).collect(),
295                metadata: opt_hashmap_to_struct(a.metadata),
296                extensions: a.extensions.unwrap_or_default(),
297            }
298        }
299    }
300
301    impl From<pb::Artifact> for Artifact {
302        fn from(a: pb::Artifact) -> Artifact {
303            Artifact {
304                artifact_id: a.artifact_id,
305                name: opt_empty(&a.name),
306                description: opt_empty(&a.description),
307                parts: a.parts.into_iter().map(Into::into).collect(),
308                metadata: opt_struct_to_hashmap(a.metadata),
309                extensions: if a.extensions.is_empty() {
310                    None
311                } else {
312                    Some(a.extensions)
313                },
314            }
315        }
316    }
317
318    // ===================================================================
319    // TaskStatus
320    // ===================================================================
321
322    impl From<TaskStatus> for pb::TaskStatus {
323        fn from(s: TaskStatus) -> pb::TaskStatus {
324            pb::TaskStatus {
325                state: i32::from(s.state),
326                message: s.message.map(Into::into),
327                timestamp: s.timestamp.and_then(|t| {
328                    chrono::DateTime::parse_from_rfc3339(&t)
329                        .ok()
330                        .map(|dt| prost_types::Timestamp {
331                            seconds: dt.timestamp(),
332                            nanos: dt.timestamp_subsec_nanos() as i32,
333                        })
334                }),
335            }
336        }
337    }
338
339    impl From<pb::TaskStatus> for TaskStatus {
340        fn from(s: pb::TaskStatus) -> TaskStatus {
341            TaskStatus {
342                state: TaskState::from(s.state),
343                message: s.message.map(Into::into),
344                timestamp: s.timestamp.and_then(|t| {
345                    chrono::DateTime::from_timestamp(t.seconds, t.nanos as u32)
346                        .map(|dt| dt.to_rfc3339())
347                }),
348            }
349        }
350    }
351
352    // ===================================================================
353    // Task (no more `kind` field)
354    // ===================================================================
355
356    impl From<Task> for pb::Task {
357        fn from(t: Task) -> pb::Task {
358            pb::Task {
359                id: t.id,
360                context_id: t.context_id.unwrap_or_default(),
361                status: Some(t.status.into()),
362                artifacts: t
363                    .artifacts
364                    .unwrap_or_default()
365                    .into_iter()
366                    .map(Into::into)
367                    .collect(),
368                history: t
369                    .history
370                    .unwrap_or_default()
371                    .into_iter()
372                    .map(Into::into)
373                    .collect(),
374                metadata: opt_hashmap_to_struct(t.metadata),
375            }
376        }
377    }
378
379    impl From<pb::Task> for Task {
380        fn from(t: pb::Task) -> Task {
381            Task {
382                id: t.id,
383                context_id: opt_empty(&t.context_id),
384                status: t.status.map(Into::into).unwrap_or(TaskStatus {
385                    state: TaskState::Unspecified,
386                    message: None,
387                    timestamp: None,
388                }),
389                artifacts: if t.artifacts.is_empty() {
390                    None
391                } else {
392                    Some(t.artifacts.into_iter().map(Into::into).collect())
393                },
394                history: if t.history.is_empty() {
395                    None
396                } else {
397                    Some(t.history.into_iter().map(Into::into).collect())
398                },
399                metadata: opt_struct_to_hashmap(t.metadata),
400            }
401        }
402    }
403
404    // ===================================================================
405    // AuthenticationInfo
406    // ===================================================================
407
408    impl From<AuthenticationInfo> for pb::AuthenticationInfo {
409        fn from(a: AuthenticationInfo) -> pb::AuthenticationInfo {
410            pb::AuthenticationInfo {
411                scheme: a.scheme,
412                credentials: a.credentials.unwrap_or_default(),
413            }
414        }
415    }
416
417    impl From<pb::AuthenticationInfo> for AuthenticationInfo {
418        fn from(a: pb::AuthenticationInfo) -> AuthenticationInfo {
419            AuthenticationInfo {
420                scheme: a.scheme,
421                credentials: opt_empty(&a.credentials),
422            }
423        }
424    }
425
426    // ===================================================================
427    // TaskPushNotificationConfig (id -> config_id)
428    // ===================================================================
429
430    impl From<TaskPushNotificationConfig> for pb::TaskPushNotificationConfig {
431        fn from(c: TaskPushNotificationConfig) -> pb::TaskPushNotificationConfig {
432            pb::TaskPushNotificationConfig {
433                tenant: c.tenant.unwrap_or_default(),
434                id: c.config_id.unwrap_or_default(),
435                task_id: c.task_id,
436                url: c.url,
437                token: c.token.unwrap_or_default(),
438                authentication: c.authentication.map(Into::into),
439            }
440        }
441    }
442
443    impl From<pb::TaskPushNotificationConfig> for TaskPushNotificationConfig {
444        fn from(c: pb::TaskPushNotificationConfig) -> TaskPushNotificationConfig {
445            TaskPushNotificationConfig {
446                tenant: opt_empty(&c.tenant),
447                config_id: opt_empty(&c.id),
448                task_id: c.task_id,
449                url: c.url,
450                token: opt_empty(&c.token),
451                authentication: c.authentication.map(Into::into),
452                created_at: None,
453            }
454        }
455    }
456
457    // ===================================================================
458    // AgentProvider
459    // ===================================================================
460
461    impl From<AgentProvider> for pb::AgentProvider {
462        fn from(p: AgentProvider) -> pb::AgentProvider {
463            pb::AgentProvider {
464                url: p.url,
465                organization: p.organization,
466            }
467        }
468    }
469
470    impl From<pb::AgentProvider> for AgentProvider {
471        fn from(p: pb::AgentProvider) -> AgentProvider {
472            AgentProvider {
473                url: p.url,
474                organization: p.organization,
475            }
476        }
477    }
478
479    // ===================================================================
480    // AgentExtension
481    // ===================================================================
482
483    impl From<AgentExtension> for pb::AgentExtension {
484        fn from(e: AgentExtension) -> pb::AgentExtension {
485            pb::AgentExtension {
486                uri: e.uri,
487                description: e.description.unwrap_or_default(),
488                required: e.required,
489                params: e.params.map(hashmap_to_struct),
490            }
491        }
492    }
493
494    impl From<pb::AgentExtension> for AgentExtension {
495        fn from(e: pb::AgentExtension) -> AgentExtension {
496            AgentExtension {
497                uri: e.uri,
498                description: opt_empty(&e.description),
499                required: e.required,
500                params: e.params.map(struct_to_hashmap),
501            }
502        }
503    }
504
505    // ===================================================================
506    // AgentCapabilities
507    // ===================================================================
508
509    impl From<AgentCapabilities> for pb::AgentCapabilities {
510        fn from(c: AgentCapabilities) -> pb::AgentCapabilities {
511            pb::AgentCapabilities {
512                streaming: c.streaming,
513                push_notifications: c.push_notifications,
514                extended_agent_card: c.extended_agent_card,
515                extensions: c
516                    .extensions
517                    .unwrap_or_default()
518                    .into_iter()
519                    .map(Into::into)
520                    .collect(),
521            }
522        }
523    }
524
525    impl From<pb::AgentCapabilities> for AgentCapabilities {
526        fn from(c: pb::AgentCapabilities) -> AgentCapabilities {
527            AgentCapabilities {
528                streaming: c.streaming,
529                push_notifications: c.push_notifications,
530                extended_agent_card: c.extended_agent_card,
531                extensions: if c.extensions.is_empty() {
532                    None
533                } else {
534                    Some(c.extensions.into_iter().map(Into::into).collect())
535                },
536            }
537        }
538    }
539
540    // ===================================================================
541    // AgentSkill
542    // ===================================================================
543
544    impl From<AgentSkill> for pb::AgentSkill {
545        fn from(s: AgentSkill) -> pb::AgentSkill {
546            pb::AgentSkill {
547                id: s.id,
548                name: s.name,
549                description: s.description,
550                tags: s.tags,
551                examples: s.examples.unwrap_or_default(),
552                input_modes: s.input_modes.unwrap_or_default(),
553                output_modes: s.output_modes.unwrap_or_default(),
554                security_requirements: s
555                    .security_requirements
556                    .unwrap_or_default()
557                    .into_iter()
558                    .map(Into::into)
559                    .collect(),
560            }
561        }
562    }
563
564    impl From<pb::AgentSkill> for AgentSkill {
565        fn from(s: pb::AgentSkill) -> AgentSkill {
566            AgentSkill {
567                id: s.id,
568                name: s.name,
569                description: s.description,
570                tags: s.tags,
571                examples: if s.examples.is_empty() {
572                    None
573                } else {
574                    Some(s.examples)
575                },
576                input_modes: if s.input_modes.is_empty() {
577                    None
578                } else {
579                    Some(s.input_modes)
580                },
581                output_modes: if s.output_modes.is_empty() {
582                    None
583                } else {
584                    Some(s.output_modes)
585                },
586                security_requirements: if s.security_requirements.is_empty() {
587                    None
588                } else {
589                    Some(
590                        s.security_requirements
591                            .into_iter()
592                            .map(Into::into)
593                            .collect(),
594                    )
595                },
596            }
597        }
598    }
599
600    // ===================================================================
601    // AgentInterface
602    // ===================================================================
603
604    impl From<AgentInterface> for pb::AgentInterface {
605        fn from(i: AgentInterface) -> pb::AgentInterface {
606            pb::AgentInterface {
607                url: i.url,
608                protocol_binding: i.protocol_binding,
609                tenant: i.tenant.unwrap_or_default(),
610                protocol_version: i.protocol_version,
611            }
612        }
613    }
614
615    impl From<pb::AgentInterface> for AgentInterface {
616        fn from(i: pb::AgentInterface) -> AgentInterface {
617            AgentInterface {
618                url: i.url,
619                protocol_binding: i.protocol_binding,
620                tenant: opt_empty(&i.tenant),
621                protocol_version: i.protocol_version,
622            }
623        }
624    }
625
626    // ===================================================================
627    // AgentCardSignature
628    // ===================================================================
629
630    impl From<AgentCardSignature> for pb::AgentCardSignature {
631        fn from(s: AgentCardSignature) -> pb::AgentCardSignature {
632            pb::AgentCardSignature {
633                protected: s.protected,
634                signature: s.signature,
635                header: s.header.map(hashmap_to_struct),
636            }
637        }
638    }
639
640    impl From<pb::AgentCardSignature> for AgentCardSignature {
641        fn from(s: pb::AgentCardSignature) -> AgentCardSignature {
642            AgentCardSignature {
643                protected: s.protected,
644                signature: s.signature,
645                header: s.header.map(struct_to_hashmap),
646            }
647        }
648    }
649
650    // ===================================================================
651    // SecurityRequirement
652    // ===================================================================
653
654    impl From<SecurityRequirement> for pb::SecurityRequirement {
655        fn from(r: SecurityRequirement) -> pb::SecurityRequirement {
656            pb::SecurityRequirement {
657                schemes: r
658                    .schemes
659                    .into_iter()
660                    .map(|(k, v)| (k, pb::StringList { list: v }))
661                    .collect(),
662            }
663        }
664    }
665
666    impl From<pb::SecurityRequirement> for SecurityRequirement {
667        fn from(r: pb::SecurityRequirement) -> SecurityRequirement {
668            SecurityRequirement {
669                schemes: r.schemes.into_iter().map(|(k, v)| (k, v.list)).collect(),
670            }
671        }
672    }
673
674    // ===================================================================
675    // SecurityScheme (now a struct with optional wrapper fields)
676    // ===================================================================
677
678    impl From<SecurityScheme> for pb::SecurityScheme {
679        fn from(s: SecurityScheme) -> pb::SecurityScheme {
680            let scheme = if let Some(api_key) = s.api_key {
681                Some(pb::security_scheme::Scheme::ApiKeySecurityScheme(
682                    pb::ApiKeySecurityScheme {
683                        description: api_key.description.unwrap_or_default(),
684                        location: api_key.location,
685                        name: api_key.name,
686                    },
687                ))
688            } else if let Some(http_auth) = s.http_auth {
689                Some(pb::security_scheme::Scheme::HttpAuthSecurityScheme(
690                    pb::HttpAuthSecurityScheme {
691                        description: http_auth.description.unwrap_or_default(),
692                        scheme: http_auth.scheme,
693                        bearer_format: http_auth.bearer_format.unwrap_or_default(),
694                    },
695                ))
696            } else if let Some(oauth2) = s.oauth2 {
697                Some(pb::security_scheme::Scheme::Oauth2SecurityScheme(
698                    pb::OAuth2SecurityScheme {
699                        description: oauth2.description.unwrap_or_default(),
700                        flows: Some(oauth2.flows.into()),
701                        oauth2_metadata_url: oauth2.oauth2_metadata_url.unwrap_or_default(),
702                    },
703                ))
704            } else if let Some(oidc) = s.open_id_connect {
705                Some(pb::security_scheme::Scheme::OpenIdConnectSecurityScheme(
706                    pb::OpenIdConnectSecurityScheme {
707                        description: oidc.description.unwrap_or_default(),
708                        open_id_connect_url: oidc.open_id_connect_url,
709                    },
710                ))
711            } else if let Some(mtls) = s.mtls {
712                Some(pb::security_scheme::Scheme::MtlsSecurityScheme(
713                    pb::MutualTlsSecurityScheme {
714                        description: mtls.description.unwrap_or_default(),
715                    },
716                ))
717            } else {
718                None
719            };
720            pb::SecurityScheme { scheme }
721        }
722    }
723
724    impl From<pb::SecurityScheme> for SecurityScheme {
725        fn from(s: pb::SecurityScheme) -> SecurityScheme {
726            match s.scheme {
727                Some(pb::security_scheme::Scheme::ApiKeySecurityScheme(a)) => SecurityScheme {
728                    api_key: Some(ApiKeySecurityScheme {
729                        name: a.name,
730                        location: a.location,
731                        description: opt_empty(&a.description),
732                    }),
733                    http_auth: None,
734                    oauth2: None,
735                    open_id_connect: None,
736                    mtls: None,
737                },
738                Some(pb::security_scheme::Scheme::HttpAuthSecurityScheme(h)) => SecurityScheme {
739                    api_key: None,
740                    http_auth: Some(HttpAuthSecurityScheme {
741                        scheme: h.scheme,
742                        bearer_format: opt_empty(&h.bearer_format),
743                        description: opt_empty(&h.description),
744                    }),
745                    oauth2: None,
746                    open_id_connect: None,
747                    mtls: None,
748                },
749                Some(pb::security_scheme::Scheme::Oauth2SecurityScheme(o)) => SecurityScheme {
750                    api_key: None,
751                    http_auth: None,
752                    oauth2: Some(OAuth2SecurityScheme {
753                        flows: o.flows.map(Into::into).unwrap_or(OAuthFlows {
754                            authorization_code: None,
755                            client_credentials: Some(ClientCredentialsOAuthFlow {
756                                token_url: String::new(),
757                                refresh_url: None,
758                                scopes: HashMap::new(),
759                            }),
760                            implicit: None,
761                            password: None,
762                            device_code: None,
763                        }),
764                        description: opt_empty(&o.description),
765                        oauth2_metadata_url: opt_empty(&o.oauth2_metadata_url),
766                    }),
767                    open_id_connect: None,
768                    mtls: None,
769                },
770                Some(pb::security_scheme::Scheme::OpenIdConnectSecurityScheme(o)) => {
771                    SecurityScheme {
772                        api_key: None,
773                        http_auth: None,
774                        oauth2: None,
775                        open_id_connect: Some(OpenIdConnectSecurityScheme {
776                            open_id_connect_url: o.open_id_connect_url,
777                            description: opt_empty(&o.description),
778                        }),
779                        mtls: None,
780                    }
781                }
782                Some(pb::security_scheme::Scheme::MtlsSecurityScheme(m)) => SecurityScheme {
783                    api_key: None,
784                    http_auth: None,
785                    oauth2: None,
786                    open_id_connect: None,
787                    mtls: Some(MutualTlsSecurityScheme {
788                        description: opt_empty(&m.description),
789                    }),
790                },
791                None => SecurityScheme {
792                    api_key: None,
793                    http_auth: None,
794                    oauth2: None,
795                    open_id_connect: None,
796                    mtls: None,
797                },
798            }
799        }
800    }
801
802    // ===================================================================
803    // OAuthFlows (now a struct with optional wrapper fields)
804    // ===================================================================
805
806    impl From<OAuthFlows> for pb::OAuthFlows {
807        fn from(f: OAuthFlows) -> pb::OAuthFlows {
808            let flow = if let Some(ac) = f.authorization_code {
809                Some(pb::o_auth_flows::Flow::AuthorizationCode(
810                    pb::AuthorizationCodeOAuthFlow {
811                        authorization_url: ac.authorization_url,
812                        token_url: ac.token_url,
813                        refresh_url: ac.refresh_url.unwrap_or_default(),
814                        scopes: ac.scopes,
815                        pkce_required: ac.pkce_required.unwrap_or(false),
816                    },
817                ))
818            } else if let Some(cc) = f.client_credentials {
819                Some(pb::o_auth_flows::Flow::ClientCredentials(
820                    pb::ClientCredentialsOAuthFlow {
821                        token_url: cc.token_url,
822                        refresh_url: cc.refresh_url.unwrap_or_default(),
823                        scopes: cc.scopes,
824                    },
825                ))
826            } else if let Some(imp) = f.implicit {
827                Some(pb::o_auth_flows::Flow::Implicit(pb::ImplicitOAuthFlow {
828                    authorization_url: imp.authorization_url.unwrap_or_default(),
829                    refresh_url: imp.refresh_url.unwrap_or_default(),
830                    scopes: imp.scopes,
831                }))
832            } else if let Some(pw) = f.password {
833                Some(pb::o_auth_flows::Flow::Password(pb::PasswordOAuthFlow {
834                    token_url: pw.token_url.unwrap_or_default(),
835                    refresh_url: pw.refresh_url.unwrap_or_default(),
836                    scopes: pw.scopes,
837                }))
838            } else if let Some(dc) = f.device_code {
839                Some(pb::o_auth_flows::Flow::DeviceCode(
840                    pb::DeviceCodeOAuthFlow {
841                        device_authorization_url: dc.device_authorization_url,
842                        token_url: dc.token_url,
843                        refresh_url: dc.refresh_url.unwrap_or_default(),
844                        scopes: dc.scopes,
845                    },
846                ))
847            } else {
848                None
849            };
850            pb::OAuthFlows { flow }
851        }
852    }
853
854    impl From<pb::OAuthFlows> for OAuthFlows {
855        fn from(f: pb::OAuthFlows) -> OAuthFlows {
856            match f.flow {
857                Some(pb::o_auth_flows::Flow::AuthorizationCode(a)) => OAuthFlows {
858                    authorization_code: Some(AuthorizationCodeOAuthFlow {
859                        authorization_url: a.authorization_url,
860                        token_url: a.token_url,
861                        refresh_url: opt_empty(&a.refresh_url),
862                        scopes: a.scopes,
863                        pkce_required: Some(a.pkce_required),
864                    }),
865                    client_credentials: None,
866                    implicit: None,
867                    password: None,
868                    device_code: None,
869                },
870                Some(pb::o_auth_flows::Flow::ClientCredentials(c)) => OAuthFlows {
871                    authorization_code: None,
872                    client_credentials: Some(ClientCredentialsOAuthFlow {
873                        token_url: c.token_url,
874                        refresh_url: opt_empty(&c.refresh_url),
875                        scopes: c.scopes,
876                    }),
877                    implicit: None,
878                    password: None,
879                    device_code: None,
880                },
881                #[allow(deprecated)]
882                Some(pb::o_auth_flows::Flow::Implicit(i)) => OAuthFlows {
883                    authorization_code: None,
884                    client_credentials: None,
885                    implicit: Some(ImplicitOAuthFlow {
886                        authorization_url: opt_empty(&i.authorization_url),
887                        refresh_url: opt_empty(&i.refresh_url),
888                        scopes: i.scopes,
889                    }),
890                    password: None,
891                    device_code: None,
892                },
893                #[allow(deprecated)]
894                Some(pb::o_auth_flows::Flow::Password(p)) => OAuthFlows {
895                    authorization_code: None,
896                    client_credentials: None,
897                    implicit: None,
898                    password: Some(PasswordOAuthFlow {
899                        token_url: opt_empty(&p.token_url),
900                        refresh_url: opt_empty(&p.refresh_url),
901                        scopes: p.scopes,
902                    }),
903                    device_code: None,
904                },
905                Some(pb::o_auth_flows::Flow::DeviceCode(d)) => OAuthFlows {
906                    authorization_code: None,
907                    client_credentials: None,
908                    implicit: None,
909                    password: None,
910                    device_code: Some(DeviceCodeOAuthFlow {
911                        device_authorization_url: d.device_authorization_url,
912                        token_url: d.token_url,
913                        refresh_url: opt_empty(&d.refresh_url),
914                        scopes: d.scopes,
915                    }),
916                },
917                None => OAuthFlows {
918                    authorization_code: None,
919                    client_credentials: None,
920                    implicit: None,
921                    password: None,
922                    device_code: None,
923                },
924            }
925        }
926    }
927
928    // ===================================================================
929    // AgentCard (supported_interfaces is now Vec, not Option<Vec>)
930    // ===================================================================
931
932    impl From<AgentCard> for pb::AgentCard {
933        fn from(c: AgentCard) -> pb::AgentCard {
934            pb::AgentCard {
935                name: c.name,
936                description: c.description,
937                version: c.version,
938                supported_interfaces: c.supported_interfaces.into_iter().map(Into::into).collect(),
939                provider: c.provider.map(Into::into),
940                capabilities: Some(c.capabilities.into()),
941                security_schemes: c
942                    .security_schemes
943                    .unwrap_or_default()
944                    .into_iter()
945                    .map(|(k, v)| (k, v.into()))
946                    .collect(),
947                security_requirements: c
948                    .security_requirements
949                    .unwrap_or_default()
950                    .into_iter()
951                    .map(Into::into)
952                    .collect(),
953                default_input_modes: c.default_input_modes,
954                default_output_modes: c.default_output_modes,
955                skills: c.skills.into_iter().map(Into::into).collect(),
956                signatures: c
957                    .signatures
958                    .unwrap_or_default()
959                    .into_iter()
960                    .map(Into::into)
961                    .collect(),
962                documentation_url: c.documentation_url,
963                icon_url: c.icon_url,
964            }
965        }
966    }
967
968    impl From<pb::AgentCard> for AgentCard {
969        fn from(c: pb::AgentCard) -> AgentCard {
970            AgentCard {
971                name: c.name,
972                description: c.description,
973                version: c.version,
974                supported_interfaces: c.supported_interfaces.into_iter().map(Into::into).collect(),
975                provider: c.provider.map(Into::into),
976                capabilities: c.capabilities.map(Into::into).unwrap_or_default(),
977                security_schemes: {
978                    let m: HashMap<String, SecurityScheme> = c
979                        .security_schemes
980                        .into_iter()
981                        .map(|(k, v)| (k, v.into()))
982                        .collect();
983                    if m.is_empty() { None } else { Some(m) }
984                },
985                security_requirements: if c.security_requirements.is_empty() {
986                    None
987                } else {
988                    Some(
989                        c.security_requirements
990                            .into_iter()
991                            .map(Into::into)
992                            .collect(),
993                    )
994                },
995                default_input_modes: c.default_input_modes,
996                default_output_modes: c.default_output_modes,
997                skills: c.skills.into_iter().map(Into::into).collect(),
998                signatures: if c.signatures.is_empty() {
999                    None
1000                } else {
1001                    Some(c.signatures.into_iter().map(Into::into).collect())
1002                },
1003                documentation_url: c.documentation_url,
1004                icon_url: c.icon_url,
1005            }
1006        }
1007    }
1008}