mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Support standalone named function call outputs (#39782)
## Why External tool events may need to enter thread history without a preceding function call and therefore do not have a `call_id`. ## What changed - Allow `function_call_output` items to omit `call_id` and carry optional `name` and `namespace` fields. - Preserve named standalone outputs during history normalization and agent forks while retaining existing pairing behavior for outputs with a `call_id`. - Accept, persist, and forward these outputs through `thread/inject_items`, and update the app-server schemas and documentation. ## Testing - Cover paired and standalone JSON round trips, history normalization, agent forks, and injected thread history. GitOrigin-RevId: a3258163a7dc93777c7c3023116fe204819bdbb0
This commit is contained in:
+16
-2
@@ -3051,7 +3051,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
@@ -3069,6 +3072,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"$ref": "#/definitions/FunctionCallOutputBody"
|
||||
},
|
||||
@@ -3081,7 +3096,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"output",
|
||||
"type"
|
||||
],
|
||||
|
||||
+16
-2
@@ -17372,7 +17372,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
@@ -17390,6 +17393,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"$ref": "#/definitions/v2/FunctionCallOutputBody"
|
||||
},
|
||||
@@ -17402,7 +17417,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"output",
|
||||
"type"
|
||||
],
|
||||
|
||||
+16
-2
@@ -13582,7 +13582,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
@@ -13600,6 +13603,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"$ref": "#/definitions/FunctionCallOutputBody"
|
||||
},
|
||||
@@ -13612,7 +13627,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"output",
|
||||
"type"
|
||||
],
|
||||
|
||||
+16
-2
@@ -736,7 +736,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
@@ -754,6 +757,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"$ref": "#/definitions/FunctionCallOutputBody"
|
||||
},
|
||||
@@ -766,7 +781,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"output",
|
||||
"type"
|
||||
],
|
||||
|
||||
@@ -806,7 +806,10 @@
|
||||
{
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
@@ -824,6 +827,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespace": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"$ref": "#/definitions/FunctionCallOutputBody"
|
||||
},
|
||||
@@ -836,7 +851,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"output",
|
||||
"type"
|
||||
],
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -21,4 +21,4 @@ id?: ResponseItemId,
|
||||
/**
|
||||
* Set when using the Responses API.
|
||||
*/
|
||||
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: ResponseItemId, name: string, namespace?: string, arguments: string, encrypted_function_args?: Array<string>, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: ResponseItemId, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: ResponseItemId, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: ResponseItemId, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: ResponseItemId, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: ResponseItemId, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: ResponseItemId, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: ResponseItemId, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: ResponseItemId, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: ResponseItemId, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
|
||||
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: ResponseItemId, name: string, namespace?: string, arguments: string, encrypted_function_args?: Array<string>, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: ResponseItemId, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: ResponseItemId, call_id?: string, name?: string, namespace?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: ResponseItemId, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: ResponseItemId, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: ResponseItemId, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: ResponseItemId, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: ResponseItemId, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: ResponseItemId, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: ResponseItemId, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
|
||||
|
||||
@@ -1018,7 +1018,7 @@ Invoke a plugin by including a UI mention token such as `@sample` in the text in
|
||||
|
||||
### Example: Inject raw history items
|
||||
|
||||
Use `thread/inject_items` to append prebuilt Responses API items to a loaded thread’s prompt history without starting a user turn. These items are persisted to the rollout and included in subsequent model requests. Any `input_image` items must use inline data URLs; remote HTTP(S) image URLs are rejected.
|
||||
Use `thread/inject_items` to append prebuilt Responses API items to a loaded thread’s prompt history without starting a user turn. These items are persisted to the rollout and included in subsequent model requests. A standalone `function_call_output` can omit `call_id` when it has a nonempty `name`; `namespace` is optional, and the output retains tool-tier authority. Any `input_image` items must use inline data URLs; remote HTTP(S) image URLs are rejected.
|
||||
|
||||
```json
|
||||
{ "method": "thread/inject_items", "id": 36, "params": {
|
||||
@@ -1028,6 +1028,12 @@ Use `thread/inject_items` to append prebuilt Responses API items to a loaded thr
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{ "type": "output_text", "text": "Previously computed context." }]
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "send_message_to_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "Another agent delegated this task."
|
||||
}
|
||||
]
|
||||
} }
|
||||
|
||||
@@ -168,7 +168,9 @@ async fn request_handlers_reject_remote_image_urls() -> Result<()> {
|
||||
|
||||
let remote_tool_output = serde_json::to_value(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "https://example.com/tool.png".to_string(),
|
||||
|
||||
@@ -21,6 +21,7 @@ use core_test_support::responses;
|
||||
use core_test_support::responses::strip_response_item_id;
|
||||
use core_test_support::responses::strip_response_item_ids_from_json;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
@@ -78,6 +79,13 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
let injected_developer_item = developer_item("Injected developer context");
|
||||
let marker_shaped_developer_item =
|
||||
developer_item("<image_resize_notice>\nclient message\n</image_resize_notice>");
|
||||
let named_tool_output = json!({
|
||||
"type": "function_call_output",
|
||||
"name": "send_message_to_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "Another agent delegated this task.",
|
||||
});
|
||||
let named_tool_item: ResponseItem = serde_json::from_value(named_tool_output.clone())?;
|
||||
|
||||
let inject_req = mcp
|
||||
.send_thread_inject_items_request(ThreadInjectItemsParams {
|
||||
@@ -86,6 +94,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
serde_json::to_value(&injected_item)?,
|
||||
serde_json::to_value(&injected_developer_item)?,
|
||||
serde_json::to_value(&marker_shaped_developer_item)?,
|
||||
named_tool_output.clone(),
|
||||
],
|
||||
})
|
||||
.await?;
|
||||
@@ -114,6 +123,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
item == &injected_item
|
||||
|| item == &injected_developer_item
|
||||
|| item == &marker_shaped_developer_item
|
||||
|| item == &named_tool_item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
@@ -122,6 +132,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
(injected_item.clone(), None),
|
||||
(injected_developer_item.clone(), Some(true)),
|
||||
(marker_shaped_developer_item.clone(), Some(true)),
|
||||
(named_tool_item, None),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -233,6 +244,10 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
injected_index < user_prompt_index,
|
||||
"injected items should be sent before the user prompt"
|
||||
);
|
||||
assert!(
|
||||
model_input.contains(&named_tool_output),
|
||||
"named unpaired tool output should be sent in the next model request"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -59,13 +59,16 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
||||
"assistant" => *phase == Some(MessagePhase::FinalAnswer),
|
||||
_ => false,
|
||||
},
|
||||
ResponseItem::FunctionCallOutput { call_id: None, .. } => true,
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(_), ..
|
||||
}
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
|
||||
@@ -35,6 +35,7 @@ use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -1352,6 +1353,14 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
"parent trigger message".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
let standalone_output = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: None,
|
||||
name: Some("notifications".to_string()),
|
||||
namespace: Some("slack".to_string()),
|
||||
output: FunctionCallOutputPayload::from_text("parent notification".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
parent_thread
|
||||
.session
|
||||
.record_conversation_items(
|
||||
@@ -1399,6 +1408,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
},
|
||||
assistant_message("parent commentary", Some(MessagePhase::Commentary)),
|
||||
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
|
||||
standalone_output,
|
||||
assistant_message("parent unknown phase", /*phase*/ None),
|
||||
ResponseItem::Reasoning {
|
||||
id: Some(ResponseItemId::with_suffix("rs", "parent-reasoning")),
|
||||
@@ -1412,6 +1422,14 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let expected_standalone_output = parent_thread
|
||||
.session
|
||||
.clone_history()
|
||||
.await
|
||||
.raw_items()
|
||||
.find(|item| matches!(item, ResponseItem::FunctionCallOutput { call_id: None, .. }))
|
||||
.cloned()
|
||||
.expect("standalone output should be recorded");
|
||||
let parent_reference_context_item = turn_context.to_turn_context_item();
|
||||
parent_thread
|
||||
.session
|
||||
@@ -1494,6 +1512,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
expected_parent_seed,
|
||||
expected_developer_message,
|
||||
expected_final_answer,
|
||||
expected_standalone_output,
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
|
||||
@@ -30,7 +30,9 @@ fn prompt_with_image_outputs() -> Prompt {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "function-call".to_string(),
|
||||
call_id: Some("function-call".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,function".to_string(),
|
||||
@@ -78,7 +80,9 @@ fn responses_lite_request_copies_strip_image_details() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "function-call".to_string(),
|
||||
call_id: Some("function-call".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,function".to_string(),
|
||||
|
||||
@@ -461,11 +461,15 @@ fn rewritten_output_for_context_window(
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id,
|
||||
call_id,
|
||||
name,
|
||||
namespace,
|
||||
output,
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
} => ResponseItem::FunctionCallOutput {
|
||||
id: id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
name: name.clone(),
|
||||
namespace: namespace.clone(),
|
||||
output: truncated_output_payload(output),
|
||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@ fn rewritten_output_preserves_harness_metadata() {
|
||||
let envelope = ResponseItemEnvelope {
|
||||
item: ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text("large output".repeat(100)),
|
||||
success: Some(true),
|
||||
|
||||
@@ -439,7 +439,7 @@ impl ContextManager {
|
||||
|
||||
/// This function enforces a couple of invariants on the in-memory history:
|
||||
/// 1. every call (function/custom) has a corresponding output entry
|
||||
/// 2. every output has a corresponding call entry
|
||||
/// 2. every output has a corresponding call entry or names an external tool event
|
||||
/// 3. unsupported image and audio content is stripped from messages and tool outputs
|
||||
fn normalize_history(&mut self, input_modalities: &[InputModality]) {
|
||||
let items = Arc::make_mut(&mut self.items);
|
||||
@@ -447,7 +447,7 @@ impl ContextManager {
|
||||
// all function/tool calls must have a corresponding output
|
||||
normalize::ensure_call_outputs_present(items);
|
||||
|
||||
// all outputs must have a corresponding function/tool call
|
||||
// Paired outputs must have a corresponding call; named external outputs stand alone.
|
||||
normalize::remove_orphan_outputs(items);
|
||||
|
||||
// strip images when model does not support them
|
||||
@@ -463,11 +463,15 @@ impl ContextManager {
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id,
|
||||
call_id,
|
||||
name,
|
||||
namespace,
|
||||
output,
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
} => ResponseItem::FunctionCallOutput {
|
||||
id: id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
name: name.clone(),
|
||||
namespace: namespace.clone(),
|
||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
||||
},
|
||||
|
||||
@@ -547,7 +547,9 @@ fn record_annotated_items_preserves_metadata_while_processing_item() {
|
||||
let envelope = ResponseItemEnvelope {
|
||||
item: ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text("word ".repeat(100)),
|
||||
success: Some(true),
|
||||
@@ -684,7 +686,9 @@ fn for_prompt_strips_media_when_model_does_not_support_it() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "image result".to_string(),
|
||||
@@ -766,7 +770,9 @@ fn for_prompt_strips_media_when_model_does_not_support_it() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "image result".to_string(),
|
||||
@@ -989,7 +995,9 @@ fn remove_first_item_removes_matching_output_for_function_call() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1004,7 +1012,9 @@ fn remove_first_item_removes_matching_call_for_output() {
|
||||
let items = vec![
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-2".to_string(),
|
||||
call_id: Some("call-2".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1041,7 +1051,9 @@ fn remove_first_item_handles_local_shell_pair() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-3".to_string(),
|
||||
call_id: Some("call-3".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1338,7 +1350,9 @@ fn normalization_retains_local_shell_outputs() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "shell-1".to_string(),
|
||||
call_id: Some("shell-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("Total output lines: 1\n\nok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1360,7 +1374,9 @@ fn record_items_truncates_function_call_output_content() {
|
||||
let long_output = long_line.repeat(2_500);
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-100".to_string(),
|
||||
call_id: Some("call-100".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(long_output.clone()),
|
||||
success: Some(true),
|
||||
@@ -1433,7 +1449,9 @@ fn record_items_respects_custom_token_limit() {
|
||||
let long_output = "tokenized content repeated many times ".repeat(200);
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-custom-limit".to_string(),
|
||||
call_id: Some("call-custom-limit".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(long_output),
|
||||
success: Some(true),
|
||||
@@ -1579,7 +1597,9 @@ fn normalize_adds_missing_output_for_function_call() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-x".to_string(),
|
||||
call_id: Some("call-x".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1664,7 +1684,9 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "shell-1".to_string(),
|
||||
call_id: Some("shell-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1677,7 +1699,9 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
|
||||
fn normalize_removes_orphan_function_call_output() {
|
||||
let items = vec![ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "orphan-1".to_string(),
|
||||
call_id: Some("orphan-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}];
|
||||
@@ -1722,7 +1746,9 @@ fn normalize_mixed_inserts_and_removals() {
|
||||
// Orphan output that should be removed
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "c2".to_string(),
|
||||
call_id: Some("c2".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1769,7 +1795,9 @@ fn normalize_mixed_inserts_and_removals() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "c1".to_string(),
|
||||
call_id: Some("c1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1804,7 +1832,9 @@ fn normalize_mixed_inserts_and_removals() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "s1".to_string(),
|
||||
call_id: Some("s1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1839,7 +1869,9 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-x".to_string(),
|
||||
call_id: Some("call-x".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1847,6 +1879,21 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_preserves_named_function_call_output_without_call_id() {
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: None,
|
||||
name: Some("send_message_to_thread".to_string()),
|
||||
namespace: Some("codex_app".to_string()),
|
||||
output: FunctionCallOutputPayload::from_text("cross-thread message".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
let history = create_history_with_items(vec![item.clone()]);
|
||||
|
||||
assert_eq!(history.for_prompt(&default_input_modalities()), vec![item]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_prompt_assigns_stable_id_to_synthetic_output_without_reordering_history() {
|
||||
let items = vec![
|
||||
@@ -1972,7 +2019,9 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id_panics_in_debug()
|
||||
fn normalize_removes_orphan_function_call_output_panics_in_debug() {
|
||||
let items = vec![ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "orphan-1".to_string(),
|
||||
call_id: Some("orphan-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}];
|
||||
@@ -2072,7 +2121,9 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "c2".to_string(),
|
||||
call_id: Some("c2".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -2148,7 +2199,9 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() {
|
||||
let image_url = format!("data:image/png;base64,{payload}");
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-abc".to_string(),
|
||||
call_id: Some("call-abc".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "Screenshot captured".to_string(),
|
||||
@@ -2221,7 +2274,9 @@ fn audio_data_url_payload_does_not_dominate_function_call_output_estimate() {
|
||||
let (audio_url, payload_len) = pcm_wav_data_url(/*sample_count*/ 800);
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-audio".to_string(),
|
||||
call_id: Some("call-audio".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputAudio { audio_url },
|
||||
]),
|
||||
@@ -2281,7 +2336,9 @@ fn record_items_omits_audio_that_exceeds_the_output_budget() {
|
||||
let (audio_url, _) = pcm_wav_data_url(/*sample_count*/ 80_000);
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-audio".to_string(),
|
||||
call_id: Some("call-audio".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputAudio { audio_url },
|
||||
@@ -2298,7 +2355,9 @@ fn record_items_omits_audio_that_exceeds_the_output_budget() {
|
||||
raw_items(&history),
|
||||
&[ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-audio".to_string(),
|
||||
call_id: Some("call-audio".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
@@ -2326,7 +2385,9 @@ fn non_base64_image_urls_are_unchanged() {
|
||||
};
|
||||
let function_output_item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "file:///tmp/foo.png".to_string(),
|
||||
@@ -2351,7 +2412,9 @@ fn encrypted_function_output_uses_plaintext_byte_estimate() {
|
||||
let encrypted_content = "A".repeat(1_868);
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-encrypted".to_string(),
|
||||
call_id: Some("call-encrypted".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::EncryptedContent {
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
@@ -2410,7 +2473,9 @@ fn non_image_base64_data_url_is_unchanged() {
|
||||
let image_url = format!("data:application/octet-stream;base64,{payload}");
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-octet".to_string(),
|
||||
call_id: Some("call-octet".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url,
|
||||
@@ -2499,7 +2564,9 @@ fn original_detail_images_scale_with_dimensions() {
|
||||
let image_url = format!("data:image/png;base64,{payload}");
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-original".to_string(),
|
||||
call_id: Some("call-original".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url,
|
||||
@@ -2531,7 +2598,9 @@ fn original_detail_images_are_capped_at_max_patch_count() {
|
||||
let image_url = format!("data:image/png;base64,{payload}");
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-original-capped".to_string(),
|
||||
call_id: Some("call-original-capped".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url,
|
||||
@@ -2566,7 +2635,9 @@ fn original_detail_webp_images_scale_with_dimensions() {
|
||||
let image_url = format!("data:image/webp;base64,{payload}");
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-original-webp".to_string(),
|
||||
call_id: Some("call-original-webp".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url,
|
||||
|
||||
@@ -24,7 +24,10 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItemEnvelope>)
|
||||
let mut custom_tool_output_ids = HashSet::new();
|
||||
for envelope in items.iter() {
|
||||
match &envelope.item {
|
||||
ResponseItem::FunctionCallOutput { call_id, .. } => {
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => {
|
||||
function_output_ids.insert(call_id.as_str());
|
||||
}
|
||||
ResponseItem::ToolSearchOutput {
|
||||
@@ -55,7 +58,9 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItemEnvelope>)
|
||||
idx,
|
||||
ResponseItemEnvelope::new(ResponseItem::FunctionCallOutput {
|
||||
id: synthetic_output_id("fco", id.as_deref()),
|
||||
call_id: call_id.clone(),
|
||||
call_id: Some(call_id.clone()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}),
|
||||
@@ -109,7 +114,9 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItemEnvelope>)
|
||||
idx,
|
||||
ResponseItemEnvelope::new(ResponseItem::FunctionCallOutput {
|
||||
id: synthetic_output_id("fco", id.as_deref()),
|
||||
call_id: call_id.clone(),
|
||||
call_id: Some(call_id.clone()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}),
|
||||
@@ -174,9 +181,10 @@ pub(crate) fn remove_orphan_outputs(items: &mut Vec<ResponseItemEnvelope>) {
|
||||
let mut orphan_positions = Vec::new();
|
||||
for (position, envelope) in items.iter().enumerate() {
|
||||
match &envelope.item {
|
||||
ResponseItem::FunctionCallOutput { call_id, .. }
|
||||
if !function_call_ids.contains(call_id.as_str()) =>
|
||||
{
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} if !function_call_ids.contains(call_id.as_str()) => {
|
||||
error_or_panic(format!(
|
||||
"Orphan function call output for call id: {call_id}"
|
||||
));
|
||||
@@ -223,12 +231,16 @@ pub(crate) fn remove_corresponding_for(items: &mut Vec<ResponseItemEnvelope>, it
|
||||
matches!(
|
||||
i,
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: existing, ..
|
||||
call_id: Some(existing),
|
||||
..
|
||||
} if existing == call_id
|
||||
)
|
||||
});
|
||||
}
|
||||
ResponseItem::FunctionCallOutput { call_id, .. } => {
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(pos) = items.iter().position(|envelope| {
|
||||
matches!(&envelope.item, ResponseItem::FunctionCall { call_id: existing, .. } if existing == call_id)
|
||||
}) {
|
||||
@@ -294,7 +306,8 @@ pub(crate) fn remove_corresponding_for(items: &mut Vec<ResponseItemEnvelope>, it
|
||||
matches!(
|
||||
i,
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: existing, ..
|
||||
call_id: Some(existing),
|
||||
..
|
||||
} if existing == call_id
|
||||
)
|
||||
});
|
||||
|
||||
@@ -566,7 +566,9 @@ pub(crate) fn collect_guardian_transcript_entries<'a>(
|
||||
)
|
||||
}),
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id, output, ..
|
||||
call_id: Some(call_id),
|
||||
output,
|
||||
..
|
||||
}
|
||||
| ResponseItem::CustomToolCallOutput {
|
||||
call_id, output, ..
|
||||
|
||||
@@ -358,7 +358,9 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
|
||||
"repo visibility: public".to_string(),
|
||||
),
|
||||
@@ -999,7 +1001,9 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: codex_protocol::models::FunctionCallOutputPayload::from_text(
|
||||
"repo is public".to_string(),
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_analytics::ImagePreparationMetadata;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
@@ -99,6 +100,26 @@ pub(crate) fn prepare_response_items(
|
||||
) -> Vec<ImagePreparationMetadata> {
|
||||
let mut metadata = Vec::new();
|
||||
let mut prepared_items = Vec::with_capacity(items.len());
|
||||
let prepare_tool_output =
|
||||
|output: &mut FunctionCallOutputPayload,
|
||||
item_id: Option<&str>,
|
||||
metadata: &mut Vec<ImagePreparationMetadata>| {
|
||||
output.content_items_mut().and_then(|content| {
|
||||
let resized_images = prepare_tool_output_content(
|
||||
content,
|
||||
ImageOrigin {
|
||||
message_role: None,
|
||||
item_id,
|
||||
},
|
||||
resize_notice_mode,
|
||||
metadata,
|
||||
mode,
|
||||
);
|
||||
(!resized_images.is_empty()).then(|| {
|
||||
ImageResizeNotice::new(ImageResizeNoticeSource::ToolOutput, resized_images)
|
||||
})
|
||||
})
|
||||
};
|
||||
for mut item in std::mem::take(items) {
|
||||
let resize_notice = match &mut item {
|
||||
ResponseItem::Message { role, content, .. } => {
|
||||
@@ -122,24 +143,10 @@ pub(crate) fn prepare_response_items(
|
||||
}
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id, output, ..
|
||||
}
|
||||
| ResponseItem::CustomToolCallOutput {
|
||||
} => prepare_tool_output(output, call_id.as_deref(), &mut metadata),
|
||||
ResponseItem::CustomToolCallOutput {
|
||||
call_id, output, ..
|
||||
} => output.content_items_mut().and_then(|content| {
|
||||
let resized_images = prepare_tool_output_content(
|
||||
content,
|
||||
ImageOrigin {
|
||||
message_role: None,
|
||||
item_id: Some(call_id),
|
||||
},
|
||||
resize_notice_mode,
|
||||
&mut metadata,
|
||||
mode,
|
||||
);
|
||||
(!resized_images.is_empty()).then(|| {
|
||||
ImageResizeNotice::new(ImageResizeNoticeSource::ToolOutput, resized_images)
|
||||
})
|
||||
}),
|
||||
} => prepare_tool_output(output, Some(call_id.as_str()), &mut metadata),
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
|
||||
@@ -143,7 +143,9 @@ fn preparation_reports_tool_output_item_id() {
|
||||
let (image_url, _) = png_data_url(/*width*/ 64, /*height*/ 32);
|
||||
let mut items = vec![ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: call_id.to_string(),
|
||||
call_id: Some(call_id.to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url,
|
||||
@@ -199,7 +201,9 @@ fn resize_notices_preserve_original_image_positions_and_skip_failed_images() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-image".to_string(),
|
||||
call_id: Some("call-image".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: "data:image/png;base64,%%%".to_string(),
|
||||
|
||||
@@ -2383,7 +2383,9 @@ async fn prepares_image_failures_before_history_insertion() {
|
||||
.await;
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
@@ -2421,7 +2423,9 @@ async fn prepares_image_failures_before_history_insertion() {
|
||||
assert_eq!(parsed_id.get_version(), Some(uuid::Version::SortRand));
|
||||
let expected = vec![ResponseItem::FunctionCallOutput {
|
||||
id: Some(id.clone()),
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
|
||||
@@ -504,7 +504,9 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
Some(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: call_id.clone(),
|
||||
call_id: Some(call_id.clone()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: output.clone(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
})
|
||||
@@ -524,7 +526,9 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
|
||||
let output = output.as_function_call_output_payload();
|
||||
Some(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: call_id.clone(),
|
||||
call_id: Some(call_id.clone()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
})
|
||||
|
||||
@@ -106,7 +106,9 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
|
||||
@@ -208,7 +208,10 @@ impl ExecutedToolCallRecorder {
|
||||
break;
|
||||
}
|
||||
let call_id = match &*item {
|
||||
ResponseItem::FunctionCallOutput { call_id, .. }
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
}
|
||||
| ResponseItem::CustomToolCallOutput { call_id, .. }
|
||||
| ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
@@ -266,7 +269,10 @@ impl ExecutedToolCallRecorder {
|
||||
state.retained_calls.clear();
|
||||
for item in items {
|
||||
let call_id = match &*item {
|
||||
ResponseItem::FunctionCallOutput { call_id, .. }
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
}
|
||||
| ResponseItem::CustomToolCallOutput { call_id, .. }
|
||||
| ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
|
||||
@@ -77,7 +77,9 @@ fn executed_tool_call_recorder_bounds_pending_calls_and_preserves_overflow() {
|
||||
|
||||
let mut items = [ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "bounded-output".to_string(),
|
||||
call_id: Some("bounded-output".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text(String::new()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}];
|
||||
@@ -116,7 +118,9 @@ fn executed_tool_call_recorder_bounds_pending_calls_and_preserves_overflow() {
|
||||
|
||||
let mut replayed_items = [ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "bounded-output".to_string(),
|
||||
call_id: Some("bounded-output".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text(String::new()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}];
|
||||
@@ -162,7 +166,9 @@ fn executed_tool_call_recorder_bounds_retained_history_and_reports_omissions() {
|
||||
);
|
||||
history.push(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
call_id: Some(call_id),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text(String::new()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
|
||||
@@ -192,7 +192,9 @@ fn response_item_records_turn_ttft_ignores_empty_non_output_items() {
|
||||
assert!(!response_item_records_turn_ttft(
|
||||
&ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}
|
||||
|
||||
@@ -1210,7 +1210,9 @@ async fn resume_replays_image_tool_outputs_with_detail() {
|
||||
ordinal: None,
|
||||
item: rollout_response_item(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: function_call_id.to_string(),
|
||||
call_id: Some(function_call_id.to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputImage {
|
||||
image_url: image_url.to_string(),
|
||||
@@ -3056,7 +3058,9 @@ async fn azure_responses_request_does_not_store_and_preserves_prefixed_item_ids(
|
||||
});
|
||||
prompt.input.push(ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "function-call-id".into(),
|
||||
call_id: Some("function-call-id".into()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("ok".into()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
|
||||
@@ -142,7 +142,10 @@ async fn tool_start_receives_conversation_history() -> Result<()> {
|
||||
let second_history = &histories[1].items;
|
||||
assert!(second_history.iter().any(|item| matches!(
|
||||
item,
|
||||
ResponseItem::FunctionCallOutput { call_id, .. } if call_id == first_call_id
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} if call_id == first_call_id
|
||||
)));
|
||||
assert!(second_history.iter().any(|item| matches!(
|
||||
item,
|
||||
|
||||
@@ -184,7 +184,7 @@ async fn wait_for_raw_unified_exec_output(
|
||||
let content = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::RawResponseItem(raw) => match &raw.item {
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: output_call_id,
|
||||
call_id: Some(output_call_id),
|
||||
output,
|
||||
..
|
||||
} if output_call_id == call_id => output.text_content().map(str::to_string),
|
||||
|
||||
@@ -918,7 +918,9 @@ max_recent_non_user_entries = 8
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "previous-call".to_owned(),
|
||||
call_id: Some("previous-call".to_owned()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("README.md".to_owned()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1174,7 +1176,9 @@ async fn contributor_includes_configured_transcript_images() -> Result<()> {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "previous-call".to_owned(),
|
||||
call_id: Some("previous-call".to_owned()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "Screenshot captured.".to_owned(),
|
||||
@@ -1386,7 +1390,9 @@ async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "previous-call".to_owned(),
|
||||
call_id: Some("previous-call".to_owned()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("README.md".to_owned()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1662,7 +1668,9 @@ async fn contributor_sends_compacted_conversation_history_to_luna() -> Result<()
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
call_id: Some(call_id),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text(format!(
|
||||
"result evidence {index}: {}",
|
||||
"signal ".repeat(1_000)
|
||||
|
||||
@@ -228,7 +228,9 @@ impl TranscriptConfig {
|
||||
(format!("tool {name} call"), arguments.clone())
|
||||
}
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id, output, ..
|
||||
call_id: Some(call_id),
|
||||
output,
|
||||
..
|
||||
}
|
||||
| ResponseItem::CustomToolCallOutput {
|
||||
call_id, output, ..
|
||||
@@ -287,6 +289,7 @@ impl TranscriptConfig {
|
||||
("tool web_search call".to_owned(), text)
|
||||
}
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::FunctionCallOutput { call_id: None, .. }
|
||||
| ResponseItem::ImageGenerationCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
|
||||
@@ -40,7 +40,9 @@ fn transcript_keeps_conversation_and_configured_sources() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("Workspace inspected.".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -459,7 +461,9 @@ fn transcript_truncates_tool_results_using_standard_budget() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_owned(),
|
||||
call_id: Some("call-1".to_owned()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text(output),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -596,7 +600,9 @@ fn transcript_omits_media_payloads_and_keeps_readable_content() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_content_items(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: "Screenshot captured.".to_string(),
|
||||
|
||||
@@ -107,7 +107,9 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "mcp-call".to_string(),
|
||||
call_id: Some("mcp-call".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: image_output("mcp"),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -136,7 +138,9 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "orphan-call".to_string(),
|
||||
call_id: Some("orphan-call".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: image_output("orphan"),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
|
||||
@@ -511,7 +511,9 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
|
||||
}));
|
||||
}
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id, output, ..
|
||||
call_id: Some(call_id),
|
||||
output,
|
||||
..
|
||||
} if function_call_ids.contains(call_id.as_str()) => {
|
||||
image_urls.extend(output_image_urls(output));
|
||||
}
|
||||
|
||||
@@ -758,7 +758,9 @@ mod tests {
|
||||
job::serialize_filtered_rollout_response_items(&[RolloutItem::ResponseItem(
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call_123".to_string(),
|
||||
call_id: Some("call_123".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: codex_protocol::models::FunctionCallOutputPayload {
|
||||
body: codex_protocol::models::FunctionCallOutputBody::Text(
|
||||
r#"{"token":"sk-abcdefghijklmnopqrstuvwxyz123456"}"#.to_string(),
|
||||
|
||||
@@ -1043,7 +1043,15 @@ pub enum ResponseItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
call_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
namespace: Option<String>,
|
||||
#[ts(as = "FunctionCallOutputBody")]
|
||||
#[schemars(with = "FunctionCallOutputBody")]
|
||||
output: FunctionCallOutputPayload,
|
||||
@@ -1776,7 +1784,9 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
},
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
call_id: Some(call_id),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
@@ -1784,7 +1794,9 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
let output = output.into_function_call_output_payload();
|
||||
Self::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
call_id: Some(call_id),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}
|
||||
@@ -3099,6 +3111,55 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paired_function_call_output_preserves_existing_wire_shape() {
|
||||
let value = serde_json::json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": "call-1",
|
||||
"output": "done",
|
||||
});
|
||||
let item: ResponseItem = serde_json::from_value(value.clone())
|
||||
.expect("paired function call output should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("done".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(serde_json::to_value(item).expect("serialize item"), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_unpaired_function_call_output_round_trips_without_call_id() {
|
||||
let value = serde_json::json!({
|
||||
"type": "function_call_output",
|
||||
"name": "notifications",
|
||||
"namespace": "slack",
|
||||
"output": "Alice mentioned you.",
|
||||
});
|
||||
let item: ResponseItem = serde_json::from_value(value.clone())
|
||||
.expect("named unpaired function call output should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: None,
|
||||
name: Some("notifications".to_string()),
|
||||
namespace: Some("slack".to_string()),
|
||||
output: FunctionCallOutputPayload::from_text("Alice mentioned you.".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(serde_json::to_value(item).expect("serialize item"), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_call_preserves_empty_encrypted_function_args() {
|
||||
let value = serde_json::json!({
|
||||
|
||||
@@ -29,7 +29,9 @@ fn executed_tool_call_prompt_budget_includes_metadata_fields() -> Result<()> {
|
||||
.map(|index| {
|
||||
let mut item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: index.to_string(),
|
||||
call_id: Some(index.to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(String::new()),
|
||||
success: None,
|
||||
@@ -78,7 +80,9 @@ fn executed_tool_call_prompt_budget_includes_metadata_fields() -> Result<()> {
|
||||
.map(|index| {
|
||||
let mut item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: index.to_string(),
|
||||
call_id: Some(index.to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(String::new()),
|
||||
success: None,
|
||||
@@ -141,7 +145,9 @@ fn model_arguments_cannot_forge_executed_tool_call_truncation() -> Result<()> {
|
||||
|
||||
let mut item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(String::new()),
|
||||
success: None,
|
||||
|
||||
@@ -1030,7 +1030,9 @@ mod tests {
|
||||
RolloutItem::ResponseItem(
|
||||
ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload::from_text("tool output".to_string()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ fn preparation_replaces_invalid_message_audio_with_placeholders() {
|
||||
fn preparation_replaces_only_failed_tool_audio_and_preserves_metadata() {
|
||||
let mut items = vec![ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
@@ -114,7 +116,9 @@ fn preparation_replaces_only_failed_tool_audio_and_preserves_metadata() {
|
||||
items,
|
||||
vec![ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: "call-1".to_string(),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
|
||||
Reference in New Issue
Block a user