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:
rka-oai
2026-08-20 19:19:41 +00:00
committed by copyberry
parent 854cbb2fd4
commit 763787d061
42 changed files with 473 additions and 114 deletions
+16 -2
View File
@@ -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"
],
@@ -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"
],
@@ -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"
],
@@ -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"
],
@@ -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" };
+7 -1
View File
@@ -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(())
}
+4 -1
View File
@@ -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 { .. }
+19
View File
@@ -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(),
+6 -2
View File
@@ -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(),
+4
View File
@@ -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),
+6 -2
View File
@@ -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,
+22 -9
View File
@@ -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
)
});
+3 -1
View File
@@ -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, ..
+6 -2
View File
@@ -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(),
),
+24 -17
View File
@@ -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 { .. }
+6 -2
View File
@@ -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(),
+6 -2
View File
@@ -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 {
+6 -2
View File
@@ -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,
});
+3 -1
View File
@@ -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,
}
+6 -2
View File
@@ -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,
});
+4 -1
View File
@@ -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,
+1 -1
View File
@@ -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(),
+6 -2
View File
@@ -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,
},
+3 -1
View File
@@ -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));
}
+3 -1
View File
@@ -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(),
+64 -3
View File
@@ -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,
+3 -1
View File
@@ -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 {