Skip to main content

codetether_agent/a2a/
bridge.rs

1//! Bridge between hand-written serde types (`a2a::types`) and
2//! tonic/prost-generated types (`a2a::proto`).
3//!
4//! This module provides `From`/conversion functions in both directions so
5//! the gRPC layer can speak proto types on the wire while the rest of the
6//! codebase works with the ergonomic serde types.
7
8#![allow(dead_code)]
9
10use crate::a2a::proto;
11use crate::a2a::types as local;
12
13// ═══════════════════════════════════════════════════════════════════════
14// Proto → Local
15// ═══════════════════════════════════════════════════════════════════════
16
17/// Convert a proto `Message` to a local `Message`.
18pub fn proto_message_to_local(msg: &proto::Message) -> local::Message {
19    local::Message {
20        message_id: msg.message_id.clone(),
21        role: match msg.role() {
22            proto::Role::User => local::MessageRole::User,
23            _ => local::MessageRole::Agent,
24        },
25        parts: msg.content.iter().filter_map(proto_part_to_local).collect(),
26        context_id: if msg.context_id.is_empty() {
27            None
28        } else {
29            Some(msg.context_id.clone())
30        },
31        task_id: if msg.task_id.is_empty() {
32            None
33        } else {
34            Some(msg.task_id.clone())
35        },
36        metadata: Default::default(),
37        extensions: msg.extensions.clone(),
38    }
39}
40
41fn proto_part_to_local(part: &proto::Part) -> Option<local::Part> {
42    match &part.part {
43        Some(proto::part::Part::Text(text)) => Some(local::Part::Text { text: text.clone() }),
44        Some(proto::part::Part::File(file)) => {
45            let (bytes, uri) = match &file.file {
46                Some(proto::file_part::File::FileWithUri(u)) => (None, Some(u.clone())),
47                Some(proto::file_part::File::FileWithBytes(b)) => {
48                    use base64::Engine;
49                    (
50                        Some(base64::engine::general_purpose::STANDARD.encode(b)),
51                        None,
52                    )
53                }
54                None => (None, None),
55            };
56            Some(local::Part::File {
57                file: local::FileContent {
58                    bytes,
59                    uri,
60                    mime_type: if file.mime_type.is_empty() {
61                        None
62                    } else {
63                        Some(file.mime_type.clone())
64                    },
65                    name: if file.name.is_empty() {
66                        None
67                    } else {
68                        Some(file.name.clone())
69                    },
70                },
71            })
72        }
73        Some(proto::part::Part::Data(data_part)) => {
74            let val = data_part
75                .data
76                .as_ref()
77                .map(prost_struct_to_json)
78                .unwrap_or(serde_json::Value::Null);
79            Some(local::Part::Data { data: val })
80        }
81        None => None,
82    }
83}
84
85fn proto_task_state_to_local(state: proto::TaskState) -> local::TaskState {
86    match state {
87        proto::TaskState::Submitted => local::TaskState::Submitted,
88        proto::TaskState::Working => local::TaskState::Working,
89        proto::TaskState::Completed => local::TaskState::Completed,
90        proto::TaskState::Failed => local::TaskState::Failed,
91        proto::TaskState::Cancelled => local::TaskState::Cancelled,
92        proto::TaskState::InputRequired => local::TaskState::InputRequired,
93        proto::TaskState::Rejected => local::TaskState::Rejected,
94        proto::TaskState::AuthRequired => local::TaskState::AuthRequired,
95        proto::TaskState::Unspecified => local::TaskState::Submitted,
96    }
97}
98
99/// Convert a proto `Task` to a local `Task`.
100pub fn proto_task_to_local(task: &proto::Task) -> local::Task {
101    let status = task
102        .status
103        .as_ref()
104        .map(|s| local::TaskStatus {
105            state: proto_task_state_to_local(s.state()),
106            message: s.update.as_ref().map(proto_message_to_local),
107            timestamp: s.timestamp.as_ref().map(|t| {
108                chrono::DateTime::from_timestamp(t.seconds, t.nanos as u32)
109                    .map(|dt| dt.to_rfc3339())
110                    .unwrap_or_default()
111            }),
112        })
113        .unwrap_or(local::TaskStatus {
114            state: local::TaskState::Submitted,
115            message: None,
116            timestamp: None,
117        });
118
119    local::Task {
120        id: task.id.clone(),
121        context_id: if task.context_id.is_empty() {
122            None
123        } else {
124            Some(task.context_id.clone())
125        },
126        status,
127        artifacts: task.artifacts.iter().map(proto_artifact_to_local).collect(),
128        history: task.history.iter().map(proto_message_to_local).collect(),
129        metadata: Default::default(),
130    }
131}
132
133fn proto_artifact_to_local(art: &proto::Artifact) -> local::Artifact {
134    local::Artifact {
135        artifact_id: art.artifact_id.clone(),
136        parts: art.parts.iter().filter_map(proto_part_to_local).collect(),
137        name: if art.name.is_empty() {
138            None
139        } else {
140            Some(art.name.clone())
141        },
142        description: if art.description.is_empty() {
143            None
144        } else {
145            Some(art.description.clone())
146        },
147        metadata: Default::default(),
148        extensions: art.extensions.clone(),
149    }
150}
151
152// ═══════════════════════════════════════════════════════════════════════
153// Local → Proto
154// ═══════════════════════════════════════════════════════════════════════
155
156/// Convert a local `Task` to a proto `Task`.
157pub fn local_task_to_proto(task: &local::Task) -> proto::Task {
158    proto::Task {
159        id: task.id.clone(),
160        context_id: task.context_id.clone().unwrap_or_default(),
161        status: Some(local_task_status_to_proto(&task.status)),
162        artifacts: task.artifacts.iter().map(local_artifact_to_proto).collect(),
163        history: task.history.iter().map(local_message_to_proto).collect(),
164        metadata: None,
165    }
166}
167
168pub fn local_task_status_to_proto(status: &local::TaskStatus) -> proto::TaskStatus {
169    proto::TaskStatus {
170        state: local_task_state_to_proto(status.state).into(),
171        update: status.message.as_ref().map(local_message_to_proto),
172        timestamp: status.timestamp.as_ref().and_then(|ts| {
173            chrono::DateTime::parse_from_rfc3339(ts)
174                .ok()
175                .map(|dt| prost_types::Timestamp {
176                    seconds: dt.timestamp(),
177                    nanos: dt.timestamp_subsec_nanos() as i32,
178                })
179        }),
180    }
181}
182
183fn local_task_state_to_proto(state: local::TaskState) -> proto::TaskState {
184    match state {
185        local::TaskState::Submitted => proto::TaskState::Submitted,
186        local::TaskState::Working => proto::TaskState::Working,
187        local::TaskState::Completed => proto::TaskState::Completed,
188        local::TaskState::Failed => proto::TaskState::Failed,
189        local::TaskState::Cancelled => proto::TaskState::Cancelled,
190        local::TaskState::InputRequired => proto::TaskState::InputRequired,
191        local::TaskState::Rejected => proto::TaskState::Rejected,
192        local::TaskState::AuthRequired => proto::TaskState::AuthRequired,
193    }
194}
195
196pub fn local_message_to_proto(msg: &local::Message) -> proto::Message {
197    proto::Message {
198        message_id: msg.message_id.clone(),
199        context_id: msg.context_id.clone().unwrap_or_default(),
200        task_id: msg.task_id.clone().unwrap_or_default(),
201        role: match msg.role {
202            local::MessageRole::User => proto::Role::User.into(),
203            local::MessageRole::Agent => proto::Role::Agent.into(),
204        },
205        content: msg.parts.iter().map(local_part_to_proto).collect(),
206        metadata: None,
207        extensions: msg.extensions.clone(),
208    }
209}
210
211fn local_part_to_proto(part: &local::Part) -> proto::Part {
212    match part {
213        local::Part::Text { text } => proto::Part {
214            part: Some(proto::part::Part::Text(text.clone())),
215            metadata: None,
216        },
217        local::Part::File { file } => proto::Part {
218            part: Some(proto::part::Part::File(proto::FilePart {
219                file: file
220                    .uri
221                    .as_ref()
222                    .map(|u| proto::file_part::File::FileWithUri(u.clone()))
223                    .or_else(|| {
224                        file.bytes.as_ref().and_then(|b| {
225                            use base64::Engine;
226                            base64::engine::general_purpose::STANDARD
227                                .decode(b)
228                                .ok()
229                                .map(proto::file_part::File::FileWithBytes)
230                        })
231                    }),
232                mime_type: file.mime_type.clone().unwrap_or_default(),
233                name: file.name.clone().unwrap_or_default(),
234            })),
235            metadata: None,
236        },
237        local::Part::Data { data } => proto::Part {
238            part: Some(proto::part::Part::Data(proto::DataPart {
239                data: Some(json_to_prost_struct(data)),
240            })),
241            metadata: None,
242        },
243    }
244}
245
246fn local_artifact_to_proto(art: &local::Artifact) -> proto::Artifact {
247    proto::Artifact {
248        artifact_id: art.artifact_id.clone(),
249        name: art.name.clone().unwrap_or_default(),
250        description: art.description.clone().unwrap_or_default(),
251        parts: art.parts.iter().map(local_part_to_proto).collect(),
252        metadata: None,
253        extensions: art.extensions.clone(),
254    }
255}
256
257/// Convert a local `AgentCard` to a proto `AgentCard`.
258pub fn local_card_to_proto(card: &local::AgentCard) -> proto::AgentCard {
259    proto::AgentCard {
260        protocol_version: card.protocol_version.clone(),
261        name: card.name.clone(),
262        description: card.description.clone(),
263        url: card.url.clone(),
264        preferred_transport: card.preferred_transport.clone().unwrap_or_default(),
265        additional_interfaces: card
266            .additional_interfaces
267            .iter()
268            .map(|i| proto::AgentInterface {
269                url: i.url.clone(),
270                transport: i.transport.clone(),
271            })
272            .collect(),
273        provider: card.provider.as_ref().map(|p| proto::AgentProvider {
274            url: p.url.clone(),
275            organization: p.organization.clone(),
276        }),
277        version: card.version.clone(),
278        documentation_url: card.documentation_url.clone().unwrap_or_default(),
279        capabilities: Some(proto::AgentCapabilities {
280            streaming: card.capabilities.streaming,
281            push_notifications: card.capabilities.push_notifications,
282            extensions: card
283                .capabilities
284                .extensions
285                .iter()
286                .map(|e| proto::AgentExtension {
287                    uri: e.uri.clone(),
288                    description: e.description.clone().unwrap_or_default(),
289                    required: e.required,
290                    params: None,
291                })
292                .collect(),
293        }),
294        security_schemes: Default::default(), // complex mapping deferred
295        security: vec![],
296        default_input_modes: card.default_input_modes.clone(),
297        default_output_modes: card.default_output_modes.clone(),
298        skills: card
299            .skills
300            .iter()
301            .map(|s| proto::AgentSkill {
302                id: s.id.clone(),
303                name: s.name.clone(),
304                description: s.description.clone(),
305                tags: s.tags.clone(),
306                examples: s.examples.clone(),
307                input_modes: s.input_modes.clone(),
308                output_modes: s.output_modes.clone(),
309                security: vec![],
310            })
311            .collect(),
312        supports_authenticated_extended_card: card.supports_authenticated_extended_card,
313        signatures: card
314            .signatures
315            .iter()
316            .map(|s| proto::AgentCardSignature {
317                protected: s.algorithm.clone().unwrap_or_default(),
318                signature: s.signature.clone(),
319                header: None,
320            })
321            .collect(),
322        icon_url: card.icon_url.clone().unwrap_or_default(),
323    }
324}
325
326// ═══════════════════════════════════════════════════════════════════════
327// prost_types::Struct ↔ serde_json::Value helpers
328// ═══════════════════════════════════════════════════════════════════════
329
330/// Convert a `prost_types::Struct` to a `serde_json::Value`.
331pub fn prost_struct_to_json(s: &prost_types::Struct) -> serde_json::Value {
332    let map: serde_json::Map<String, serde_json::Value> = s
333        .fields
334        .iter()
335        .map(|(k, v)| (k.clone(), prost_value_to_json(v)))
336        .collect();
337    serde_json::Value::Object(map)
338}
339
340fn prost_value_to_json(v: &prost_types::Value) -> serde_json::Value {
341    match &v.kind {
342        Some(prost_types::value::Kind::NullValue(_)) => serde_json::Value::Null,
343        Some(prost_types::value::Kind::NumberValue(n)) => {
344            serde_json::Value::Number(serde_json::Number::from_f64(*n).unwrap_or_else(|| 0.into()))
345        }
346        Some(prost_types::value::Kind::StringValue(s)) => serde_json::Value::String(s.clone()),
347        Some(prost_types::value::Kind::BoolValue(b)) => serde_json::Value::Bool(*b),
348        Some(prost_types::value::Kind::StructValue(s)) => prost_struct_to_json(s),
349        Some(prost_types::value::Kind::ListValue(l)) => {
350            serde_json::Value::Array(l.values.iter().map(prost_value_to_json).collect())
351        }
352        None => serde_json::Value::Null,
353    }
354}
355
356/// Convert a `serde_json::Value` to a `prost_types::Struct`.
357pub fn json_to_prost_struct(v: &serde_json::Value) -> prost_types::Struct {
358    match v {
359        serde_json::Value::Object(map) => prost_types::Struct {
360            fields: map
361                .iter()
362                .map(|(k, v)| (k.clone(), json_to_prost_value(v)))
363                .collect(),
364        },
365        _ => prost_types::Struct::default(),
366    }
367}
368
369fn json_to_prost_value(v: &serde_json::Value) -> prost_types::Value {
370    let kind = match v {
371        serde_json::Value::Null => prost_types::value::Kind::NullValue(0),
372        serde_json::Value::Bool(b) => prost_types::value::Kind::BoolValue(*b),
373        serde_json::Value::Number(n) => {
374            prost_types::value::Kind::NumberValue(n.as_f64().unwrap_or(0.0))
375        }
376        serde_json::Value::String(s) => prost_types::value::Kind::StringValue(s.clone()),
377        serde_json::Value::Array(arr) => {
378            prost_types::value::Kind::ListValue(prost_types::ListValue {
379                values: arr.iter().map(json_to_prost_value).collect(),
380            })
381        }
382        serde_json::Value::Object(map) => {
383            prost_types::value::Kind::StructValue(prost_types::Struct {
384                fields: map
385                    .iter()
386                    .map(|(k, v)| (k.clone(), json_to_prost_value(v)))
387                    .collect(),
388            })
389        }
390    };
391    prost_types::Value { kind: Some(kind) }
392}