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 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
25pub 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 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 = ¶ms["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}