1#![allow(missing_docs, dead_code, unused_imports)]
2
3#![recursion_limit = "256"]
6use serde_json::{Value, json};
7
8mod collaboration;
9mod json_schema;
10#[cfg(feature = "mcp")]
11mod mcp_tool;
12mod responses_api;
13mod tool_kind;
14
15pub use collaboration::{
16 agent_parameters, close_agent_parameters, request_user_input_description, request_user_input_parameters,
17 resume_agent_parameters, send_input_parameters, spawn_agent_parameters, spawn_background_subprocess_parameters,
18 wait_agent_parameters,
19};
20pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
21#[cfg(feature = "mcp")]
22pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
23pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
24pub(crate) use tool_kind::{CanonicalToolMeta, TokenBucket, ToolKind, ToolNamespace};
25
26pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
27 "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
28
29pub 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).";
36pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str =
37 "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch";
38
39pub const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
41pub const MIN_MAX_OUTPUT_TOKENS: usize = 1;
43pub const MAX_MAX_OUTPUT_TOKENS: usize = 50_000;
45
46#[must_use]
52pub fn with_max_output_tokens_parameter(mut schema: Value) -> Value {
53 let Some(schema_object) = schema.as_object_mut() else {
54 return schema;
55 };
56 let properties = schema_object
57 .entry("properties")
58 .or_insert_with(|| Value::Object(serde_json::Map::new()));
59 let Some(properties_object) = properties.as_object_mut() else {
60 return schema;
61 };
62 properties_object.entry("max_output_tokens").or_insert_with(|| {
63 json!({
64 "type": "integer",
65 "minimum": MIN_MAX_OUTPUT_TOKENS,
66 "maximum": MAX_MAX_OUTPUT_TOKENS,
67 "default": DEFAULT_MAX_OUTPUT_TOKENS,
68 "description": "Maximum number of result tokens returned to the model. Full oversized output is spooled when available."
69 })
70 });
71 schema
72}
73
74#[must_use]
75pub fn with_semantic_anchor_guidance(base: &str) -> String {
76 let trimmed = base.trim_end();
77 if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
78 trimmed.to_string()
79 } else if trimmed.ends_with('.') {
80 format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
81 } else {
82 format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
83 }
84}
85
86#[must_use]
87pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
88 json!({
89 "type": "object",
90 "properties": {
91 "input": {
92 "type": "string",
93 "description": with_semantic_anchor_guidance(input_description)
94 },
95 "patch": {
96 "type": "string",
97 "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
98 }
99 },
100 "anyOf": [
101 {"required": ["input"]},
102 {"required": ["patch"]}
103 ]
104 })
105}
106
107#[must_use]
108pub fn apply_patch_parameters() -> Value {
109 apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
110}
111
112#[must_use]
113pub fn cron_parameters() -> Value {
114 json!({
115 "type": "object",
116 "required": ["action"],
117 "additionalProperties": false,
118 "properties": {
119 "action": {
120 "type": "string",
121 "enum": ["create", "list", "delete"],
122 "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."
123 },
124 "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
125 "name": {"type": "string", "description": "create: optional short label for the task."},
126 "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
127 "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
128 "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
129 "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
130 }
131 })
132}
133
134#[must_use]
135pub fn mcp_parameters() -> Value {
136 json!({
137 "type": "object",
138 "required": ["action"],
139 "properties": {
140 "action": {
141 "type": "string",
142 "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
143 "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."
144 },
145 "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
146 "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
147 "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
148 "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
149 },
150 "additionalProperties": false
151 })
152}
153
154#[must_use]
155pub fn cron_create_parameters() -> Value {
156 json!({
157 "type": "object",
158 "required": ["prompt"],
159 "additionalProperties": false,
160 "properties": {
161 "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
162 "name": {"type": "string", "description": "Optional short label for the task."},
163 "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
164 "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
165 "run_at": {
166 "type": "string",
167 "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
168 }
169 }
170 })
171}
172
173#[must_use]
174pub fn cron_list_parameters() -> Value {
175 json!({
176 "type": "object",
177 "properties": {},
178 "additionalProperties": false
179 })
180}
181
182#[must_use]
183pub fn cron_delete_parameters() -> Value {
184 json!({
185 "type": "object",
186 "required": ["id"],
187 "properties": {
188 "id": {"type": "string", "description": "Session scheduled task id to delete."}
189 }
190 })
191}
192
193#[must_use]
194pub fn exec_command_parameters() -> Value {
195 json!({
196 "type": "object",
197 "required": ["cmd"],
198 "properties": {
199 "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."},
200 "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},
201 "max_output_tokens": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 10000, "description": "Output token cap. Large or truncated output can return a spool_path for the full output."},
202 "workdir": {"type": "string", "description": "Working directory."},
203 "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
204 "sandbox_permissions": {
205 "type": "string",
206 "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
207 "description": "Sandbox permission mode for this command.",
208 "default": "use_default"
209 },
210 "additional_permissions": {
211 "type": "object",
212 "description": "Additional filesystem access requested with sandbox_permissions set to with_additional_permissions.",
213 "properties": {
214 "fs_read": {"type": "array", "items": {"type": "string"}},
215 "fs_write": {"type": "array", "items": {"type": "string"}}
216 },
217 "additionalProperties": false
218 },
219 "justification": {"type": "string", "description": "Reason for requesting the expanded sandbox permission scope."}
220 },
221 "additionalProperties": false
222 })
223}
224
225#[must_use]
226pub fn write_stdin_parameters() -> Value {
227 json!({
228 "type": "object",
229 "required": ["session_id", "chars"],
230 "properties": {
231 "session_id": {"type": "string", "description": "Active execution session id."},
232 "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
233 "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
234 "max_output_tokens": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 10000, "description": "Output token cap for the continuation response. Large or truncated output can return a spool_path for the full output."}
235 },
236 "additionalProperties": false
237 })
238}
239
240#[must_use]
241pub fn search_tools_parameters() -> Value {
242 with_max_output_tokens_parameter(json!({
243 "type": "object",
244 "required": ["query"],
245 "properties": {
246 "query": {
247 "type": "string",
248 "minLength": 1,
249 "description": "Natural-language description of the capability to discover."
250 },
251 "limit": {
252 "type": "integer",
253 "minimum": 1,
254 "maximum": 25,
255 "default": 5
256 },
257 "detail_level": {
258 "type": "string",
259 "enum": ["name", "name_description", "full"],
260 "default": "name_description"
261 }
262 },
263 "additionalProperties": false
264 }))
265}
266
267#[must_use]
268pub fn code_search_parameters() -> Value {
269 json!({
270 "type": "object",
271 "required": ["query"],
272 "additionalProperties": false,
273 "properties": {
274 "query": {
275 "type": "string",
276 "minLength": 1,
277 "pattern": "\\S",
278 "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."
279 },
280 "path": {
281 "type": "string",
282 "minLength": 1,
283 "pattern": "\\S",
284 "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
285 },
286 "file_types": {
287 "type": "array",
288 "minItems": 1,
289 "items": {
290 "type": "string",
291 "minLength": 1,
292 "pattern": "\\S"
293 },
294 "description": "Language names or common file extensions, with or without one leading dot."
295 },
296 "result_types": {
297 "type": "array",
298 "minItems": 1,
299 "items": {
300 "type": "string",
301 "enum": ["definition", "usage", "text", "path"]
302 },
303 "description": "Result categories to include. Omit to include all four categories."
304 },
305 "max_results": {
306 "type": "integer",
307 "minimum": 1,
308 "maximum": 100,
309 "description": "Maximum number of merged results to return. Omit for 20."
310 }
311 }
312 })
313}
314
315#[must_use]
316pub fn list_files_parameters() -> Value {
317 json!({
318 "type": "object",
319 "properties": {
320 "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
321 "mode": {
322 "type": "string",
323 "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
324 "description": "Listing mode. Use page/per_page to continue paginated results.",
325 "default": "list"
326 },
327 "pattern": {"type": "string", "description": "Optional glob-style path filter."},
328 "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
329 "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
330 "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
331 "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
332 "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
333 "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
334 "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
335 "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
336 }
337 })
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use serde_json::json;
344
345 #[test]
346 fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
347 let schema = apply_patch_parameter_schema("Patch in VT Code format");
348
349 assert_eq!(
353 schema["properties"]["patch"]["description"],
354 with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
355 );
356 let patch_description = schema["properties"]["patch"]["description"]
357 .as_str()
358 .expect("patch description");
359 assert!(patch_description.contains("*** Begin Patch"));
360 assert!(patch_description.contains("unified diff"));
361 assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
362
363 let input_description = schema["properties"]["input"]["description"]
364 .as_str()
365 .expect("input description");
366 assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
367 }
368
369 #[test]
370 fn common_output_limit_parameter_preserves_strict_schema() {
371 let schema = with_max_output_tokens_parameter(json!({
372 "type": "object",
373 "properties": {"query": {"type": "string"}},
374 "additionalProperties": false
375 }));
376 assert_eq!(schema["additionalProperties"], json!(false));
377 assert_eq!(schema["properties"]["max_output_tokens"]["default"], json!(DEFAULT_MAX_OUTPUT_TOKENS));
378 assert_eq!(schema["properties"]["max_output_tokens"]["minimum"], json!(MIN_MAX_OUTPUT_TOKENS));
379 assert_eq!(schema["properties"]["max_output_tokens"]["maximum"], json!(MAX_MAX_OUTPUT_TOKENS));
380 }
381
382 #[test]
383 fn codex_baseline_exec_schemas_use_public_names_shape() {
384 let exec_params = exec_command_parameters();
385 assert_eq!(exec_params["required"], json!(["cmd"]));
386 assert!(exec_params["properties"]["cmd"].is_object());
387 assert!(exec_params["properties"]["workdir"].is_object());
388 assert!(
389 exec_params["properties"]["yield_time_ms"]["description"]
390 .as_str()
391 .expect("exec yield description")
392 .contains("session_id")
393 );
394 assert!(
395 exec_params["properties"]["max_output_tokens"]["description"]
396 .as_str()
397 .expect("exec max output description")
398 .contains("spool_path")
399 );
400 assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
401 assert_eq!(exec_params["properties"]["tty"]["default"], false);
402 assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
403 assert_eq!(
404 exec_params["properties"]["sandbox_permissions"]["enum"],
405 json!([
406 "use_default",
407 "with_additional_permissions",
408 "require_escalated",
409 "bypass_sandbox"
410 ])
411 );
412 assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
413 assert_eq!(
414 exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
415 "string"
416 );
417 assert_eq!(
418 exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
419 "string"
420 );
421 assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
422 assert_eq!(exec_params["properties"]["justification"]["type"], "string");
423 assert_eq!(exec_params["additionalProperties"], false);
424 for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
425 assert!(
426 exec_params["properties"]["cmd"]["description"]
427 .as_str()
428 .expect("cmd description")
429 .contains(command),
430 "{command} should be described as an exec_command.cmd example"
431 );
432 assert!(
433 exec_params["properties"].get(command).is_none(),
434 "{command} must not be modelled as a separate exec_command field"
435 );
436 }
437
438 let stdin_params = write_stdin_parameters();
439 assert_eq!(stdin_params["required"], json!(["session_id", "chars"]));
440 assert!(stdin_params["properties"]["session_id"].is_object());
441 assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
442 assert!(
443 stdin_params["properties"]["chars"]["description"]
444 .as_str()
445 .is_some_and(|description| description.contains("empty string"))
446 );
447 assert!(stdin_params["properties"]["chars"].is_object());
448 assert!(
449 stdin_params["properties"]["yield_time_ms"]["description"]
450 .as_str()
451 .expect("stdin yield description")
452 .contains("fresh session output")
453 );
454 assert!(
455 stdin_params["properties"]["max_output_tokens"]["description"]
456 .as_str()
457 .expect("stdin max output description")
458 .contains("spool_path")
459 );
460 assert_eq!(stdin_params["additionalProperties"], false);
461 }
462
463 #[test]
464 fn code_search_schema_exposes_exact_five_property_contract() {
465 let params = code_search_parameters();
466 let properties = params["properties"].as_object().expect("properties");
467 let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
468 property_names.sort_unstable();
469
470 assert_eq!(params["required"], json!(["query"]));
471 assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
472 assert_eq!(params["additionalProperties"], false);
473 assert_eq!(params["properties"]["query"]["pattern"], "\\S");
474 assert_eq!(params["properties"]["file_types"]["minItems"], 1);
475 assert_eq!(params["properties"]["result_types"]["minItems"], 1);
476 assert_eq!(
477 params["properties"]["result_types"]["items"]["enum"],
478 json!(["definition", "usage", "text", "path"])
479 );
480 assert_eq!(params["properties"]["max_results"]["minimum"], 1);
481 assert_eq!(params["properties"]["max_results"]["maximum"], 100);
482 assert!(params.get("anyOf").is_none());
483 }
484
485 #[test]
486 fn legacy_list_files_schema_exposes_pagination_fields() {
487 let list_params = list_files_parameters();
488 assert!(list_params["properties"]["page"].is_object());
489 assert!(list_params["properties"]["per_page"].is_object());
490 assert!(
491 list_params["properties"]["mode"]["enum"]
492 .as_array()
493 .expect("mode enum")
494 .iter()
495 .any(|value| value == "recursive")
496 );
497 }
498
499 #[test]
500 fn semantic_anchor_guidance_is_appended_once() {
501 let base = "Patch in VT Code format.";
502 let with_guidance = with_semantic_anchor_guidance(base);
503
504 assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
505 assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
506 }
507
508 #[test]
509 fn default_apply_patch_parameters_keep_expected_alias_shape() {
510 let schema = apply_patch_parameters();
511
512 assert_eq!(
513 schema["anyOf"],
514 json!([
515 {"required": ["input"]},
516 {"required": ["patch"]}
517 ])
518 );
519 }
520}