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/// Default model-visible preview budget for function-tool results.
40pub const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
41/// Smallest valid model-visible preview budget for a function-tool result.
42pub const MIN_MAX_OUTPUT_TOKENS: usize = 1;
43/// Largest valid model-visible preview budget for a function-tool result.
44pub const MAX_MAX_OUTPUT_TOKENS: usize = 50_000;
45
46/// Adds the common result-preview budget field to an object-shaped tool schema.
47///
48/// The returned schema deliberately preserves every existing constraint,
49/// including `additionalProperties`, so callers can use it for legacy schemas
50/// without weakening their argument validation.
51#[must_use]
52pub fn with_max_output_tokens_parameter(mut schema: Value) -> Value {
53    let Some(schema_object) = schema.as_object_mut() else {
54        return schema;
55    };
56    let properties = schema_object
57        .entry("properties")
58        .or_insert_with(|| Value::Object(serde_json::Map::new()));
59    let Some(properties_object) = properties.as_object_mut() else {
60        return schema;
61    };
62    properties_object.entry("max_output_tokens").or_insert_with(|| {
63        json!({
64            "type": "integer",
65            "minimum": MIN_MAX_OUTPUT_TOKENS,
66            "maximum": MAX_MAX_OUTPUT_TOKENS,
67            "default": DEFAULT_MAX_OUTPUT_TOKENS,
68            "description": "Maximum number of result tokens returned to the model. Full oversized output is spooled when available."
69        })
70    });
71    schema
72}
73
74#[must_use]
75pub fn with_semantic_anchor_guidance(base: &str) -> String {
76    let trimmed = base.trim_end();
77    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
78        trimmed.to_string()
79    } else if trimmed.ends_with('.') {
80        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
81    } else {
82        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
83    }
84}
85
86#[must_use]
87pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
88    json!({
89        "type": "object",
90        "properties": {
91            "input": {
92                "type": "string",
93                "description": with_semantic_anchor_guidance(input_description)
94            },
95            "patch": {
96                "type": "string",
97                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
98            }
99        },
100        "anyOf": [
101            {"required": ["input"]},
102            {"required": ["patch"]}
103        ]
104    })
105}
106
107#[must_use]
108pub fn apply_patch_parameters() -> Value {
109    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
110}
111
112#[must_use]
113pub fn cron_parameters() -> Value {
114    json!({
115        "type": "object",
116        "required": ["action"],
117        "additionalProperties": false,
118        "properties": {
119            "action": {
120                "type": "string",
121                "enum": ["create", "list", "delete"],
122                "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."
123            },
124            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
125            "name": {"type": "string", "description": "create: optional short label for the task."},
126            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
127            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
128            "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
129            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
130        }
131    })
132}
133
134#[must_use]
135pub fn mcp_parameters() -> Value {
136    json!({
137        "type": "object",
138        "required": ["action"],
139        "properties": {
140            "action": {
141                "type": "string",
142                "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
143                "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."
144            },
145            "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
146            "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
147            "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
148            "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
149        },
150        "additionalProperties": false
151    })
152}
153
154#[must_use]
155pub fn cron_create_parameters() -> Value {
156    json!({
157        "type": "object",
158        "required": ["prompt"],
159        "additionalProperties": false,
160        "properties": {
161            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
162            "name": {"type": "string", "description": "Optional short label for the task."},
163            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
164            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
165            "run_at": {
166                "type": "string",
167                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
168            }
169        }
170    })
171}
172
173#[must_use]
174pub fn cron_list_parameters() -> Value {
175    json!({
176        "type": "object",
177        "properties": {},
178        "additionalProperties": false
179    })
180}
181
182#[must_use]
183pub fn cron_delete_parameters() -> Value {
184    json!({
185        "type": "object",
186        "required": ["id"],
187        "properties": {
188            "id": {"type": "string", "description": "Session scheduled task id to delete."}
189        }
190    })
191}
192
193#[must_use]
194pub fn exec_command_parameters() -> Value {
195    json!({
196        "type": "object",
197        "required": ["cmd"],
198        "properties": {
199            "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."},
200            "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},
201            "max_output_tokens": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 10000, "description": "Output token cap. Large or truncated output can return a spool_path; an active session may set spool_complete=false for a readable partial snapshot, while an exited pending spool is withheld until a later wait."},
202            "workdir": {"type": "string", "description": "Working directory."},
203            "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
204            "sandbox_permissions": {
205                "type": "string",
206                "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
207                "description": "Sandbox permission mode for this command.",
208                "default": "use_default"
209            },
210            "additional_permissions": {
211                "type": "object",
212                "description": "Additional filesystem access requested with sandbox_permissions set to with_additional_permissions.",
213                "properties": {
214                    "fs_read": {"type": "array", "items": {"type": "string"}},
215                    "fs_write": {"type": "array", "items": {"type": "string"}}
216                },
217                "additionalProperties": false
218            },
219            "justification": {"type": "string", "description": "Reason for requesting the expanded sandbox permission scope."}
220        },
221        "additionalProperties": false
222    })
223}
224
225#[must_use]
226pub fn write_stdin_parameters() -> Value {
227    json!({
228        "type": "object",
229        "required": ["session_id"],
230        "properties": {
231            "session_id": {"type": "string", "description": "Active execution session id."},
232            "action": {"type": "string", "enum": ["write", "poll", "wait"], "description": "Use wait to block until the command exits or wait_timeout_seconds expires; wait never kills the session."},
233            "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
234            "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
235            "wait_timeout_seconds": {"type": "integer", "minimum": 1, "description": "Explicit wait deadline in seconds. A deadline returns an in-progress session that can be waited on again."},
236            "timeout_seconds": {"type": "integer", "minimum": 1, "description": "Alias for wait_timeout_seconds."},
237            "max_output_tokens": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 10000, "description": "Output token cap for the continuation response. Large or truncated output can return a spool_path; the response reports whether an active session has finished writing it."}
238        },
239        "anyOf": [
240            {"required": ["chars"]},
241            {"required": ["action"], "properties": {"action": {"const": "wait"}}}
242        ],
243        "additionalProperties": false
244    })
245}
246
247#[must_use]
248pub fn search_tools_parameters() -> Value {
249    with_max_output_tokens_parameter(json!({
250        "type": "object",
251        "required": ["query"],
252        "properties": {
253            "query": {
254                "type": "string",
255                "minLength": 1,
256                "description": "Natural-language description of the capability to discover."
257            },
258            "limit": {
259                "type": "integer",
260                "minimum": 1,
261                "maximum": 25,
262                "default": 5
263            },
264            "detail_level": {
265                "type": "string",
266                "enum": ["name", "name_description", "full"],
267                "default": "name_description"
268            }
269        },
270        "additionalProperties": false
271    }))
272}
273
274#[must_use]
275pub fn code_search_parameters() -> Value {
276    json!({
277        "type": "object",
278        "required": ["query"],
279        "additionalProperties": false,
280        "properties": {
281            "query": {
282                "type": "string",
283                "minLength": 1,
284                "pattern": "\\S",
285                "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."
286            },
287            "path": {
288                "type": "string",
289                "minLength": 1,
290                "pattern": "\\S",
291                "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
292            },
293            "file_types": {
294                "type": "array",
295                "minItems": 1,
296                "items": {
297                    "type": "string",
298                    "minLength": 1,
299                    "pattern": "\\S"
300                },
301                "description": "Language names or common file extensions, with or without one leading dot."
302            },
303            "result_types": {
304                "type": "array",
305                "minItems": 1,
306                "items": {
307                    "type": "string",
308                    "enum": ["definition", "usage", "text", "path"]
309                },
310                "description": "Result categories to include. Omit to include all four categories."
311            },
312            "max_results": {
313                "type": "integer",
314                "minimum": 1,
315                "maximum": 100,
316                "description": "Maximum number of merged results to return. Omit for 20."
317            }
318        }
319    })
320}
321
322#[must_use]
323pub fn list_files_parameters() -> Value {
324    json!({
325        "type": "object",
326        "properties": {
327            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
328            "mode": {
329                "type": "string",
330                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
331                "description": "Listing mode. Use page/per_page to continue paginated results.",
332                "default": "list"
333            },
334            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
335            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
336            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
337            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
338            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
339            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
340            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
341            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
342            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
343        }
344    })
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use serde_json::json;
351
352    #[test]
353    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
354        let schema = apply_patch_parameter_schema("Patch in VT Code format");
355
356        // Both `input` and `patch` alias fields now carry the format
357        // description AND the semantic-anchor guidance, preventing the model
358        // from placing a unified diff in `patch` (see checkpoint turn_615).
359        assert_eq!(
360            schema["properties"]["patch"]["description"],
361            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
362        );
363        let patch_description = schema["properties"]["patch"]["description"]
364            .as_str()
365            .expect("patch description");
366        assert!(patch_description.contains("*** Begin Patch"));
367        assert!(patch_description.contains("unified diff"));
368        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
369
370        let input_description = schema["properties"]["input"]["description"]
371            .as_str()
372            .expect("input description");
373        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
374    }
375
376    #[test]
377    fn common_output_limit_parameter_preserves_strict_schema() {
378        let schema = with_max_output_tokens_parameter(json!({
379            "type": "object",
380            "properties": {"query": {"type": "string"}},
381            "additionalProperties": false
382        }));
383        assert_eq!(schema["additionalProperties"], json!(false));
384        assert_eq!(schema["properties"]["max_output_tokens"]["default"], json!(DEFAULT_MAX_OUTPUT_TOKENS));
385        assert_eq!(schema["properties"]["max_output_tokens"]["minimum"], json!(MIN_MAX_OUTPUT_TOKENS));
386        assert_eq!(schema["properties"]["max_output_tokens"]["maximum"], json!(MAX_MAX_OUTPUT_TOKENS));
387    }
388
389    #[test]
390    fn codex_baseline_exec_schemas_use_public_names_shape() {
391        let exec_params = exec_command_parameters();
392        assert_eq!(exec_params["required"], json!(["cmd"]));
393        assert!(exec_params["properties"]["cmd"].is_object());
394        assert!(exec_params["properties"]["workdir"].is_object());
395        assert!(
396            exec_params["properties"]["yield_time_ms"]["description"]
397                .as_str()
398                .expect("exec yield description")
399                .contains("session_id")
400        );
401        assert!(
402            exec_params["properties"]["max_output_tokens"]["description"]
403                .as_str()
404                .expect("exec max output description")
405                .contains("spool_path")
406        );
407        assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
408        assert_eq!(exec_params["properties"]["tty"]["default"], false);
409        assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
410        assert_eq!(
411            exec_params["properties"]["sandbox_permissions"]["enum"],
412            json!([
413                "use_default",
414                "with_additional_permissions",
415                "require_escalated",
416                "bypass_sandbox"
417            ])
418        );
419        assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
420        assert_eq!(
421            exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
422            "string"
423        );
424        assert_eq!(
425            exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
426            "string"
427        );
428        assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
429        assert_eq!(exec_params["properties"]["justification"]["type"], "string");
430        assert_eq!(exec_params["additionalProperties"], false);
431        for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
432            assert!(
433                exec_params["properties"]["cmd"]["description"]
434                    .as_str()
435                    .expect("cmd description")
436                    .contains(command),
437                "{command} should be described as an exec_command.cmd example"
438            );
439            assert!(
440                exec_params["properties"].get(command).is_none(),
441                "{command} must not be modelled as a separate exec_command field"
442            );
443        }
444
445        let stdin_params = write_stdin_parameters();
446        assert_eq!(stdin_params["required"], json!(["session_id"]));
447        assert!(stdin_params["properties"]["session_id"].is_object());
448        assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
449        assert_eq!(stdin_params["properties"]["action"]["enum"], json!(["write", "poll", "wait"]));
450        assert!(stdin_params["properties"]["wait_timeout_seconds"].is_object());
451        assert_eq!(stdin_params["anyOf"][1]["required"], json!(["action"]));
452        assert_eq!(stdin_params["anyOf"][1]["properties"]["action"]["const"], "wait");
453        assert!(
454            stdin_params["properties"]["chars"]["description"]
455                .as_str()
456                .is_some_and(|description| description.contains("empty string"))
457        );
458        assert!(stdin_params["properties"]["chars"].is_object());
459        assert!(
460            stdin_params["properties"]["yield_time_ms"]["description"]
461                .as_str()
462                .expect("stdin yield description")
463                .contains("fresh session output")
464        );
465        assert!(
466            stdin_params["properties"]["max_output_tokens"]["description"]
467                .as_str()
468                .expect("stdin max output description")
469                .contains("spool_path")
470        );
471        assert_eq!(stdin_params["additionalProperties"], false);
472    }
473
474    #[test]
475    fn code_search_schema_exposes_exact_five_property_contract() {
476        let params = code_search_parameters();
477        let properties = params["properties"].as_object().expect("properties");
478        let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
479        property_names.sort_unstable();
480
481        assert_eq!(params["required"], json!(["query"]));
482        assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
483        assert_eq!(params["additionalProperties"], false);
484        assert_eq!(params["properties"]["query"]["pattern"], "\\S");
485        assert_eq!(params["properties"]["file_types"]["minItems"], 1);
486        assert_eq!(params["properties"]["result_types"]["minItems"], 1);
487        assert_eq!(
488            params["properties"]["result_types"]["items"]["enum"],
489            json!(["definition", "usage", "text", "path"])
490        );
491        assert_eq!(params["properties"]["max_results"]["minimum"], 1);
492        assert_eq!(params["properties"]["max_results"]["maximum"], 100);
493        assert!(params.get("anyOf").is_none());
494    }
495
496    #[test]
497    fn legacy_list_files_schema_exposes_pagination_fields() {
498        let list_params = list_files_parameters();
499        assert!(list_params["properties"]["page"].is_object());
500        assert!(list_params["properties"]["per_page"].is_object());
501        assert!(
502            list_params["properties"]["mode"]["enum"]
503                .as_array()
504                .expect("mode enum")
505                .iter()
506                .any(|value| value == "recursive")
507        );
508    }
509
510    #[test]
511    fn semantic_anchor_guidance_is_appended_once() {
512        let base = "Patch in VT Code format.";
513        let with_guidance = with_semantic_anchor_guidance(base);
514
515        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
516        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
517    }
518
519    #[test]
520    fn default_apply_patch_parameters_keep_expected_alias_shape() {
521        let schema = apply_patch_parameters();
522
523        assert_eq!(
524            schema["anyOf"],
525            json!([
526                {"required": ["input"]},
527                {"required": ["patch"]}
528            ])
529        );
530    }
531}