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