Skip to main content

agent_core_runtime/client/providers/
common.rs

1//! Common utilities shared across LLM providers.
2
3use crate::client::models::{Content, Message};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6/// Global counter for generating unique IDs.
7static ID_COUNTER: AtomicU64 = AtomicU64::new(0);
8
9/// Generate a unique ID for tool calls.
10/// Uses an atomic counter combined with timestamp for uniqueness across restarts.
11pub fn generate_unique_id(prefix: &str) -> String {
12    use std::time::{SystemTime, UNIX_EPOCH};
13    let counter = ID_COUNTER.fetch_add(1, Ordering::SeqCst);
14    let timestamp = SystemTime::now()
15        .duration_since(UNIX_EPOCH)
16        .unwrap_or_default()
17        .as_millis();
18    format!("{}{}_{}", prefix, timestamp, counter)
19}
20
21/// Escapes special characters for JSON string values.
22pub fn escape_json_string(s: &str) -> String {
23    let mut result = String::with_capacity(s.len());
24    for c in s.chars() {
25        match c {
26            '"' => result.push_str(r#"\""#),
27            '\\' => result.push_str(r#"\\"#),
28            '\n' => result.push_str(r#"\n"#),
29            '\r' => result.push_str(r#"\r"#),
30            '\t' => result.push_str(r#"\t"#),
31            c if c.is_control() => {
32                result.push_str(&format!(r#"\u{:04x}"#, c as u32));
33            }
34            c => result.push(c),
35        }
36    }
37    result
38}
39
40/// Extract text content from a message, joining multiple text blocks with newlines.
41pub fn extract_text_content(msg: &Message) -> String {
42    msg.content
43        .iter()
44        .filter_map(|c| match c {
45            Content::Text(t) => Some(t.as_str()),
46            Content::Image(_) | Content::ToolUse(_) | Content::ToolResult(_) => None,
47        })
48        .collect::<Vec<_>>()
49        .join("\n")
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn test_escape_json_string() {
58        assert_eq!(escape_json_string(r#"hello"world"#), r#"hello\"world"#);
59        assert_eq!(escape_json_string("line1\nline2"), r#"line1\nline2"#);
60        assert_eq!(escape_json_string("tab\there"), r#"tab\there"#);
61        assert_eq!(escape_json_string(r#"back\slash"#), r#"back\\slash"#);
62    }
63
64    #[test]
65    fn test_generate_unique_id() {
66        let id1 = generate_unique_id("test_");
67        let id2 = generate_unique_id("test_");
68        let id3 = generate_unique_id("other_");
69
70        assert!(id1.starts_with("test_"));
71        assert!(id2.starts_with("test_"));
72        assert!(id3.starts_with("other_"));
73        assert_ne!(id1, id2); // Should be unique
74    }
75
76    #[test]
77    fn test_extract_text_content() {
78        let msg = Message::user("Hello world");
79        assert_eq!(extract_text_content(&msg), "Hello world");
80    }
81
82    #[test]
83    fn test_extract_text_content_multiple() {
84        let msg = Message::with_content(
85            crate::client::models::Role::User,
86            vec![
87                Content::Text("First".to_string()),
88                Content::Text("Second".to_string()),
89            ],
90        );
91        assert_eq!(extract_text_content(&msg), "First\nSecond");
92    }
93}