mirror of
https://github.com/openai/codex.git
synced 2026-11-01 16:17:58 +00:00
Support --output-schema for exec resume (#23123)
## Why `codex exec resume` should have the same structured-output support as top-level `codex exec`. Without `--output-schema`, multi-turn automation has to choose between resumed session context and schema-validated JSON output. Fixes #22998. ## What changed - Marked `--output-schema` as a global `codex exec` flag so it can be passed after `resume`. - Reused the existing output schema plumbing so resumed turns attach the schema to the final response request while preserving session context.
This commit is contained in:
@@ -2346,7 +2346,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exec_resume_accepts_output_last_message_flag_after_subcommand() {
|
fn exec_resume_accepts_output_flags_after_subcommand() {
|
||||||
let cli = MultitoolCli::try_parse_from([
|
let cli = MultitoolCli::try_parse_from([
|
||||||
"codex",
|
"codex",
|
||||||
"exec",
|
"exec",
|
||||||
@@ -2354,6 +2354,8 @@ mod tests {
|
|||||||
"session-123",
|
"session-123",
|
||||||
"-o",
|
"-o",
|
||||||
"/tmp/resume-output.md",
|
"/tmp/resume-output.md",
|
||||||
|
"--output-schema",
|
||||||
|
"/tmp/schema.json",
|
||||||
"re-review",
|
"re-review",
|
||||||
])
|
])
|
||||||
.expect("parse should succeed");
|
.expect("parse should succeed");
|
||||||
@@ -2369,6 +2371,10 @@ mod tests {
|
|||||||
exec.last_message_file,
|
exec.last_message_file,
|
||||||
Some(std::path::PathBuf::from("/tmp/resume-output.md"))
|
Some(std::path::PathBuf::from("/tmp/resume-output.md"))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
exec.output_schema,
|
||||||
|
Some(std::path::PathBuf::from("/tmp/schema.json"))
|
||||||
|
);
|
||||||
assert_eq!(args.session_id.as_deref(), Some("session-123"));
|
assert_eq!(args.session_id.as_deref(), Some("session-123"));
|
||||||
assert_eq!(args.prompt.as_deref(), Some("re-review"));
|
assert_eq!(args.prompt.as_deref(), Some("re-review"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ pub struct Cli {
|
|||||||
pub removed_full_auto: bool,
|
pub removed_full_auto: bool,
|
||||||
|
|
||||||
/// Path to a JSON Schema file describing the model's final response shape.
|
/// Path to a JSON Schema file describing the model's final response shape.
|
||||||
#[arg(long = "output-schema", value_name = "FILE")]
|
#[arg(long = "output-schema", value_name = "FILE", global = true)]
|
||||||
pub output_schema: Option<PathBuf>,
|
pub output_schema: Option<PathBuf>,
|
||||||
|
|
||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fn resume_parses_prompt_after_global_flags() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_accepts_output_last_message_flag_after_subcommand() {
|
fn resume_accepts_output_flags_after_subcommand() {
|
||||||
const PROMPT: &str = "echo resume-with-output-file";
|
const PROMPT: &str = "echo resume-with-output-file";
|
||||||
let cli = Cli::parse_from([
|
let cli = Cli::parse_from([
|
||||||
"codex-exec",
|
"codex-exec",
|
||||||
@@ -44,6 +44,8 @@ fn resume_accepts_output_last_message_flag_after_subcommand() {
|
|||||||
"session-123",
|
"session-123",
|
||||||
"-o",
|
"-o",
|
||||||
"/tmp/resume-output.md",
|
"/tmp/resume-output.md",
|
||||||
|
"--output-schema",
|
||||||
|
"/tmp/schema.json",
|
||||||
PROMPT,
|
PROMPT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ fn resume_accepts_output_last_message_flag_after_subcommand() {
|
|||||||
cli.last_message_file,
|
cli.last_message_file,
|
||||||
Some(PathBuf::from("/tmp/resume-output.md"))
|
Some(PathBuf::from("/tmp/resume-output.md"))
|
||||||
);
|
);
|
||||||
|
assert_eq!(cli.output_schema, Some(PathBuf::from("/tmp/schema.json")));
|
||||||
let Some(Command::Resume(args)) = cli.command else {
|
let Some(Command::Resume(args)) = cli.command else {
|
||||||
panic!("expected resume command");
|
panic!("expected resume command");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -375,6 +375,62 @@ async fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn exec_resume_includes_output_schema_in_request() -> anyhow::Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let test = test_codex_exec();
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
let response_mock = mount_exec_responses(&server, /*count*/ 2).await;
|
||||||
|
|
||||||
|
let schema_contents = serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"answer": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["answer"],
|
||||||
|
"additionalProperties": false
|
||||||
|
});
|
||||||
|
let schema_path = test.cwd_path().join("schema.json");
|
||||||
|
std::fs::write(&schema_path, serde_json::to_vec_pretty(&schema_contents)?)?;
|
||||||
|
|
||||||
|
test.cmd_with_server(&server)
|
||||||
|
.arg("--skip-git-repo-check")
|
||||||
|
.arg("echo seed-resume-session")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
test.cmd_with_server(&server)
|
||||||
|
.arg("--skip-git-repo-check")
|
||||||
|
.arg("resume")
|
||||||
|
.arg("--last")
|
||||||
|
.arg("--json")
|
||||||
|
.arg("--output-schema")
|
||||||
|
.arg(&schema_path)
|
||||||
|
.arg("echo resume-with-schema")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
let requests = response_mock.requests();
|
||||||
|
assert_eq!(requests.len(), 2);
|
||||||
|
let payload: Value = requests[1].body_json();
|
||||||
|
let text = payload.get("text").expect("request missing text field");
|
||||||
|
let format = text
|
||||||
|
.get("format")
|
||||||
|
.expect("request missing text.format field");
|
||||||
|
assert_eq!(
|
||||||
|
format,
|
||||||
|
&serde_json::json!({
|
||||||
|
"name": "codex_output_schema",
|
||||||
|
"type": "json_schema",
|
||||||
|
"strict": true,
|
||||||
|
"schema": schema_contents,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
|
async fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
Reference in New Issue
Block a user