Skip to main content

vtcode_utility_tool_specs/
lib.rs

1#![allow(missing_docs, dead_code, unused_imports)]
2
3//! Passive JSON schemas for utility, file, scheduling, and collaboration tool surfaces.
4
5#![recursion_limit = "256"]
6use serde_json::{Value, json};
7
8mod collaboration;
9mod json_schema;
10#[cfg(feature = "mcp")]
11mod mcp_tool;
12mod responses_api;
13mod tool_kind;
14
15pub use collaboration::{
16    agent_parameters, close_agent_parameters, request_user_input_description, request_user_input_parameters,
17    resume_agent_parameters, send_input_parameters, spawn_agent_parameters, spawn_background_subprocess_parameters,
18    wait_agent_parameters,
19};
20pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
21#[cfg(feature = "mcp")]
22pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
23pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
24pub(crate) use tool_kind::{CanonicalToolMeta, TokenBucket, ToolKind, ToolNamespace};
25
26pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
27    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
28
29/// Explicit, format-bearing description for the `patch` alias field. The old
30/// value ("Alias for input") gave the model no format guidance, so it often
31/// placed a standard unified diff (`---`/`+++`) there — which `apply_patch`
32/// rejects. This mirrors the `input` description so both alias fields carry
33/// identical, complete format guidance (see checkpoint turn_615 for the
34/// failure this prevents).
35pub 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).";
36pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str =
37    "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch";
38
39#[must_use]
40pub fn with_semantic_anchor_guidance(base: &str) -> String {
41    let trimmed = base.trim_end();
42    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
43        trimmed.to_string()
44    } else if trimmed.ends_with('.') {
45        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
46    } else {
47        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
48    }
49}
50
51#[must_use]
52pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
53    json!({
54        "type": "object",
55        "properties": {
56            "input": {
57                "type": "string",
58                "description": with_semantic_anchor_guidance(input_description)
59            },
60            "patch": {
61                "type": "string",
62                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
63            }
64        },
65        "anyOf": [
66            {"required": ["input"]},
67            {"required": ["patch"]}
68        ]
69    })
70}
71
72#[must_use]
73pub fn apply_patch_parameters() -> Value {
74    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
75}
76
77#[must_use]
78pub fn cron_parameters() -> Value {
79    json!({
80        "type": "object",
81        "required": ["action"],
82        "additionalProperties": false,
83        "properties": {
84            "action": {
85                "type": "string",
86                "enum": ["create", "list", "delete"],
87                "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."
88            },
89            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
90            "name": {"type": "string", "description": "create: optional short label for the task."},
91            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
92            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
93            "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
94            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
95        }
96    })
97}
98
99#[must_use]
100pub fn mcp_parameters() -> Value {
101    json!({
102        "type": "object",
103        "required": ["action"],
104        "properties": {
105            "action": {
106                "type": "string",
107                "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
108                "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."
109            },
110            "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
111            "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
112            "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
113            "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
114        },
115        "additionalProperties": false
116    })
117}
118
119#[must_use]
120pub fn cron_create_parameters() -> Value {
121    json!({
122        "type": "object",
123        "required": ["prompt"],
124        "additionalProperties": false,
125        "properties": {
126            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
127            "name": {"type": "string", "description": "Optional short label for the task."},
128            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
129            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
130            "run_at": {
131                "type": "string",
132                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
133            }
134        }
135    })
136}
137
138#[must_use]
139pub fn cron_list_parameters() -> Value {
140    json!({
141        "type": "object",
142        "properties": {},
143        "additionalProperties": false
144    })
145}
146
147#[must_use]
148pub fn cron_delete_parameters() -> Value {
149    json!({
150        "type": "object",
151        "required": ["id"],
152        "properties": {
153            "id": {"type": "string", "description": "Session scheduled task id to delete."}
154        }
155    })
156}
157
158#[must_use]
159pub fn exec_command_parameters() -> Value {
160    json!({
161        "type": "object",
162        "required": ["cmd"],
163        "properties": {
164            "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."},
165            "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},
166            "max_output_tokens": {"type": "integer", "description": "Output token cap. Large or truncated output can return a spool_path for the full output."},
167            "workdir": {"type": "string", "description": "Working directory."},
168            "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
169            "sandbox_permissions": {
170                "type": "string",
171                "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
172                "description": "Sandbox permission mode for this command.",
173                "default": "use_default"
174            },
175            "additional_permissions": {
176                "type": "object",
177                "description": "Additional filesystem access requested with sandbox_permissions set to with_additional_permissions.",
178                "properties": {
179                    "fs_read": {"type": "array", "items": {"type": "string"}},
180                    "fs_write": {"type": "array", "items": {"type": "string"}}
181                },
182                "additionalProperties": false
183            },
184            "justification": {"type": "string", "description": "Short approval question required for escalated or bypassed sandbox execution."}
185        },
186        "additionalProperties": false
187    })
188}
189
190#[must_use]
191pub fn write_stdin_parameters() -> Value {
192    json!({
193        "type": "object",
194        "required": ["session_id", "chars"],
195        "properties": {
196            "session_id": {"type": "string", "description": "Active execution session id."},
197            "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
198            "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
199            "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."}
200        },
201        "additionalProperties": false
202    })
203}
204
205#[must_use]
206pub fn code_search_parameters() -> Value {
207    json!({
208        "type": "object",
209        "required": ["query"],
210        "additionalProperties": false,
211        "properties": {
212            "query": {
213                "type": "string",
214                "minLength": 1,
215                "pattern": "\\S",
216                "description": "Literal code or path query. Smart-case applies to content and exact symbol-name matching: a wholly lower-case query matches case-insensitively, while an upper-case character makes matching case-sensitive. Path matching remains fuzzy and case-insensitive."
217            },
218            "path": {
219                "type": "string",
220                "minLength": 1,
221                "pattern": "\\S",
222                "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
223            },
224            "file_types": {
225                "type": "array",
226                "minItems": 1,
227                "items": {
228                    "type": "string",
229                    "minLength": 1,
230                    "pattern": "\\S"
231                },
232                "description": "Language names or common file extensions, with or without one leading dot."
233            },
234            "result_types": {
235                "type": "array",
236                "minItems": 1,
237                "items": {
238                    "type": "string",
239                    "enum": ["definition", "usage", "text", "path"]
240                },
241                "description": "Result categories to include. Omit to include all four categories."
242            },
243            "max_results": {
244                "type": "integer",
245                "minimum": 1,
246                "maximum": 100,
247                "description": "Maximum number of merged results to return. Omit for 20."
248            }
249        }
250    })
251}
252
253#[must_use]
254pub fn list_files_parameters() -> Value {
255    json!({
256        "type": "object",
257        "properties": {
258            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
259            "mode": {
260                "type": "string",
261                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
262                "description": "Listing mode. Use page/per_page to continue paginated results.",
263                "default": "list"
264            },
265            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
266            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
267            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
268            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
269            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
270            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
271            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
272            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
273            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
274        }
275    })
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use serde_json::json;
282
283    #[test]
284    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
285        let schema = apply_patch_parameter_schema("Patch in VT Code format");
286
287        // Both `input` and `patch` alias fields now carry the format
288        // description AND the semantic-anchor guidance, preventing the model
289        // from placing a unified diff in `patch` (see checkpoint turn_615).
290        assert_eq!(
291            schema["properties"]["patch"]["description"],
292            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
293        );
294        let patch_description = schema["properties"]["patch"]["description"]
295            .as_str()
296            .expect("patch description");
297        assert!(patch_description.contains("*** Begin Patch"));
298        assert!(patch_description.contains("unified diff"));
299        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
300
301        let input_description = schema["properties"]["input"]["description"]
302            .as_str()
303            .expect("input description");
304        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
305    }
306
307    #[test]
308    fn codex_baseline_exec_schemas_use_public_names_shape() {
309        let exec_params = exec_command_parameters();
310        assert_eq!(exec_params["required"], json!(["cmd"]));
311        assert!(exec_params["properties"]["cmd"].is_object());
312        assert!(exec_params["properties"]["workdir"].is_object());
313        assert!(
314            exec_params["properties"]["yield_time_ms"]["description"]
315                .as_str()
316                .expect("exec yield description")
317                .contains("session_id")
318        );
319        assert!(
320            exec_params["properties"]["max_output_tokens"]["description"]
321                .as_str()
322                .expect("exec max output description")
323                .contains("spool_path")
324        );
325        assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
326        assert_eq!(exec_params["properties"]["tty"]["default"], false);
327        assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
328        assert_eq!(
329            exec_params["properties"]["sandbox_permissions"]["enum"],
330            json!([
331                "use_default",
332                "with_additional_permissions",
333                "require_escalated",
334                "bypass_sandbox"
335            ])
336        );
337        assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
338        assert_eq!(
339            exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
340            "string"
341        );
342        assert_eq!(
343            exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
344            "string"
345        );
346        assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
347        assert_eq!(exec_params["properties"]["justification"]["type"], "string");
348        assert_eq!(exec_params["additionalProperties"], false);
349        for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
350            assert!(
351                exec_params["properties"]["cmd"]["description"]
352                    .as_str()
353                    .expect("cmd description")
354                    .contains(command),
355                "{command} should be described as an exec_command.cmd example"
356            );
357            assert!(
358                exec_params["properties"].get(command).is_none(),
359                "{command} must not be modelled as a separate exec_command field"
360            );
361        }
362
363        let stdin_params = write_stdin_parameters();
364        assert_eq!(stdin_params["required"], json!(["session_id", "chars"]));
365        assert!(stdin_params["properties"]["session_id"].is_object());
366        assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
367        assert!(
368            stdin_params["properties"]["chars"]["description"]
369                .as_str()
370                .is_some_and(|description| description.contains("empty string"))
371        );
372        assert!(stdin_params["properties"]["chars"].is_object());
373        assert!(
374            stdin_params["properties"]["yield_time_ms"]["description"]
375                .as_str()
376                .expect("stdin yield description")
377                .contains("fresh session output")
378        );
379        assert!(
380            stdin_params["properties"]["max_output_tokens"]["description"]
381                .as_str()
382                .expect("stdin max output description")
383                .contains("spool_path")
384        );
385        assert_eq!(stdin_params["additionalProperties"], false);
386    }
387
388    #[test]
389    fn code_search_schema_exposes_exact_five_property_contract() {
390        let params = code_search_parameters();
391        let properties = params["properties"].as_object().expect("properties");
392        let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
393        property_names.sort_unstable();
394
395        assert_eq!(params["required"], json!(["query"]));
396        assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
397        assert_eq!(params["additionalProperties"], false);
398        assert_eq!(params["properties"]["query"]["pattern"], "\\S");
399        assert_eq!(params["properties"]["file_types"]["minItems"], 1);
400        assert_eq!(params["properties"]["result_types"]["minItems"], 1);
401        assert_eq!(
402            params["properties"]["result_types"]["items"]["enum"],
403            json!(["definition", "usage", "text", "path"])
404        );
405        assert_eq!(params["properties"]["max_results"]["minimum"], 1);
406        assert_eq!(params["properties"]["max_results"]["maximum"], 100);
407        assert!(params.get("anyOf").is_none());
408    }
409
410    #[test]
411    fn legacy_list_files_schema_exposes_pagination_fields() {
412        let list_params = list_files_parameters();
413        assert!(list_params["properties"]["page"].is_object());
414        assert!(list_params["properties"]["per_page"].is_object());
415        assert!(
416            list_params["properties"]["mode"]["enum"]
417                .as_array()
418                .expect("mode enum")
419                .iter()
420                .any(|value| value == "recursive")
421        );
422    }
423
424    #[test]
425    fn semantic_anchor_guidance_is_appended_once() {
426        let base = "Patch in VT Code format.";
427        let with_guidance = with_semantic_anchor_guidance(base);
428
429        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
430        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
431    }
432
433    #[test]
434    fn default_apply_patch_parameters_keep_expected_alias_shape() {
435        let schema = apply_patch_parameters();
436
437        assert_eq!(
438            schema["anyOf"],
439            json!([
440                {"required": ["input"]},
441                {"required": ["patch"]}
442            ])
443        );
444    }
445}