agents_core/
toon.rs

1//! TOON (Token-Oriented Object Notation) encoding support
2//!
3//! This module provides utilities for encoding data in TOON format,
4//! a compact, human-readable format that reduces token usage when
5//! sending structured data to LLMs.
6//!
7//! TOON typically provides 30-60% token reduction compared to JSON,
8//! making it ideal for LLM prompts and tool descriptions.
9//!
10//! See: <https://github.com/toon-format/toon>
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use agents_core::toon::ToonEncoder;
16//! use serde_json::json;
17//!
18//! let encoder = ToonEncoder::default();
19//! let data = json!({
20//!     "users": [
21//!         {"id": 1, "name": "Alice"},
22//!         {"id": 2, "name": "Bob"}
23//!     ]
24//! });
25//!
26//! let toon_str = encoder.encode(&data).unwrap();
27//! // Output:
28//! // users[2]{id,name}:
29//! //   1,Alice
30//! //   2,Bob
31//! ```
32
33use serde::Serialize;
34
35#[cfg(feature = "toon")]
36use toon_format::{encode_default, EncodeOptions, ToonError};
37
38/// TOON encoder for converting data to token-efficient format
39///
40/// When the `toon` feature is enabled, this encoder uses the official
41/// TOON format implementation. When disabled, it falls back to JSON.
42#[derive(Debug, Clone, Default)]
43pub struct ToonEncoder {
44    /// Use tab delimiter for even more compact output
45    pub use_tabs: bool,
46    /// Enable key folding for nested objects (e.g., `data.user.name: Alice`)
47    pub fold_keys: bool,
48}
49
50impl ToonEncoder {
51    /// Create a new TOON encoder with default settings
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Create an encoder optimized for maximum token savings
57    pub fn compact() -> Self {
58        Self {
59            use_tabs: true,
60            fold_keys: true,
61        }
62    }
63
64    /// Set whether to use tab delimiters
65    pub fn with_tabs(mut self, use_tabs: bool) -> Self {
66        self.use_tabs = use_tabs;
67        self
68    }
69
70    /// Set whether to fold nested keys
71    pub fn with_key_folding(mut self, fold_keys: bool) -> Self {
72        self.fold_keys = fold_keys;
73        self
74    }
75
76    /// Encode a value to TOON format
77    ///
78    /// When the `toon` feature is enabled, this produces compact TOON output.
79    /// When disabled, it falls back to pretty-printed JSON.
80    #[cfg(feature = "toon")]
81    pub fn encode<T: Serialize>(&self, value: &T) -> Result<String, ToonEncodeError> {
82        let options = self.build_options();
83        toon_format::encode(value, &options).map_err(ToonEncodeError::from)
84    }
85
86    /// Encode a value to TOON format (fallback to JSON when toon feature disabled)
87    #[cfg(not(feature = "toon"))]
88    pub fn encode<T: Serialize>(&self, value: &T) -> Result<String, ToonEncodeError> {
89        serde_json::to_string_pretty(value).map_err(ToonEncodeError::from)
90    }
91
92    /// Encode a value using default options
93    #[cfg(feature = "toon")]
94    pub fn encode_default<T: Serialize>(value: &T) -> Result<String, ToonEncodeError> {
95        encode_default(value).map_err(ToonEncodeError::from)
96    }
97
98    /// Encode a value using default options (fallback to JSON)
99    #[cfg(not(feature = "toon"))]
100    pub fn encode_default<T: Serialize>(value: &T) -> Result<String, ToonEncodeError> {
101        serde_json::to_string_pretty(value).map_err(ToonEncodeError::from)
102    }
103
104    /// Encode a JSON value to TOON format
105    pub fn encode_json(&self, value: &serde_json::Value) -> Result<String, ToonEncodeError> {
106        self.encode(value)
107    }
108
109    /// Build encoding options
110    #[cfg(feature = "toon")]
111    fn build_options(&self) -> EncodeOptions {
112        use toon_format::types::{Delimiter, KeyFoldingMode};
113        let mut options = EncodeOptions::default();
114
115        if self.use_tabs {
116            options = options.with_delimiter(Delimiter::Tab);
117        }
118
119        if self.fold_keys {
120            options = options.with_key_folding(KeyFoldingMode::Safe);
121        }
122
123        options
124    }
125}
126
127/// Error type for TOON encoding
128#[derive(Debug)]
129pub enum ToonEncodeError {
130    /// Error during TOON encoding
131    #[cfg(feature = "toon")]
132    Toon(ToonError),
133    /// Error during JSON serialization (fallback mode)
134    Json(serde_json::Error),
135}
136
137impl std::fmt::Display for ToonEncodeError {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        match self {
140            #[cfg(feature = "toon")]
141            ToonEncodeError::Toon(e) => write!(f, "TOON encoding error: {}", e),
142            ToonEncodeError::Json(e) => write!(f, "JSON encoding error: {}", e),
143        }
144    }
145}
146
147impl std::error::Error for ToonEncodeError {}
148
149#[cfg(feature = "toon")]
150impl From<ToonError> for ToonEncodeError {
151    fn from(err: ToonError) -> Self {
152        ToonEncodeError::Toon(err)
153    }
154}
155
156impl From<serde_json::Error> for ToonEncodeError {
157    fn from(err: serde_json::Error) -> Self {
158        ToonEncodeError::Json(err)
159    }
160}
161
162/// Convert a tool schema to TOON format for compact prompt inclusion
163///
164/// This is useful when describing tools in system prompts rather than
165/// using the provider's native tool API.
166#[cfg(feature = "toon")]
167pub fn tool_schema_to_toon(schema: &crate::tools::ToolSchema) -> Result<String, ToonEncodeError> {
168    use serde_json::json;
169
170    let value = json!({
171        "name": schema.name,
172        "description": schema.description,
173        "parameters": schema.parameters
174    });
175
176    ToonEncoder::encode_default(&value)
177}
178
179/// Convert a tool schema to TOON format (fallback)
180#[cfg(not(feature = "toon"))]
181pub fn tool_schema_to_toon(schema: &crate::tools::ToolSchema) -> Result<String, ToonEncodeError> {
182    serde_json::to_string_pretty(schema).map_err(ToonEncodeError::from)
183}
184
185/// Format tool call examples in TOON format for prompts
186///
187/// # Example
188///
189/// ```rust,ignore
190/// use agents_core::toon::format_tool_call_toon;
191///
192/// let example = format_tool_call_toon("search", &json!({"query": "rust", "limit": 10}));
193/// // Output:
194/// // tool: search
195/// // args:
196/// //   query: rust
197/// //   limit: 10
198/// ```
199#[cfg(feature = "toon")]
200pub fn format_tool_call_toon(
201    tool_name: &str,
202    args: &serde_json::Value,
203) -> Result<String, ToonEncodeError> {
204    use serde_json::json;
205
206    let value = json!({
207        "tool": tool_name,
208        "args": args
209    });
210
211    ToonEncoder::encode_default(&value)
212}
213
214/// Format tool call examples in TOON format (fallback to JSON)
215#[cfg(not(feature = "toon"))]
216pub fn format_tool_call_toon(
217    tool_name: &str,
218    args: &serde_json::Value,
219) -> Result<String, ToonEncodeError> {
220    use serde_json::json;
221
222    let value = json!({
223        "tool": tool_name,
224        "args": args
225    });
226
227    serde_json::to_string_pretty(&value).map_err(ToonEncodeError::from)
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use serde_json::json;
234
235    #[test]
236    fn test_encoder_basic() {
237        let encoder = ToonEncoder::new();
238        let data = json!({"name": "Alice", "age": 30});
239        let result = encoder.encode(&data).unwrap();
240        assert!(!result.is_empty());
241    }
242
243    #[test]
244    fn test_encoder_array() {
245        let encoder = ToonEncoder::new();
246        let data = json!({
247            "users": [
248                {"id": 1, "name": "Alice"},
249                {"id": 2, "name": "Bob"}
250            ]
251        });
252        let result = encoder.encode(&data).unwrap();
253        assert!(!result.is_empty());
254
255        #[cfg(feature = "toon")]
256        {
257            // TOON format uses compact tabular notation
258            assert!(result.contains("users"));
259        }
260    }
261
262    #[test]
263    fn test_encode_default() {
264        let data = json!({"key": "value"});
265        let result = ToonEncoder::encode_default(&data).unwrap();
266        assert!(!result.is_empty());
267    }
268
269    #[test]
270    fn test_format_tool_call() {
271        let result = format_tool_call_toon("search", &json!({"query": "rust"})).unwrap();
272        assert!(result.contains("search"));
273        assert!(result.contains("rust"));
274    }
275}