1use serde_json::Value;
5
6use super::model::{AgentResponse, questions_field_description};
7
8pub fn agent_response_output_schema() -> Value {
14 let mut value = agent_response_json_schema();
15 normalize_schema_for_transport(&mut value);
16
17 value
18}
19
20pub fn agent_response_json_schema_json() -> String {
26 let schema = agent_response_json_schema();
27
28 stringify_schema_json(&schema)
29}
30
31pub fn agent_response_output_schema_json() -> String {
39 let schema = agent_response_output_schema();
40
41 stringify_schema_json(&schema)
42}
43
44fn agent_response_json_schema() -> Value {
50 let schema = schemars::schema_for!(AgentResponse);
51 let mut schema_value = serde_json::to_value(schema).unwrap_or(Value::Null);
52
53 inject_dynamic_schema_guidance(&mut schema_value);
54 inject_additional_properties_false(&mut schema_value);
55 inject_minimum_required_protocol_key(&mut schema_value);
56
57 schema_value
58}
59
60fn inject_dynamic_schema_guidance(schema: &mut Value) {
63 let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
64 return;
65 };
66 let Some(questions_property) = properties
67 .get_mut("questions")
68 .and_then(Value::as_object_mut)
69 else {
70 return;
71 };
72
73 questions_property.insert(
74 "description".to_string(),
75 Value::String(questions_field_description()),
76 );
77}
78
79fn inject_additional_properties_false(value: &mut Value) {
89 match value {
90 Value::Object(object) => {
91 if object.contains_key("properties") && !object.contains_key("additionalProperties") {
92 object.insert("additionalProperties".to_string(), Value::Bool(false));
93 }
94
95 for nested_value in object.values_mut() {
96 inject_additional_properties_false(nested_value);
97 }
98 }
99 Value::Array(array) => {
100 for nested_value in array {
101 inject_additional_properties_false(nested_value);
102 }
103 }
104 _ => {}
105 }
106}
107
108fn inject_minimum_required_protocol_key(schema: &mut Value) {
117 let Some(object) = schema.as_object_mut() else {
118 return;
119 };
120
121 let required = object
122 .entry("required")
123 .or_insert_with(|| Value::Array(Vec::new()));
124
125 let Some(required_array) = required.as_array_mut() else {
126 return;
127 };
128
129 let already_listed = required_array
130 .iter()
131 .any(|value| value.as_str() == Some("answer"));
132
133 if !already_listed {
134 required_array.push(Value::String("answer".to_string()));
135 }
136}
137
138fn stringify_schema_json(schema: &Value) -> String {
140 match serde_json::to_string_pretty(schema) {
141 Ok(schema_json) => schema_json,
142 Err(_) => "null".to_string(),
143 }
144}
145
146fn normalize_schema_for_transport(value: &mut Value) {
152 match value {
153 Value::Object(object) => {
154 for nested_value in object.values_mut() {
155 normalize_schema_for_transport(nested_value);
156 }
157
158 normalize_ref_object_for_codex(object);
159 normalize_required_for_codex(object);
160
161 let one_of_values = object
162 .get("oneOf")
163 .and_then(Value::as_array)
164 .map(|items| {
165 items
166 .iter()
167 .filter_map(Value::as_object)
168 .map(|item| item.get("const").and_then(Value::as_str))
169 .collect::<Option<Vec<_>>>()
170 })
171 .map(|option| {
172 option.map(|values| {
173 values
174 .into_iter()
175 .map(ToString::to_string)
176 .collect::<Vec<_>>()
177 })
178 });
179
180 if let Some(Some(enum_variants)) = one_of_values {
181 object.remove("oneOf");
182 object.insert("type".to_string(), Value::String("string".to_string()));
183 object.insert(
184 "enum".to_string(),
185 Value::Array(enum_variants.into_iter().map(Value::String).collect()),
186 );
187 }
188 }
189 Value::Array(array) => {
190 for nested_value in array {
191 normalize_schema_for_transport(nested_value);
192 }
193 }
194 _ => {}
195 }
196}
197
198fn normalize_required_for_codex(object: &mut serde_json::Map<String, Value>) {
204 let Some(properties) = object.get("properties").and_then(Value::as_object) else {
205 return;
206 };
207
208 let property_keys: Vec<String> = properties.keys().cloned().collect();
209 if property_keys.is_empty() {
210 return;
211 }
212
213 let required = object
214 .entry("required")
215 .or_insert_with(|| Value::Array(Vec::new()));
216
217 let Some(required_array) = required.as_array_mut() else {
218 return;
219 };
220
221 for key in &property_keys {
222 let already_listed = required_array
223 .iter()
224 .any(|value| value.as_str() == Some(key));
225
226 if !already_listed {
227 required_array.push(Value::String(key.clone()));
228 }
229 }
230}
231
232fn normalize_ref_object_for_codex(object: &mut serde_json::Map<String, Value>) {
238 let Some(reference) = object.get("$ref").cloned() else {
239 return;
240 };
241
242 object.clear();
243 object.insert("$ref".to_string(), reference);
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_agent_response_output_schema_contains_required_fields() {
253 let schema = agent_response_output_schema();
255 let required_fields = schema
256 .get("required")
257 .and_then(Value::as_array)
258 .expect("schema required fields should exist");
259 let properties = schema
260 .get("properties")
261 .and_then(Value::as_object)
262 .expect("schema properties should exist");
263
264 assert!(
266 required_fields
267 .iter()
268 .any(|value| value.as_str() == Some("answer"))
269 );
270 assert!(
271 required_fields
272 .iter()
273 .any(|value| value.as_str() == Some("questions"))
274 );
275 assert!(
276 required_fields
277 .iter()
278 .any(|value| value.as_str() == Some("summary"))
279 );
280 assert!(properties.contains_key("answer"));
281 assert!(properties.contains_key("questions"));
282 assert!(properties.contains_key("summary"));
283 }
284
285 #[test]
286 fn test_agent_response_output_schema_all_properties_are_required() {
289 let schema = agent_response_output_schema();
291
292 assert!(
294 all_properties_in_required(&schema),
295 "every object with `properties` should list all keys in `required`"
296 );
297 }
298
299 #[test]
300 fn test_agent_response_output_schema_does_not_contain_one_of() {
303 let schema = agent_response_output_schema();
305
306 assert!(!contains_schema_key(&schema, "oneOf"));
308 }
309
310 #[test]
311 fn test_agent_response_json_schema_requires_answer_key() {
314 let schema = agent_response_json_schema();
316 let required_fields = schema
317 .get("required")
318 .and_then(Value::as_array)
319 .expect("schema required fields should exist");
320
321 assert!(
323 required_fields
324 .iter()
325 .any(|value| value.as_str() == Some("answer")),
326 "prompt schema should require `answer` to align with parser key-presence check"
327 );
328 }
329
330 #[test]
331 fn test_agent_response_json_schema_sets_additional_properties_false() {
335 let schema = agent_response_json_schema();
337
338 assert!(
340 all_properties_objects_deny_additional(&schema),
341 "every object with `properties` should set `additionalProperties: false`"
342 );
343 }
344
345 #[test]
346 fn test_inject_additional_properties_false_preserves_existing_value() {
349 let mut schema = serde_json::json!({
351 "type": "object",
352 "properties": {
353 "extra": { "type": "object", "additionalProperties": { "type": "string" } }
354 }
355 });
356
357 inject_additional_properties_false(&mut schema);
359
360 assert_eq!(schema["additionalProperties"], Value::Bool(false));
362 assert_eq!(
364 schema["properties"]["extra"]["additionalProperties"],
365 serde_json::json!({ "type": "string" })
366 );
367 }
368
369 #[test]
370 fn test_agent_response_output_schema_ref_objects_have_no_sibling_keywords() {
372 let schema = agent_response_output_schema();
374
375 assert!(!contains_ref_with_sibling_keywords(&schema));
377 }
378
379 #[test]
380 fn test_agent_response_json_schema_json_is_parseable_value() {
382 let schema_json = agent_response_json_schema_json();
384 let parsed_schema: Value =
385 serde_json::from_str(&schema_json).expect("schema string should parse as JSON");
386 let schema_value = agent_response_json_schema();
387
388 assert_eq!(parsed_schema, schema_value);
390 }
391
392 #[test]
393 fn test_agent_response_json_schema_preserves_explicit_payload_metadata() {
396 let schema = agent_response_json_schema();
398
399 assert_eq!(
401 schema.get("title").and_then(Value::as_str),
402 Some("AgentResponse")
403 );
404 assert_eq!(
405 schema.get("description").and_then(Value::as_str),
406 Some(
407 "Wire-format protocol payload used for schema-driven provider output. Return this \
408 object as the entire assistant response payload. Providers that support output \
409 schemas (for example, Codex app-server) are asked to emit this object directly."
410 )
411 );
412 }
413
414 #[test]
415 fn test_agent_response_json_schema_preserves_nested_metadata() {
417 let schema = agent_response_json_schema();
419 let question_definition = schema
420 .get("$defs")
421 .and_then(|value| value.get("QuestionItem"))
422 .and_then(Value::as_object)
423 .expect("question definition should exist");
424 let summary_definition = schema
425 .get("$defs")
426 .and_then(|value| value.get("AgentResponseSummary"))
427 .and_then(Value::as_object)
428 .expect("summary definition should exist");
429
430 assert_eq!(
432 question_definition.get("title").and_then(Value::as_str),
433 Some("QuestionItem")
434 );
435 assert_eq!(
436 summary_definition.get("title").and_then(Value::as_str),
437 Some("AgentResponseSummary")
438 );
439 }
440
441 #[test]
442 fn test_agent_response_json_schema_preserves_field_metadata() {
444 let schema = agent_response_json_schema();
446 let response_properties = schema
447 .get("properties")
448 .and_then(Value::as_object)
449 .expect("response properties should exist");
450 let question_definition = schema
451 .get("$defs")
452 .and_then(|value| value.get("QuestionItem"))
453 .and_then(Value::as_object)
454 .expect("question definition should exist");
455 let question_properties = question_definition
456 .get("properties")
457 .and_then(Value::as_object)
458 .expect("question properties should exist");
459 let summary_definition = schema
460 .get("$defs")
461 .and_then(|value| value.get("AgentResponseSummary"))
462 .and_then(Value::as_object)
463 .expect("summary definition should exist");
464 let summary_properties = summary_definition
465 .get("properties")
466 .and_then(Value::as_object)
467 .expect("summary properties should exist");
468 let expected_questions_description = questions_field_description();
469
470 assert_schema_property_title_and_description(
472 response_properties,
473 "answer",
474 "answer",
475 "Markdown answer text for delivered work, status updates, or concise completion \
476 notes. Keep clarification requests out of this field and emit them through \
477 `questions` instead.",
478 );
479 assert_eq!(
480 response_properties
481 .get("questions")
482 .and_then(|value| value.get("description"))
483 .and_then(Value::as_str),
484 Some(expected_questions_description.as_str())
485 );
486 assert_schema_property_title_and_description(
487 response_properties,
488 "summary",
489 "summary",
490 "Structured summary for session-discussion turns, kept outside `answer` markdown. Use \
491 `null` for one-shot prompts and legacy payloads.",
492 );
493 assert_schema_property_title_and_description(
494 question_properties,
495 "text",
496 "text",
497 "Human-readable markdown text for this question. Ask one specific actionable question \
498 instead of bundling multiple decisions into one item.",
499 );
500 assert_schema_property_title(question_properties, "options", "options");
501 assert_schema_property_title_and_description(
502 summary_properties,
503 "turn",
504 "turn",
505 "Concise summary of only the work completed in the current turn.",
506 );
507 assert_schema_property_title_and_description(
508 summary_properties,
509 "session",
510 "session",
511 "Cumulative summary of active changes on the current session branch.",
512 );
513 }
514
515 #[test]
516 fn test_agent_response_json_schema_keeps_optional_summary_field() {
519 let schema = agent_response_json_schema();
521 let response_required_fields = schema
522 .get("required")
523 .and_then(Value::as_array)
524 .cloned()
525 .unwrap_or_default();
526 let question_definition = schema
527 .get("$defs")
528 .and_then(|value| value.get("QuestionItem"))
529 .and_then(Value::as_object)
530 .expect("question definition should exist");
531 let question_required_fields = question_definition
532 .get("required")
533 .and_then(Value::as_array)
534 .cloned()
535 .unwrap_or_default();
536
537 assert!(
539 response_required_fields
540 .iter()
541 .all(|field| field.as_str() != Some("summary")),
542 "raw prompt schema should keep optional summary fields optional"
543 );
544 assert!(
545 question_required_fields
546 .iter()
547 .all(|field| field.as_str() != Some("options")),
548 "question schema should keep `options` optional for omitted empty lists"
549 );
550 }
551
552 #[test]
553 fn test_agent_response_output_schema_json_is_parseable_value() {
556 let schema_json = agent_response_output_schema_json();
558 let parsed_schema: Value =
559 serde_json::from_str(&schema_json).expect("schema string should parse as JSON");
560 let schema_value = agent_response_output_schema();
561
562 assert_eq!(parsed_schema, schema_value);
564 }
565
566 fn contains_schema_key(value: &Value, key: &str) -> bool {
568 match value {
569 Value::Object(object) => {
570 if object.contains_key(key) {
571 return true;
572 }
573
574 object
575 .values()
576 .any(|nested_value| contains_schema_key(nested_value, key))
577 }
578 Value::Array(array) => array
579 .iter()
580 .any(|nested_value| contains_schema_key(nested_value, key)),
581 _ => false,
582 }
583 }
584
585 fn contains_ref_with_sibling_keywords(value: &Value) -> bool {
587 match value {
588 Value::Object(object) => {
589 if object.contains_key("$ref") && object.len() > 1 {
590 return true;
591 }
592
593 object.values().any(contains_ref_with_sibling_keywords)
594 }
595 Value::Array(array) => array.iter().any(contains_ref_with_sibling_keywords),
596 _ => false,
597 }
598 }
599
600 fn all_properties_in_required(value: &Value) -> bool {
603 match value {
604 Value::Object(object) => {
605 if let Some(properties) = object.get("properties").and_then(Value::as_object) {
606 let required_keys: Vec<&str> = object
607 .get("required")
608 .and_then(Value::as_array)
609 .map(|array| array.iter().filter_map(Value::as_str).collect())
610 .unwrap_or_default();
611
612 for key in properties.keys() {
613 if !required_keys.contains(&key.as_str()) {
614 return false;
615 }
616 }
617 }
618
619 object.values().all(all_properties_in_required)
620 }
621 Value::Array(array) => array.iter().all(all_properties_in_required),
622 _ => true,
623 }
624 }
625
626 fn all_properties_objects_deny_additional(value: &Value) -> bool {
629 match value {
630 Value::Object(object) => {
631 if object.contains_key("properties")
632 && object.get("additionalProperties") != Some(&Value::Bool(false))
633 {
634 return false;
635 }
636
637 object.values().all(all_properties_objects_deny_additional)
638 }
639 Value::Array(array) => array.iter().all(all_properties_objects_deny_additional),
640 _ => true,
641 }
642 }
643
644 fn assert_schema_property_title(
646 properties: &serde_json::Map<String, Value>,
647 property_name: &str,
648 expected_title: &str,
649 ) {
650 assert_eq!(
651 properties
652 .get(property_name)
653 .and_then(|value| value.get("title"))
654 .and_then(Value::as_str),
655 Some(expected_title)
656 );
657 }
658
659 fn assert_schema_property_title_and_description(
662 properties: &serde_json::Map<String, Value>,
663 property_name: &str,
664 expected_title: &str,
665 expected_description: &str,
666 ) {
667 assert_schema_property_title(properties, property_name, expected_title);
668 assert_eq!(
669 properties
670 .get(property_name)
671 .and_then(|value| value.get("description"))
672 .and_then(Value::as_str),
673 Some(expected_description)
674 );
675 }
676}