1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[cfg(feature = "schemars")]
5use schemars::{schema_for, JsonSchema};
6
7#[cfg(feature = "schemars")]
13pub fn tool_parameters<T: JsonSchema>() -> Value {
14 let mut v = serde_json::to_value(schema_for!(T)).expect("can't parse value from schema");
15
16 if let Some(value) = v.as_object_mut() {
18 value.remove("$schema");
19 value.remove("title");
20 }
21
22 let mut v_str = serde_json::to_string(&v).unwrap();
23 v_str = v_str
24 .replace("/definitions/", "/$defs/")
25 .replace("\"definitions\":", "\"$defs\":");
26
27 v_str = v_str.replace("\"oneOf\":", "\"anyOf\":");
29
30 let mut v: Value = serde_json::from_str(&v_str).expect("can't parse value from updated schema");
31 enforce_openai_strict_schema(&mut v);
32 v
33}
34
35#[cfg(feature = "schemars")]
36fn enforce_openai_strict_schema(v: &mut Value) {
37 match v {
38 Value::Object(map) => {
39 for (_k, child) in map.iter_mut() {
41 enforce_openai_strict_schema(child);
42 }
43
44 let is_object = map
46 .get("type")
47 .and_then(|t| t.as_str())
48 .is_some_and(|t| t == "object");
49 let has_props = map.get("properties").is_some();
50 if is_object || has_props {
51 map.entry("additionalProperties".to_string())
52 .or_insert(Value::Bool(false));
53
54 if let Some(Value::Object(props)) = map.get("properties") {
55 let mut keys: Vec<String> = props.keys().cloned().collect();
56 keys.sort();
57 map.insert(
58 "required".to_string(),
59 Value::Array(keys.into_iter().map(Value::String).collect()),
60 );
61 }
62 }
63 }
64 Value::Array(arr) => {
65 for child in arr.iter_mut() {
66 enforce_openai_strict_schema(child);
67 }
68 }
69 _ => {}
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[cfg_attr(feature = "schemars", derive(JsonSchema))]
75pub struct ReadFileArgs {
76 pub path: String,
78 pub offset: Option<usize>,
80 pub limit: Option<usize>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[cfg_attr(feature = "schemars", derive(JsonSchema))]
86pub struct ListDirArgs {
87 pub path: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[cfg_attr(feature = "schemars", derive(JsonSchema))]
93pub struct GrepArgs {
94 pub pattern: String,
96 pub path: Option<String>,
98 pub glob: Option<String>,
100 pub head_limit: Option<usize>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[cfg_attr(feature = "schemars", derive(JsonSchema))]
106pub struct RunShellArgs {
107 pub command: String,
109 pub cwd: Option<String>,
111 pub timeout_ms: Option<u64>,
113 pub max_output_bytes: Option<u64>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[cfg_attr(feature = "schemars", derive(JsonSchema))]
119pub struct ApplyDiffArgs {
120 pub diff: String,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "schemars", derive(JsonSchema))]
130pub struct DeleteFilesArgs {
131 pub paths: Vec<String>,
133}
134
135#[cfg(feature = "schemars")]
137pub fn openai_tools() -> Vec<Value> {
138 vec![
139 serde_json::json!({
140 "type": "function",
141 "name": "read_file",
142 "description": "Read a local file (by path), optionally with offset/limit. Returns a JSON string with keys: path, offset, limit, total_lines, content, numbered_content, fingerprint{hash64,len_bytes}, truncated.",
143 "strict": true,
144 "parameters": tool_parameters::<ReadFileArgs>(),
145 }),
146 serde_json::json!({
147 "type": "function",
148 "name": "list_dir",
149 "description": "List a local directory (by path). Returns a JSON string: { path, entries: [{ name, is_dir, is_file }, ...] }.",
150 "strict": true,
151 "parameters": tool_parameters::<ListDirArgs>(),
152 }),
153 serde_json::json!({
154 "type": "function",
155 "name": "grep",
156 "description": "Search for a regex pattern in files. Returns a JSON string including matches (file, line_number, line). May be truncated to head_limit.",
157 "strict": true,
158 "parameters": tool_parameters::<GrepArgs>(),
159 }),
160 serde_json::json!({
161 "type": "function",
162 "name": "run_shell",
163 "description": "Run a shell command via `bash -lc` (supports pipes/redirection). Requires user confirmation. Returns a JSON string: { command, exit_code, timed_out, duration_ms, stdout, stderr, stdout_truncated, stderr_truncated }.",
164 "strict": true,
165 "parameters": tool_parameters::<RunShellArgs>(),
166 }),
167 serde_json::json!({
168 "type": "function",
169 "name": "apply_diff",
170 "description": "Apply a git-style unified diff to the local working tree (create/update files). Returns a JSON string describing what changed or an error.",
171 "strict": true,
172 "parameters": tool_parameters::<ApplyDiffArgs>(),
173 }),
174 serde_json::json!({
175 "type": "function",
176 "name": "delete_files",
177 "description": "Delete one or more files by path (relative to project root). Returns a JSON string listing deleted and missing paths.",
178 "strict": true,
179 "parameters": tool_parameters::<DeleteFilesArgs>(),
180 }),
181 ]
182}
183