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