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) {
154 match value {
155 Value::Object(object) => {
156 object.remove("$schema");
157
158 for nested_value in object.values_mut() {
159 normalize_schema_for_transport(nested_value);
160 }
161
162 normalize_ref_object_for_codex(object);
163 normalize_required_for_codex(object);
164
165 let one_of_values = object
166 .get("oneOf")
167 .and_then(Value::as_array)
168 .map(|items| {
169 items
170 .iter()
171 .filter_map(Value::as_object)
172 .map(|item| item.get("const").and_then(Value::as_str))
173 .collect::<Option<Vec<_>>>()
174 })
175 .map(|option| {
176 option.map(|values| {
177 values
178 .into_iter()
179 .map(ToString::to_string)
180 .collect::<Vec<_>>()
181 })
182 });
183
184 if let Some(Some(enum_variants)) = one_of_values {
185 object.remove("oneOf");
186 object.insert("type".to_string(), Value::String("string".to_string()));
187 object.insert(
188 "enum".to_string(),
189 Value::Array(enum_variants.into_iter().map(Value::String).collect()),
190 );
191 }
192 }
193 Value::Array(array) => {
194 for nested_value in array {
195 normalize_schema_for_transport(nested_value);
196 }
197 }
198 _ => {}
199 }
200}
201
202fn normalize_required_for_codex(object: &mut serde_json::Map<String, Value>) {
208 let Some(properties) = object.get("properties").and_then(Value::as_object) else {
209 return;
210 };
211
212 let property_keys: Vec<String> = properties.keys().cloned().collect();
213 if property_keys.is_empty() {
214 return;
215 }
216
217 let required = object
218 .entry("required")
219 .or_insert_with(|| Value::Array(Vec::new()));
220
221 let Some(required_array) = required.as_array_mut() else {
222 return;
223 };
224
225 for key in &property_keys {
226 let already_listed = required_array
227 .iter()
228 .any(|value| value.as_str() == Some(key));
229
230 if !already_listed {
231 required_array.push(Value::String(key.clone()));
232 }
233 }
234}
235
236fn normalize_ref_object_for_codex(object: &mut serde_json::Map<String, Value>) {
242 let Some(reference) = object.get("$ref").cloned() else {
243 return;
244 };
245
246 object.clear();
247 object.insert("$ref".to_string(), reference);
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_agent_response_output_schema_contains_required_fields() {
257 let schema = agent_response_output_schema();
259 let required_fields = schema
260 .get("required")
261 .and_then(Value::as_array)
262 .expect("schema required fields should exist");
263 let properties = schema
264 .get("properties")
265 .and_then(Value::as_object)
266 .expect("schema properties should exist");
267
268 assert!(
270 required_fields
271 .iter()
272 .any(|value| value.as_str() == Some("answer"))
273 );
274 assert!(
275 required_fields
276 .iter()
277 .any(|value| value.as_str() == Some("questions"))
278 );
279 assert!(
280 required_fields
281 .iter()
282 .any(|value| value.as_str() == Some("summary"))
283 );
284 assert!(properties.contains_key("answer"));
285 assert!(properties.contains_key("questions"));
286 assert!(properties.contains_key("summary"));
287 }
288
289 #[test]
290 fn test_agent_response_output_schema_all_properties_are_required() {
293 let schema = agent_response_output_schema();
295
296 assert!(
298 all_properties_in_required(&schema),
299 "every object with `properties` should list all keys in `required`"
300 );
301 }
302
303 #[test]
304 fn test_agent_response_output_schema_does_not_contain_one_of() {
307 let schema = agent_response_output_schema();
309
310 assert!(!contains_schema_key(&schema, "oneOf"));
312 }
313
314 #[test]
315 fn test_agent_response_output_schema_does_not_contain_schema_metadata() {
318 let schema = agent_response_output_schema();
320
321 assert!(!contains_schema_key(&schema, "$schema"));
323 }
324
325 #[test]
326 fn test_agent_response_json_schema_requires_answer_key() {
329 let schema = agent_response_json_schema();
331 let required_fields = schema
332 .get("required")
333 .and_then(Value::as_array)
334 .expect("schema required fields should exist");
335
336 assert!(
338 required_fields
339 .iter()
340 .any(|value| value.as_str() == Some("answer")),
341 "prompt schema should require `answer` to align with parser key-presence check"
342 );
343 }
344
345 #[test]
346 fn test_agent_response_json_schema_sets_additional_properties_false() {
350 let schema = agent_response_json_schema();
352
353 assert!(
355 all_properties_objects_deny_additional(&schema),
356 "every object with `properties` should set `additionalProperties: false`"
357 );
358 }
359
360 #[test]
361 fn test_inject_additional_properties_false_preserves_existing_value() {
364 let mut schema = serde_json::json!({
366 "type": "object",
367 "properties": {
368 "extra": { "type": "object", "additionalProperties": { "type": "string" } }
369 }
370 });
371
372 inject_additional_properties_false(&mut schema);
374
375 assert_eq!(schema["additionalProperties"], Value::Bool(false));
377 assert_eq!(
379 schema["properties"]["extra"]["additionalProperties"],
380 serde_json::json!({ "type": "string" })
381 );
382 }
383
384 #[test]
385 fn test_agent_response_output_schema_ref_objects_have_no_sibling_keywords() {
387 let schema = agent_response_output_schema();
389
390 assert!(!contains_ref_with_sibling_keywords(&schema));
392 }
393
394 #[test]
395 fn test_agent_response_json_schema_json_is_parseable_value() {
397 let schema_json = agent_response_json_schema_json();
399 let parsed_schema: Value =
400 serde_json::from_str(&schema_json).expect("schema string should parse as JSON");
401 let schema_value = agent_response_json_schema();
402
403 assert_eq!(parsed_schema, schema_value);
405 }
406
407 #[test]
408 fn test_agent_response_json_schema_preserves_explicit_payload_metadata() {
411 let schema = agent_response_json_schema();
413
414 assert_eq!(
416 schema.get("title").and_then(Value::as_str),
417 Some("AgentResponse")
418 );
419 assert_eq!(
420 schema.get("description").and_then(Value::as_str),
421 Some(
422 "Wire-format protocol payload used for schema-driven provider output. Return this \
423 object as the entire assistant response payload. Providers that support output \
424 schemas (for example, Codex app-server) are asked to emit this object directly."
425 )
426 );
427 }
428
429 #[test]
430 fn test_agent_response_json_schema_preserves_nested_metadata() {
432 let schema = agent_response_json_schema();
434 let question_definition = schema
435 .get("$defs")
436 .and_then(|value| value.get("QuestionItem"))
437 .and_then(Value::as_object)
438 .expect("question definition should exist");
439 let summary_definition = schema
440 .get("$defs")
441 .and_then(|value| value.get("AgentResponseSummary"))
442 .and_then(Value::as_object)
443 .expect("summary definition should exist");
444
445 assert_eq!(
447 question_definition.get("title").and_then(Value::as_str),
448 Some("QuestionItem")
449 );
450 assert_eq!(
451 summary_definition.get("title").and_then(Value::as_str),
452 Some("AgentResponseSummary")
453 );
454 }
455
456 #[test]
457 fn test_agent_response_json_schema_preserves_field_metadata() {
459 let schema = agent_response_json_schema();
461 let response_properties = schema
462 .get("properties")
463 .and_then(Value::as_object)
464 .expect("response properties should exist");
465 let question_definition = schema
466 .get("$defs")
467 .and_then(|value| value.get("QuestionItem"))
468 .and_then(Value::as_object)
469 .expect("question definition should exist");
470 let question_properties = question_definition
471 .get("properties")
472 .and_then(Value::as_object)
473 .expect("question properties should exist");
474 let summary_definition = schema
475 .get("$defs")
476 .and_then(|value| value.get("AgentResponseSummary"))
477 .and_then(Value::as_object)
478 .expect("summary definition should exist");
479 let summary_properties = summary_definition
480 .get("properties")
481 .and_then(Value::as_object)
482 .expect("summary properties should exist");
483 let expected_questions_description = questions_field_description();
484
485 assert_schema_property_title_and_description(
487 response_properties,
488 "answer",
489 "answer",
490 "Markdown answer text for delivered work, status updates, or concise completion \
491 notes. Keep clarification requests out of this field and emit them through \
492 `questions` instead.",
493 );
494 assert_eq!(
495 response_properties
496 .get("questions")
497 .and_then(|value| value.get("description"))
498 .and_then(Value::as_str),
499 Some(expected_questions_description.as_str())
500 );
501 assert_schema_property_title_and_description(
502 response_properties,
503 "summary",
504 "summary",
505 "Structured summary for session-discussion turns, kept outside `answer` markdown. Use \
506 `null` for one-shot prompts and legacy payloads.",
507 );
508 assert_schema_property_title_and_description(
509 question_properties,
510 "text",
511 "text",
512 "Human-readable markdown text for this question. Ask one specific actionable question \
513 instead of bundling multiple decisions into one item.",
514 );
515 assert_schema_property_title(question_properties, "options", "options");
516 assert_schema_property_title_and_description(
517 summary_properties,
518 "turn",
519 "turn",
520 "Concise summary of only the work completed in the current turn.",
521 );
522 assert_schema_property_title_and_description(
523 summary_properties,
524 "session",
525 "session",
526 "Cumulative summary of active changes on the current session branch.",
527 );
528 }
529
530 #[test]
531 fn test_agent_response_json_schema_keeps_optional_summary_field() {
534 let schema = agent_response_json_schema();
536 let response_required_fields = schema
537 .get("required")
538 .and_then(Value::as_array)
539 .cloned()
540 .unwrap_or_default();
541 let question_definition = schema
542 .get("$defs")
543 .and_then(|value| value.get("QuestionItem"))
544 .and_then(Value::as_object)
545 .expect("question definition should exist");
546 let question_required_fields = question_definition
547 .get("required")
548 .and_then(Value::as_array)
549 .cloned()
550 .unwrap_or_default();
551
552 assert!(
554 response_required_fields
555 .iter()
556 .all(|field| field.as_str() != Some("summary")),
557 "raw prompt schema should keep optional summary fields optional"
558 );
559 assert!(
560 question_required_fields
561 .iter()
562 .all(|field| field.as_str() != Some("options")),
563 "question schema should keep `options` optional for omitted empty lists"
564 );
565 }
566
567 #[test]
568 fn test_agent_response_output_schema_json_is_parseable_value() {
571 let schema_json = agent_response_output_schema_json();
573 let parsed_schema: Value =
574 serde_json::from_str(&schema_json).expect("schema string should parse as JSON");
575 let schema_value = agent_response_output_schema();
576
577 assert_eq!(parsed_schema, schema_value);
579 }
580
581 fn contains_schema_key(value: &Value, key: &str) -> bool {
583 match value {
584 Value::Object(object) => {
585 if object.contains_key(key) {
586 return true;
587 }
588
589 object
590 .values()
591 .any(|nested_value| contains_schema_key(nested_value, key))
592 }
593 Value::Array(array) => array
594 .iter()
595 .any(|nested_value| contains_schema_key(nested_value, key)),
596 _ => false,
597 }
598 }
599
600 fn contains_ref_with_sibling_keywords(value: &Value) -> bool {
602 match value {
603 Value::Object(object) => {
604 if object.contains_key("$ref") && object.len() > 1 {
605 return true;
606 }
607
608 object.values().any(contains_ref_with_sibling_keywords)
609 }
610 Value::Array(array) => array.iter().any(contains_ref_with_sibling_keywords),
611 _ => false,
612 }
613 }
614
615 fn all_properties_in_required(value: &Value) -> bool {
618 match value {
619 Value::Object(object) => {
620 if let Some(properties) = object.get("properties").and_then(Value::as_object) {
621 let required_keys: Vec<&str> = object
622 .get("required")
623 .and_then(Value::as_array)
624 .map(|array| array.iter().filter_map(Value::as_str).collect())
625 .unwrap_or_default();
626
627 for key in properties.keys() {
628 if !required_keys.contains(&key.as_str()) {
629 return false;
630 }
631 }
632 }
633
634 object.values().all(all_properties_in_required)
635 }
636 Value::Array(array) => array.iter().all(all_properties_in_required),
637 _ => true,
638 }
639 }
640
641 fn all_properties_objects_deny_additional(value: &Value) -> bool {
644 match value {
645 Value::Object(object) => {
646 if object.contains_key("properties")
647 && object.get("additionalProperties") != Some(&Value::Bool(false))
648 {
649 return false;
650 }
651
652 object.values().all(all_properties_objects_deny_additional)
653 }
654 Value::Array(array) => array.iter().all(all_properties_objects_deny_additional),
655 _ => true,
656 }
657 }
658
659 fn assert_schema_property_title(
661 properties: &serde_json::Map<String, Value>,
662 property_name: &str,
663 expected_title: &str,
664 ) {
665 assert_eq!(
666 properties
667 .get(property_name)
668 .and_then(|value| value.get("title"))
669 .and_then(Value::as_str),
670 Some(expected_title)
671 );
672 }
673
674 fn assert_schema_property_title_and_description(
677 properties: &serde_json::Map<String, Value>,
678 property_name: &str,
679 expected_title: &str,
680 expected_description: &str,
681 ) {
682 assert_schema_property_title(properties, property_name, expected_title);
683 assert_eq!(
684 properties
685 .get(property_name)
686 .and_then(|value| value.get("description"))
687 .and_then(Value::as_str),
688 Some(expected_description)
689 );
690 }
691}