use serde_json::Value;
use super::model::{AgentResponse, questions_field_description};
pub fn agent_response_output_schema() -> Value {
let mut value = agent_response_json_schema();
normalize_schema_for_transport(&mut value);
value
}
pub fn agent_response_json_schema_json() -> String {
let schema = agent_response_json_schema();
stringify_schema_json(&schema)
}
pub fn agent_response_output_schema_json() -> String {
let schema = agent_response_output_schema();
stringify_schema_json(&schema)
}
fn agent_response_json_schema() -> Value {
let schema = schemars::schema_for!(AgentResponse);
let mut schema_value = serde_json::to_value(schema).unwrap_or(Value::Null);
inject_dynamic_schema_guidance(&mut schema_value);
inject_additional_properties_false(&mut schema_value);
inject_minimum_required_protocol_key(&mut schema_value);
schema_value
}
fn inject_dynamic_schema_guidance(schema: &mut Value) {
let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
return;
};
let Some(questions_property) = properties
.get_mut("questions")
.and_then(Value::as_object_mut)
else {
return;
};
questions_property.insert(
"description".to_string(),
Value::String(questions_field_description()),
);
}
fn inject_additional_properties_false(value: &mut Value) {
match value {
Value::Object(object) => {
if object.contains_key("properties") && !object.contains_key("additionalProperties") {
object.insert("additionalProperties".to_string(), Value::Bool(false));
}
for nested_value in object.values_mut() {
inject_additional_properties_false(nested_value);
}
}
Value::Array(array) => {
for nested_value in array {
inject_additional_properties_false(nested_value);
}
}
_ => {}
}
}
fn inject_minimum_required_protocol_key(schema: &mut Value) {
let Some(object) = schema.as_object_mut() else {
return;
};
let required = object
.entry("required")
.or_insert_with(|| Value::Array(Vec::new()));
let Some(required_array) = required.as_array_mut() else {
return;
};
let already_listed = required_array
.iter()
.any(|value| value.as_str() == Some("answer"));
if !already_listed {
required_array.push(Value::String("answer".to_string()));
}
}
fn stringify_schema_json(schema: &Value) -> String {
match serde_json::to_string_pretty(schema) {
Ok(schema_json) => schema_json,
Err(_) => "null".to_string(),
}
}
fn normalize_schema_for_transport(value: &mut Value) {
match value {
Value::Object(object) => {
object.remove("$schema");
for nested_value in object.values_mut() {
normalize_schema_for_transport(nested_value);
}
normalize_ref_object_for_codex(object);
normalize_required_for_codex(object);
let one_of_values = object
.get("oneOf")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_object)
.map(|item| item.get("const").and_then(Value::as_str))
.collect::<Option<Vec<_>>>()
})
.map(|option| {
option.map(|values| {
values
.into_iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
})
});
if let Some(Some(enum_variants)) = one_of_values {
object.remove("oneOf");
object.insert("type".to_string(), Value::String("string".to_string()));
object.insert(
"enum".to_string(),
Value::Array(enum_variants.into_iter().map(Value::String).collect()),
);
}
}
Value::Array(array) => {
for nested_value in array {
normalize_schema_for_transport(nested_value);
}
}
_ => {}
}
}
fn normalize_required_for_codex(object: &mut serde_json::Map<String, Value>) {
let Some(properties) = object.get("properties").and_then(Value::as_object) else {
return;
};
let property_keys: Vec<String> = properties.keys().cloned().collect();
if property_keys.is_empty() {
return;
}
let required = object
.entry("required")
.or_insert_with(|| Value::Array(Vec::new()));
let Some(required_array) = required.as_array_mut() else {
return;
};
for key in &property_keys {
let already_listed = required_array
.iter()
.any(|value| value.as_str() == Some(key));
if !already_listed {
required_array.push(Value::String(key.clone()));
}
}
}
fn normalize_ref_object_for_codex(object: &mut serde_json::Map<String, Value>) {
let Some(reference) = object.get("$ref").cloned() else {
return;
};
object.clear();
object.insert("$ref".to_string(), reference);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_response_output_schema_contains_required_fields() {
let schema = agent_response_output_schema();
let required_fields = schema
.get("required")
.and_then(Value::as_array)
.expect("schema required fields should exist");
let properties = schema
.get("properties")
.and_then(Value::as_object)
.expect("schema properties should exist");
assert!(
required_fields
.iter()
.any(|value| value.as_str() == Some("answer"))
);
assert!(
required_fields
.iter()
.any(|value| value.as_str() == Some("questions"))
);
assert!(
required_fields
.iter()
.any(|value| value.as_str() == Some("summary"))
);
assert!(properties.contains_key("answer"));
assert!(properties.contains_key("questions"));
assert!(properties.contains_key("summary"));
}
#[test]
fn test_agent_response_output_schema_all_properties_are_required() {
let schema = agent_response_output_schema();
assert!(
all_properties_in_required(&schema),
"every object with `properties` should list all keys in `required`"
);
}
#[test]
fn test_agent_response_output_schema_does_not_contain_one_of() {
let schema = agent_response_output_schema();
assert!(!contains_schema_key(&schema, "oneOf"));
}
#[test]
fn test_agent_response_output_schema_does_not_contain_schema_metadata() {
let schema = agent_response_output_schema();
assert!(!contains_schema_key(&schema, "$schema"));
}
#[test]
fn test_agent_response_json_schema_requires_answer_key() {
let schema = agent_response_json_schema();
let required_fields = schema
.get("required")
.and_then(Value::as_array)
.expect("schema required fields should exist");
assert!(
required_fields
.iter()
.any(|value| value.as_str() == Some("answer")),
"prompt schema should require `answer` to align with parser key-presence check"
);
}
#[test]
fn test_agent_response_json_schema_sets_additional_properties_false() {
let schema = agent_response_json_schema();
assert!(
all_properties_objects_deny_additional(&schema),
"every object with `properties` should set `additionalProperties: false`"
);
}
#[test]
fn test_inject_additional_properties_false_preserves_existing_value() {
let mut schema = serde_json::json!({
"type": "object",
"properties": {
"extra": { "type": "object", "additionalProperties": { "type": "string" } }
}
});
inject_additional_properties_false(&mut schema);
assert_eq!(schema["additionalProperties"], Value::Bool(false));
assert_eq!(
schema["properties"]["extra"]["additionalProperties"],
serde_json::json!({ "type": "string" })
);
}
#[test]
fn test_agent_response_output_schema_ref_objects_have_no_sibling_keywords() {
let schema = agent_response_output_schema();
assert!(!contains_ref_with_sibling_keywords(&schema));
}
#[test]
fn test_agent_response_json_schema_json_is_parseable_value() {
let schema_json = agent_response_json_schema_json();
let parsed_schema: Value =
serde_json::from_str(&schema_json).expect("schema string should parse as JSON");
let schema_value = agent_response_json_schema();
assert_eq!(parsed_schema, schema_value);
}
#[test]
fn test_agent_response_json_schema_preserves_explicit_payload_metadata() {
let schema = agent_response_json_schema();
assert_eq!(
schema.get("title").and_then(Value::as_str),
Some("AgentResponse")
);
assert_eq!(
schema.get("description").and_then(Value::as_str),
Some(
"Wire-format protocol payload used for schema-driven provider output. Return this \
object as the entire assistant response payload. Providers that support output \
schemas (for example, Codex app-server) are asked to emit this object directly."
)
);
}
#[test]
fn test_agent_response_json_schema_preserves_nested_metadata() {
let schema = agent_response_json_schema();
let question_definition = schema
.get("$defs")
.and_then(|value| value.get("QuestionItem"))
.and_then(Value::as_object)
.expect("question definition should exist");
let summary_definition = schema
.get("$defs")
.and_then(|value| value.get("AgentResponseSummary"))
.and_then(Value::as_object)
.expect("summary definition should exist");
assert_eq!(
question_definition.get("title").and_then(Value::as_str),
Some("QuestionItem")
);
assert_eq!(
summary_definition.get("title").and_then(Value::as_str),
Some("AgentResponseSummary")
);
}
#[test]
fn test_agent_response_json_schema_preserves_field_metadata() {
let schema = agent_response_json_schema();
let response_properties = schema
.get("properties")
.and_then(Value::as_object)
.expect("response properties should exist");
let question_definition = schema
.get("$defs")
.and_then(|value| value.get("QuestionItem"))
.and_then(Value::as_object)
.expect("question definition should exist");
let question_properties = question_definition
.get("properties")
.and_then(Value::as_object)
.expect("question properties should exist");
let summary_definition = schema
.get("$defs")
.and_then(|value| value.get("AgentResponseSummary"))
.and_then(Value::as_object)
.expect("summary definition should exist");
let summary_properties = summary_definition
.get("properties")
.and_then(Value::as_object)
.expect("summary properties should exist");
let expected_questions_description = questions_field_description();
assert_schema_property_title_and_description(
response_properties,
"answer",
"answer",
"Markdown answer text for delivered work, status updates, or concise completion \
notes. Keep clarification requests out of this field and emit them through \
`questions` instead.",
);
assert_eq!(
response_properties
.get("questions")
.and_then(|value| value.get("description"))
.and_then(Value::as_str),
Some(expected_questions_description.as_str())
);
assert_schema_property_title_and_description(
response_properties,
"summary",
"summary",
"Structured summary for session-discussion turns, kept outside `answer` markdown. Use \
`null` for one-shot prompts and legacy payloads.",
);
assert_schema_property_title_and_description(
question_properties,
"text",
"text",
"Human-readable markdown text for this question. Ask one specific actionable question \
instead of bundling multiple decisions into one item.",
);
assert_schema_property_title(question_properties, "options", "options");
assert_schema_property_title_and_description(
summary_properties,
"turn",
"turn",
"Concise summary of only the work completed in the current turn.",
);
assert_schema_property_title_and_description(
summary_properties,
"session",
"session",
"Cumulative summary of active changes on the current session branch.",
);
}
#[test]
fn test_agent_response_json_schema_keeps_optional_summary_field() {
let schema = agent_response_json_schema();
let response_required_fields = schema
.get("required")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let question_definition = schema
.get("$defs")
.and_then(|value| value.get("QuestionItem"))
.and_then(Value::as_object)
.expect("question definition should exist");
let question_required_fields = question_definition
.get("required")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(
response_required_fields
.iter()
.all(|field| field.as_str() != Some("summary")),
"raw prompt schema should keep optional summary fields optional"
);
assert!(
question_required_fields
.iter()
.all(|field| field.as_str() != Some("options")),
"question schema should keep `options` optional for omitted empty lists"
);
}
#[test]
fn test_agent_response_output_schema_json_is_parseable_value() {
let schema_json = agent_response_output_schema_json();
let parsed_schema: Value =
serde_json::from_str(&schema_json).expect("schema string should parse as JSON");
let schema_value = agent_response_output_schema();
assert_eq!(parsed_schema, schema_value);
}
fn contains_schema_key(value: &Value, key: &str) -> bool {
match value {
Value::Object(object) => {
if object.contains_key(key) {
return true;
}
object
.values()
.any(|nested_value| contains_schema_key(nested_value, key))
}
Value::Array(array) => array
.iter()
.any(|nested_value| contains_schema_key(nested_value, key)),
_ => false,
}
}
fn contains_ref_with_sibling_keywords(value: &Value) -> bool {
match value {
Value::Object(object) => {
if object.contains_key("$ref") && object.len() > 1 {
return true;
}
object.values().any(contains_ref_with_sibling_keywords)
}
Value::Array(array) => array.iter().any(contains_ref_with_sibling_keywords),
_ => false,
}
}
fn all_properties_in_required(value: &Value) -> bool {
match value {
Value::Object(object) => {
if let Some(properties) = object.get("properties").and_then(Value::as_object) {
let required_keys: Vec<&str> = object
.get("required")
.and_then(Value::as_array)
.map(|array| array.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
for key in properties.keys() {
if !required_keys.contains(&key.as_str()) {
return false;
}
}
}
object.values().all(all_properties_in_required)
}
Value::Array(array) => array.iter().all(all_properties_in_required),
_ => true,
}
}
fn all_properties_objects_deny_additional(value: &Value) -> bool {
match value {
Value::Object(object) => {
if object.contains_key("properties")
&& object.get("additionalProperties") != Some(&Value::Bool(false))
{
return false;
}
object.values().all(all_properties_objects_deny_additional)
}
Value::Array(array) => array.iter().all(all_properties_objects_deny_additional),
_ => true,
}
}
fn assert_schema_property_title(
properties: &serde_json::Map<String, Value>,
property_name: &str,
expected_title: &str,
) {
assert_eq!(
properties
.get(property_name)
.and_then(|value| value.get("title"))
.and_then(Value::as_str),
Some(expected_title)
);
}
fn assert_schema_property_title_and_description(
properties: &serde_json::Map<String, Value>,
property_name: &str,
expected_title: &str,
expected_description: &str,
) {
assert_schema_property_title(properties, property_name, expected_title);
assert_eq!(
properties
.get(property_name)
.and_then(|value| value.get("description"))
.and_then(Value::as_str),
Some(expected_description)
);
}
}