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, request_user_input_description, request_user_input_parameters,
16};
17pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
18#[cfg(feature = "mcp")]
19pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
20pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
21
22pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
23    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
24
25/// Explicit, format-bearing description for the `patch` alias field. The old
26/// value ("Alias for input") gave the model no format guidance, so it often
27/// placed a standard unified diff (`---`/`+++`) there — which `apply_patch`
28/// rejects. This mirrors the `input` description so both alias fields carry
29/// identical, complete format guidance (see checkpoint turn_615 for the
30/// failure this prevents).
31pub 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).";
32pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str = "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch";
33
34#[must_use]
35pub fn with_semantic_anchor_guidance(base: &str) -> String {
36    let trimmed = base.trim_end();
37    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
38        trimmed.to_string()
39    } else if trimmed.ends_with('.') {
40        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
41    } else {
42        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
43    }
44}
45
46#[must_use]
47pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
48    json!({
49        "type": "object",
50        "properties": {
51            "input": {
52                "type": "string",
53                "description": with_semantic_anchor_guidance(input_description)
54            },
55            "patch": {
56                "type": "string",
57                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
58            }
59        },
60        "anyOf": [
61            {"required": ["input"]},
62            {"required": ["patch"]}
63        ]
64    })
65}
66
67#[must_use]
68pub fn apply_patch_parameters() -> Value {
69    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
70}
71
72#[must_use]
73pub fn cron_parameters() -> Value {
74    json!({
75        "type": "object",
76        "required": ["action"],
77        "additionalProperties": false,
78        "properties": {
79            "action": {
80                "type": "string",
81                "enum": ["create", "list", "delete"],
82                "description": "create: schedule a prompt (requires prompt + exactly one of cron/delay_minutes/run_at). list: show scheduled prompts. delete: remove one by id."
83            },
84            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
85            "name": {"type": "string", "description": "create: optional short label for the task."},
86            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
87            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
88            "run_at": {
89                "type": "string",
90                "description": "create: one-shot fire time in RFC3339 or local datetime form. Use instead of `cron` or `delay_minutes` for reminders."
91            },
92            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
93        }
94    })
95}
96
97#[must_use]
98pub fn unified_exec_parameters() -> Value {
99    json!({
100        "type": "object",
101        "properties": {
102            "command": {
103                "description": "Command as a shell string or argv array.",
104                "anyOf": [
105                    {"type": "string"},
106                    {
107                        "type": "array",
108                        "items": {"type": "string"}
109                    }
110                ]
111            },
112            "input": {"type": "string", "description": "stdin for write or continue."},
113            "session_id": {"type": "string", "description": "Session id. Compact alias: `s`."},
114            "spool_path": {"type": "string", "description": "Spool path for inspect."},
115            "query": {"type": "string", "description": "Line filter for inspect or run output."},
116            "head_lines": {"type": "integer", "description": "Head preview lines."},
117            "tail_lines": {"type": "integer", "description": "Tail preview lines."},
118            "max_matches": {"type": "integer", "description": "Max filtered matches.", "default": 200},
119            "literal": {"type": "boolean", "description": "Treat query as literal text.", "default": false},
120            "code": {"type": "string", "description": "Raw Python or JavaScript source for `action=code`. Send the source directly, not JSON or markdown fences. Connected MCP tools are callable as library functions from this snippet, so use it to filter/aggregate large data in-process and return only small summaries instead of dumping output into context."},
121            "language": {
122                "type": "string",
123                "enum": ["python3", "javascript"],
124                "description": "Language for `action=code`. Defaults to `python3`; set `javascript` to run Node-based code execution instead.",
125                "default": "python3"
126            },
127            "action": {
128                "type": "string",
129                "enum": ["run", "write", "poll", "continue", "inspect", "list", "close", "code"],
130                "description": "Optional; inferred from command/code/input/session_id/spool_path. Use `code` to run a fresh Python or JavaScript snippet through the local code executor."
131            },
132            "workdir": {"type": "string", "description": "Working directory. Alias: cwd."},
133            "tty": {"type": "boolean", "description": "Use PTY mode.", "default": false},
134            "shell": {"type": "string", "description": "Shell binary."},
135            "login": {"type": "boolean", "description": "Use a login shell.", "default": false},
136            "sandbox_permissions": {
137                "type": "string",
138                "enum": ["use_default", "with_additional_permissions", "require_escalated"],
139                "description": "Sandbox policy. Use `require_escalated` only when needed."
140            },
141            "additional_permissions": {
142                "type": "object",
143                "description": "Extra sandboxed filesystem access.",
144                "properties": {
145                    "fs_read": {
146                        "type": "array",
147                        "items": {"type": "string"},
148                        "description": "Extra readable paths."
149                    },
150                    "fs_write": {
151                        "type": "array",
152                        "items": {"type": "string"},
153                        "description": "Extra writable paths."
154                    }
155                },
156                "additionalProperties": false
157            },
158            "justification": {"type": "string", "description": "Approval question for `require_escalated`."},
159            "prefix_rule": {
160                "type": "array",
161                "items": {"type": "string"},
162                "description": "Optional persisted approval prefix for `command`."
163            },
164            "timeout_secs": {"type": "integer", "description": "Timeout seconds.", "default": 180},
165            "yield_time_ms": {"type": "integer", "description": "Wait before returning output (ms).", "default": 1000},
166            "confirm": {"type": "boolean", "description": "Confirm destructive ops.", "default": false},
167            "max_output_tokens": {"type": "integer", "description": "Output token cap."},
168            "track_files": {"type": "boolean", "description": "Track file changes.", "default": false}
169        }
170    })
171}
172
173#[must_use]
174pub fn unified_file_parameters() -> Value {
175    json!({
176        "type": "object",
177        "properties": {
178            "action": {
179                "type": "string",
180                "enum": ["read", "write", "edit", "patch", "delete", "move", "copy"],
181                "description": "Optional; inferred from old_str/patch/content/destination/path."
182            },
183            "path": {"type": "string", "description": "File path. Accepts file_path/filepath/target_path/file/p."},
184            "content": {"type": "string", "description": "Content for write."},
185            "old_str": {"type": "string", "description": "Exact text to replace for edit. Max 800 chars or 40 lines; use patch for larger edits."},
186            "new_str": {"type": "string", "description": "Replacement text for edit. Max 800 chars or 40 lines; use patch for larger edits."},
187            "patch": {"type": "string", "description": "Patch text in `*** Update File:` format, not unified diff."},
188            "destination": {"type": "string", "description": "Destination for move or copy."},
189            "offset": {"type": "integer", "description": "Read start line (1-indexed). Compact alias: `o`."},
190            "limit": {"type": "integer", "description": "Read line count. Compact alias: `l`."},
191            "mode": {"type": "string", "description": "Read mode or write mode.", "default": "slice"},
192            "condense": {"type": "boolean", "description": "Condense long output to head/tail. Set false for full content.", "default": true},
193            "raw": {"type": "boolean", "description": "Bypass output spooling and return full content inline. Use when you need exact file content without spooling to disk.", "default": false},
194            "indentation": {
195                "description": "Indentation config. `true` uses defaults.",
196                "anyOf": [
197                    {"type": "boolean"},
198                    {
199                        "type": "object",
200                        "properties": {
201                            "anchor_line": {"type": "integer", "description": "Anchor line; defaults to offset."},
202                            "max_levels": {"type": "integer", "description": "Indent depth cap; 0 means unlimited."},
203                            "include_siblings": {"type": "boolean", "description": "Include sibling blocks."},
204                            "include_header": {"type": "boolean", "description": "Include header lines."},
205                            "max_lines": {"type": "integer", "description": "Optional line cap."}
206                        },
207                        "additionalProperties": false
208                    }
209                ]
210            },
211            "offset_bytes": {"type": "integer", "description": "Byte offset (0-indexed) for byte-range reads. Enables chunked preview of large files.", "minimum": 0},
212            "page_size_bytes": {"type": "integer", "description": "Bytes to read. Accepts alias `length`. Default: 8192.", "minimum": 1}
213        }
214    })
215}
216
217#[must_use]
218pub fn read_file_parameters() -> Value {
219    json!({
220        "type": "object",
221        "properties": {
222            "path": {"type": "string", "description": "File path. Accepts file_path/filepath/target_path/file/p."},
223            "offset": {"type": "integer", "description": "1-indexed line offset. Compact alias: `o`.", "minimum": 1},
224            "limit": {"type": "integer", "description": "Max lines for this chunk. Compact alias: `l`. Requests above the configurable absolute cap (max_read_lines, default 400) are clamped, and the response carries `next_read_args` to continue.", "default": 400, "minimum": 1},
225            "mode": {"type": "string", "enum": ["slice", "indentation"], "description": "Read mode.", "default": "slice"},
226            "indentation": {
227                "description": "Indentation-aware block selection.",
228                "anyOf": [
229                    {"type": "boolean"},
230                    {
231                        "type": "object",
232                        "properties": {
233                            "anchor_line": {"type": "integer", "description": "Anchor line; defaults to offset."},
234                            "max_levels": {"type": "integer", "description": "Indent depth cap; 0 means unlimited."},
235                            "include_siblings": {"type": "boolean", "description": "Include sibling blocks."},
236                            "include_header": {"type": "boolean", "description": "Include header lines."},
237                            "max_lines": {"type": "integer", "description": "Optional line cap."}
238                        },
239                        "additionalProperties": false
240                    }
241                ]
242            },
243            "offset_lines": {"type": "integer", "description": "Legacy alias for line offset.", "minimum": 1},
244            "page_size_lines": {"type": "integer", "description": "Legacy alias for line chunk size.", "minimum": 1},
245            "offset_bytes": {"type": "integer", "description": "Byte offset for binary or byte-paged reads.", "minimum": 0},
246            "page_size_bytes": {"type": "integer", "description": "Byte page size for binary or byte-paged reads.", "minimum": 1},
247            "max_bytes": {"type": "integer", "description": "Maximum bytes to return.", "minimum": 1},
248            "max_lines": {"type": "integer", "description": "Maximum lines to return in legacy mode.", "minimum": 1},
249            "chunk_lines": {"type": "integer", "description": "Legacy alias for chunk size in lines.", "minimum": 1},
250            "max_tokens": {"type": "integer", "description": "Optional token budget for large reads.", "minimum": 1},
251            "condense": {"type": "boolean", "description": "Condense long outputs to head/tail. Set false for full content.", "default": true}
252        }
253    })
254}
255
256#[must_use]
257pub fn unified_search_parameters() -> Value {
258    json!({
259        "type": "object",
260        "properties": {
261            "action": {
262                "type": "string",
263                "enum": ["grep", "list", "structural", "outline", "tools", "errors", "agent", "web", "skill"],
264                "description": "Search action: grep (text), list (files; paginated, default 20/page — use max_results for more), structural (ast-grep pattern search), outline (symbol map of a file/directory, no pattern needed — preferred for 'what's here?' / repo or directory overview), tools, errors, agent, web, skill. For 'web', provide 'query' to search the web, or 'url' to fetch a specific page."
265            },
266            "workflow": {
267                "type": "string",
268                "enum": ["query", "scan", "test", "rewrite", "new", "apply"],
269                "description": "Structural workflow: query (search), scan (config rules), test (rule tests), rewrite (preview), apply (write), new (scaffold).",
270                "default": "query"
271            },
272            "pattern": {"type": "string", "description": "For grep: regex/literal. For list: glob filter. For structural: ast-grep pattern ($VAR=node, $$$ARGS=many). At least one of pattern or kind required for structural query."},
273            "kind": {"type": "string", "description": "Ast-grep node kind (e.g. function_item, call_expression). Supports >, +, ~, :has(), :not() selectors. Use alone or with pattern."},
274            "path": {"type": "string", "description": "Directory or file path to search in. Used by `grep`, `list`, structural `workflow=\"query\"|\"scan\"`, and `outline`. Public structural calls take one root per request even though raw ast-grep `run` can accept multiple paths.", "default": "."},
275            "config_path": {"type": "string", "description": "Ast-grep config path for structural `workflow=\"scan\"` or `workflow=\"test\"`. Defaults to workspace `sgconfig.yml`."},
276            "filter": {"type": "string", "description": "Ast-grep rule or test filter for structural `workflow=\"scan\"` or `workflow=\"test\"`. On `scan`, this maps to `--filter` over rule ids from config."},
277            "lang": {"type": "string", "description": "Language for structural `workflow=\"query\"` or `workflow=\"rewrite\"`, and for `outline`. Set it whenever the code language is known; required for debug_query and recommended for rewrite."},
278            "selector": {"type": "string", "description": "Ast-grep selector when match is a subnode. Supports :has(), :not(), :is(), :nth-child()."},
279            "strictness": {
280                "type": "string",
281                "enum": ["cst", "smart", "ast", "relaxed", "signature", "template"],
282                "description": "Pattern strictness for structural `workflow=\"query\"`."
283            },
284            "view": {
285                "type": "string",
286                "enum": ["digest", "names", "full"],
287                "description": "Output shape for `outline`: digest (symbols grouped by kind, default for single-file queries), names (flat name groups, default for directory queries — auto-applied when `view=full` is requested on a large directory to prevent truncation), full (per-symbol records with the raw zero-based `range`, a derived 1-based inclusive `lineRange` usable with `unified_file read` pagination, `astKind`, signatures, and nested members — use on individual files, not large directories). Directory queries also receive a top-level `summary` block with `total_symbols`, `by_kind` (per-kind symbol counts), and a flat `all_symbols` array (capped at 200 entries; `truncated`/`visible_symbols` are set when the cap is hit).",
288                "default": "digest"
289            },
290            "items": {
291                "type": "string",
292                "enum": ["auto", "structure", "exports", "imports", "all"],
293                "description": "Which top-level symbols `outline` includes. `auto` (default) uses structure for file input and exports for directory input.",
294                "default": "auto"
295            },
296            "type": {
297                "description": "Comma-separated symbol types to keep in `outline` (e.g. \"function\", [\"class\",\"enum\"]).",
298                "anyOf": [
299                    {"type": "string"},
300                    {"type": "array", "items": {"type": "string"}}
301                ]
302            },
303            "match": {"type": "string", "description": "Regex for `outline` to filter item names/signatures/first lines."},
304            "pub_members": {"type": "boolean", "description": "In `outline`, show only public members.", "default": false},
305            "follow": {"type": "boolean", "description": "Follow symbolic links while traversing directories. Used by `outline` and structural workflows.", "default": false},
306            "debug_query": {
307                "type": "string",
308                "enum": ["pattern", "ast", "cst", "sexp"],
309                "description": "Print the structural query AST instead of matches for `workflow=\"query\"`. Requires lang."
310            },
311            "globs": {
312                "description": "Optional include/exclude globs for structural `workflow=\"query\"` or `workflow=\"scan\"`. Maps to repeated ast-grep `--globs` flags.",
313                "anyOf": [
314                    {"type": "string"},
315                    {"type": "array", "items": {"type": "string"}}
316                ]
317            },
318            "skip_snapshot_tests": {"type": "boolean", "description": "Skip ast-grep snapshot tests for structural `workflow=\"test\"`.", "default": false},
319            "rewrite": {"type": "string", "description": "Replacement string for structural `workflow=\"rewrite\"`. Meta variables from `pattern` can be referenced (e.g. `$VAR`, `$$$ARGS`). For simple pattern-to-pattern rewrites. Either `rewrite` or `fix_config` is required for `workflow=\"rewrite\"`."},
320            "fix_config": {
321                "type": "object",
322                "description": "Advanced fix configuration for structural `workflow=\"rewrite\"`. Use when replacing only the matched node is not enough, especially for deleting list items or key-value pairs that also need a surrounding comma removed. Either `rewrite` or `fix_config` is required for `workflow=\"rewrite\"`.",
323                "properties": {
324                    "template": {"type": "string", "description": "Replacement template string. Meta variables from `pattern` can be referenced."},
325                    "expand_start": {
326                        "type": "object",
327                        "description": "Expand fix range start backwards. Requires at least one of: regex, kind, pattern.",
328                        "properties": {
329                            "regex": {"type": "string", "description": "Regex to match node text."},
330                            "kind": {"type": "string", "description": "Tree-sitter node kind."},
331                            "pattern": {"type": "string", "description": "Ast-grep pattern."},
332                            "stop_by": {"description": "Expansion stop rule. String (\"line\"|\"end\") or rule object."}
333                        }
334                    },
335                    "expand_end": {
336                        "type": "object",
337                        "description": "Expand fix range end forwards. Requires at least one of: regex, kind, pattern.",
338                        "properties": {
339                            "regex": {"type": "string", "description": "Regex to match node text."},
340                            "kind": {"type": "string", "description": "Tree-sitter node kind."},
341                            "pattern": {"type": "string", "description": "Ast-grep pattern."},
342                            "stop_by": {"description": "Expansion stop rule. String (\"line\"|\"end\") or rule object."}
343                        }
344                    }
345                },
346                "required": ["template"]
347            },
348            "new_subcommand": {"type": "string", "enum": ["project", "rule", "test", "util"], "description": "Subcommand for structural `workflow=\"new\"`. `project` scaffolds sgconfig.yml and directories; `rule` creates a new rule YAML; `test` creates a new test YAML; `util` creates a new utility rule."},
349            "new_name": {"type": "string", "description": "Name for the new rule, test, or utility. Required for `new` subcommands `rule`, `test`, and `util`."},
350            "keyword": {"type": "string", "description": "Keyword for 'tools' search."},
351            "url": {"type": "string", "format": "uri", "description": "URL to fetch content from (for 'web' action). Mutually exclusive with 'query'."},
352            "query": {"type": "string", "description": "Search query for 'web' action. Uses keyless DuckDuckGo. Returns ranked results (title, url, snippet). Mutually exclusive with 'url'."},
353            "prompt": {"type": "string", "description": "The prompt to run on the fetched content (for 'web' action with 'url')."},
354            "name": {"type": "string", "description": "Skill name to load (for 'skill' action)."},
355            "detail_level": {
356                "type": "string",
357                "enum": ["name-only", "name-and-description", "full"],
358                "description": "Detail level for 'tools' action.",
359                "default": "name-and-description"
360            },
361            "mode": {
362                "type": "string",
363                "description": "Mode for 'list' (list|recursive|tree|etc) or 'agent' (debug|analyze|full) action.",
364                "default": "list"
365            },
366            "max_results": {"type": "integer", "description": "Max results to return.", "default": 100},
367            "case_sensitive": {"type": "boolean", "description": "Case-sensitive search.", "default": false},
368            "context_lines": {"type": "integer", "description": "Context lines for `grep` or structural `workflow=\"query\"|\"scan\"` results. Structural maps this to ast-grep `--context`; raw `--before` and `--after` are not exposed separately.", "default": 0},
369            "severities": {
370                "type": "array",
371                "items": {"type": "string", "enum": ["error", "warning", "info", "hint"]},
372                "description": "Post-run severity filter for structural `workflow=\"scan\"`. When present, only findings matching one of the listed severities are returned. Does not override rule severities at the CLI level."
373            },
374            "no_ignore": {
375                "type": "array",
376                "items": {"type": "string", "enum": ["hidden", "dot", "exclude", "global", "parent", "vcs"]},
377                "description": "Ignore file overrides: hidden, dot, exclude, global, parent, vcs."
378            },
379            "threads": {"type": "integer", "description": "Number of threads for ast-grep scan parallelism. 0 means auto. Only for `workflow=\"scan\"`.", "minimum": 0, "maximum": 256, "default": 0},
380            "format": {"type": "string", "enum": ["github", "sarif", "files_with_matches", "count"], "description": "Output format for structural `workflow=\"scan\"`. `github`/`sarif`: CI pipeline formats (raw output). `files_with_matches`: return only unique file paths. `count`: return match counts per file."},
381            "report_style": {"type": "string", "enum": ["rich", "medium", "short"], "description": "Diagnostic report style for structural `workflow=\"scan\"`. Controls verbosity of diagnostic output."},
382            "before_lines": {"type": "integer", "description": "Context lines before each match for structural workflows. Mutually exclusive with `context_lines`.", "minimum": 0, "maximum": 20},
383            "after_lines": {"type": "integer", "description": "Context lines after each match for structural workflows. Mutually exclusive with `context_lines`.", "minimum": 0, "maximum": 20},
384            "builtin_rules": {
385                "type": "array",
386                "items": {"type": "string"},
387                "description": "Built-in ast-grep rules to activate for `workflow=\"scan\"`. Valid values: `unused-suppression` (reports stale ignore directives), `no-suppress-all` (reports suppress-all comments). Use `\"rule:severity\"` format to set severity (e.g. `\"unused-suppression:error\"`). Default severity is hint."
388            },
389            "scope": {"type": "string", "description": "Scope for 'errors' action (archive|all).", "default": "archive"},
390            "max_bytes": {"type": "integer", "description": "Maximum bytes to fetch for 'web' action.", "default": 500000},
391            "timeout_secs": {"type": "integer", "description": "Timeout in seconds.", "default": 30}
392        }
393    })
394}
395
396#[must_use]
397pub fn list_files_parameters() -> Value {
398    json!({
399        "type": "object",
400        "properties": {
401            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
402            "mode": {
403                "type": "string",
404                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
405                "description": "Listing mode. Use page/per_page to continue paginated results.",
406                "default": "list"
407            },
408            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
409            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
410            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
411            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
412            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
413            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
414            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
415            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
416            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
417        }
418    })
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use serde_json::json;
425
426    #[test]
427    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
428        let schema = apply_patch_parameter_schema("Patch in VT Code format");
429
430        // Both `input` and `patch` alias fields now carry the format
431        // description AND the semantic-anchor guidance, preventing the model
432        // from placing a unified diff in `patch` (see checkpoint turn_615).
433        assert_eq!(
434            schema["properties"]["patch"]["description"],
435            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
436        );
437        let patch_description = schema["properties"]["patch"]["description"]
438            .as_str()
439            .expect("patch description");
440        assert!(patch_description.contains("*** Begin Patch"));
441        assert!(patch_description.contains("unified diff"));
442        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
443
444        let input_description = schema["properties"]["input"]["description"]
445            .as_str()
446            .expect("input description");
447        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
448    }
449
450    #[test]
451    fn unified_exec_schema_accepts_string_or_array_commands() {
452        let params = unified_exec_parameters();
453        let command = &params["properties"]["command"];
454        let variants = command["anyOf"].as_array().expect("command anyOf");
455
456        assert_eq!(variants.len(), 2);
457        assert_eq!(variants[0]["type"], "string");
458        assert_eq!(variants[1]["type"], "array");
459        assert_eq!(variants[1]["items"]["type"], "string");
460        assert_eq!(params["properties"]["tty"]["type"], "boolean");
461        assert_eq!(params["properties"]["tty"]["default"], false);
462        assert!(
463            params["properties"]["code"]["description"]
464                .as_str()
465                .expect("code description")
466                .contains("Raw Python or JavaScript source")
467        );
468        assert!(
469            params["properties"]["language"]["description"]
470                .as_str()
471                .expect("language description")
472                .contains("set `javascript`")
473        );
474    }
475
476    #[test]
477    fn unified_search_schema_advertises_structural_and_hides_intelligence() {
478        let params = unified_search_parameters();
479        let actions = params["properties"]["action"]["enum"]
480            .as_array()
481            .expect("action enum");
482
483        assert!(actions.iter().any(|value| value == "structural"));
484        assert!(actions.iter().any(|value| value == "outline"));
485        assert!(!actions.iter().any(|value| value == "intelligence"));
486        assert!(
487            params["properties"]["view"]["enum"]
488                .as_array()
489                .expect("view enum")
490                .iter()
491                .any(|value| value == "digest")
492        );
493        assert!(
494            params["properties"]["items"]["enum"]
495                .as_array()
496                .expect("items enum")
497                .iter()
498                .any(|value| value == "auto")
499        );
500        assert!(
501            params["properties"]["debug_query"]["enum"]
502                .as_array()
503                .expect("debug_query enum")
504                .iter()
505                .any(|value| value == "ast")
506        );
507        assert!(
508            params["properties"]["action"]["description"]
509                .as_str()
510                .expect("action description")
511                .contains("structural")
512        );
513        assert!(
514            params["properties"]["action"]["description"]
515                .as_str()
516                .expect("action description")
517                .contains("preferred"),
518            "action description should mark outline as preferred for repo overview"
519        );
520        assert!(
521            params["properties"]["action"]["description"]
522                .as_str()
523                .expect("action description")
524                .contains("paginated"),
525            "action description should warn that list is paginated"
526        );
527        assert!(
528            params["properties"]["pattern"]["description"]
529                .as_str()
530                .expect("pattern description")
531                .contains("ast-grep pattern")
532        );
533        assert!(
534            params["properties"]["pattern"]["description"]
535                .as_str()
536                .expect("pattern description")
537                .contains("$$$ARGS")
538        );
539        assert!(
540            params["properties"]["pattern"]["description"]
541                .as_str()
542                .expect("pattern description")
543                .contains("glob filter")
544        );
545        assert!(
546            params["properties"]["action"]["description"]
547                .as_str()
548                .expect("action description")
549                .contains("grep")
550        );
551        assert_eq!(params["properties"]["workflow"]["enum"][1], "scan");
552        assert_eq!(params["properties"]["workflow"]["enum"][2], "test");
553        assert!(
554            params["properties"]["config_path"]["description"]
555                .as_str()
556                .expect("config path description")
557                .contains("Defaults to workspace `sgconfig.yml`")
558        );
559        assert!(
560            params["properties"]["skip_snapshot_tests"]["description"]
561                .as_str()
562                .expect("skip snapshot description")
563                .contains("workflow=\"test\"")
564        );
565    }
566
567    #[test]
568    fn legacy_browse_tool_schemas_expose_chunking_and_pagination_fields() {
569        let read_params = read_file_parameters();
570        assert!(read_params["properties"]["offset"].is_object());
571        assert!(read_params["properties"]["limit"].is_object());
572        assert!(read_params["properties"]["page_size_lines"].is_object());
573
574        let list_params = list_files_parameters();
575        assert!(list_params["properties"]["page"].is_object());
576        assert!(list_params["properties"]["per_page"].is_object());
577        assert!(
578            list_params["properties"]["mode"]["enum"]
579                .as_array()
580                .expect("mode enum")
581                .iter()
582                .any(|value| value == "recursive")
583        );
584    }
585
586    #[test]
587    fn semantic_anchor_guidance_is_appended_once() {
588        let base = "Patch in VT Code format.";
589        let with_guidance = with_semantic_anchor_guidance(base);
590
591        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
592        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
593    }
594
595    #[test]
596    fn default_apply_patch_parameters_keep_expected_alias_shape() {
597        let schema = apply_patch_parameters();
598
599        assert_eq!(
600            schema["anyOf"],
601            json!([
602                {"required": ["input"]},
603                {"required": ["patch"]}
604            ])
605        );
606    }
607}