ConversationItem类型体系
本文承接UserInput类型体系和Event与EventMsg总表。这里的“ConversationItem”不是某个单独的 Rust enum,而是一条跨层数据流:输入先形成 ResponseInputItem,进入历史后成为更宽的 ResponseItem,Core 再把它记录为 ResponseItemEnvelope、解析为 TurnItem,最后由 App Server 投影成 ThreadItem。
这条链有三个必须分开的问题:item 的业务内容是什么,item 的 Responses ID 如何稳定,以及它是否适合被下一次模型请求、历史 reducer 或 App Server 展示。一个 item 能被序列化,不等于每个消费者都会保留它。
1. 两种Responses项
源码位置:codex-rs/protocol/src/models.rs :: ResponseInputItem、ResponseItem
ResponseInputItem 是向 Responses API 提交的窄集合,ResponseItem 是会话历史使用的宽集合。前者包含 user message 和工具输出;后者还要表示 assistant/agent message、reasoning、工具调用、搜索、图片生成、压缩和未知 wire 变体。
源码位置:codex-rs/protocol/src/models.rs :: ResponseInputItem
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseInputItem {
Message {
role: String,
content: Vec<ContentItem>,
phase: Option<MessagePhase>,
},
FunctionCallOutput {
call_id: String,
output: FunctionCallOutputPayload,
},
McpToolCallOutput {
call_id: String,
output: CallToolResult,
},
CustomToolCallOutput {
call_id: String,
name: Option<String>,
output: FunctionCallOutputPayload,
},
ToolSearchOutput {
call_id: String,
status: String,
execution: String,
tools: Vec<Value>,
},
}源码位置:codex-rs/protocol/src/models.rs :: ResponseItem
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseItem {
AdditionalTools { id: Option<ResponseItemId>, role: String, tools: Vec<Value> },
Message {
id: Option<ResponseItemId>,
role: String,
content: Vec<ContentItem>,
phase: Option<MessagePhase>,
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
},
AgentMessage { id: Option<ResponseItemId>, author: String, recipient: String, content: Vec<AgentMessageInputContent>, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
Reasoning { id: Option<ResponseItemId>, summary: Vec<ReasoningItemReasoningSummary>, content: Option<Vec<ReasoningItemContent>>, encrypted_content: Option<String>, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
LocalShellCall { id: Option<ResponseItemId>, call_id: Option<String>, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
FunctionCall { id: Option<ResponseItemId>, name: String, namespace: Option<String>, arguments: String, encrypted_function_args: Option<Vec<String>>, call_id: String, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
ToolSearchCall { id: Option<ResponseItemId>, call_id: Option<String>, status: Option<String>, execution: String, arguments: Value, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
FunctionCallOutput { id: Option<ResponseItemId>, call_id: Option<String>, name: Option<String>, namespace: Option<String>, output: FunctionCallOutputPayload, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
CustomToolCall { id: Option<ResponseItemId>, status: Option<String>, call_id: String, name: String, namespace: Option<String>, input: String, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
CustomToolCallOutput { id: Option<ResponseItemId>, call_id: String, name: Option<String>, output: FunctionCallOutputPayload, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
ToolSearchOutput { id: Option<ResponseItemId>, call_id: Option<String>, status: String, execution: String, tools: Vec<Value>, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
WebSearchCall { id: Option<ResponseItemId>, status: Option<String>, action: Option<WebSearchAction>, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
ImageGenerationCall { id: Option<ResponseItemId>, status: String, revised_prompt: Option<String>, result: String, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
Compaction { id: Option<ResponseItemId>, encrypted_content: String, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
CompactionTrigger {},
ContextCompaction { id: Option<ResponseItemId>, encrypted_content: Option<String>, internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough> },
#[serde(other)]
Other,
}生产代码中的字段带有 serde/TypeScript 注解;上面的展开保留了变体和关键字段,便于对照源码。Other 是兼容未知 wire 类型的兜底,不能据此推断所有下游消费者都能展示未知内容。
2. 输入提升
源码位置:codex-rs/protocol/src/models.rs :: From<ResponseInputItem> for ResponseItem
impl From<ResponseInputItem> for ResponseItem {
fn from(item: ResponseInputItem) -> Self {
match item {
ResponseInputItem::Message { role, content, phase } => Self::Message {
role,
content,
id: None,
phase,
internal_chat_message_metadata_passthrough: None,
},
ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput {
id: None,
call_id: Some(call_id),
name: None,
namespace: None,
output,
internal_chat_message_metadata_passthrough: None,
},
ResponseInputItem::McpToolCallOutput { call_id, output } => {
let output = output.into_function_call_output_payload();
Self::FunctionCallOutput {
id: None,
call_id: Some(call_id),
name: None,
namespace: None,
output,
internal_chat_message_metadata_passthrough: None,
}
}
ResponseInputItem::CustomToolCallOutput { call_id, name, output } => {
Self::CustomToolCallOutput {
id: None,
call_id,
name,
output,
internal_chat_message_metadata_passthrough: None,
}
}
ResponseInputItem::ToolSearchOutput {
call_id,
status,
execution,
tools,
} => Self::ToolSearchOutput {
call_id: Some(call_id),
status,
execution,
tools,
id: None,
internal_chat_message_metadata_passthrough: None,
},
}
}
}提升时多数 item 的 id 仍为空;工具输出的 call_id 被保留用于配对。MCP 输出被转换成通用 FunctionCallOutput,因此历史重点是 Responses 可理解的关联,而不是保留原始 Rust 变体名称。
3. ID与调用关联
ResponseItemId 是 Responses item 的稳定标识,call_id 是工具调用与结果之间的配对键。一个 output 可以没有 item ID,但如果它是配对输出,仍需要 call ID;反过来,拥有 item ID 也不代表存在对应工具调用。
源码位置:codex-rs/protocol/src/models.rs :: ResponseItem::id、set_id
pub fn id(&self) -> Option<&ResponseItemId> {
match self {
Self::AdditionalTools { id, .. }
| Self::Message { id, .. }
| Self::AgentMessage { id, .. }
| Self::LocalShellCall { id, .. }
| Self::FunctionCall { id, .. }
| Self::ToolSearchCall { id, .. }
| Self::FunctionCallOutput { id, .. }
| Self::CustomToolCall { id, .. }
| Self::CustomToolCallOutput { id, .. }
| Self::ToolSearchOutput { id, .. }
| Self::WebSearchCall { id, .. }
| Self::Reasoning { id, .. }
| Self::ImageGenerationCall { id, .. }
| Self::Compaction { id, .. }
| Self::ContextCompaction { id, .. } => id.as_ref(),
Self::CompactionTrigger { .. } | Self::Other => None,
}
}CompactionTrigger 和 Other 没有可读取的 item ID。Core 只能对有 ID 前缀的变体补 ID;控制型或未知变体保持无 ID。
4. 历史准备
源码位置:codex-rs/core/src/session/mod.rs :: prepare_conversation_items_for_history
pub(crate) fn prepare_conversation_items_for_history<'a>(
&self,
turn_context: &TurnContext,
items: &'a [ResponseItem],
) -> (Cow<'a, [ResponseItem]>, Vec<ImagePreparationMetadata>) {
let mut items = items.to_vec();
let image_preparation_mode = if unified_image_budget_enabled(
&turn_context.config.features,
&turn_context.model_info,
) {
ImagePreparationMode::UnifiedBudget
} else {
ImagePreparationMode::DetailBased
};
let image_resize_notice_mode = if turn_context
.config
.features
.enabled(Feature::ImageResizeNotice)
{
ImageResizeNoticeMode::Enabled
} else {
ImageResizeNoticeMode::Disabled
};
let image_preparations = prepare_image_response_items(
&mut items,
image_preparation_mode,
image_resize_notice_mode,
);
prepare_audio_response_items(&mut items);
for item in &mut items {
Self::stamp_response_item_for_history(item, &turn_context.sub_id);
}
let items = Cow::Owned(items);
(Self::assign_missing_response_item_ids(items), image_preparations)
}历史边界先处理图片/音频,再补 turn ID,最后补 Responses item ID。顺序决定了历史中记录的是准备后的内容,而不是模型请求前的临时对象。
源码位置:codex-rs/core/src/session/mod.rs :: stamp_response_item_for_history、assign_missing_response_item_id
pub(crate) fn stamp_response_item_for_history(item: &mut ResponseItem, turn_id: &str) {
item.set_turn_id_if_missing(turn_id);
item.set_create_time_if_missing(Self::response_item_create_time());
}
fn assign_missing_response_item_id(item: &mut ResponseItem) {
if item.id().is_some_and(|id| !id.is_empty()) {
return;
}
let Some(prefix) = item.id_prefix() else {
return;
};
item.set_id(Some(ResponseItemId::new(prefix)));
}turn ID 和创建时间属于 harness passthrough metadata;Responses item ID 属于 item 本身。已有非空 ID 不会被覆盖,因此恢复或重试不会随意改变外部关联。
5. 记录与落盘
源码位置:codex-rs/core/src/session/mod.rs :: record_conversation_items、record_prepared_conversation_items
pub(crate) async fn record_conversation_items(
&self,
turn_context: &TurnContext,
items: &[ResponseItem],
) {
let (items, image_preparations) =
self.prepare_conversation_items_for_history(turn_context, items);
let items = items
.into_owned()
.into_iter()
.map(ResponseItemEnvelope::new)
.collect();
self.record_prepared_conversation_items(turn_context, items, image_preparations)
.await;
}
async fn record_prepared_conversation_items(
&self,
turn_context: &TurnContext,
items: Vec<ResponseItemEnvelope>,
image_preparations: Vec<ImagePreparationMetadata>,
) {
let response_items = items
.iter()
.map(|envelope| envelope.item.clone())
.collect::<Vec<_>>();
{
let mut state = self.state.lock().await;
state
.history
.record_annotated_items(&items, turn_context.model_info.truncation_policy.into());
}
let rollout_items: Vec<RolloutItem> =
items.into_iter().map(RolloutItem::ResponseItem).collect();
self.persist_rollout_items(&rollout_items).await;
}内存 history 保存带 envelope 的 item,rollout 则保存 RolloutItem::ResponseItem。record_conversation_items 返回并不意味着磁盘写入已经完成;恢复问题要继续读 rollout 管线和 thread store。
6. Core投影
源码位置:codex-rs/core/src/event_mapping.rs :: parse_turn_item
pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
match item {
ResponseItem::Message { role, content, id, phase, .. } => match role.as_str() {
"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_deref(), content, phase.clone(),
))),
"system" => None,
_ => None,
},
ResponseItem::Reasoning { id, summary, content, .. } => {
let summary_text = summary.iter().map(|entry| match entry {
ReasoningItemReasoningSummary::SummaryText { text } => text.clone(),
}).collect();
let raw_content = content.clone().unwrap_or_default().into_iter().map(|entry| match entry {
ReasoningItemContent::ReasoningText { text }
| ReasoningItemContent::Text { text } => text,
}).collect();
Some(TurnItem::Reasoning(ReasoningItem {
id: id.as_deref().unwrap_or_default().to_string(),
summary_text,
raw_content,
}))
}
ResponseItem::WebSearchCall { id, action, .. } => {
let (action, query) = match action {
Some(action) => (action.clone(), web_search_action_detail(action)),
None => (WebSearchAction::Other, String::new()),
};
Some(TurnItem::WebSearch(WebSearchItem {
id: id.as_deref().unwrap_or_default().to_string(),
query,
action,
results: None,
}))
}
ResponseItem::ImageGenerationCall { id, status, revised_prompt, result, .. } => {
Some(TurnItem::ImageGeneration(codex_protocol::items::ImageGenerationItem {
id: id.as_deref()?.to_string(),
status: status.clone(),
revised_prompt: revised_prompt.clone(),
result: result.clone(),
saved_path: None,
}))
}
_ => None,
}
}这不是全量转换器。user message 可能进一步被识别为 hook prompt,assistant message 变成 agent message,reasoning/search/image generation 有专门的 TurnItem;工具调用等其他变体返回 None,由其他路径消费。
7. Prompt归一化
源码位置:codex-rs/core/src/context_manager/history.rs :: for_prompt_annotated、normalize_history
pub(crate) fn for_prompt_annotated(
mut self,
input_modalities: &[InputModality],
) -> Vec<ResponseItemEnvelope> {
self.normalize_history(input_modalities);
Arc::unwrap_or_clone(self.items)
}
fn normalize_history(&mut self, input_modalities: &[InputModality]) {
let items = Arc::make_mut(&mut self.items);
normalize::ensure_call_outputs_present(items);
normalize::remove_orphan_outputs(items);
normalize::strip_images_when_unsupported(input_modalities, items);
normalize::strip_audio_when_unsupported(input_modalities, items);
}归一化维护四类不变量:function/custom/local-shell/tool-search call 要有对应 output;成对 output 要有 call(服务器工具搜索 output 是例外);模型不支持的图片和音频要被移除。它发生在准备下一次 prompt 时,不会重新执行工具。
源码位置:codex-rs/core/src/context_manager/normalize.rs :: ensure_call_outputs_present、synthetic_output_id
pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItemEnvelope>) {
let mut function_output_ids = HashSet::new();
for envelope in items.iter() {
if let ResponseItem::FunctionCallOutput {
call_id: Some(call_id), ..
} = &envelope.item
{
function_output_ids.insert(call_id.as_str());
}
}
let mut missing_outputs_to_insert = Vec::new();
for (idx, envelope) in items.iter().enumerate() {
if let ResponseItem::FunctionCall { id, call_id, .. } = &envelope.item
&& !function_output_ids.contains(call_id.as_str())
{
missing_outputs_to_insert.push((
idx,
ResponseItemEnvelope::new(ResponseItem::FunctionCallOutput {
id: synthetic_output_id("fco", id.as_deref()),
call_id: Some(call_id.clone()),
name: None,
namespace: None,
output: FunctionCallOutputPayload::from_text("aborted".to_string()),
internal_chat_message_metadata_passthrough: None,
}),
));
}
}
for (idx, output_item) in missing_outputs_to_insert.into_iter().rev() {
items.insert(idx + 1, output_item);
}
}
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(ResponseItemId::with_suffix(
prefix,
Uuid::new_v5(&SYNTHETIC_OUTPUT_ID_NAMESPACE, name.as_bytes()),
))
}合成 output 的 UUIDv5 namespace 和 name 格式必须稳定,因为归一化可能重复运行且合成项不会持久化。稳定 ID 只保证 prompt 结构重复时的可复用性,不表示工具真的返回过成功结果。
8. App Server投影
源码位置:codex-rs/app-server-protocol/src/protocol/v2/item.rs :: From<CoreTurnItem> for ThreadItem
CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage {
id: user.id,
client_id: user.client_id,
content: user.content.into_iter().map(UserInput::from).collect(),
},
CoreTurnItem::AgentMessage(agent) => {
let text = agent
.content
.into_iter()
.map(|entry| match entry {
CoreAgentMessageContent::Text { text } => text,
})
.collect::<String>();
ThreadItem::AgentMessage {
id: agent.id,
text,
phase: agent.phase,
memory_citation: agent.memory_citation.map(Into::into),
delivery: agent.delivery,
}
}App Server 的 ThreadItem 是面向客户端的展示契约,不是 ResponseItem 的 JSON 原样转发。它会把 agent message 内容拼成 text,对命令、MCP、dynamic tool、collaboration 和 extension item 分别投影。某个 item 能进入历史,不代表它一定有一对一的客户端展示项。
9. 测试与边界
测试应分别验证协议 item 的 wire 形状、Core 历史边界的 ID/时间戳处理,以及归一化对不完整工具历史的修复。
源码位置:codex-rs/protocol/src/models.rs :: serializes_image_user_input_without_tags、response_item_id_getter_and_setter
源码位置:codex-rs/core/src/session/tests.rs :: assign_missing_response_item_ids_assigns_agent_message_ids、record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id
源码位置:codex-rs/core/src/context_manager/history_tests.rs :: normalize_adds_missing_output_for_function_call、normalize_removes_orphan_function_call_output
cd codex-rs
cargo test -p codex-protocol serializes_image_user_input_without_tags -- --nocapture --test-threads=1
cargo test -p codex-protocol response_item_id_getter_and_setter -- --nocapture --test-threads=1
cargo test -p codex-core assign_missing_response_item_ids_assigns_agent_message_ids -- --nocapture --test-threads=1
cargo test -p codex-core record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id -- --nocapture --test-threads=1
cargo test -p codex-core normalize_adds_missing_output_for_function_call -- --nocapture --test-threads=1
cargo test -p codex-core normalize_removes_orphan_function_call_output -- --nocapture --test-threads=1这些测试能说明 wire 映射、ID 补全、turn metadata stamping 和 call/output 归一化;不能证明每个未知变体都有客户端投影,也不能证明合成 output 触发了真实工具执行。
10. 源码定位练习
遇到“历史里有 item,但下一次请求没有它”,先看 History::for_prompt_annotated 的归一化和 input modalities;遇到“客户端看不到 item”,再看 parse_turn_item 和 App Server ThreadItem 投影是否支持该变体。
遇到“同一个工具重复请求”,同时记录 ResponseItemId、call_id、call/output 顺序和是否出现 synthetic output。遇到“恢复后 ID 变化”,检查 assign_missing_response_item_id 和持久化前的 item 是否已有非空 ID。
把 ResponseInputItem、ResponseItem、ResponseItemEnvelope、TurnItem 和 ThreadItem 分层阅读,才能准确判断一个会话 item 是被转换、被归一化、被过滤,还是仅仅没有对应的展示适配器。
