gglib_core/domain/agent/
messages_serde.rs

1//! Custom [`Serialize`] / [`Deserialize`] implementations for agent message types.
2//!
3//! Extracted from [`super::messages`] so the domain structs remain free of
4//! serialisation noise.  The impls are automatically linked via the orphan
5//! rules — `AssistantContent` is defined in the same crate.
6
7use serde::ser::SerializeMap;
8use serde::{Deserialize, Serialize};
9
10use super::messages::AssistantContent;
11use super::tool_types::ToolCall;
12
13// =============================================================================
14// AssistantContent — Serialize
15// =============================================================================
16
17impl Serialize for AssistantContent {
18    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
19        let has_text = self.text.is_some();
20        let has_calls = !self.tool_calls.is_empty();
21        let count = usize::from(has_text) + usize::from(has_calls);
22        let mut m = serializer.serialize_map(Some(count))?;
23        if let Some(text) = &self.text {
24            m.serialize_entry("content", text)?;
25        }
26        if has_calls {
27            m.serialize_entry("tool_calls", &self.tool_calls)?;
28        }
29        m.end()
30    }
31}
32
33// =============================================================================
34// AssistantContent — Deserialize
35// =============================================================================
36
37impl<'de> Deserialize<'de> for AssistantContent {
38    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
39        deserializer.deserialize_map(AssistantContentVisitor)
40    }
41}
42
43/// Map visitor that reconstructs [`AssistantContent`] from a flat JSON map.
44///
45/// Accepts `"content"` (optional `String`) and `"tool_calls"` (optional
46/// `Vec<ToolCall>`); at least one must be present.  Unknown keys are silently
47/// ignored so the format is forward-compatible.
48struct AssistantContentVisitor;
49
50impl<'de> serde::de::Visitor<'de> for AssistantContentVisitor {
51    type Value = AssistantContent;
52
53    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
54        f.write_str("assistant message with `content` and/or `tool_calls`")
55    }
56
57    fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
58        let mut content: Option<String> = None;
59        let mut tool_calls: Option<Vec<ToolCall>> = None;
60        while let Some(key) = map.next_key::<String>()? {
61            match key.as_str() {
62                "content" => content = map.next_value()?,
63                "tool_calls" => tool_calls = map.next_value()?,
64                _ => {
65                    map.next_value::<serde::de::IgnoredAny>()?;
66                }
67            }
68        }
69        let tool_calls = tool_calls.unwrap_or_default();
70        if content.is_none() && tool_calls.is_empty() {
71            return Err(serde::de::Error::custom(
72                "assistant message must have `content` or `tool_calls`",
73            ));
74        }
75        Ok(AssistantContent {
76            text: content,
77            tool_calls,
78        })
79    }
80}