Skip to main content

vtcode_utility_tool_specs/
lib.rs

1#![allow(
2    missing_docs,
3    dead_code,
4    unused_imports,
5    reason = "Intentional compatibility, platform, or test-only suppression."
6)]
7
8//! Passive JSON schemas for utility, file, scheduling, and collaboration tool surfaces.
9
10#![recursion_limit = "256"]
11use serde_json::{Value, json};
12
13mod collaboration;
14mod json_schema;
15#[cfg(feature = "mcp")]
16mod mcp_tool;
17mod responses_api;
18mod tool_kind;
19
20pub use collaboration::{
21    agent_parameters, close_agent_parameters, request_user_input_description, request_user_input_parameters,
22    resume_agent_parameters, send_input_parameters, spawn_agent_parameters, spawn_background_subprocess_parameters,
23    wait_agent_parameters,
24};
25pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
26#[cfg(feature = "mcp")]
27pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
28pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
29pub(crate) use tool_kind::{CanonicalToolMeta, TokenBucket, ToolKind, ToolNamespace};
30
31pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
32    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
33
34/// Explicit, format-bearing description for the `patch` alias field. The old
35/// value ("Alias for input") gave the model no format guidance, so it often
36/// placed a standard unified diff (`---`/`+++`) there — which `apply_patch`
37/// rejects. This mirrors the `input` description so both alias fields carry
38/// identical, complete format guidance (see checkpoint turn_615 for the
39/// failure this prevents).
40pub 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). Every patch path must be workspace-relative; never use absolute paths, `..`, or traversal-like forms.";
41pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str = "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch. Every patch path must be workspace-relative; never use absolute paths, `..`, or traversal-like forms.";
42
43/// Default model-visible preview budget for function-tool results.
44pub const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
45/// Smallest valid model-visible preview budget for a function-tool result.
46pub const MIN_MAX_OUTPUT_TOKENS: usize = 1;
47/// Largest valid model-visible preview budget for a function-tool result.
48pub const MAX_MAX_OUTPUT_TOKENS: usize = 50_000;
49/// Canonical JSON property name for the common result-preview budget field.
50///
51/// Centralized so the strict dedicated validator in `vtcode-core` and any
52/// compatibility coercion both reference one name and cannot drift.
53pub const MAX_OUTPUT_TOKENS_FIELD: &str = "max_output_tokens";
54
55/// Adds the common result-preview budget field to an object-shaped tool schema.
56///
57/// The returned schema deliberately preserves every existing constraint,
58/// including `additionalProperties`, so callers can use it for legacy schemas
59/// without weakening their argument validation.
60#[must_use]
61pub fn with_max_output_tokens_parameter(mut schema: Value) -> Value {
62    let Some(schema_object) = schema.as_object_mut() else {
63        return schema;
64    };
65    let properties = schema_object
66        .entry("properties")
67        .or_insert_with(|| Value::Object(serde_json::Map::new()));
68    let Some(properties_object) = properties.as_object_mut() else {
69        return schema;
70    };
71    {
72        let _entry = properties_object.entry(MAX_OUTPUT_TOKENS_FIELD).or_insert_with(|| {
73            json!({
74                "type": "integer",
75                "minimum": MIN_MAX_OUTPUT_TOKENS,
76                "maximum": MAX_MAX_OUTPUT_TOKENS,
77                "default": DEFAULT_MAX_OUTPUT_TOKENS,
78                "description": "Maximum number of result tokens returned to the model. Full oversized output is spooled when available."
79            })
80        });
81    }
82    schema
83}
84
85#[must_use]
86pub fn with_semantic_anchor_guidance(base: &str) -> String {
87    let trimmed = base.trim_end();
88    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
89        trimmed.to_string()
90    } else if trimmed.ends_with('.') {
91        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
92    } else {
93        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
94    }
95}
96
97#[must_use]
98pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
99    json!({
100        "type": "object",
101        "properties": {
102            "input": {
103                "type": "string",
104                "description": with_semantic_anchor_guidance(input_description)
105            },
106            "patch": {
107                "type": "string",
108                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
109            }
110        },
111        "anyOf": [
112            {"required": ["input"]},
113            {"required": ["patch"]}
114        ]
115    })
116}
117
118#[must_use]
119pub fn apply_patch_parameters() -> Value {
120    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
121}
122
123#[must_use]
124pub fn cron_parameters() -> Value {
125    json!({
126        "type": "object",
127        "required": ["action"],
128        "additionalProperties": false,
129        "properties": {
130            "action": {
131                "type": "string",
132                "enum": ["create", "list", "delete"],
133                "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."
134            },
135            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
136            "name": {"type": "string", "description": "create: optional short label for the task."},
137            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
138            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
139            "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
140            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
141        }
142    })
143}
144
145#[must_use]
146pub fn mcp_parameters() -> Value {
147    json!({
148        "type": "object",
149        "required": ["action"],
150        "properties": {
151            "action": {
152                "type": "string",
153                "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
154                "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."
155            },
156            "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
157            "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
158            "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
159            "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
160        },
161        "additionalProperties": false
162    })
163}
164
165#[must_use]
166pub fn cron_create_parameters() -> Value {
167    json!({
168        "type": "object",
169        "required": ["prompt"],
170        "additionalProperties": false,
171        "properties": {
172            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
173            "name": {"type": "string", "description": "Optional short label for the task."},
174            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
175            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
176            "run_at": {
177                "type": "string",
178                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
179            }
180        }
181    })
182}
183
184#[must_use]
185pub fn cron_list_parameters() -> Value {
186    json!({
187        "type": "object",
188        "properties": {},
189        "additionalProperties": false
190    })
191}
192
193#[must_use]
194pub fn cron_delete_parameters() -> Value {
195    json!({
196        "type": "object",
197        "required": ["id"],
198        "properties": {
199            "id": {"type": "string", "description": "Session scheduled task id to delete."}
200        }
201    })
202}
203
204#[must_use]
205pub fn exec_command_parameters() -> Value {
206    json!({
207        "type": "object",
208        "required": ["cmd"],
209        "properties": {
210            "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."},
211            "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. Values above 10000 turn this into a single-call long run: no outer timeout applies and the response returns after the yield window or command exit, whichever is first.", "default": 10000},
212            "background": {"type": "boolean", "description": "Start a retained background process and return after a bounded initial output window. At most three live background processes are allowed per VT Code runtime; use the returned session_id with write_stdin to wait, poll, write, inspect, terminate, or close.", "default": false},
213            "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."},
214            "workdir": {"type": "string", "description": "Working directory."},
215            "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
216            "sandbox_permissions": {
217                "type": "string",
218                "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
219                "description": "Sandbox permission mode for this command. Omit it or use `use_default` for the normal sandbox. Non-empty `additional_permissions` is normalized to `with_additional_permissions`. `require_escalated` and `bypass_sandbox` require a non-empty `justification` and cannot be combined with `additional_permissions`.",
220                "default": "use_default"
221            },
222            "additional_permissions": {
223                "type": "object",
224                "description": "Optional extra filesystem roots to grant inside the sandbox. Non-empty `additional_permissions` implicitly requests `with_additional_permissions`; every path is normalized and must stay within allowed workspace or temp roots.",
225                "properties": {
226                    "fs_read": {"type": "array", "items": {"type": "string"}},
227                    "fs_write": {"type": "array", "items": {"type": "string"}}
228                },
229                "additionalProperties": false
230            },
231            "justification": {"type": "string", "description": "Short approval question for expanded sandbox scope. Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`."}
232        },
233        "additionalProperties": false
234    })
235}
236
237#[must_use]
238pub fn write_stdin_parameters() -> Value {
239    json!({
240        "type": "object",
241        "required": ["session_id"],
242        "properties": {
243            "session_id": {"type": "string", "description": "Active execution session id."},
244            "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."},
245            "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
246            "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
247            "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."},
248            "timeout_seconds": {"type": "integer", "minimum": 1, "description": "Alias for wait_timeout_seconds."},
249            "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."}
250        },
251        "anyOf": [
252            {"required": ["chars"]},
253            {"required": ["action"], "properties": {"action": {"const": "wait"}}}
254        ],
255        "additionalProperties": false
256    })
257}
258
259#[must_use]
260pub fn search_tools_parameters() -> Value {
261    with_max_output_tokens_parameter(json!({
262        "type": "object",
263        "required": ["query"],
264        "properties": {
265            "query": {
266                "type": "string",
267                "minLength": 1,
268                "description": "Natural-language description of the capability to discover."
269            },
270            "limit": {
271                "type": "integer",
272                "minimum": 1,
273                "maximum": 25,
274                "default": 5
275            },
276            "detail_level": {
277                "type": "string",
278                "enum": ["name", "name_description", "full"],
279                "default": "name_description"
280            }
281        },
282        "additionalProperties": false
283    }))
284}
285
286#[must_use]
287pub fn code_search_parameters() -> Value {
288    json!({
289        "type": "object",
290        "required": ["query"],
291        "additionalProperties": false,
292        "properties": {
293            "query": {
294                "type": "string",
295                "minLength": 1,
296                "pattern": "\\S",
297                "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."
298            },
299            "path": {
300                "type": "string",
301                "minLength": 1,
302                "pattern": "\\S",
303                "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
304            },
305            "file_types": {
306                "type": "array",
307                "minItems": 1,
308                "items": {
309                    "type": "string",
310                    "minLength": 1,
311                    "pattern": "\\S"
312                },
313                "description": "Language names or common file extensions, with or without one leading dot."
314            },
315            "result_types": {
316                "type": "array",
317                "minItems": 1,
318                "items": {
319                    "type": "string",
320                    "enum": ["definition", "usage", "text", "path"]
321                },
322                "description": "Result categories to include. Omit to include all four categories."
323            },
324            "max_results": {
325                "type": "integer",
326                "minimum": 1,
327                "maximum": 100,
328                "description": "Maximum number of merged results to return. Omit for 20."
329            }
330        }
331    })
332}
333
334#[must_use]
335pub fn list_files_parameters() -> Value {
336    json!({
337        "type": "object",
338        "properties": {
339            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
340            "mode": {
341                "type": "string",
342                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
343                "description": "Listing mode. Use page/per_page to continue paginated results.",
344                "default": "list"
345            },
346            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
347            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
348            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
349            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
350            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
351            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
352            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
353            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
354            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
355        }
356    })
357}
358
359#[cfg(test)]
360mod tests {
361    use serde_json::json;
362
363    use super::*;
364
365    #[test]
366    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
367        let schema = apply_patch_parameter_schema("Patch in VT Code format");
368
369        // Both `input` and `patch` alias fields now carry the format
370        // description AND the semantic-anchor guidance, preventing the model
371        // from placing a unified diff in `patch` (see checkpoint turn_615).
372        assert_eq!(
373            schema["properties"]["patch"]["description"],
374            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
375        );
376        let patch_description = schema["properties"]["patch"]["description"]
377            .as_str()
378            .expect("patch description");
379        assert!(patch_description.contains("*** Begin Patch"));
380        assert!(patch_description.contains("unified diff"));
381        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
382
383        let input_description = schema["properties"]["input"]["description"]
384            .as_str()
385            .expect("input description");
386        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
387    }
388
389    #[test]
390    fn common_output_limit_parameter_preserves_strict_schema() {
391        let schema = with_max_output_tokens_parameter(json!({
392            "type": "object",
393            "properties": {"query": {"type": "string"}},
394            "additionalProperties": false
395        }));
396        assert_eq!(schema["additionalProperties"], json!(false));
397        assert_eq!(schema["properties"]["max_output_tokens"]["default"], json!(DEFAULT_MAX_OUTPUT_TOKENS));
398        assert_eq!(schema["properties"]["max_output_tokens"]["minimum"], json!(MIN_MAX_OUTPUT_TOKENS));
399        assert_eq!(schema["properties"]["max_output_tokens"]["maximum"], json!(MAX_MAX_OUTPUT_TOKENS));
400    }
401
402    #[test]
403    fn codex_baseline_exec_schemas_use_public_names_shape() {
404        let exec_params = exec_command_parameters();
405        assert_eq!(exec_params["required"], json!(["cmd"]));
406        assert!(exec_params["properties"]["cmd"].is_object());
407        assert!(exec_params["properties"]["workdir"].is_object());
408        assert!(
409            exec_params["properties"]["yield_time_ms"]["description"]
410                .as_str()
411                .expect("exec yield description")
412                .contains("session_id")
413        );
414        assert!(
415            exec_params["properties"]["yield_time_ms"]["description"]
416                .as_str()
417                .expect("exec yield description")
418                .contains("single-call long run")
419        );
420        assert!(
421            exec_params["properties"]["max_output_tokens"]["description"]
422                .as_str()
423                .expect("exec max output description")
424                .contains("spool_path")
425        );
426        assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
427        assert_eq!(exec_params["properties"]["tty"]["default"], false);
428        assert_eq!(exec_params["properties"]["background"]["type"], "boolean");
429        assert_eq!(exec_params["properties"]["background"]["default"], false);
430        assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
431        assert_eq!(
432            exec_params["properties"]["sandbox_permissions"]["enum"],
433            json!([
434                "use_default",
435                "with_additional_permissions",
436                "require_escalated",
437                "bypass_sandbox"
438            ])
439        );
440        assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
441        assert_eq!(
442            exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
443            "string"
444        );
445        assert_eq!(
446            exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
447            "string"
448        );
449        assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
450        assert_eq!(exec_params["properties"]["justification"]["type"], "string");
451        assert!(
452            exec_params["properties"]["sandbox_permissions"]["description"]
453                .as_str()
454                .expect("sandbox_permissions description")
455                .contains("normalized to `with_additional_permissions`")
456        );
457        assert!(
458            exec_params["properties"]["additional_permissions"]["description"]
459                .as_str()
460                .expect("additional_permissions description")
461                .contains("allowed workspace or temp roots")
462        );
463        assert!(
464            exec_params["properties"]["justification"]["description"]
465                .as_str()
466                .expect("justification description")
467                .contains("Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`")
468        );
469        assert_eq!(exec_params["additionalProperties"], false);
470        for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
471            assert!(
472                exec_params["properties"]["cmd"]["description"]
473                    .as_str()
474                    .expect("cmd description")
475                    .contains(command),
476                "{command} should be described as an exec_command.cmd example"
477            );
478            assert!(
479                exec_params["properties"].get(command).is_none(),
480                "{command} must not be modelled as a separate exec_command field"
481            );
482        }
483
484        let stdin_params = write_stdin_parameters();
485        assert_eq!(stdin_params["required"], json!(["session_id"]));
486        assert!(stdin_params["properties"]["session_id"].is_object());
487        assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
488        assert_eq!(stdin_params["properties"]["action"]["enum"], json!(["write", "poll", "wait"]));
489        assert!(stdin_params["properties"]["wait_timeout_seconds"].is_object());
490        assert_eq!(stdin_params["anyOf"][1]["required"], json!(["action"]));
491        assert_eq!(stdin_params["anyOf"][1]["properties"]["action"]["const"], "wait");
492        assert!(
493            stdin_params["properties"]["chars"]["description"]
494                .as_str()
495                .is_some_and(|description| description.contains("empty string"))
496        );
497        assert!(stdin_params["properties"]["chars"].is_object());
498        assert!(
499            stdin_params["properties"]["yield_time_ms"]["description"]
500                .as_str()
501                .expect("stdin yield description")
502                .contains("fresh session output")
503        );
504        assert!(
505            stdin_params["properties"]["max_output_tokens"]["description"]
506                .as_str()
507                .expect("stdin max output description")
508                .contains("spool_path")
509        );
510        assert_eq!(stdin_params["additionalProperties"], false);
511    }
512
513    #[test]
514    fn code_search_schema_exposes_exact_five_property_contract() {
515        let params = code_search_parameters();
516        let properties = params["properties"].as_object().expect("properties");
517        let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
518        property_names.sort_unstable();
519
520        assert_eq!(params["required"], json!(["query"]));
521        assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
522        assert_eq!(params["additionalProperties"], false);
523        assert_eq!(params["properties"]["query"]["pattern"], "\\S");
524        assert_eq!(params["properties"]["file_types"]["minItems"], 1);
525        assert_eq!(params["properties"]["result_types"]["minItems"], 1);
526        assert_eq!(
527            params["properties"]["result_types"]["items"]["enum"],
528            json!(["definition", "usage", "text", "path"])
529        );
530        assert_eq!(params["properties"]["max_results"]["minimum"], 1);
531        assert_eq!(params["properties"]["max_results"]["maximum"], 100);
532        assert!(params.get("anyOf").is_none());
533    }
534
535    #[test]
536    fn legacy_list_files_schema_exposes_pagination_fields() {
537        let list_params = list_files_parameters();
538        assert!(list_params["properties"]["page"].is_object());
539        assert!(list_params["properties"]["per_page"].is_object());
540        assert!(
541            list_params["properties"]["mode"]["enum"]
542                .as_array()
543                .expect("mode enum")
544                .iter()
545                .any(|value| value == "recursive")
546        );
547    }
548
549    #[test]
550    fn semantic_anchor_guidance_is_appended_once() {
551        let base = "Patch in VT Code format.";
552        let with_guidance = with_semantic_anchor_guidance(base);
553
554        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
555        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
556    }
557
558    #[test]
559    fn default_apply_patch_parameters_keep_expected_alias_shape() {
560        let schema = apply_patch_parameters();
561
562        assert_eq!(
563            schema["anyOf"],
564            json!([
565                {"required": ["input"]},
566                {"required": ["patch"]}
567            ])
568        );
569    }
570}