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