Skip to main content

edgecrab_types/
tool.rs

1//! Tool call types for LLM function calling.
2
3use serde::{Deserialize, Serialize};
4
5/// A tool call requested by the LLM (OpenAI function-calling format).
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct ToolCall {
8    pub id: String,
9    pub r#type: String,
10    pub function: FunctionCall,
11    /// Gemini 3.x thought signature. Opaque blob that must round-trip through
12    /// conversation history so Gemini can resume its reasoning state after a
13    /// function call.  None for all non-Gemini providers.
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub thought_signature: Option<String>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct FunctionCall {
20    pub name: String,
21    /// JSON-encoded arguments string
22    pub arguments: String,
23}
24
25impl ToolCall {
26    /// Parse the JSON arguments string into a Value.
27    pub fn parsed_args(&self) -> std::result::Result<serde_json::Value, serde_json::Error> {
28        serde_json::from_str(&self.function.arguments)
29    }
30
31    /// Convert from edgequake-llm's ToolCall into our internal type.
32    ///
33    /// WHY a manual conversion: edgequake-llm uses `call_type` while we
34    /// use `r#type` (matching OpenAI's raw JSON key). Both represent the
35    /// same concept so this is a straightforward field rename.
36    pub fn from_llm(tc: &edgequake_llm::ToolCall) -> Self {
37        Self {
38            id: tc.id.clone(),
39            r#type: tc.call_type.clone(),
40            function: FunctionCall {
41                name: tc.function.name.clone(),
42                arguments: tc.function.arguments.clone(),
43            },
44            thought_signature: tc.thought_signature.clone(),
45        }
46    }
47
48    /// Convert to edgequake-llm's ToolCall for ChatMessage construction.
49    pub fn to_llm(&self) -> edgequake_llm::ToolCall {
50        edgequake_llm::ToolCall {
51            id: self.id.clone(),
52            call_type: self.r#type.clone(),
53            function: edgequake_llm::FunctionCall {
54                name: self.function.name.clone(),
55                arguments: self.function.arguments.clone(),
56            },
57            thought_signature: self.thought_signature.clone(),
58        }
59    }
60}
61
62/// Tool schema in OpenAI function-calling format — sent to the LLM.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64pub struct ToolSchema {
65    pub name: String,
66    pub description: String,
67    pub parameters: serde_json::Value,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub strict: Option<bool>,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn tool_call_roundtrip() {
78        let tc = ToolCall {
79            id: "call_abc123".into(),
80            r#type: "function".into(),
81            function: FunctionCall {
82                name: "read_file".into(),
83                arguments: r#"{"path":"src/lib.rs","line_start":1}"#.into(),
84            },
85            thought_signature: None,
86        };
87        let json = serde_json::to_string(&tc).expect("serialize");
88        let deser: ToolCall = serde_json::from_str(&json).expect("deserialize");
89        assert_eq!(tc, deser);
90    }
91
92    #[test]
93    fn parsed_args_valid_json() {
94        let tc = ToolCall {
95            id: "1".into(),
96            r#type: "function".into(),
97            function: FunctionCall {
98                name: "write_file".into(),
99                arguments: r#"{"path":"out.txt","content":"hello"}"#.into(),
100            },
101            thought_signature: None,
102        };
103        let args = tc.parsed_args().expect("valid json");
104        assert_eq!(args["path"], "out.txt");
105        assert_eq!(args["content"], "hello");
106    }
107
108    #[test]
109    fn parsed_args_invalid_json() {
110        let tc = ToolCall {
111            id: "1".into(),
112            r#type: "function".into(),
113            function: FunctionCall {
114                name: "bad".into(),
115                arguments: "not json".into(),
116            },
117            thought_signature: None,
118        };
119        assert!(tc.parsed_args().is_err());
120    }
121
122    #[test]
123    fn tool_schema_roundtrip() {
124        let schema = ToolSchema {
125            name: "read_file".into(),
126            description: "Read a file".into(),
127            parameters: serde_json::json!({
128                "type": "object",
129                "properties": {
130                    "path": { "type": "string" }
131                },
132                "required": ["path"]
133            }),
134            strict: Some(true),
135        };
136        let json = serde_json::to_string(&schema).expect("serialize");
137        let deser: ToolSchema = serde_json::from_str(&json).expect("deserialize");
138        assert_eq!(schema, deser);
139    }
140}