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, close_agent_parameters, request_user_input_description,
16 request_user_input_parameters, resume_agent_parameters, send_input_parameters,
17 spawn_agent_parameters, 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_parameters() -> Value {
76 json!({
77 "type": "object",
78 "required": ["action"],
79 "additionalProperties": false,
80 "properties": {
81 "action": {
82 "type": "string",
83 "enum": ["create", "list", "delete"],
84 "description": "create: schedule a prompt (requires prompt and exactly one of cron, delay_minutes, or run_at). list: show scheduled prompts. delete: remove one by id."
85 },
86 "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
87 "name": {"type": "string", "description": "create: optional short label for the task."},
88 "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
89 "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
90 "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
91 "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
92 }
93 })
94}
95
96#[must_use]
97pub fn mcp_parameters() -> Value {
98 json!({
99 "type": "object",
100 "required": ["action"],
101 "properties": {
102 "action": {
103 "type": "string",
104 "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
105 "description": "search_tools: find MCP tools by natural-language query. get_tool_details: fetch the full input schema for one MCP tool name. list_servers: list configured servers and their connection state. connect or disconnect: manage one configured MCP server by name."
106 },
107 "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
108 "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
109 "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
110 "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
111 },
112 "additionalProperties": false
113 })
114}
115
116#[must_use]
117pub fn cron_create_parameters() -> Value {
118 json!({
119 "type": "object",
120 "required": ["prompt"],
121 "additionalProperties": false,
122 "properties": {
123 "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
124 "name": {"type": "string", "description": "Optional short label for the task."},
125 "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
126 "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
127 "run_at": {
128 "type": "string",
129 "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
130 }
131 }
132 })
133}
134
135#[must_use]
136pub fn cron_list_parameters() -> Value {
137 json!({
138 "type": "object",
139 "properties": {},
140 "additionalProperties": false
141 })
142}
143
144#[must_use]
145pub fn cron_delete_parameters() -> Value {
146 json!({
147 "type": "object",
148 "required": ["id"],
149 "properties": {
150 "id": {"type": "string", "description": "Session scheduled task id to delete."}
151 }
152 })
153}
154
155#[must_use]
156pub fn exec_command_parameters() -> Value {
157 json!({
158 "type": "object",
159 "required": ["cmd"],
160 "properties": {
161 "cmd": {"type": "string", "description": "Shell command to execute, subject to command policy. Examples include `ls`, `rg`, `find`, `cat`, `sed`, `awk`, build tools, and test tools."},
162 "yield_time_ms": {"type": "integer", "description": "Wait before returning output (ms). If the command is still running, the response includes a session_id for write_stdin.", "default": 10000},
163 "max_output_tokens": {"type": "integer", "description": "Output token cap. Large or truncated output can return a spool_path for the full output."},
164 "workdir": {"type": "string", "description": "Working directory."},
165 "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
166 "sandbox_permissions": {
167 "type": "string",
168 "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
169 "description": "Sandbox permission mode for this command.",
170 "default": "use_default"
171 },
172 "additional_permissions": {
173 "type": "object",
174 "description": "Additional filesystem access requested with sandbox_permissions set to with_additional_permissions.",
175 "properties": {
176 "fs_read": {"type": "array", "items": {"type": "string"}},
177 "fs_write": {"type": "array", "items": {"type": "string"}}
178 },
179 "additionalProperties": false
180 },
181 "justification": {"type": "string", "description": "Short approval question required for escalated or bypassed sandbox execution."}
182 },
183 "additionalProperties": false
184 })
185}
186
187#[must_use]
188pub fn write_stdin_parameters() -> Value {
189 json!({
190 "type": "object",
191 "required": ["session_id", "chars"],
192 "properties": {
193 "session_id": {"type": "string", "description": "Active execution session id."},
194 "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
195 "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
196 "max_output_tokens": {"type": "integer", "description": "Output token cap for the continuation response. Large or truncated output can return a spool_path for the full output."}
197 },
198 "additionalProperties": false
199 })
200}
201
202#[must_use]
203pub fn code_search_parameters() -> Value {
204 json!({
205 "type": "object",
206 "required": ["query"],
207 "additionalProperties": false,
208 "properties": {
209 "query": {
210 "type": "string",
211 "minLength": 1,
212 "pattern": "\\S",
213 "description": "Literal code or path query. Smart-case applies to content and exact symbol-name matching: a wholly lower-case query matches case-insensitively, while an upper-case character makes matching case-sensitive. Path matching remains fuzzy and case-insensitive."
214 },
215 "path": {
216 "type": "string",
217 "minLength": 1,
218 "pattern": "\\S",
219 "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
220 },
221 "file_types": {
222 "type": "array",
223 "minItems": 1,
224 "items": {
225 "type": "string",
226 "minLength": 1,
227 "pattern": "\\S"
228 },
229 "description": "Language names or common file extensions, with or without one leading dot."
230 },
231 "result_types": {
232 "type": "array",
233 "minItems": 1,
234 "items": {
235 "type": "string",
236 "enum": ["definition", "usage", "text", "path"]
237 },
238 "description": "Result categories to include. Omit to include all four categories."
239 },
240 "max_results": {
241 "type": "integer",
242 "minimum": 1,
243 "maximum": 100,
244 "description": "Maximum number of merged results to return. Omit for 20."
245 }
246 }
247 })
248}
249
250#[must_use]
251pub fn list_files_parameters() -> Value {
252 json!({
253 "type": "object",
254 "properties": {
255 "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
256 "mode": {
257 "type": "string",
258 "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
259 "description": "Listing mode. Use page/per_page to continue paginated results.",
260 "default": "list"
261 },
262 "pattern": {"type": "string", "description": "Optional glob-style path filter."},
263 "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
264 "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
265 "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
266 "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
267 "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
268 "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
269 "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
270 "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
271 }
272 })
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use serde_json::json;
279
280 #[test]
281 fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
282 let schema = apply_patch_parameter_schema("Patch in VT Code format");
283
284 assert_eq!(
288 schema["properties"]["patch"]["description"],
289 with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
290 );
291 let patch_description = schema["properties"]["patch"]["description"]
292 .as_str()
293 .expect("patch description");
294 assert!(patch_description.contains("*** Begin Patch"));
295 assert!(patch_description.contains("unified diff"));
296 assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
297
298 let input_description = schema["properties"]["input"]["description"]
299 .as_str()
300 .expect("input description");
301 assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
302 }
303
304 #[test]
305 fn codex_baseline_exec_schemas_use_public_names_shape() {
306 let exec_params = exec_command_parameters();
307 assert_eq!(exec_params["required"], json!(["cmd"]));
308 assert!(exec_params["properties"]["cmd"].is_object());
309 assert!(exec_params["properties"]["workdir"].is_object());
310 assert!(
311 exec_params["properties"]["yield_time_ms"]["description"]
312 .as_str()
313 .expect("exec yield description")
314 .contains("session_id")
315 );
316 assert!(
317 exec_params["properties"]["max_output_tokens"]["description"]
318 .as_str()
319 .expect("exec max output description")
320 .contains("spool_path")
321 );
322 assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
323 assert_eq!(exec_params["properties"]["tty"]["default"], false);
324 assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
325 assert_eq!(
326 exec_params["properties"]["sandbox_permissions"]["enum"],
327 json!([
328 "use_default",
329 "with_additional_permissions",
330 "require_escalated",
331 "bypass_sandbox"
332 ])
333 );
334 assert_eq!(
335 exec_params["properties"]["sandbox_permissions"]["default"],
336 "use_default"
337 );
338 assert_eq!(
339 exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
340 "string"
341 );
342 assert_eq!(
343 exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]
344 ["type"],
345 "string"
346 );
347 assert_eq!(
348 exec_params["properties"]["additional_permissions"]["additionalProperties"],
349 false
350 );
351 assert_eq!(exec_params["properties"]["justification"]["type"], "string");
352 assert_eq!(exec_params["additionalProperties"], false);
353 for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
354 assert!(
355 exec_params["properties"]["cmd"]["description"]
356 .as_str()
357 .expect("cmd description")
358 .contains(command),
359 "{command} should be described as an exec_command.cmd example"
360 );
361 assert!(
362 exec_params["properties"].get(command).is_none(),
363 "{command} must not be modelled as a separate exec_command field"
364 );
365 }
366
367 let stdin_params = write_stdin_parameters();
368 assert_eq!(stdin_params["required"], json!(["session_id", "chars"]));
369 assert!(stdin_params["properties"]["session_id"].is_object());
370 assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
371 assert!(
372 stdin_params["properties"]["chars"]["description"]
373 .as_str()
374 .is_some_and(|description| description.contains("empty string"))
375 );
376 assert!(stdin_params["properties"]["chars"].is_object());
377 assert!(
378 stdin_params["properties"]["yield_time_ms"]["description"]
379 .as_str()
380 .expect("stdin yield description")
381 .contains("fresh session output")
382 );
383 assert!(
384 stdin_params["properties"]["max_output_tokens"]["description"]
385 .as_str()
386 .expect("stdin max output description")
387 .contains("spool_path")
388 );
389 assert_eq!(stdin_params["additionalProperties"], false);
390 }
391
392 #[test]
393 fn code_search_schema_exposes_exact_five_property_contract() {
394 let params = code_search_parameters();
395 let properties = params["properties"].as_object().expect("properties");
396 let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
397 property_names.sort_unstable();
398
399 assert_eq!(params["required"], json!(["query"]));
400 assert_eq!(
401 property_names,
402 ["file_types", "max_results", "path", "query", "result_types"]
403 );
404 assert_eq!(params["additionalProperties"], false);
405 assert_eq!(params["properties"]["query"]["pattern"], "\\S");
406 assert_eq!(params["properties"]["file_types"]["minItems"], 1);
407 assert_eq!(params["properties"]["result_types"]["minItems"], 1);
408 assert_eq!(
409 params["properties"]["result_types"]["items"]["enum"],
410 json!(["definition", "usage", "text", "path"])
411 );
412 assert_eq!(params["properties"]["max_results"]["minimum"], 1);
413 assert_eq!(params["properties"]["max_results"]["maximum"], 100);
414 assert!(params.get("anyOf").is_none());
415 }
416
417 #[test]
418 fn legacy_list_files_schema_exposes_pagination_fields() {
419 let list_params = list_files_parameters();
420 assert!(list_params["properties"]["page"].is_object());
421 assert!(list_params["properties"]["per_page"].is_object());
422 assert!(
423 list_params["properties"]["mode"]["enum"]
424 .as_array()
425 .expect("mode enum")
426 .iter()
427 .any(|value| value == "recursive")
428 );
429 }
430
431 #[test]
432 fn semantic_anchor_guidance_is_appended_once() {
433 let base = "Patch in VT Code format.";
434 let with_guidance = with_semantic_anchor_guidance(base);
435
436 assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
437 assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
438 }
439
440 #[test]
441 fn default_apply_patch_parameters_keep_expected_alias_shape() {
442 let schema = apply_patch_parameters();
443
444 assert_eq!(
445 schema["anyOf"],
446 json!([
447 {"required": ["input"]},
448 {"required": ["patch"]}
449 ])
450 );
451 }
452}