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