Skip to main content

vtcode_utility_tool_specs/
lib.rs

1#![allow(missing_docs)]
2//! Passive JSON schemas for utility, file, scheduling, and collaboration tool surfaces.
3
4#![recursion_limit = "256"]
5
6use serde_json::{Value, json};
7
8mod collaboration;
9mod json_schema;
10#[cfg(feature = "mcp")]
11mod mcp_tool;
12mod responses_api;
13
14pub use collaboration::{
15    agent_parameters, close_agent_parameters, request_user_input_description,
16    request_user_input_parameters, resume_agent_parameters, send_input_parameters,
17    spawn_agent_parameters, spawn_background_subprocess_parameters, wait_agent_parameters,
18};
19pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
20#[cfg(feature = "mcp")]
21pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
22pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
23
24pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
25    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
26
27/// Explicit, format-bearing description for the `patch` alias field. The old
28/// value ("Alias for input") gave the model no format guidance, so it often
29/// placed a standard unified diff (`---`/`+++`) there — which `apply_patch`
30/// rejects. This mirrors the `input` description so both alias fields carry
31/// identical, complete format guidance (see checkpoint turn_615 for the
32/// failure this prevents).
33pub const APPLY_PATCH_ALIAS_DESCRIPTION: &str = "Patch in VT Code format (*** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch). Same envelope as 'input'; do NOT use unified diff (--- /+++ format).";
34pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str = "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch";
35
36#[must_use]
37pub fn with_semantic_anchor_guidance(base: &str) -> String {
38    let trimmed = base.trim_end();
39    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
40        trimmed.to_string()
41    } else if trimmed.ends_with('.') {
42        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
43    } else {
44        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
45    }
46}
47
48#[must_use]
49pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
50    json!({
51        "type": "object",
52        "properties": {
53            "input": {
54                "type": "string",
55                "description": with_semantic_anchor_guidance(input_description)
56            },
57            "patch": {
58                "type": "string",
59                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
60            }
61        },
62        "anyOf": [
63            {"required": ["input"]},
64            {"required": ["patch"]}
65        ]
66    })
67}
68
69#[must_use]
70pub fn apply_patch_parameters() -> Value {
71    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
72}
73
74#[must_use]
75pub fn cron_parameters() -> Value {
76    json!({
77        "type": "object",
78        "required": ["action"],
79        "additionalProperties": false,
80        "properties": {
81            "action": {
82                "type": "string",
83                "enum": ["create", "list", "delete"],
84                "description": "create: schedule a prompt (requires prompt and exactly one of cron, delay_minutes, or run_at). list: show scheduled prompts. delete: remove one by id."
85            },
86            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
87            "name": {"type": "string", "description": "create: optional short label for the task."},
88            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
89            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
90            "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
91            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
92        }
93    })
94}
95
96#[must_use]
97pub fn mcp_parameters() -> Value {
98    json!({
99        "type": "object",
100        "required": ["action"],
101        "properties": {
102            "action": {
103                "type": "string",
104                "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
105                "description": "search_tools: find MCP tools by natural-language query. get_tool_details: fetch the full input schema for one MCP tool name. list_servers: list configured servers and their connection state. connect or disconnect: manage one configured MCP server by name."
106            },
107            "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
108            "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
109            "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
110            "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
111        },
112        "additionalProperties": false
113    })
114}
115
116#[must_use]
117pub fn cron_create_parameters() -> Value {
118    json!({
119        "type": "object",
120        "required": ["prompt"],
121        "additionalProperties": false,
122        "properties": {
123            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
124            "name": {"type": "string", "description": "Optional short label for the task."},
125            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
126            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
127            "run_at": {
128                "type": "string",
129                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
130            }
131        }
132    })
133}
134
135#[must_use]
136pub fn cron_list_parameters() -> Value {
137    json!({
138        "type": "object",
139        "properties": {},
140        "additionalProperties": false
141    })
142}
143
144#[must_use]
145pub fn cron_delete_parameters() -> Value {
146    json!({
147        "type": "object",
148        "required": ["id"],
149        "properties": {
150            "id": {"type": "string", "description": "Session scheduled task id to delete."}
151        }
152    })
153}
154
155#[must_use]
156pub fn exec_command_parameters() -> Value {
157    json!({
158        "type": "object",
159        "required": ["cmd"],
160        "properties": {
161            "cmd": {"type": "string", "description": "Shell command to execute, subject to command policy. Examples include `ls`, `rg`, `find`, `cat`, `sed`, `awk`, build tools, and test tools."},
162            "yield_time_ms": {"type": "integer", "description": "Wait before returning output (ms). If the command is still running, the response includes a session_id for write_stdin.", "default": 10000},
163            "max_output_tokens": {"type": "integer", "description": "Output token cap. Large or truncated output can return a spool_path for the full output."},
164            "workdir": {"type": "string", "description": "Working directory."},
165            "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
166            "sandbox_permissions": {
167                "type": "string",
168                "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
169                "description": "Sandbox permission mode for this command.",
170                "default": "use_default"
171            },
172            "additional_permissions": {
173                "type": "object",
174                "description": "Additional filesystem access requested with sandbox_permissions set to with_additional_permissions.",
175                "properties": {
176                    "fs_read": {"type": "array", "items": {"type": "string"}},
177                    "fs_write": {"type": "array", "items": {"type": "string"}}
178                },
179                "additionalProperties": false
180            },
181            "justification": {"type": "string", "description": "Short approval question required for escalated or bypassed sandbox execution."}
182        },
183        "additionalProperties": false
184    })
185}
186
187#[must_use]
188pub fn write_stdin_parameters() -> Value {
189    json!({
190        "type": "object",
191        "required": ["session_id", "chars"],
192        "properties": {
193            "session_id": {"type": "string", "description": "Active execution session id."},
194            "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
195            "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
196            "max_output_tokens": {"type": "integer", "description": "Output token cap for the continuation response. Large or truncated output can return a spool_path for the full output."}
197        },
198        "additionalProperties": false
199    })
200}
201
202#[must_use]
203pub fn code_search_parameters() -> Value {
204    json!({
205        "type": "object",
206        "required": ["action"],
207        "additionalProperties": false,
208        "properties": {
209            "action": {
210                "type": "string",
211                "enum": ["structural", "outline"],
212                "description": "Semantic code search action: structural for ast-grep pattern search, or outline for Tree-sitter symbol maps. Use exec_command.cmd with rg for plain text search."
213            },
214            "workflow": {
215                "type": "string",
216                "enum": ["query", "scan", "test"],
217                "description": "Structural workflow for ast-grep.",
218                "default": "query"
219            },
220            "pattern": {"type": "string", "description": "Ast-grep pattern such as $VAR or $$$ARGS for structural search."},
221            "kind": {"type": "string", "description": "Ast-grep node kind, such as function_item or call_expression."},
222            "path": {"type": "string", "description": "Directory or file path to search or outline.", "default": "."},
223            "config_path": {"type": "string", "description": "Ast-grep config path for scan or test workflows. Defaults to workspace sgconfig.yml."},
224            "filter": {"type": "string", "description": "Ast-grep rule or test filter for scan or test workflows."},
225            "lang": {"type": "string", "description": "Language for structural search or outline. Set this when the language is known."},
226            "selector": {"type": "string", "description": "Ast-grep selector when the match is a subnode."},
227            "strictness": {
228                "type": "string",
229                "enum": ["cst", "smart", "ast", "relaxed", "signature", "template"],
230                "description": "Pattern strictness for structural query workflow."
231            },
232            "view": {
233                "type": "string",
234                "enum": ["digest", "names", "full"],
235                "description": "Output shape for outline results.",
236                "default": "digest"
237            },
238            "items": {
239                "type": "string",
240                "enum": ["auto", "structure", "exports", "imports", "all"],
241                "description": "Which top-level symbols outline includes.",
242                "default": "auto"
243            },
244            "type": {
245                "description": "Symbol types to keep in outline.",
246                "anyOf": [
247                    {"type": "string"},
248                    {"type": "array", "items": {"type": "string"}}
249                ]
250            },
251            "match": {"type": "string", "description": "Regex for outline to filter item names, signatures, or first lines."},
252            "pub_members": {"type": "boolean", "description": "In outline, show only public members.", "default": false},
253            "follow": {"type": "boolean", "description": "Follow symbolic links while traversing directories.", "default": false},
254            "debug_query": {
255                "type": "string",
256                "enum": ["pattern", "ast", "cst", "sexp"],
257                "description": "Print the structural query AST instead of matches. Requires lang."
258            },
259            "globs": {
260                "description": "Optional include or exclude globs for structural workflows.",
261                "anyOf": [
262                    {"type": "string"},
263                    {"type": "array", "items": {"type": "string"}}
264                ]
265            },
266            "skip_snapshot_tests": {"type": "boolean", "description": "Skip ast-grep snapshot tests for test workflow.", "default": false},
267            "max_results": {"type": "integer", "description": "Maximum results to return.", "default": 100},
268            "context_lines": {"type": "integer", "description": "Context lines for structural results.", "default": 0},
269            "severities": {
270                "type": "array",
271                "items": {"type": "string", "enum": ["error", "warning", "info", "hint"]},
272                "description": "Post-run severity filter for structural scan workflow."
273            },
274            "no_ignore": {
275                "type": "array",
276                "items": {"type": "string", "enum": ["hidden", "dot", "exclude", "global", "parent", "vcs"]},
277                "description": "Ignore file overrides."
278            },
279            "threads": {"type": "integer", "description": "Number of threads for ast-grep scan parallelism. 0 means auto.", "minimum": 0, "maximum": 256, "default": 0},
280            "format": {"type": "string", "enum": ["github", "sarif", "files_with_matches", "count"], "description": "Output format for structural scan workflow."},
281            "report_style": {"type": "string", "enum": ["rich", "medium", "short"], "description": "Diagnostic report style for structural scan workflow."},
282            "before_lines": {"type": "integer", "description": "Context lines before each structural match.", "minimum": 0, "maximum": 20},
283            "after_lines": {"type": "integer", "description": "Context lines after each structural match.", "minimum": 0, "maximum": 20},
284            "builtin_rules": {
285                "type": "array",
286                "items": {"type": "string"},
287                "description": "Built-in ast-grep rules to activate for structural scan workflow."
288            }
289        }
290    })
291}
292
293#[must_use]
294pub fn list_files_parameters() -> Value {
295    json!({
296        "type": "object",
297        "properties": {
298            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
299            "mode": {
300                "type": "string",
301                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
302                "description": "Listing mode. Use page/per_page to continue paginated results.",
303                "default": "list"
304            },
305            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
306            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
307            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
308            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
309            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
310            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
311            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
312            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
313            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
314        }
315    })
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use serde_json::json;
322
323    #[test]
324    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
325        let schema = apply_patch_parameter_schema("Patch in VT Code format");
326
327        // Both `input` and `patch` alias fields now carry the format
328        // description AND the semantic-anchor guidance, preventing the model
329        // from placing a unified diff in `patch` (see checkpoint turn_615).
330        assert_eq!(
331            schema["properties"]["patch"]["description"],
332            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
333        );
334        let patch_description = schema["properties"]["patch"]["description"]
335            .as_str()
336            .expect("patch description");
337        assert!(patch_description.contains("*** Begin Patch"));
338        assert!(patch_description.contains("unified diff"));
339        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
340
341        let input_description = schema["properties"]["input"]["description"]
342            .as_str()
343            .expect("input description");
344        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
345    }
346
347    #[test]
348    fn codex_baseline_exec_schemas_use_public_names_shape() {
349        let exec_params = exec_command_parameters();
350        assert_eq!(exec_params["required"], json!(["cmd"]));
351        assert!(exec_params["properties"]["cmd"].is_object());
352        assert!(exec_params["properties"]["workdir"].is_object());
353        assert!(
354            exec_params["properties"]["yield_time_ms"]["description"]
355                .as_str()
356                .expect("exec yield description")
357                .contains("session_id")
358        );
359        assert!(
360            exec_params["properties"]["max_output_tokens"]["description"]
361                .as_str()
362                .expect("exec max output description")
363                .contains("spool_path")
364        );
365        assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
366        assert_eq!(exec_params["properties"]["tty"]["default"], false);
367        assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
368        assert_eq!(
369            exec_params["properties"]["sandbox_permissions"]["enum"],
370            json!([
371                "use_default",
372                "with_additional_permissions",
373                "require_escalated",
374                "bypass_sandbox"
375            ])
376        );
377        assert_eq!(
378            exec_params["properties"]["sandbox_permissions"]["default"],
379            "use_default"
380        );
381        assert_eq!(
382            exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
383            "string"
384        );
385        assert_eq!(
386            exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]
387                ["type"],
388            "string"
389        );
390        assert_eq!(
391            exec_params["properties"]["additional_permissions"]["additionalProperties"],
392            false
393        );
394        assert_eq!(exec_params["properties"]["justification"]["type"], "string");
395        assert_eq!(exec_params["additionalProperties"], false);
396        for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
397            assert!(
398                exec_params["properties"]["cmd"]["description"]
399                    .as_str()
400                    .expect("cmd description")
401                    .contains(command),
402                "{command} should be described as an exec_command.cmd example"
403            );
404            assert!(
405                exec_params["properties"].get(command).is_none(),
406                "{command} must not be modelled as a separate exec_command field"
407            );
408        }
409
410        let stdin_params = write_stdin_parameters();
411        assert_eq!(stdin_params["required"], json!(["session_id", "chars"]));
412        assert!(stdin_params["properties"]["session_id"].is_object());
413        assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
414        assert!(
415            stdin_params["properties"]["chars"]["description"]
416                .as_str()
417                .is_some_and(|description| description.contains("empty string"))
418        );
419        assert!(stdin_params["properties"]["chars"].is_object());
420        assert!(
421            stdin_params["properties"]["yield_time_ms"]["description"]
422                .as_str()
423                .expect("stdin yield description")
424                .contains("fresh session output")
425        );
426        assert!(
427            stdin_params["properties"]["max_output_tokens"]["description"]
428                .as_str()
429                .expect("stdin max output description")
430                .contains("spool_path")
431        );
432        assert_eq!(stdin_params["additionalProperties"], false);
433    }
434
435    #[test]
436    fn code_search_schema_exposes_only_code_search_actions() {
437        let params = code_search_parameters();
438        let actions = params["properties"]["action"]["enum"]
439            .as_array()
440            .expect("action enum");
441
442        assert_eq!(params["required"], json!(["action"]));
443        assert_eq!(
444            actions.as_slice(),
445            json!(["structural", "outline"])
446                .as_array()
447                .expect("expected array")
448        );
449        for removed_action in ["grep", "list", "tools", "errors", "agent", "web", "skill"] {
450            assert!(
451                !actions.iter().any(|value| value == removed_action),
452                "{removed_action} must not be a code_search action"
453            );
454        }
455        assert_eq!(params["additionalProperties"], false);
456        assert!(params["properties"].get("case_sensitive").is_none());
457        assert!(params["properties"]["lang"].is_object());
458        assert!(params["properties"]["selector"].is_object());
459        assert!(
460            params["properties"]["action"]["description"]
461                .as_str()
462                .expect("action description")
463                .contains("exec_command.cmd with rg")
464        );
465        let workflows = params["properties"]["workflow"]["enum"]
466            .as_array()
467            .expect("workflow enum");
468        assert!(workflows.iter().any(|value| value == "query"));
469        assert!(!workflows.iter().any(|value| value == "rewrite"));
470        assert!(!workflows.iter().any(|value| value == "apply"));
471    }
472
473    #[test]
474    fn legacy_list_files_schema_exposes_pagination_fields() {
475        let list_params = list_files_parameters();
476        assert!(list_params["properties"]["page"].is_object());
477        assert!(list_params["properties"]["per_page"].is_object());
478        assert!(
479            list_params["properties"]["mode"]["enum"]
480                .as_array()
481                .expect("mode enum")
482                .iter()
483                .any(|value| value == "recursive")
484        );
485    }
486
487    #[test]
488    fn semantic_anchor_guidance_is_appended_once() {
489        let base = "Patch in VT Code format.";
490        let with_guidance = with_semantic_anchor_guidance(base);
491
492        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
493        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
494    }
495
496    #[test]
497    fn default_apply_patch_parameters_keep_expected_alias_shape() {
498        let schema = apply_patch_parameters();
499
500        assert_eq!(
501            schema["anyOf"],
502            json!([
503                {"required": ["input"]},
504                {"required": ["patch"]}
505            ])
506        );
507    }
508}