Skip to main content

vtcode_utility_tool_specs/
lib.rs

1//! Passive JSON schemas for utility, file, and scheduling tool surfaces.
2
3use serde_json::{Value, json};
4
5mod json_schema;
6mod mcp_tool;
7mod responses_api;
8
9pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
10pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
11pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
12
13pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
14    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
15pub const APPLY_PATCH_ALIAS_DESCRIPTION: &str = "Alias for input";
16pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str = "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch";
17
18#[must_use]
19pub fn with_semantic_anchor_guidance(base: &str) -> String {
20    let trimmed = base.trim_end();
21    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
22        trimmed.to_string()
23    } else if trimmed.ends_with('.') {
24        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
25    } else {
26        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
27    }
28}
29
30#[must_use]
31pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
32    json!({
33        "type": "object",
34        "properties": {
35            "input": {
36                "type": "string",
37                "description": with_semantic_anchor_guidance(input_description)
38            },
39            "patch": {
40                "type": "string",
41                "description": APPLY_PATCH_ALIAS_DESCRIPTION
42            }
43        },
44        "anyOf": [
45            {"required": ["input"]},
46            {"required": ["patch"]}
47        ]
48    })
49}
50
51#[must_use]
52pub fn apply_patch_parameters() -> Value {
53    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
54}
55
56#[must_use]
57pub fn cron_create_parameters() -> Value {
58    json!({
59        "type": "object",
60        "required": ["prompt"],
61        "properties": {
62            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
63            "name": {"type": "string", "description": "Optional short label for the task."},
64            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
65            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
66            "run_at": {
67                "type": "string",
68                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
69            }
70        }
71    })
72}
73
74#[must_use]
75pub fn cron_list_parameters() -> Value {
76    json!({
77        "type": "object",
78        "properties": {}
79    })
80}
81
82#[must_use]
83pub fn cron_delete_parameters() -> Value {
84    json!({
85        "type": "object",
86        "required": ["id"],
87        "properties": {
88            "id": {"type": "string", "description": "Session scheduled task id to delete."}
89        }
90    })
91}
92
93#[must_use]
94pub fn unified_exec_parameters() -> Value {
95    json!({
96        "type": "object",
97        "properties": {
98            "command": {
99                "description": "Command as a shell string or argv array.",
100                "anyOf": [
101                    {"type": "string"},
102                    {
103                        "type": "array",
104                        "items": {"type": "string"}
105                    }
106                ]
107            },
108            "input": {"type": "string", "description": "stdin for write or continue."},
109            "session_id": {"type": "string", "description": "Session id. Compact alias: `s`."},
110            "spool_path": {"type": "string", "description": "Spool path for inspect."},
111            "query": {"type": "string", "description": "Line filter for inspect or run output."},
112            "head_lines": {"type": "integer", "description": "Head preview lines."},
113            "tail_lines": {"type": "integer", "description": "Tail preview lines."},
114            "max_matches": {"type": "integer", "description": "Max filtered matches.", "default": 200},
115            "literal": {"type": "boolean", "description": "Treat query as literal text.", "default": false},
116            "code": {"type": "string", "description": "Raw Python or JavaScript source for `action=code`. Send the source directly, not JSON or markdown fences."},
117            "language": {
118                "type": "string",
119                "enum": ["python3", "javascript"],
120                "description": "Language for `action=code`. Defaults to `python3`; set `javascript` to run Node-based code execution instead.",
121                "default": "python3"
122            },
123            "action": {
124                "type": "string",
125                "enum": ["run", "write", "poll", "continue", "inspect", "list", "close", "code"],
126                "description": "Optional; inferred from command/code/input/session_id/spool_path. Use `code` to run a fresh Python or JavaScript snippet through the local code executor."
127            },
128            "workdir": {"type": "string", "description": "Working directory."},
129            "cwd": {"type": "string", "description": "Alias for workdir."},
130            "tty": {"type": "boolean", "description": "Use PTY mode.", "default": false},
131            "shell": {"type": "string", "description": "Shell binary."},
132            "login": {"type": "boolean", "description": "Use a login shell.", "default": false},
133            "sandbox_permissions": {
134                "type": "string",
135                "enum": ["use_default", "with_additional_permissions", "require_escalated"],
136                "description": "Sandbox mode. Use `require_escalated` only when needed."
137            },
138            "additional_permissions": {
139                "type": "object",
140                "description": "Extra sandboxed filesystem access.",
141                "properties": {
142                    "fs_read": {
143                        "type": "array",
144                        "items": {"type": "string"},
145                        "description": "Extra readable paths."
146                    },
147                    "fs_write": {
148                        "type": "array",
149                        "items": {"type": "string"},
150                        "description": "Extra writable paths."
151                    }
152                },
153                "additionalProperties": false
154            },
155            "justification": {"type": "string", "description": "Approval question for `require_escalated`."},
156            "prefix_rule": {
157                "type": "array",
158                "items": {"type": "string"},
159                "description": "Optional persisted approval prefix for `command`."
160            },
161            "timeout_secs": {"type": "integer", "description": "Timeout seconds.", "default": 180},
162            "yield_time_ms": {"type": "integer", "description": "Wait before returning output (ms).", "default": 1000},
163            "confirm": {"type": "boolean", "description": "Confirm destructive ops.", "default": false},
164            "max_output_tokens": {"type": "integer", "description": "Output token cap."},
165            "track_files": {"type": "boolean", "description": "Track file changes.", "default": false}
166        }
167    })
168}
169
170#[must_use]
171pub fn unified_file_parameters() -> Value {
172    json!({
173        "type": "object",
174        "properties": {
175            "action": {
176                "type": "string",
177                "enum": ["read", "write", "edit", "patch", "delete", "move", "copy"],
178                "description": "Optional; inferred from old_str/patch/content/destination/path."
179            },
180            "path": {"type": "string", "description": "File path. Compact alias: `p`."},
181            "content": {"type": "string", "description": "Content for write."},
182            "old_str": {"type": "string", "description": "Exact text to replace for edit."},
183            "new_str": {"type": "string", "description": "Replacement text for edit."},
184            "patch": {"type": "string", "description": "Patch text in `*** Update File:` format, not unified diff."},
185            "destination": {"type": "string", "description": "Destination for move or copy."},
186            "start_line": {"type": "integer", "description": "Read start line (1-indexed)."},
187            "end_line": {"type": "integer", "description": "Read end line (inclusive)."},
188            "offset": {"type": "integer", "description": "Read start line. Compact alias: `o`."},
189            "limit": {"type": "integer", "description": "Read line count. Compact alias: `l`."},
190            "mode": {"type": "string", "description": "Read mode or write mode.", "default": "slice"},
191            "condense": {"type": "boolean", "description": "Condense long output.", "default": true},
192            "indentation": {
193                "description": "Indentation config. `true` uses defaults.",
194                "anyOf": [
195                    {"type": "boolean"},
196                    {
197                        "type": "object",
198                        "properties": {
199                            "anchor_line": {"type": "integer", "description": "Anchor line; defaults to offset."},
200                            "max_levels": {"type": "integer", "description": "Indent depth cap; 0 means unlimited."},
201                            "include_siblings": {"type": "boolean", "description": "Include sibling blocks."},
202                            "include_header": {"type": "boolean", "description": "Include header lines."},
203                            "max_lines": {"type": "integer", "description": "Optional line cap."}
204                        },
205                        "additionalProperties": false
206                    }
207                ]
208            }
209        }
210    })
211}
212
213#[must_use]
214pub fn read_file_parameters() -> Value {
215    json!({
216        "type": "object",
217        "properties": {
218            "path": {"type": "string", "description": "File path. Accepts file_path/filepath/target_path/p."},
219            "offset": {"type": "integer", "description": "1-indexed line offset. Compact alias: `o`.", "minimum": 1},
220            "limit": {"type": "integer", "description": "Max lines for this chunk. Compact alias: `l`.", "minimum": 1},
221            "mode": {"type": "string", "enum": ["slice", "indentation"], "description": "Read mode.", "default": "slice"},
222            "indentation": {
223                "description": "Indentation-aware block selection.",
224                "anyOf": [
225                    {"type": "boolean"},
226                    {
227                        "type": "object",
228                        "properties": {
229                            "anchor_line": {"type": "integer", "description": "Anchor line; defaults to offset."},
230                            "max_levels": {"type": "integer", "description": "Indent depth cap; 0 means unlimited."},
231                            "include_siblings": {"type": "boolean", "description": "Include sibling blocks."},
232                            "include_header": {"type": "boolean", "description": "Include header lines."},
233                            "max_lines": {"type": "integer", "description": "Optional line cap."}
234                        },
235                        "additionalProperties": false
236                    }
237                ]
238            },
239            "offset_lines": {"type": "integer", "description": "Legacy alias for line offset.", "minimum": 1},
240            "page_size_lines": {"type": "integer", "description": "Legacy alias for line chunk size.", "minimum": 1},
241            "offset_bytes": {"type": "integer", "description": "Byte offset for binary or byte-paged reads.", "minimum": 0},
242            "page_size_bytes": {"type": "integer", "description": "Byte page size for binary or byte-paged reads.", "minimum": 1},
243            "max_bytes": {"type": "integer", "description": "Maximum bytes to return.", "minimum": 1},
244            "max_lines": {"type": "integer", "description": "Maximum lines to return in legacy mode.", "minimum": 1},
245            "chunk_lines": {"type": "integer", "description": "Legacy alias for chunk size in lines.", "minimum": 1},
246            "max_tokens": {"type": "integer", "description": "Optional token budget for large reads.", "minimum": 1},
247            "condense": {"type": "boolean", "description": "Condense long outputs to head/tail.", "default": true}
248        }
249    })
250}
251
252#[must_use]
253pub fn unified_search_parameters() -> Value {
254    json!({
255        "type": "object",
256        "properties": {
257            "action": {
258                "type": "string",
259                "enum": ["grep", "list", "structural", "tools", "errors", "agent", "web", "skill"],
260                "description": "Action to perform. Default to `structural` for code or syntax-aware search, including read-only ast-grep `run` query, project scan, and project test workflows; use `grep` for raw text and `list` for file discovery. Refine and retry `grep` or `structural` here before switching tools."
261            },
262            "workflow": {
263                "type": "string",
264                "enum": ["query", "scan", "test"],
265                "description": "Structural workflow. `query` is the default parseable-pattern search and maps to read-only ast-grep `run`; `scan` maps to read-only ast-grep `scan` from config, and `test` runs ast-grep rule tests.",
266                "default": "query"
267            },
268            "pattern": {"type": "string", "description": "For `grep` or `errors`, regex or literal text. For `list`, a glob filter for returned paths or names; nested globs such as `**/*.rs` promote `list` to recursive discovery. For `structural` `workflow=\"query\"`, valid parseable code for the selected language using ast-grep pattern syntax, not a raw code fragment; `$VAR` matches one named node, `$$$ARGS` matches zero or more nodes, `$$VAR` includes unnamed nodes, and `$_` suppresses capture. If a fragment fails, retry `action='structural'` with a larger parseable pattern such as a full function signature."},
269            "path": {"type": "string", "description": "Directory or file path to search in. Used by `grep`, `list`, and structural `workflow=\"query\"|\"scan\"`. Public structural calls take one root per request even though raw ast-grep `run` can accept multiple paths.", "default": "."},
270            "config_path": {"type": "string", "description": "Ast-grep config path for structural `workflow=\"scan\"` or `workflow=\"test\"`. Defaults to workspace `sgconfig.yml`."},
271            "filter": {"type": "string", "description": "Ast-grep rule or test filter for structural `workflow=\"scan\"` or `workflow=\"test\"`. On `scan`, this maps to `--filter` over rule ids from config."},
272            "lang": {"type": "string", "description": "Language for structural `workflow=\"query\"`. Set it whenever the code language is known; required for debug_query."},
273            "selector": {"type": "string", "description": "Ast-grep selector for structural `workflow=\"query\"` when the real match is a subnode inside the parseable pattern."},
274            "strictness": {
275                "type": "string",
276                "enum": ["cst", "smart", "ast", "relaxed", "signature", "template"],
277                "description": "Pattern strictness for structural `workflow=\"query\"`."
278            },
279            "debug_query": {
280                "type": "string",
281                "enum": ["pattern", "ast", "cst", "sexp"],
282                "description": "Print the structural query AST instead of matches for `workflow=\"query\"`. Requires lang."
283            },
284            "globs": {
285                "description": "Optional include/exclude globs for structural `workflow=\"query\"` or `workflow=\"scan\"`. Maps to repeated ast-grep `--globs` flags.",
286                "anyOf": [
287                    {"type": "string"},
288                    {"type": "array", "items": {"type": "string"}}
289                ]
290            },
291            "skip_snapshot_tests": {"type": "boolean", "description": "Skip ast-grep snapshot tests for structural `workflow=\"test\"`.", "default": false},
292            "keyword": {"type": "string", "description": "Keyword for 'tools' search."},
293            "url": {"type": "string", "format": "uri", "description": "The URL to fetch content from (for 'web' action)."},
294            "prompt": {"type": "string", "description": "The prompt to run on the fetched content (for 'web' action)."},
295            "name": {"type": "string", "description": "Skill name to load (for 'skill' action)."},
296            "detail_level": {
297                "type": "string",
298                "enum": ["name-only", "name-and-description", "full"],
299                "description": "Detail level for 'tools' action.",
300                "default": "name-and-description"
301            },
302            "mode": {
303                "type": "string",
304                "description": "Mode for 'list' (list|recursive|tree|etc) or 'agent' (debug|analyze|full) action.",
305                "default": "list"
306            },
307            "max_results": {"type": "integer", "description": "Max results to return.", "default": 100},
308            "case_sensitive": {"type": "boolean", "description": "Case-sensitive search.", "default": false},
309            "context_lines": {"type": "integer", "description": "Context lines for `grep` or structural `workflow=\"query\"|\"scan\"` results. Structural maps this to ast-grep `--context`; raw `--before` and `--after` are not exposed separately.", "default": 0},
310            "scope": {"type": "string", "description": "Scope for 'errors' action (archive|all).", "default": "archive"},
311            "max_bytes": {"type": "integer", "description": "Maximum bytes to fetch for 'web' action.", "default": 500000},
312            "timeout_secs": {"type": "integer", "description": "Timeout in seconds.", "default": 30}
313        }
314    })
315}
316
317#[must_use]
318pub fn list_files_parameters() -> Value {
319    json!({
320        "type": "object",
321        "properties": {
322            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
323            "mode": {
324                "type": "string",
325                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
326                "description": "Listing mode. Use page/per_page to continue paginated results.",
327                "default": "list"
328            },
329            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
330            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
331            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
332            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
333            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
334            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
335            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
336            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
337            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
338        }
339    })
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use serde_json::json;
346
347    #[test]
348    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
349        let schema = apply_patch_parameter_schema("Patch in VT Code format");
350
351        assert_eq!(
352            schema["properties"]["patch"]["description"],
353            APPLY_PATCH_ALIAS_DESCRIPTION
354        );
355        let input_description = schema["properties"]["input"]["description"]
356            .as_str()
357            .expect("input description");
358        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
359    }
360
361    #[test]
362    fn unified_exec_schema_accepts_string_or_array_commands() {
363        let params = unified_exec_parameters();
364        let command = &params["properties"]["command"];
365        let variants = command["anyOf"].as_array().expect("command anyOf");
366
367        assert_eq!(variants.len(), 2);
368        assert_eq!(variants[0]["type"], "string");
369        assert_eq!(variants[1]["type"], "array");
370        assert_eq!(variants[1]["items"]["type"], "string");
371        assert_eq!(params["properties"]["tty"]["type"], "boolean");
372        assert_eq!(params["properties"]["tty"]["default"], false);
373        assert!(
374            params["properties"]["code"]["description"]
375                .as_str()
376                .expect("code description")
377                .contains("Raw Python or JavaScript source")
378        );
379        assert!(
380            params["properties"]["language"]["description"]
381                .as_str()
382                .expect("language description")
383                .contains("set `javascript`")
384        );
385    }
386
387    #[test]
388    fn unified_search_schema_advertises_structural_and_hides_intelligence() {
389        let params = unified_search_parameters();
390        let actions = params["properties"]["action"]["enum"]
391            .as_array()
392            .expect("action enum");
393
394        assert!(actions.iter().any(|value| value == "structural"));
395        assert!(!actions.iter().any(|value| value == "intelligence"));
396        assert!(
397            params["properties"]["debug_query"]["enum"]
398                .as_array()
399                .expect("debug_query enum")
400                .iter()
401                .any(|value| value == "ast")
402        );
403        assert!(
404            params["properties"]["action"]["description"]
405                .as_str()
406                .expect("action description")
407                .contains("Default to `structural`")
408        );
409        assert!(
410            params["properties"]["pattern"]["description"]
411                .as_str()
412                .expect("pattern description")
413                .contains("valid parseable code")
414        );
415        assert!(
416            params["properties"]["pattern"]["description"]
417                .as_str()
418                .expect("pattern description")
419                .contains("$$$ARGS")
420        );
421        assert!(
422            params["properties"]["pattern"]["description"]
423                .as_str()
424                .expect("pattern description")
425                .contains("glob filter")
426        );
427        assert!(
428            params["properties"]["action"]["description"]
429                .as_str()
430                .expect("action description")
431                .contains("Refine and retry `grep` or `structural`")
432        );
433        assert_eq!(params["properties"]["workflow"]["enum"][1], "scan");
434        assert_eq!(params["properties"]["workflow"]["enum"][2], "test");
435        assert!(
436            params["properties"]["config_path"]["description"]
437                .as_str()
438                .expect("config path description")
439                .contains("Defaults to workspace `sgconfig.yml`")
440        );
441        assert!(
442            params["properties"]["skip_snapshot_tests"]["description"]
443                .as_str()
444                .expect("skip snapshot description")
445                .contains("workflow=\"test\"")
446        );
447    }
448
449    #[test]
450    fn legacy_browse_tool_schemas_expose_chunking_and_pagination_fields() {
451        let read_params = read_file_parameters();
452        assert!(read_params["properties"]["offset"].is_object());
453        assert!(read_params["properties"]["limit"].is_object());
454        assert!(read_params["properties"]["page_size_lines"].is_object());
455
456        let list_params = list_files_parameters();
457        assert!(list_params["properties"]["page"].is_object());
458        assert!(list_params["properties"]["per_page"].is_object());
459        assert!(
460            list_params["properties"]["mode"]["enum"]
461                .as_array()
462                .expect("mode enum")
463                .iter()
464                .any(|value| value == "recursive")
465        );
466    }
467
468    #[test]
469    fn semantic_anchor_guidance_is_appended_once() {
470        let base = "Patch in VT Code format.";
471        let with_guidance = with_semantic_anchor_guidance(base);
472
473        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
474        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
475    }
476
477    #[test]
478    fn default_apply_patch_parameters_keep_expected_alias_shape() {
479        let schema = apply_patch_parameters();
480
481        assert_eq!(
482            schema["anyOf"],
483            json!([
484                {"required": ["input"]},
485                {"required": ["patch"]}
486            ])
487        );
488    }
489}