mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Require prefixes for outbound response item IDs (#32312)
## What changed - Add a `ResponseItemId` type that generates item-specific prefixes with UUIDv7 suffixes and use it across response items. - Keep deserialization permissive for legacy histories, but omit empty or unprefixed item IDs from HTTP and WebSocket requests. - Export the new type in the generated TypeScript protocol schema. ## Testing - Cover prefixed ID generation, legacy deserialization, prefix recognition, and outbound request filtering. GitOrigin-RevId: 0209fe430c826d0ae88bc4648652c4ebf390c4a4
This commit is contained in:
@@ -10,14 +10,15 @@ import type { LocalShellStatus } from "./LocalShellStatus";
|
||||
import type { MessagePhase } from "./MessagePhase";
|
||||
import type { ReasoningItemContent } from "./ReasoningItemContent";
|
||||
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
||||
import type { ResponseItemId } from "./ResponseItemId";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ResponseItem = { "type": "message", id?: string, role: string, content: Array<ContentItem>, phase?: MessagePhase, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "agent_message", id?: string, author: string, recipient: string, content: Array<AgentMessageInputContent>, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "reasoning", id?: string, summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "local_shell_call",
|
||||
export type ResponseItem = { "type": "message", id?: ResponseItemId, role: string, content: Array<ContentItem>, phase?: MessagePhase, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "agent_message", id?: ResponseItemId, author: string, recipient: string, content: Array<AgentMessageInputContent>, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "reasoning", id?: ResponseItemId, summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "local_shell_call",
|
||||
/**
|
||||
* Legacy id field retained for compatibility with older payloads.
|
||||
*/
|
||||
id?: string,
|
||||
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?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, 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, 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" };
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* A Responses API item ID. New IDs require an explicit prefix; deserialization
|
||||
* remains permissive so legacy rollouts can still be read.
|
||||
*/
|
||||
export type ResponseItemId = string;
|
||||
@@ -70,6 +70,7 @@ export type { Resource } from "./Resource";
|
||||
export type { ResourceContent } from "./ResourceContent";
|
||||
export type { ResourceTemplate } from "./ResourceTemplate";
|
||||
export type { ResponseItem } from "./ResponseItem";
|
||||
export type { ResponseItemId } from "./ResponseItemId";
|
||||
export type { ReviewDecision } from "./ReviewDecision";
|
||||
export type { ServerNotification } from "./ServerNotification";
|
||||
export type { ServerRequest } from "./ServerRequest";
|
||||
|
||||
@@ -447,7 +447,7 @@ impl ThreadHistoryBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_deref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -4113,7 +4113,7 @@ mod tests {
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::ResponseItem(codex_protocol::models::ResponseItem::Message {
|
||||
id: Some("msg-1".into()),
|
||||
id: Some(codex_protocol::ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "user".into(),
|
||||
content: vec![codex_protocol::models::ContentItem::InputText {
|
||||
text: "plain text".into(),
|
||||
|
||||
@@ -892,6 +892,7 @@ fn serialize_websocket_request(request: &ResponsesWsRequest) -> Result<String, A
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::ResponseCreateWsRequest;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -905,7 +906,7 @@ mod tests {
|
||||
instructions: "Use the available tools.".to_string(),
|
||||
previous_response_id: Some("resp-1".to_string()),
|
||||
input: vec![ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "hello".to_string(),
|
||||
|
||||
@@ -70,6 +70,7 @@ mod tests {
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_client::TransportError;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use http::StatusCode;
|
||||
@@ -150,7 +151,7 @@ mod tests {
|
||||
model: "gpt-test".to_string(),
|
||||
reasoning: None,
|
||||
input: Some(SearchInput::Items(vec![ResponseItem::Message {
|
||||
id: Some("msg_search".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "search")),
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_client::RequestBody;
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_client::TransportError;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -309,7 +310,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> {
|
||||
model: "gpt-test".into(),
|
||||
instructions: "Say hi".into(),
|
||||
input: vec![ResponseItem::Message {
|
||||
id: Some("msg_1".into()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText { text: "hi".into() }],
|
||||
phase: None,
|
||||
@@ -516,7 +517,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> {
|
||||
model: "gpt-test".into(),
|
||||
instructions: "Say hi".into(),
|
||||
input: vec![ResponseItem::Message {
|
||||
id: Some("msg_1".into()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText { text: "hi".into() }],
|
||||
phase: None,
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -979,7 +980,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
|
||||
assistant_message("parent unknown phase", /*phase*/ None),
|
||||
ResponseItem::Reasoning {
|
||||
id: Some("parent-reasoning".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "parent-reasoning")),
|
||||
summary: Vec::new(),
|
||||
content: None,
|
||||
encrypted_content: None,
|
||||
|
||||
@@ -908,6 +908,12 @@ impl ModelClient {
|
||||
}
|
||||
|
||||
fn prepare_response_items_for_request(&self, input: &mut [ResponseItem], store: bool) {
|
||||
for item in input.iter_mut() {
|
||||
if item.id().is_some_and(|id| !id.is_prefixed()) {
|
||||
item.set_id(/*new_id*/ None);
|
||||
}
|
||||
}
|
||||
|
||||
if self.state.item_ids_enabled || store {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ fn started_inference_attempt(temp: &TempDir) -> anyhow::Result<InferenceTraceAtt
|
||||
|
||||
fn output_message(id: &str, text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: Some(id.to_string()),
|
||||
id: Some(codex_protocol::ResponseItemId::with_suffix("msg", id)),
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
@@ -581,7 +581,7 @@ async fn dropped_response_stream_traces_cancelled_partial_output() -> anyhow::Re
|
||||
// response.completed event. The harness has enough information to keep this
|
||||
// item in history, so the trace should preserve it when the stream is
|
||||
// abandoned.
|
||||
let item = output_message("msg-1", "partial answer");
|
||||
let item = output_message("1", "partial answer");
|
||||
let api_stream = futures::stream::iter([Ok(ResponseEvent::OutputItemDone(item))])
|
||||
.chain(futures::stream::pending());
|
||||
let (mut stream, _) = super::map_response_events(
|
||||
@@ -699,7 +699,7 @@ async fn dropped_backpressured_response_stream_traces_cancelled_partial_output()
|
||||
events.push_back(ResponseEvent::Created);
|
||||
}
|
||||
events.push_back(ResponseEvent::OutputItemDone(output_message(
|
||||
"msg-1",
|
||||
"1",
|
||||
"partial answer",
|
||||
)));
|
||||
let api_stream = NotifyAfterEventStream {
|
||||
|
||||
@@ -434,13 +434,14 @@ fn rewritten_output_for_context_window(item: &ResponseItem) -> Option<ResponseIt
|
||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
||||
},
|
||||
ResponseItem::ToolSearchOutput {
|
||||
id,
|
||||
call_id,
|
||||
status,
|
||||
execution,
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
} => ResponseItem::ToolSearchOutput {
|
||||
id: item.id().map(str::to_string),
|
||||
id: id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
status: status.clone(),
|
||||
execution: execution.clone(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::session::tests::build_world_state_from_turn_context;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
||||
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -85,7 +86,7 @@ fn content_items_to_text_ignores_image_only_content() {
|
||||
fn collect_user_messages_extracts_user_text_only() {
|
||||
let items = vec![
|
||||
ResponseItem::Message {
|
||||
id: Some("assistant".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "assistant")),
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "ignored".to_string(),
|
||||
@@ -94,7 +95,7 @@ fn collect_user_messages_extracts_user_text_only() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
id: Some("user".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "user")),
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "first".to_string(),
|
||||
|
||||
@@ -75,7 +75,7 @@ pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
|
||||
}
|
||||
|
||||
pub(crate) fn parse_visible_hook_prompt_message(
|
||||
id: Option<&String>,
|
||||
id: Option<&str>,
|
||||
content: &[ContentItem],
|
||||
) -> Option<HookPromptItem> {
|
||||
let mut fragments = Vec::new();
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::context::world_state::WorldStateSection;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
||||
@@ -639,7 +640,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
|
||||
fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
|
||||
let history = create_history_with_items(vec![
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig_123".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "123")),
|
||||
status: "generating".to_string(),
|
||||
revised_prompt: Some("lobster".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -660,7 +661,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
|
||||
history.for_prompt(&default_input_modalities()),
|
||||
vec![
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig_123".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "123")),
|
||||
status: "generating".to_string(),
|
||||
revised_prompt: Some("lobster".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -692,7 +693,7 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig_123".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "123")),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("lobster".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -713,7 +714,7 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig_123".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "123")),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("lobster".to_string()),
|
||||
result: String::new(),
|
||||
@@ -1642,7 +1643,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
|
||||
fn for_prompt_assigns_stable_id_to_synthetic_output_without_reordering_history() {
|
||||
let items = vec![
|
||||
ResponseItem::FunctionCall {
|
||||
id: Some("fc_existing".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("fc", "existing")),
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
@@ -1650,7 +1651,7 @@ fn for_prompt_assigns_stable_id_to_synthetic_output_without_reordering_history()
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
id: Some("msg_later".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "later")),
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "later turn".to_string(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
@@ -132,12 +133,12 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
|
||||
/// outputs, so the namespace and name format must remain stable across retries
|
||||
/// and resumes to preserve prompt-cache reuse. Returning `None` when the source
|
||||
/// call has no ID preserves the legacy behavior for older history items.
|
||||
fn synthetic_output_id(prefix: &str, item_id: Option<&str>) -> Option<String> {
|
||||
fn synthetic_output_id(prefix: &str, item_id: Option<&str>) -> Option<ResponseItemId> {
|
||||
let source_id = item_id.filter(|id| !id.is_empty())?;
|
||||
let name = format!("{prefix}:{source_id}");
|
||||
Some(format!(
|
||||
"{prefix}_{}",
|
||||
Uuid::new_v5(&SYNTHETIC_OUTPUT_ID_NAMESPACE, name.as_bytes())
|
||||
Some(ResponseItemId::with_suffix(
|
||||
prefix,
|
||||
Uuid::new_v5(&SYNTHETIC_OUTPUT_ID_NAMESPACE, name.as_bytes()),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
|
||||
}
|
||||
|
||||
fn parse_agent_message(
|
||||
id: Option<&String>,
|
||||
id: Option<&str>,
|
||||
message: &[ContentItem],
|
||||
phase: Option<MessagePhase>,
|
||||
) -> AgentMessageItem {
|
||||
@@ -139,7 +139,9 @@ fn parse_agent_message(
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = id.cloned().unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
let id = id
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
AgentMessageItem {
|
||||
id,
|
||||
content,
|
||||
@@ -157,11 +159,11 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
phase,
|
||||
..
|
||||
} => match role.as_str() {
|
||||
"user" => parse_visible_hook_prompt_message(id.as_ref(), content)
|
||||
"user" => parse_visible_hook_prompt_message(id.as_deref(), content)
|
||||
.map(TurnItem::HookPrompt)
|
||||
.or_else(|| parse_user_message(content).map(TurnItem::UserMessage)),
|
||||
"assistant" => Some(TurnItem::AgentMessage(parse_agent_message(
|
||||
id.as_ref(),
|
||||
id.as_deref(),
|
||||
content,
|
||||
phase.clone(),
|
||||
))),
|
||||
@@ -190,7 +192,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
})
|
||||
.collect();
|
||||
Some(TurnItem::Reasoning(ReasoningItem {
|
||||
id: id.clone().unwrap_or_default(),
|
||||
id: id.as_deref().unwrap_or_default().to_string(),
|
||||
summary_text,
|
||||
raw_content,
|
||||
}))
|
||||
@@ -201,7 +203,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
None => (WebSearchAction::Other, String::new()),
|
||||
};
|
||||
Some(TurnItem::WebSearch(WebSearchItem {
|
||||
id: id.clone().unwrap_or_default(),
|
||||
id: id.as_deref().unwrap_or_default().to_string(),
|
||||
query,
|
||||
action,
|
||||
}))
|
||||
@@ -214,7 +216,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
..
|
||||
} => Some(TurnItem::ImageGeneration(
|
||||
codex_protocol::items::ImageGenerationItem {
|
||||
id: id.clone()?,
|
||||
id: id.as_deref()?.to_string(),
|
||||
status: status.clone(),
|
||||
revised_prompt: revised_prompt.clone(),
|
||||
result: result.clone(),
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::parse_turn_item;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::InternalContextSource;
|
||||
use crate::context::InternalModelContextFragment;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::TurnItem;
|
||||
@@ -342,7 +343,7 @@ fn parses_hook_prompt_message_as_distinct_turn_item() {
|
||||
#[test]
|
||||
fn parses_hook_prompt_and_hides_other_contextual_fragments() {
|
||||
let item = ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
@@ -361,7 +362,7 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() {
|
||||
|
||||
match turn_item {
|
||||
TurnItem::HookPrompt(hook_prompt) => {
|
||||
assert_eq!(hook_prompt.id, "msg-1");
|
||||
assert_eq!(hook_prompt.id, "msg_1");
|
||||
assert_eq!(
|
||||
hook_prompt.fragments,
|
||||
vec![HookPromptFragment {
|
||||
@@ -377,7 +378,7 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() {
|
||||
#[test]
|
||||
fn internal_model_context_does_not_parse_as_visible_turn_item() {
|
||||
let item = ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: InternalModelContextFragment::new(
|
||||
@@ -396,7 +397,7 @@ fn internal_model_context_does_not_parse_as_visible_turn_item() {
|
||||
#[test]
|
||||
fn parses_agent_message() {
|
||||
let item = ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "Hello from Codex".to_string(),
|
||||
@@ -421,7 +422,7 @@ fn parses_agent_message() {
|
||||
#[test]
|
||||
fn parses_reasoning_summary_and_raw_content() {
|
||||
let item = ResponseItem::Reasoning {
|
||||
id: Some("reasoning_1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "1")),
|
||||
summary: vec![
|
||||
ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "Step 1".to_string(),
|
||||
@@ -454,7 +455,7 @@ fn parses_reasoning_summary_and_raw_content() {
|
||||
#[test]
|
||||
fn parses_reasoning_including_raw_content() {
|
||||
let item = ResponseItem::Reasoning {
|
||||
id: Some("reasoning_2".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "2")),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "Summarized step".to_string(),
|
||||
}],
|
||||
@@ -487,7 +488,7 @@ fn parses_reasoning_including_raw_content() {
|
||||
#[test]
|
||||
fn parses_web_search_call() {
|
||||
let item = ResponseItem::WebSearchCall {
|
||||
id: Some("ws_1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ws", "1")),
|
||||
status: Some("completed".to_string()),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("weather".to_string()),
|
||||
@@ -517,7 +518,7 @@ fn parses_web_search_call() {
|
||||
#[test]
|
||||
fn parses_web_search_open_page_call() {
|
||||
let item = ResponseItem::WebSearchCall {
|
||||
id: Some("ws_open".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ws", "open")),
|
||||
status: Some("completed".to_string()),
|
||||
action: Some(WebSearchAction::OpenPage {
|
||||
url: Some("https://example.com".to_string()),
|
||||
@@ -545,7 +546,7 @@ fn parses_web_search_open_page_call() {
|
||||
#[test]
|
||||
fn parses_web_search_find_in_page_call() {
|
||||
let item = ResponseItem::WebSearchCall {
|
||||
id: Some("ws_find".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ws", "find")),
|
||||
status: Some("completed".to_string()),
|
||||
action: Some(WebSearchAction::FindInPage {
|
||||
url: Some("https://example.com".to_string()),
|
||||
@@ -575,7 +576,7 @@ fn parses_web_search_find_in_page_call() {
|
||||
#[test]
|
||||
fn parses_partial_web_search_call_without_action_as_other() {
|
||||
let item = ResponseItem::WebSearchCall {
|
||||
id: Some("ws_partial".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ws", "partial")),
|
||||
status: Some("in_progress".to_string()),
|
||||
action: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
|
||||
@@ -337,6 +337,7 @@ use codex_mcp::effective_mcp_servers;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::THREAD_STARTED_METRIC;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
@@ -2784,7 +2785,10 @@ impl Session {
|
||||
}
|
||||
|
||||
fn assign_missing_response_item_ids(items: Cow<'_, [ResponseItem]>) -> Cow<'_, [ResponseItem]> {
|
||||
if items.iter().all(|item| item.id().is_some()) {
|
||||
if items
|
||||
.iter()
|
||||
.all(|item| item.id().is_some_and(|id| !id.is_empty()))
|
||||
{
|
||||
return items;
|
||||
}
|
||||
let mut items = items;
|
||||
@@ -2795,27 +2799,13 @@ impl Session {
|
||||
}
|
||||
|
||||
fn assign_missing_response_item_id(item: &mut ResponseItem) {
|
||||
if item.id().is_some() {
|
||||
if item.id().is_some_and(|id| !id.is_empty()) {
|
||||
return;
|
||||
}
|
||||
let prefix = match item {
|
||||
ResponseItem::AdditionalTools { .. } => "at",
|
||||
ResponseItem::Message { .. } => "msg",
|
||||
ResponseItem::Reasoning { .. } => "rs",
|
||||
ResponseItem::LocalShellCall { .. } => "lsh",
|
||||
ResponseItem::FunctionCall { .. } => "fc",
|
||||
ResponseItem::ToolSearchCall { .. } => "tsc",
|
||||
ResponseItem::FunctionCallOutput { .. } => "fco",
|
||||
ResponseItem::CustomToolCall { .. } => "ctc",
|
||||
ResponseItem::CustomToolCallOutput { .. } => "ctco",
|
||||
ResponseItem::ToolSearchOutput { .. } => "tso",
|
||||
ResponseItem::WebSearchCall { .. } => "ws",
|
||||
ResponseItem::ImageGenerationCall { .. } => "ig",
|
||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => "cmp",
|
||||
ResponseItem::AgentMessage { .. } => "amsg",
|
||||
ResponseItem::CompactionTrigger { .. } | ResponseItem::Other => return,
|
||||
let Some(prefix) = item.id_prefix() else {
|
||||
return;
|
||||
};
|
||||
item.set_id(Some(format!("{prefix}_{}", Uuid::now_v7())));
|
||||
item.set_id(Some(ResponseItemId::new(prefix)));
|
||||
}
|
||||
|
||||
pub(crate) fn response_item_from_user_input(&self, input: Vec<UserInput>) -> ResponseItem {
|
||||
|
||||
@@ -43,6 +43,7 @@ use codex_models_manager::model_info;
|
||||
use codex_models_manager::test_support::construct_model_info_offline_for_tests;
|
||||
use codex_models_manager::test_support::get_model_offline_for_tests;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
@@ -1922,7 +1923,10 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
|
||||
RolloutItem::ResponseItem(item @ ResponseItem::AgentMessage { .. }) => item.id(),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(persisted_item_id, Some(live_item_id.as_str()));
|
||||
assert_eq!(
|
||||
persisted_item_id.map(ResponseItemId::as_str),
|
||||
Some(live_item_id.as_str())
|
||||
);
|
||||
|
||||
let (resumed_session, _resumed_turn_context, _rx) =
|
||||
make_session_and_context_with_auth_and_config_and_rx(
|
||||
@@ -1940,7 +1944,10 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
|
||||
let [resumed_item] = resumed_history.raw_items() else {
|
||||
panic!("expected exactly one resumed history item");
|
||||
};
|
||||
assert_eq!(resumed_item.id(), Some(live_item_id.as_str()));
|
||||
assert_eq!(
|
||||
resumed_item.id().map(ResponseItemId::as_str),
|
||||
Some(live_item_id.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1982,15 +1989,14 @@ async fn prepares_image_failures_before_history_insertion() {
|
||||
let history = session.state.lock().await.clone_history();
|
||||
let id = history.raw_items()[0]
|
||||
.id()
|
||||
.expect("history item should have an ID")
|
||||
.to_string();
|
||||
.expect("history item should have an ID");
|
||||
let uuid = id
|
||||
.strip_prefix("fco_")
|
||||
.expect("function call output ID should have the Responses API prefix");
|
||||
let parsed_id = Uuid::parse_str(uuid).expect("history item should have a UUID ID");
|
||||
assert_eq!(parsed_id.get_version(), Some(uuid::Version::SortRand));
|
||||
let expected = vec![ResponseItem::FunctionCallOutput {
|
||||
id: Some(id),
|
||||
id: Some(id.clone()),
|
||||
call_id: "call-1".to_string(),
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
@@ -2869,7 +2875,7 @@ async fn record_initial_history_assigns_and_persists_id_for_forked_response_item
|
||||
.expect("forked response item should have an id")
|
||||
.to_string();
|
||||
assert!(live_item_id.starts_with("msg_"));
|
||||
expected_item.set_id(Some(live_item_id.clone()));
|
||||
expected_item.set_id(live_item.id().cloned());
|
||||
assert_eq!(live_history.raw_items(), &[expected_item]);
|
||||
|
||||
session.flush_rollout().await.expect("rollout should flush");
|
||||
@@ -2889,7 +2895,10 @@ async fn record_initial_history_assigns_and_persists_id_for_forked_response_item
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
});
|
||||
assert_eq!(persisted_item_id, Some(live_item_id.as_str()));
|
||||
assert_eq!(
|
||||
persisted_item_id.map(ResponseItemId::as_str),
|
||||
Some(live_item_id.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -80,6 +80,7 @@ use codex_extension_api::TurnInputContext;
|
||||
use codex_extension_api::TurnInputEnvironment;
|
||||
use codex_features::Feature;
|
||||
use codex_git_utils::get_git_repo_root_with_fs;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
@@ -1919,13 +1920,11 @@ fn assign_missing_streamed_response_item_id(
|
||||
item: &mut ResponseItem,
|
||||
active_item: Option<&TurnItem>,
|
||||
) {
|
||||
if item.id().is_some() {
|
||||
if item.id().is_some_and(|id| !id.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let active_item_id = active_item
|
||||
.map(TurnItem::id)
|
||||
.filter(|item_id| !item_id.is_empty());
|
||||
let active_item_id = active_item.map(|item| ResponseItemId::from_server(item.id()));
|
||||
item.set_id(active_item_id);
|
||||
Session::assign_missing_response_item_id(item);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::TurnItemContributor;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
@@ -27,7 +28,7 @@ impl TurnItemContributor for RewriteAgentMessageContributor {
|
||||
|
||||
fn assistant_output_text(text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_utils_stream_parser::strip_citations;
|
||||
@@ -444,7 +445,11 @@ pub(crate) async fn handle_non_tool_response_item(
|
||||
ResponseItem::ContextCompaction { .. } => "context_compaction",
|
||||
ResponseItem::Other => "other",
|
||||
};
|
||||
debug!(item_type, item_id = item.id(), "Output item");
|
||||
debug!(
|
||||
item_type,
|
||||
item_id = item.id().map(ResponseItemId::as_str),
|
||||
"Output item"
|
||||
);
|
||||
|
||||
match item {
|
||||
ResponseItem::Message { .. }
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::tools::parallel::ToolCallRuntime;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::TurnItemContributor;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::memory_citation::MemoryCitation;
|
||||
@@ -33,7 +34,7 @@ fn assistant_output_text(text: &str) -> ResponseItem {
|
||||
|
||||
fn assistant_output_text_with_phase(text: &str, phase: Option<MessagePhase>) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "1")),
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use codex_prompts::render_review_exit_interrupted;
|
||||
use codex_prompts::render_review_exit_success;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::items::ExitedReviewModeItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
@@ -242,7 +243,7 @@ pub(crate) async fn exit_review_mode(
|
||||
.record_conversation_items(
|
||||
&ctx,
|
||||
&[ResponseItem::Message {
|
||||
id: Some(uuid::Uuid::now_v7().to_string()),
|
||||
id: Some(ResponseItemId::new("msg")),
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText { text: user_message }],
|
||||
phase: None,
|
||||
@@ -261,7 +262,7 @@ pub(crate) async fn exit_review_mode(
|
||||
.record_response_item_and_emit_turn_item(
|
||||
ctx.as_ref(),
|
||||
ResponseItem::Message {
|
||||
id: Some(uuid::Uuid::now_v7().to_string()),
|
||||
id: Some(ResponseItemId::new("msg")),
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: assistant_message,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::tasks::InterruptedTurnHistoryMarker;
|
||||
use crate::tasks::interrupted_turn_history_marker;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -169,7 +170,7 @@ fn truncates_before_requested_user_message() {
|
||||
user_msg("u2"),
|
||||
assistant_msg("a3"),
|
||||
ResponseItem::Reasoning {
|
||||
id: Some("r1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "1")),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "s".to_string(),
|
||||
}],
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::session::tests::build_world_state_from_turn_context;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
@@ -178,7 +179,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
|
||||
user_msg("u2"),
|
||||
assistant_msg("a3"),
|
||||
ResponseItem::Reasoning {
|
||||
id: Some("r1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "1")),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "s".to_string(),
|
||||
}],
|
||||
|
||||
@@ -110,7 +110,7 @@ async fn websocket_test_codex_shell_chain() -> Result<()> {
|
||||
],
|
||||
vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_assistant_message("msg_1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
],
|
||||
]])
|
||||
@@ -155,7 +155,7 @@ async fn websocket_first_turn_uses_startup_prewarm_and_create() -> Result<()> {
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "hello"),
|
||||
ev_assistant_message("msg_1", "hello"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
]])
|
||||
@@ -213,7 +213,7 @@ async fn websocket_first_turn_handles_handshake_delay_with_startup_prewarm() ->
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "hello"),
|
||||
ev_assistant_message("msg_1", "hello"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
],
|
||||
@@ -268,7 +268,7 @@ async fn websocket_v2_test_codex_shell_chain() -> Result<()> {
|
||||
],
|
||||
vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_assistant_message("msg_1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
],
|
||||
]])
|
||||
@@ -347,7 +347,7 @@ async fn websocket_v2_first_turn_uses_updated_fast_tier_after_startup_prewarm()
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "fast"),
|
||||
ev_assistant_message("msg_1", "fast"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
]])
|
||||
@@ -402,7 +402,7 @@ async fn websocket_v2_first_turn_drops_fast_tier_after_startup_prewarm() -> Resu
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "standard"),
|
||||
ev_assistant_message("msg_1", "standard"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
]])
|
||||
@@ -458,12 +458,12 @@ async fn websocket_v2_next_turn_uses_updated_service_tier() -> Result<()> {
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "fast"),
|
||||
ev_assistant_message("msg_1", "fast"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-2", "standard"),
|
||||
ev_assistant_message("msg_2", "standard"),
|
||||
ev_completed("resp-2"),
|
||||
],
|
||||
]])
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_model_provider_info::built_in_model_providers;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -388,7 +389,7 @@ async fn synthetic_call_output_id_is_stable_across_resumes() -> anyhow::Result<(
|
||||
RolloutLine {
|
||||
timestamp: "2024-01-01T00:00:01.000Z".to_string(),
|
||||
item: RolloutItem::ResponseItem(ResponseItem::FunctionCall {
|
||||
id: Some("fc_existing".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("fc", "existing")),
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
@@ -2992,7 +2993,7 @@ async fn includes_developer_instructions_message_in_request() {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
async fn azure_responses_request_includes_store_and_prefixed_item_ids() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = MockServer::start().await;
|
||||
@@ -3071,7 +3072,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.input.push(ResponseItem::Reasoning {
|
||||
id: Some("reasoning-id".into()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "reasoning-id")),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "summary".into(),
|
||||
}],
|
||||
@@ -3082,7 +3083,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
prompt.input.push(ResponseItem::Message {
|
||||
id: Some("message-id".into()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", "message-id")),
|
||||
role: "assistant".into(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "message".into(),
|
||||
@@ -3091,7 +3092,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
prompt.input.push(ResponseItem::WebSearchCall {
|
||||
id: Some("web-search-id".into()),
|
||||
id: Some(ResponseItemId::with_suffix("ws", "web-search-id")),
|
||||
status: Some("completed".into()),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("weather".into()),
|
||||
@@ -3100,7 +3101,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
prompt.input.push(ResponseItem::FunctionCall {
|
||||
id: Some("function-id".into()),
|
||||
id: Some(ResponseItemId::with_suffix("fc", "function-id")),
|
||||
name: "do_thing".into(),
|
||||
namespace: None,
|
||||
arguments: "{}".into(),
|
||||
@@ -3114,7 +3115,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
prompt.input.push(ResponseItem::LocalShellCall {
|
||||
id: Some("local-shell-id".into()),
|
||||
id: Some(ResponseItemId::with_suffix("lsh", "local-shell-id")),
|
||||
call_id: Some("local-shell-call-id".into()),
|
||||
status: LocalShellStatus::Completed,
|
||||
action: LocalShellAction::Exec(LocalShellExecAction {
|
||||
@@ -3127,7 +3128,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
prompt.input.push(ResponseItem::CustomToolCall {
|
||||
id: Some("custom-tool-id".into()),
|
||||
id: Some(ResponseItemId::with_suffix("ctc", "custom-tool-id")),
|
||||
status: Some("completed".into()),
|
||||
call_id: "custom-tool-call-id".into(),
|
||||
name: "custom_tool".into(),
|
||||
@@ -3142,6 +3143,24 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
output: FunctionCallOutputPayload::from_text("ok".into()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
prompt.input.push(
|
||||
serde_json::from_value(json!({
|
||||
"type": "message",
|
||||
"id": "018f9e15-7a6a-7000-8000-000000000001",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "legacy message"}],
|
||||
}))
|
||||
.expect("legacy response item should deserialize"),
|
||||
);
|
||||
prompt.input.push(
|
||||
serde_json::from_value(json!({
|
||||
"type": "message",
|
||||
"id": "",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "empty-id message"}],
|
||||
}))
|
||||
.expect("response item with an empty id should deserialize"),
|
||||
);
|
||||
|
||||
let mut stream = client_session
|
||||
.stream(
|
||||
@@ -3169,21 +3188,23 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
|
||||
assert_eq!(body["store"], serde_json::Value::Bool(true));
|
||||
assert_eq!(body["stream"], serde_json::Value::Bool(true));
|
||||
assert_eq!(body["input"].as_array().map(Vec::len), Some(8));
|
||||
assert_eq!(body["input"][0]["id"].as_str(), Some("reasoning-id"));
|
||||
assert_eq!(body["input"][1]["id"].as_str(), Some("message-id"));
|
||||
assert_eq!(body["input"][2]["id"].as_str(), Some("web-search-id"));
|
||||
assert_eq!(body["input"][3]["id"].as_str(), Some("function-id"));
|
||||
assert_eq!(body["input"].as_array().map(Vec::len), Some(10));
|
||||
assert_eq!(body["input"][0]["id"].as_str(), Some("rs_reasoning-id"));
|
||||
assert_eq!(body["input"][1]["id"].as_str(), Some("msg_message-id"));
|
||||
assert_eq!(body["input"][2]["id"].as_str(), Some("ws_web-search-id"));
|
||||
assert_eq!(body["input"][3]["id"].as_str(), Some("fc_function-id"));
|
||||
assert_eq!(
|
||||
body["input"][4]["call_id"].as_str(),
|
||||
Some("function-call-id")
|
||||
);
|
||||
assert_eq!(body["input"][5]["id"].as_str(), Some("local-shell-id"));
|
||||
assert_eq!(body["input"][6]["id"].as_str(), Some("custom-tool-id"));
|
||||
assert_eq!(body["input"][5]["id"].as_str(), Some("lsh_local-shell-id"));
|
||||
assert_eq!(body["input"][6]["id"].as_str(), Some("ctc_custom-tool-id"));
|
||||
assert_eq!(
|
||||
body["input"][7]["call_id"].as_str(),
|
||||
Some("custom-tool-call-id")
|
||||
);
|
||||
assert_eq!(body["input"][8].get("id"), None);
|
||||
assert_eq!(body["input"][9].get("id"), None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_otel::MetricsConfig;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
@@ -161,7 +162,7 @@ async fn responses_websocket_streams_request() {
|
||||
let harness = websocket_harness(&server).await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let mut prompt = prompt_with_input(vec![message_item("hello")]);
|
||||
prompt.input[0].set_id(Some("msg_existing".to_string()));
|
||||
prompt.input[0].set_id(Some(ResponseItemId::with_suffix("msg", "existing")));
|
||||
|
||||
stream_until_complete(&mut client_session, &harness, &prompt).await;
|
||||
|
||||
@@ -210,6 +211,59 @@ async fn responses_websocket_streams_request() {
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_omits_unprefixed_item_ids_without_mutating_prompt() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_websocket_server(vec![vec![vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_completed("resp-1"),
|
||||
]]])
|
||||
.await;
|
||||
|
||||
let harness = websocket_harness_with_provider_options(
|
||||
websocket_provider(&server),
|
||||
/*runtime_metrics_enabled*/ false,
|
||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||
/*enabled_features*/ &[Feature::ItemIds],
|
||||
)
|
||||
.await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let mut prefixed = message_item("prefixed");
|
||||
prefixed.set_id(Some(ResponseItemId::with_suffix("msg", "existing")));
|
||||
let unprefixed = serde_json::from_value(json!({
|
||||
"type": "message",
|
||||
"id": "018f9e15-7a6a-7000-8000-000000000001",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "legacy message"}],
|
||||
}))
|
||||
.expect("legacy response item should deserialize");
|
||||
let empty = serde_json::from_value(json!({
|
||||
"type": "message",
|
||||
"id": "",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "empty-id message"}],
|
||||
}))
|
||||
.expect("response item with an empty id should deserialize");
|
||||
let prompt = prompt_with_input(vec![prefixed, unprefixed, empty]);
|
||||
|
||||
stream_until_complete(&mut client_session, &harness, &prompt).await;
|
||||
|
||||
let connection = server.single_connection();
|
||||
let body = connection.first().expect("missing request").body_json();
|
||||
assert_eq!(body["input"].as_array().map(Vec::len), Some(3));
|
||||
assert_eq!(body["input"][0]["id"].as_str(), Some("msg_existing"));
|
||||
assert_eq!(body["input"][1].get("id"), None);
|
||||
assert_eq!(body["input"][2].get("id"), None);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&prompt.input).expect("prompt input should serialize")[1]["id"]
|
||||
.as_str(),
|
||||
Some("018f9e15-7a6a-7000-8000-000000000001")
|
||||
);
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_websocket_streams_without_feature_flag_when_provider_supports_websockets() {
|
||||
skip_if_no_network!();
|
||||
@@ -969,7 +1023,7 @@ async fn responses_websocket_v2_requests_use_v2_when_provider_supports_websocket
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "assistant output"),
|
||||
ev_assistant_message("msg_1", "assistant output"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||
@@ -981,7 +1035,7 @@ async fn responses_websocket_v2_requests_use_v2_when_provider_supports_websocket
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
assistant_message_item("msg-1", "assistant output"),
|
||||
assistant_message_item("1", "assistant output"),
|
||||
message_item("second"),
|
||||
]);
|
||||
|
||||
@@ -1015,11 +1069,11 @@ async fn responses_websocket_v2_requests_use_v2_when_provider_supports_websocket
|
||||
async fn responses_websocket_v2_incremental_requests_are_reused_across_turns() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let mut assistant_output_with_turn_id = ev_assistant_message("msg-1", "assistant output");
|
||||
let mut assistant_output_with_turn_id = ev_assistant_message("msg_1", "assistant output");
|
||||
assistant_output_with_turn_id["item"]["internal_chat_message_metadata_passthrough"] =
|
||||
json!({ "turn_id": "turn-1" });
|
||||
|
||||
let assistant_output_without_turn_id = ev_assistant_message("msg-2", "second assistant output");
|
||||
let assistant_output_without_turn_id = ev_assistant_message("msg_2", "second assistant output");
|
||||
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
@@ -1055,7 +1109,7 @@ async fn responses_websocket_v2_incremental_requests_are_reused_across_turns() {
|
||||
}
|
||||
|
||||
// Turn two: the response and reconstructed history have matching metadata.
|
||||
let mut first_assistant_output = assistant_message_item("msg-1", "assistant output");
|
||||
let mut first_assistant_output = assistant_message_item("1", "assistant output");
|
||||
first_assistant_output.set_turn_id_if_missing("turn-1");
|
||||
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
@@ -1070,7 +1124,7 @@ async fn responses_websocket_v2_incremental_requests_are_reused_across_turns() {
|
||||
}
|
||||
|
||||
// Turn three: the reconstructed history has metadata that the response omitted.
|
||||
let mut second_assistant_output = assistant_message_item("msg-2", "second assistant output");
|
||||
let mut second_assistant_output = assistant_message_item("2", "second assistant output");
|
||||
second_assistant_output.set_turn_id_if_missing("turn-2");
|
||||
|
||||
let prompt_three = prompt_with_input(vec![
|
||||
@@ -1118,7 +1172,7 @@ async fn responses_websocket_v2_wins_when_both_features_enabled() {
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "assistant output"),
|
||||
ev_assistant_message("msg_1", "assistant output"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||
@@ -1130,7 +1184,7 @@ async fn responses_websocket_v2_wins_when_both_features_enabled() {
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
assistant_message_item("msg-1", "assistant output"),
|
||||
assistant_message_item("1", "assistant output"),
|
||||
message_item("second"),
|
||||
]);
|
||||
|
||||
@@ -1628,7 +1682,7 @@ async fn responses_websocket_uses_incremental_create_on_prefix() {
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "assistant output"),
|
||||
ev_assistant_message("msg_1", "assistant output"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||
@@ -1640,7 +1694,7 @@ async fn responses_websocket_uses_incremental_create_on_prefix() {
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
assistant_message_item("msg-1", "assistant output"),
|
||||
assistant_message_item("1", "assistant output"),
|
||||
message_item("second"),
|
||||
]);
|
||||
|
||||
@@ -1670,7 +1724,7 @@ async fn responses_websocket_uses_incremental_create_on_prefix() {
|
||||
async fn responses_websocket_forwards_turn_metadata_on_initial_and_incremental_create() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let mut first_output_item = ev_assistant_message("msg-1", "assistant output");
|
||||
let mut first_output_item = ev_assistant_message("msg_1", "assistant output");
|
||||
first_output_item["item"]["internal_chat_message_metadata_passthrough"] =
|
||||
json!({"turn_id": "turn-123"});
|
||||
let server = start_websocket_server(vec![vec![
|
||||
@@ -1686,7 +1740,7 @@ async fn responses_websocket_forwards_turn_metadata_on_initial_and_incremental_c
|
||||
let harness = websocket_harness(&server).await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let mut prior_assistant_output = assistant_message_item("msg-1", "assistant output");
|
||||
let mut prior_assistant_output = assistant_message_item("1", "assistant output");
|
||||
prior_assistant_output.set_turn_id_if_missing("turn-123");
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
@@ -1801,7 +1855,7 @@ async fn responses_websocket_uses_previous_response_id_when_prefix_after_complet
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "assistant output"),
|
||||
ev_assistant_message("msg_1", "assistant output"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||
@@ -1813,7 +1867,7 @@ async fn responses_websocket_uses_previous_response_id_when_prefix_after_complet
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
assistant_message_item("msg-1", "assistant output"),
|
||||
assistant_message_item("1", "assistant output"),
|
||||
message_item("second"),
|
||||
]);
|
||||
|
||||
@@ -1910,7 +1964,7 @@ async fn responses_websocket_v2_creates_with_previous_response_id_on_prefix() {
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "assistant output"),
|
||||
ev_assistant_message("msg_1", "assistant output"),
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||
@@ -1922,7 +1976,7 @@ async fn responses_websocket_v2_creates_with_previous_response_id_on_prefix() {
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
assistant_message_item("msg-1", "assistant output"),
|
||||
assistant_message_item("1", "assistant output"),
|
||||
message_item("second"),
|
||||
]);
|
||||
|
||||
@@ -2176,7 +2230,7 @@ fn message_item(text: &str) -> ResponseItem {
|
||||
|
||||
fn assistant_message_item(id: &str, text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: Some(id.to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("msg", id)),
|
||||
role: "assistant".into(),
|
||||
content: vec![ContentItem::OutputText { text: text.into() }],
|
||||
phase: None,
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_core::context::extension_image_generation_output_hint;
|
||||
use codex_extension_api::ToolOutput;
|
||||
use codex_extension_api::ToolPayload;
|
||||
use codex_extension_api::ToolSpec;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -109,7 +110,7 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("generated-call".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "generated-call")),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
result: "generated".to_string(),
|
||||
|
||||
@@ -82,6 +82,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_api::SearchInput;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -111,9 +112,12 @@ mod tests {
|
||||
#[test]
|
||||
fn keeps_current_user_and_previous_visible_turn() {
|
||||
let mut previous_user = message(USER_ROLE, "previous user");
|
||||
previous_user.set_id(Some("msg_previous_user".to_string()));
|
||||
previous_user.set_id(Some(ResponseItemId::with_suffix("msg", "previous_user")));
|
||||
let mut previous_assistant = message(ASSISTANT_ROLE, "previous assistant");
|
||||
previous_assistant.set_id(Some("msg_previous_assistant".to_string()));
|
||||
previous_assistant.set_id(Some(ResponseItemId::with_suffix(
|
||||
"msg",
|
||||
"previous_assistant",
|
||||
)));
|
||||
let items = vec![
|
||||
message("system", "system"),
|
||||
message(USER_ROLE, "old user"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::AgentPath;
|
||||
use crate::ResponseItemId;
|
||||
use crate::ThreadId;
|
||||
use crate::dynamic_tools::DynamicToolCallOutputContentItem;
|
||||
use crate::mcp::CallToolResult;
|
||||
@@ -528,9 +529,9 @@ fn trim_trailing_default_image_details(
|
||||
}
|
||||
|
||||
impl HookPromptItem {
|
||||
pub fn from_fragments(id: Option<&String>, fragments: Vec<HookPromptFragment>) -> Self {
|
||||
pub fn from_fragments(id: Option<&str>, fragments: Vec<HookPromptFragment>) -> Self {
|
||||
Self {
|
||||
id: id.cloned().unwrap_or_else(new_item_id),
|
||||
id: id.map(str::to_string).unwrap_or_else(new_item_id),
|
||||
fragments,
|
||||
}
|
||||
}
|
||||
@@ -560,7 +561,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
|
||||
}
|
||||
|
||||
Some(ResponseItem::Message {
|
||||
id: Some(new_item_id()),
|
||||
id: Some(ResponseItemId::new("msg")),
|
||||
role: "user".to_string(),
|
||||
content,
|
||||
phase: None,
|
||||
@@ -569,7 +570,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
|
||||
}
|
||||
|
||||
pub fn parse_hook_prompt_message(
|
||||
id: Option<&String>,
|
||||
id: Option<&str>,
|
||||
content: &[ContentItem],
|
||||
) -> Option<HookPromptItem> {
|
||||
let fragments = content
|
||||
@@ -660,9 +661,10 @@ mod tests {
|
||||
];
|
||||
let message = build_hook_prompt_message(&original).expect("hook prompt");
|
||||
|
||||
let ResponseItem::Message { content, .. } = message else {
|
||||
let ResponseItem::Message { id, content, .. } = message else {
|
||||
panic!("expected hook prompt message");
|
||||
};
|
||||
assert!(id.is_some_and(|id| id.starts_with("msg_")));
|
||||
|
||||
let parsed = parse_hook_prompt_message(/*id*/ None, &content).expect("parsed hook prompt");
|
||||
assert_eq!(parsed.fragments, original);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
pub mod account;
|
||||
mod agent_path;
|
||||
pub mod auth;
|
||||
mod response_item_id;
|
||||
mod session_id;
|
||||
mod thread_id;
|
||||
mod tool_name;
|
||||
pub use agent_path::AgentPath;
|
||||
pub use response_item_id::ResponseItemId;
|
||||
pub use session_id::SessionId;
|
||||
pub use thread_id::ThreadId;
|
||||
pub use tool_name::ToolName;
|
||||
|
||||
@@ -26,6 +26,7 @@ use codex_utils_image::ImageProcessingError;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use crate::ResponseItemId;
|
||||
use crate::mcp::CallToolResult;
|
||||
|
||||
/// Controls the per-command sandbox override requested by a shell-like tool call.
|
||||
@@ -936,14 +937,14 @@ pub enum ResponseItem {
|
||||
#[ts(skip)]
|
||||
AdditionalTools {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
role: String,
|
||||
tools: Vec<serde_json::Value>,
|
||||
},
|
||||
Message {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
role: String,
|
||||
content: Vec<ContentItem>,
|
||||
// Optional output-message phase (for example: "commentary", "final_answer").
|
||||
@@ -959,7 +960,7 @@ pub enum ResponseItem {
|
||||
AgentMessage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
author: String,
|
||||
recipient: String,
|
||||
content: Vec<AgentMessageInputContent>,
|
||||
@@ -970,7 +971,7 @@ pub enum ResponseItem {
|
||||
Reasoning {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
summary: Vec<ReasoningItemReasoningSummary>,
|
||||
#[serde(default, skip_serializing_if = "should_serialize_reasoning_content")]
|
||||
#[ts(optional)]
|
||||
@@ -984,7 +985,7 @@ pub enum ResponseItem {
|
||||
/// Legacy id field retained for compatibility with older payloads.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
/// Set when using the Responses API.
|
||||
call_id: Option<String>,
|
||||
status: LocalShellStatus,
|
||||
@@ -996,7 +997,7 @@ pub enum ResponseItem {
|
||||
FunctionCall {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1013,7 +1014,7 @@ pub enum ResponseItem {
|
||||
ToolSearchCall {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1033,7 +1034,7 @@ pub enum ResponseItem {
|
||||
FunctionCallOutput {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: String,
|
||||
#[ts(as = "FunctionCallOutputBody")]
|
||||
#[schemars(with = "FunctionCallOutputBody")]
|
||||
@@ -1045,7 +1046,7 @@ pub enum ResponseItem {
|
||||
CustomToolCall {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
status: Option<String>,
|
||||
@@ -1066,7 +1067,7 @@ pub enum ResponseItem {
|
||||
CustomToolCallOutput {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1081,7 +1082,7 @@ pub enum ResponseItem {
|
||||
ToolSearchOutput {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
call_id: Option<String>,
|
||||
status: String,
|
||||
execution: String,
|
||||
@@ -1102,7 +1103,7 @@ pub enum ResponseItem {
|
||||
WebSearchCall {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
status: Option<String>,
|
||||
@@ -1125,7 +1126,7 @@ pub enum ResponseItem {
|
||||
ImageGenerationCall {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1139,7 +1140,7 @@ pub enum ResponseItem {
|
||||
Compaction {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
encrypted_content: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1150,7 +1151,7 @@ pub enum ResponseItem {
|
||||
ContextCompaction {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
id: Option<ResponseItemId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
encrypted_content: Option<String>,
|
||||
@@ -1168,8 +1169,8 @@ impl ResponseItem {
|
||||
matches!(self, Self::Message { role, .. } if role == "user")
|
||||
}
|
||||
|
||||
/// Returns the non-empty Responses API item ID, if present.
|
||||
pub fn id(&self) -> Option<&str> {
|
||||
/// Returns the Responses API item ID, if present.
|
||||
pub fn id(&self) -> Option<&ResponseItemId> {
|
||||
match self {
|
||||
Self::AdditionalTools { id, .. }
|
||||
| Self::Message { id, .. }
|
||||
@@ -1185,13 +1186,13 @@ impl ResponseItem {
|
||||
| Self::Reasoning { id, .. }
|
||||
| Self::ImageGenerationCall { id, .. }
|
||||
| Self::Compaction { id, .. }
|
||||
| Self::ContextCompaction { id, .. } => id.as_deref().filter(|id| !id.is_empty()),
|
||||
| Self::ContextCompaction { id, .. } => id.as_ref(),
|
||||
Self::CompactionTrigger { .. } | Self::Other => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets or clears the Responses API item ID for variants that carry one.
|
||||
pub fn set_id(&mut self, new_id: Option<String>) {
|
||||
pub fn set_id(&mut self, new_id: Option<ResponseItemId>) {
|
||||
match self {
|
||||
Self::AdditionalTools { id, .. }
|
||||
| Self::Message { id, .. }
|
||||
@@ -1212,6 +1213,27 @@ impl ResponseItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Responses API item ID prefix for variants that carry an ID.
|
||||
pub fn id_prefix(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::AdditionalTools { .. } => Some("at"),
|
||||
Self::Message { .. } => Some("msg"),
|
||||
Self::AgentMessage { .. } => Some("amsg"),
|
||||
Self::Reasoning { .. } => Some("rs"),
|
||||
Self::LocalShellCall { .. } => Some("lsh"),
|
||||
Self::FunctionCall { .. } => Some("fc"),
|
||||
Self::ToolSearchCall { .. } => Some("tsc"),
|
||||
Self::FunctionCallOutput { .. } => Some("fco"),
|
||||
Self::CustomToolCall { .. } => Some("ctc"),
|
||||
Self::CustomToolCallOutput { .. } => Some("ctco"),
|
||||
Self::ToolSearchOutput { .. } => Some("tso"),
|
||||
Self::WebSearchCall { .. } => Some("ws"),
|
||||
Self::ImageGenerationCall { .. } => Some("ig"),
|
||||
Self::Compaction { .. } | Self::ContextCompaction { .. } => Some("cmp"),
|
||||
Self::CompactionTrigger { .. } | Self::Other => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the non-empty turn ID stamped onto this item, if present.
|
||||
pub fn turn_id(&self) -> Option<&str> {
|
||||
self.internal_chat_message_metadata_passthrough()
|
||||
@@ -2243,9 +2265,9 @@ mod tests {
|
||||
);
|
||||
assert_eq!(item.id(), None);
|
||||
|
||||
item.set_id(Some("msg_test".to_string()));
|
||||
item.set_id(Some(ResponseItemId::with_suffix("msg", "test")));
|
||||
|
||||
assert_eq!(item.id(), Some("msg_test"));
|
||||
assert_eq!(item.id().map(ResponseItemId::as_str), Some("msg_test"));
|
||||
|
||||
item.set_id(/*new_id*/ None);
|
||||
|
||||
@@ -2256,8 +2278,11 @@ mod tests {
|
||||
role: "developer".to_string(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
additional_tools.set_id(Some("at_test".to_string()));
|
||||
assert_eq!(additional_tools.id(), Some("at_test"));
|
||||
additional_tools.set_id(Some(ResponseItemId::with_suffix("at", "test")));
|
||||
assert_eq!(
|
||||
additional_tools.id().map(ResponseItemId::as_str),
|
||||
Some("at_test")
|
||||
);
|
||||
}
|
||||
|
||||
fn response_item_with_passthrough_metadata(
|
||||
@@ -2377,7 +2402,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig_123".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "123")),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("A small blue square".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -2399,7 +2424,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig_123".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("ig", "123")),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -3225,7 +3250,7 @@ mod tests {
|
||||
"status": "in_progress",
|
||||
"id": "ws_partial"
|
||||
}"#,
|
||||
Some("ws_partial".into()),
|
||||
Some(ResponseItemId::with_suffix("ws", "partial")),
|
||||
None,
|
||||
Some("in_progress".into()),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::time::Duration;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::AgentPath;
|
||||
use crate::ResponseItemId;
|
||||
use crate::SessionId;
|
||||
use crate::ThreadId;
|
||||
use crate::approvals::ElicitationRequestEvent;
|
||||
@@ -736,7 +737,7 @@ impl From<Vec<UserInput>> for Op {
|
||||
pub struct InterAgentCommunication {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub id: Option<String>,
|
||||
pub id: Option<ResponseItemId>,
|
||||
pub author: AgentPath,
|
||||
pub recipient: AgentPath,
|
||||
#[serde(default)]
|
||||
@@ -4509,7 +4510,7 @@ mod tests {
|
||||
#[test]
|
||||
fn inter_agent_communication_response_input_item_preserves_commentary_phase() {
|
||||
let mut communication = InterAgentCommunication {
|
||||
id: Some("amsg_1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("amsg", "1")),
|
||||
author: AgentPath::root(),
|
||||
recipient: AgentPath::root().join("reviewer").expect("recipient path"),
|
||||
other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// A Responses API item ID. New IDs require an explicit prefix; deserialization
|
||||
/// remains permissive so legacy rollouts can still be read.
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
#[schemars(with = "String")]
|
||||
#[ts(type = "string")]
|
||||
pub struct ResponseItemId(String);
|
||||
|
||||
impl ResponseItemId {
|
||||
pub fn new(prefix: &str) -> Self {
|
||||
Self::with_suffix(prefix, uuid::Uuid::now_v7())
|
||||
}
|
||||
|
||||
pub fn with_suffix(prefix: &str, suffix: impl fmt::Display) -> Self {
|
||||
Self(format!("{prefix}_{suffix}"))
|
||||
}
|
||||
|
||||
pub fn from_server(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn is_prefixed(&self) -> bool {
|
||||
self.split_once('_')
|
||||
.is_some_and(|(prefix, suffix)| !prefix.is_empty() && !suffix.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ResponseItemId {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ResponseItemId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ResponseItemId {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseItemId> for String {
|
||||
fn from(value: ResponseItemId) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "response_item_id_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,53 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::ResponseItemId;
|
||||
|
||||
#[test]
|
||||
fn creates_prefixed_uuid_v7_ids() {
|
||||
let id = ResponseItemId::new("msg");
|
||||
let uuid = id.strip_prefix("msg_").expect("message prefix");
|
||||
assert_eq!(
|
||||
Uuid::parse_str(uuid).expect("UUID suffix").get_version(),
|
||||
Some(uuid::Version::SortRand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_prefixed_ids_with_explicit_suffix() {
|
||||
let id = ResponseItemId::with_suffix("msg", "test");
|
||||
assert_eq!(id.as_str(), "msg_test");
|
||||
assert_eq!(id.to_string(), "msg_test");
|
||||
assert_eq!(String::from(id), "msg_test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_server_ids_verbatim() {
|
||||
let id = ResponseItemId::from_server("legacy-id".to_string());
|
||||
assert_eq!(id.as_str(), "legacy-id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_arbitrary_ids_as_strings() {
|
||||
let id: ResponseItemId = serde_json::from_str("\"legacy-id\"").expect("id");
|
||||
assert_eq!(id.as_str(), "legacy-id");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&id).expect("serialized id"),
|
||||
"\"legacy-id\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_non_empty_prefix_and_suffix() {
|
||||
for (value, expected) in [
|
||||
("msg_test", true),
|
||||
("legacy-id", false),
|
||||
("", false),
|
||||
("_test", false),
|
||||
("msg_", false),
|
||||
] {
|
||||
let id: ResponseItemId =
|
||||
serde_json::from_value(value.into()).expect("ID should deserialize");
|
||||
assert_eq!(id.is_prefixed(), expected, "{value}");
|
||||
}
|
||||
}
|
||||
@@ -391,6 +391,7 @@ fn append_with_context_best_effort(
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ReasoningItemContent;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -496,7 +497,7 @@ mod tests {
|
||||
#[test]
|
||||
fn traced_response_item_preserves_reasoning_content_omitted_by_normal_serializer() {
|
||||
let item = ResponseItem::Reasoning {
|
||||
id: Some("rs-1".to_string()),
|
||||
id: Some(ResponseItemId::with_suffix("rs", "1")),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
@@ -515,7 +516,7 @@ mod tests {
|
||||
traced,
|
||||
json!({
|
||||
"type": "reasoning",
|
||||
"id": "rs-1",
|
||||
"id": "rs_1",
|
||||
"summary": [{"type": "summary_text", "text": "summary"}],
|
||||
"content": [{"type": "text", "text": "raw reasoning"}],
|
||||
"encrypted_content": "encoded",
|
||||
|
||||
Reference in New Issue
Block a user