Skip to main content

mur_core/model/
schema.rs

1//! Structured output schemas for AI model tool calling.
2//!
3//! Uses `schemars` to generate JSON Schema definitions that constrain
4//! model outputs to well-typed Rust structs, ensuring type safety
5//! throughout the execution pipeline.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10/// A tool call request from the AI model to execute a command.
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
12pub struct ExecuteCommand {
13    /// The command to execute.
14    pub command: String,
15    /// Working directory for execution.
16    pub working_dir: Option<String>,
17    /// Whether this action needs user approval.
18    pub needs_approval: bool,
19}
20
21/// A tool call request to read a file.
22#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
23pub struct ReadFile {
24    /// Path to the file to read.
25    pub path: String,
26    /// Optional line range (start, end).
27    pub line_range: Option<(usize, usize)>,
28}
29
30/// A tool call request to write a file.
31#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
32pub struct WriteFile {
33    /// Path to the file to write.
34    pub path: String,
35    /// Content to write.
36    pub content: String,
37    /// If true, append instead of overwrite.
38    pub append: bool,
39}
40
41/// A tool call from the model — one of the defined tool types.
42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
43#[serde(tag = "tool", rename_all = "snake_case")]
44pub enum ToolCall {
45    ExecuteCommand(ExecuteCommand),
46    ReadFile(ReadFile),
47    WriteFile(WriteFile),
48}
49
50/// Generate a JSON Schema string for a given type.
51pub fn generate_schema<T: JsonSchema>() -> String {
52    let schema = schemars::schema_for!(T);
53    serde_json::to_string_pretty(&schema).unwrap_or_default()
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_tool_call_serialization() {
62        let call = ToolCall::ExecuteCommand(ExecuteCommand {
63            command: "cargo test".into(),
64            working_dir: Some("/project".into()),
65            needs_approval: false,
66        });
67
68        let json = serde_json::to_string(&call).unwrap();
69        assert!(json.contains("execute_command"));
70        assert!(json.contains("cargo test"));
71
72        let deserialized: ToolCall = serde_json::from_str(&json).unwrap();
73        assert!(matches!(deserialized, ToolCall::ExecuteCommand(_)));
74    }
75
76    #[test]
77    fn test_generate_schema() {
78        let schema = generate_schema::<ExecuteCommand>();
79        assert!(schema.contains("command"));
80        assert!(schema.contains("working_dir"));
81        assert!(schema.contains("needs_approval"));
82    }
83
84    #[test]
85    fn test_read_file_schema() {
86        let schema = generate_schema::<ReadFile>();
87        assert!(schema.contains("path"));
88        assert!(schema.contains("line_range"));
89    }
90
91    #[test]
92    fn test_write_file_tool_call() {
93        let call = ToolCall::WriteFile(WriteFile {
94            path: "/tmp/test.txt".into(),
95            content: "hello world".into(),
96            append: false,
97        });
98        let json = serde_json::to_string(&call).unwrap();
99        assert!(json.contains("write_file"));
100
101        let deserialized: ToolCall = serde_json::from_str(&json).unwrap();
102        if let ToolCall::WriteFile(wf) = deserialized {
103            assert_eq!(wf.path, "/tmp/test.txt");
104            assert!(!wf.append);
105        } else {
106            panic!("Expected WriteFile variant");
107        }
108    }
109}