Compare commits

...

2 Commits

Author SHA1 Message Date
won
f11d249215 simplify 2026-05-12 12:50:43 -07:00
won
52b68b803d draft 2026-05-12 09:20:50 -07:00
13 changed files with 147 additions and 39 deletions

View File

@@ -2728,6 +2728,12 @@
"id": {
"type": "string"
},
"output_hint": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},

View File

@@ -13965,6 +13965,12 @@
"id": {
"type": "string"
},
"output_hint": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},

View File

@@ -10514,6 +10514,12 @@
"id": {
"type": "string"
},
"output_hint": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},

View File

@@ -683,6 +683,12 @@
"id": {
"type": "string"
},
"output_hint": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},

View File

@@ -813,6 +813,12 @@
"id": {
"type": "string"
},
"output_hint": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},

View File

@@ -14,4 +14,4 @@ export type ResponseItem = { "type": "message", role: string, content: Array<Con
/**
* Set when using the Responses API.
*/
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "compaction", encrypted_content: string, } | { "type": "context_compaction", encrypted_content?: string, } | { "type": "other" };
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, output_hint?: string, } | { "type": "compaction", encrypted_content: string, } | { "type": "context_compaction", encrypted_content?: string, } | { "type": "other" };

View File

@@ -521,6 +521,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
output_hint: None,
},
ResponseItem::Message {
id: None,
@@ -540,6 +541,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
output_hint: None,
},
ResponseItem::Message {
id: None,
@@ -569,6 +571,9 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
output_hint: Some(
"Generated images are saved to /tmp as /tmp/ig_123.png by default.".to_string(),
),
},
]);
@@ -588,6 +593,9 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: String::new(),
output_hint: Some(
"Generated images are saved to /tmp as /tmp/ig_123.png by default.".to_string(),
),
},
]
);

View File

@@ -195,6 +195,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
status,
revised_prompt,
result,
..
} => Some(TurnItem::ImageGeneration(
codex_protocol::items::ImageGenerationItem {
id: id.clone(),

View File

@@ -6333,6 +6333,10 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
output_hint: Some(
"Generated images are saved to /tmp as /tmp/ig-test.png by default."
.to_string(),
),
}],
/*reference_context_item*/ None,
)
@@ -6569,7 +6573,7 @@ async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_buil
}
#[tokio::test]
async fn handle_output_item_done_records_image_save_history_message() {
async fn handle_output_item_done_records_image_save_history_hint() {
let (session, turn_context) = make_session_and_context().await;
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
@@ -6585,6 +6589,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
output_hint: None,
};
let mut ctx = HandleOutputCtx {
@@ -6598,21 +6603,20 @@ async fn handle_output_item_done_records_image_save_history_message() {
.expect("image generation item should succeed");
let history = session.clone_history().await;
let image_output_path = crate::stream_events_utils::image_generation_artifact_path(
&turn_context.config.codex_home,
&session.conversation_id.to_string(),
"<image_id>",
);
let image_output_dir = image_output_path
let image_output_dir = expected_saved_path
.parent()
.expect("generated image path should have a parent");
let image_message: ResponseItem = crate::context::ContextualUserFragment::into(
crate::context::ImageGenerationInstructions::new(
image_output_dir.display(),
image_output_path.display(),
),
);
assert_eq!(history.raw_items(), &[image_message, item]);
let mut expected_item = item.clone();
if let ResponseItem::ImageGenerationCall { output_hint, .. } = &mut expected_item {
*output_hint = Some(
crate::context::ImageGenerationInstructions::new(
image_output_dir.display(),
expected_saved_path.display(),
)
.body(),
);
}
assert_eq!(history.raw_items(), &[expected_item]);
assert_eq!(
std::fs::read(&expected_saved_path).expect("saved file"),
b"foo"
@@ -6637,6 +6641,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
status: "completed".to_string(),
revised_prompt: Some("broken payload".to_string()),
result: "_-8".to_string(),
output_hint: None,
};
let mut ctx = HandleOutputCtx {

View File

@@ -265,7 +265,7 @@ pub(crate) async fn handle_output_item_done(
plan_mode,
)
.await;
if let Some(turn_item) = turn_item {
let item_for_history = if let Some(turn_item) = turn_item {
if previously_active_item.is_none() {
let mut started_item = turn_item.clone();
if let TurnItem::ImageGeneration(item) = &mut started_item {
@@ -279,12 +279,21 @@ pub(crate) async fn handle_output_item_done(
.await;
}
let item_for_history =
item_for_completed_history(&item, &turn_item, ctx.turn_context.as_ref());
ctx.sess
.emit_turn_item_completed(&ctx.turn_context, turn_item)
.await;
}
record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item)
.await;
item_for_history
} else {
item.clone()
};
record_completed_response_item(
ctx.sess.as_ref(),
ctx.turn_context.as_ref(),
&item_for_history,
)
.await;
let last_agent_message = last_assistant_message_from_item(&item, plan_mode);
output.last_agent_message = last_agent_message;
@@ -388,21 +397,6 @@ pub(crate) async fn handle_non_tool_response_item(
{
Ok(path) => {
image_item.saved_path = Some(path);
let image_output_path = image_generation_artifact_path(
&turn_context.config.codex_home,
&session_id,
"<image_id>",
);
let image_output_dir = image_output_path
.parent()
.unwrap_or_else(|| turn_context.config.codex_home.clone());
let message: ResponseItem =
ContextualUserFragment::into(ImageGenerationInstructions::new(
image_output_dir.display(),
image_output_path.display(),
));
sess.record_conversation_items(turn_context, &[message])
.await;
}
Err(err) => {
let output_path = image_generation_artifact_path(
@@ -433,6 +427,31 @@ pub(crate) async fn handle_non_tool_response_item(
}
}
fn item_for_completed_history(
item: &ResponseItem,
turn_item: &TurnItem,
turn_context: &TurnContext,
) -> ResponseItem {
let mut item_for_history = item.clone();
if let (
ResponseItem::ImageGenerationCall { output_hint, .. },
TurnItem::ImageGeneration(image_item),
) = (&mut item_for_history, turn_item)
&& let Some(saved_path) = image_item.saved_path.as_ref()
{
let image_output_dir = saved_path
.parent()
.unwrap_or_else(|| turn_context.config.codex_home.clone());
*output_hint = Some(
ImageGenerationInstructions::new(image_output_dir.display(), saved_path.display())
.body(),
);
}
item_for_history
}
pub(crate) fn last_assistant_message_from_item(
item: &ResponseItem,
plan_mode: bool,

View File

@@ -201,6 +201,7 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
output_hint: None,
};
assert!(completed_item_defers_mailbox_delivery_to_next_turn(

View File

@@ -593,12 +593,18 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
Some("Zm9v"),
"expected the original generated image payload to be preserved"
);
assert!(
image_generation_calls[0]["output_hint"]
.as_str()
.is_some_and(|text| text.contains("Generated images are saved to")),
"second request should keep the saved-path note on the image_generation_call"
);
assert!(
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Generated images are saved to")),
"second request should include the saved-path note in model-visible history"
.all(|text| !text.contains("Generated images are saved to")),
"second request should not include a separate developer saved-path note"
);
let _ = std::fs::remove_file(&saved_path);
@@ -710,6 +716,12 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
Some(""),
"second request should strip generated image bytes for text-only models"
);
assert!(
image_generation_calls[0]["output_hint"]
.as_str()
.is_some_and(|text| text.contains("Generated images are saved to")),
"second request should preserve the saved-path note on the image_generation_call"
);
assert!(
second_request
.message_input_texts("user")
@@ -721,8 +733,8 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Generated images are saved to")),
"second request should include the saved-path note in model-visible history"
.all(|text| !text.contains("Generated images are saved to")),
"second request should not include a separate developer saved-path note"
);
let _ = std::fs::remove_file(&saved_path);

View File

@@ -869,7 +869,8 @@ pub enum ResponseItem {
// "type":"image_generation_call",
// "status":"completed",
// "revised_prompt":"A gray tabby cat hugging an otter...",
// "result":"..."
// "result":"...",
// "output_hint":"Generated images are saved to ..."
// }
ImageGenerationCall {
id: String,
@@ -878,6 +879,9 @@ pub enum ResponseItem {
#[ts(optional)]
revised_prompt: Option<String>,
result: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
output_hint: Option<String>,
},
#[serde(alias = "compaction_summary")]
Compaction { encrypted_content: String },
@@ -1741,6 +1745,7 @@ mod tests {
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
result: "Zm9v".to_string(),
output_hint: None,
}
);
}
@@ -1762,6 +1767,33 @@ mod tests {
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
output_hint: None,
}
);
}
#[test]
fn response_item_parses_image_generation_call_with_output_hint() {
let item = serde_json::from_value::<ResponseItem>(serde_json::json!({
"id": "ig_123",
"type": "image_generation_call",
"status": "completed",
"result": "Zm9v",
"output_hint": "Generated images are saved to /tmp/generated_images/session as /tmp/generated_images/session/ig_123.png by default.",
}))
.expect("image generation item should deserialize");
assert_eq!(
item,
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
output_hint: Some(
"Generated images are saved to /tmp/generated_images/session as /tmp/generated_images/session/ig_123.png by default."
.to_string(),
),
}
);
}