mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
use stable item IDs for Responses API calls
This commit is contained in:
@@ -386,12 +386,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ResponseItem::Message {
|
||||
id: Some(uuid::Uuid::new_v4().to_string()),
|
||||
role: "user".to_string(),
|
||||
content,
|
||||
phase: None,
|
||||
})
|
||||
Some(ResponseItem::new_message("user", content))
|
||||
}
|
||||
|
||||
pub fn parse_hook_prompt_message(
|
||||
|
||||
+178
-23
@@ -9,7 +9,9 @@ use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::ser::Serializer;
|
||||
use serde_json::Value;
|
||||
use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::permissions::FileSystemAccessMode;
|
||||
use crate::permissions::FileSystemPath;
|
||||
@@ -765,6 +767,10 @@ pub enum ResponseItem {
|
||||
phase: Option<MessagePhase>,
|
||||
},
|
||||
AgentMessage {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
author: String,
|
||||
recipient: String,
|
||||
content: Vec<AgentMessageInputContent>,
|
||||
@@ -822,6 +828,10 @@ pub enum ResponseItem {
|
||||
// - an array of structured content items (`content_items`)
|
||||
// We keep this behavior centralized in `FunctionCallOutputPayload`.
|
||||
FunctionCallOutput {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: String,
|
||||
#[ts(as = "FunctionCallOutputBody")]
|
||||
#[schemars(with = "FunctionCallOutputBody")]
|
||||
@@ -843,6 +853,10 @@ pub enum ResponseItem {
|
||||
// `function_call_output.output` so freeform tools can return either plain
|
||||
// text or structured content items.
|
||||
CustomToolCallOutput {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -852,6 +866,10 @@ pub enum ResponseItem {
|
||||
output: FunctionCallOutputPayload,
|
||||
},
|
||||
ToolSearchOutput {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: Option<String>,
|
||||
status: String,
|
||||
execution: String,
|
||||
@@ -896,6 +914,10 @@ pub enum ResponseItem {
|
||||
},
|
||||
#[serde(alias = "compaction_summary")]
|
||||
Compaction {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
encrypted_content: String,
|
||||
},
|
||||
CompactionTrigger,
|
||||
@@ -908,13 +930,137 @@ pub enum ResponseItem {
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Responses API item kinds for stable IDs Codex assigns to new local response items.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ResponseItemIdKind {
|
||||
Message,
|
||||
FunctionCallOutput,
|
||||
CustomToolCallOutput,
|
||||
ToolSearchOutput,
|
||||
}
|
||||
|
||||
impl ResponseItemIdKind {
|
||||
fn prefix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Message => "msg",
|
||||
Self::FunctionCallOutput => "fco",
|
||||
Self::CustomToolCallOutput => "ctco",
|
||||
Self::ToolSearchOutput => "tso",
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new stable Responses API item ID with this kind's server-recognized prefix.
|
||||
pub fn new_id(self) -> String {
|
||||
format!("{}_{}", self.prefix(), Uuid::new_v4().simple())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseItem {
|
||||
/// Constructs a new Codex-owned message with a fresh stable Responses API ID.
|
||||
pub fn new_message(role: impl Into<String>, content: Vec<ContentItem>) -> Self {
|
||||
Self::Message {
|
||||
id: Some(ResponseItemIdKind::Message.new_id()),
|
||||
role: role.into(),
|
||||
content,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new Codex-owned function call output with a fresh stable Responses API ID.
|
||||
pub fn new_function_call_output(
|
||||
call_id: impl Into<String>,
|
||||
output: FunctionCallOutputPayload,
|
||||
) -> Self {
|
||||
Self::FunctionCallOutput {
|
||||
id: Some(ResponseItemIdKind::FunctionCallOutput.new_id()),
|
||||
call_id: call_id.into(),
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new Codex-owned unnamed custom tool output with a fresh stable Responses API ID.
|
||||
pub fn new_custom_tool_call_output(
|
||||
call_id: impl Into<String>,
|
||||
output: FunctionCallOutputPayload,
|
||||
) -> Self {
|
||||
Self::CustomToolCallOutput {
|
||||
id: Some(ResponseItemIdKind::CustomToolCallOutput.new_id()),
|
||||
call_id: call_id.into(),
|
||||
name: None,
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new Codex-owned named custom tool output with a fresh stable Responses API ID.
|
||||
pub fn new_named_custom_tool_call_output(
|
||||
call_id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
output: FunctionCallOutputPayload,
|
||||
) -> Self {
|
||||
Self::CustomToolCallOutput {
|
||||
id: Some(ResponseItemIdKind::CustomToolCallOutput.new_id()),
|
||||
call_id: call_id.into(),
|
||||
name: Some(name.into()),
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new Codex-owned tool search output with a fresh stable Responses API ID.
|
||||
pub fn new_tool_search_output(
|
||||
call_id: impl Into<String>,
|
||||
status: impl Into<String>,
|
||||
execution: impl Into<String>,
|
||||
tools: Vec<Value>,
|
||||
) -> Self {
|
||||
Self::ToolSearchOutput {
|
||||
id: Some(ResponseItemIdKind::ToolSearchOutput.new_id()),
|
||||
call_id: Some(call_id.into()),
|
||||
status: status.into(),
|
||||
execution: execution.into(),
|
||||
tools,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<&str> {
|
||||
match self {
|
||||
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::Compaction { id, .. } => id.as_deref(),
|
||||
Self::Reasoning { id, .. } | Self::ImageGenerationCall { id, .. } => Some(id),
|
||||
Self::CompactionTrigger | Self::ContextCompaction { .. } | Self::Other => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_id_to_json(&self, value: &mut Value) {
|
||||
let Some(id) = self.id().filter(|id| !id.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
let Some(obj) = value.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
obj.insert("id".to_string(), Value::String(id.to_string()));
|
||||
}
|
||||
|
||||
/// Returns whether this item is an ordinary user-role message.
|
||||
pub fn is_user_message(&self) -> bool {
|
||||
matches!(self, Self::Message { role, .. } if role == "user")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attach_all_response_item_ids(values: &mut [Value], items: &[ResponseItem]) {
|
||||
for (value, item) in values.iter_mut().zip(items) {
|
||||
item.attach_id_to_json(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md");
|
||||
|
||||
/// Base instructions for the model in a thread. Corresponds to the `instructions` field in the ResponsesAPI.
|
||||
@@ -1131,38 +1277,32 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
content,
|
||||
phase,
|
||||
} => Self::Message {
|
||||
id: Some(ResponseItemIdKind::Message.new_id()),
|
||||
role,
|
||||
content,
|
||||
id: None,
|
||||
phase,
|
||||
},
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
Self::FunctionCallOutput { call_id, output }
|
||||
Self::new_function_call_output(call_id, output)
|
||||
}
|
||||
ResponseInputItem::McpToolCallOutput { call_id, output } => {
|
||||
let output = output.into_function_call_output_payload();
|
||||
Self::FunctionCallOutput { call_id, output }
|
||||
Self::new_function_call_output(call_id, output)
|
||||
}
|
||||
ResponseInputItem::CustomToolCallOutput {
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
} => Self::CustomToolCallOutput {
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
} => match name {
|
||||
Some(name) => Self::new_named_custom_tool_call_output(call_id, name, output),
|
||||
None => Self::new_custom_tool_call_output(call_id, output),
|
||||
},
|
||||
ResponseInputItem::ToolSearchOutput {
|
||||
call_id,
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
} => Self::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
},
|
||||
} => Self::new_tool_search_output(call_id, status, execution, tools),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1672,17 +1812,24 @@ mod tests {
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
});
|
||||
|
||||
let ResponseItem::Message {
|
||||
id: Some(id),
|
||||
role,
|
||||
content,
|
||||
phase,
|
||||
} = item
|
||||
else {
|
||||
panic!("expected assistant message with generated id");
|
||||
};
|
||||
assert!(id.starts_with("msg_"));
|
||||
assert_eq!(role, "assistant");
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "still working".to_string(),
|
||||
}],
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
}
|
||||
content,
|
||||
vec![ContentItem::OutputText {
|
||||
text: "still working".to_string(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(phase, Some(MessagePhase::Commentary));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2503,6 +2650,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::Compaction {
|
||||
id: None,
|
||||
encrypted_content: "abc".into(),
|
||||
}
|
||||
);
|
||||
@@ -2767,9 +2915,15 @@ mod tests {
|
||||
}
|
||||
})],
|
||||
};
|
||||
let item = ResponseItem::from(input.clone());
|
||||
let ResponseItem::ToolSearchOutput { id: Some(id), .. } = &item else {
|
||||
panic!("expected tool search output id");
|
||||
};
|
||||
assert!(id.starts_with("tso_"));
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
item,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
id: Some(id.clone()),
|
||||
call_id: Some("search-1".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
@@ -2855,6 +3009,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
parsed_output,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
id: None,
|
||||
call_id: None,
|
||||
status: "completed".to_string(),
|
||||
execution: "server".to_string(),
|
||||
|
||||
@@ -43,6 +43,7 @@ use crate::models::ResponseInputItem;
|
||||
use crate::models::ResponseItem;
|
||||
use crate::models::SandboxEnforcement;
|
||||
use crate::models::WebSearchAction;
|
||||
use crate::models::attach_all_response_item_ids;
|
||||
use crate::num_format::format_with_separators;
|
||||
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
@@ -705,15 +706,22 @@ impl InterAgentCommunication {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_model_input_item(&self) -> ResponseItem {
|
||||
pub fn to_model_input_item(&self, source_message_id: Option<&str>) -> ResponseItem {
|
||||
match &self.encrypted_content {
|
||||
Some(encrypted_content) => ResponseItem::AgentMessage {
|
||||
author: self.author.to_string(),
|
||||
recipient: self.recipient.to_string(),
|
||||
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
}],
|
||||
},
|
||||
Some(encrypted_content) => {
|
||||
let id = source_message_id
|
||||
.and_then(|id| id.strip_prefix("msg_"))
|
||||
.filter(|suffix| !suffix.is_empty())
|
||||
.map(|suffix| format!("amsg_{suffix}"));
|
||||
ResponseItem::AgentMessage {
|
||||
id,
|
||||
author: self.author.to_string(),
|
||||
recipient: self.recipient.to_string(),
|
||||
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
None => self.to_response_input_item().into(),
|
||||
}
|
||||
}
|
||||
@@ -2857,6 +2865,34 @@ pub enum RolloutItem {
|
||||
EventMsg(EventMsg),
|
||||
}
|
||||
|
||||
impl RolloutItem {
|
||||
pub fn attach_response_item_ids_to_json(&self, value: &mut Value) {
|
||||
let Some(payload) = value.get_mut("payload") else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::ResponseItem(item) => item.attach_id_to_json(payload),
|
||||
Self::Compacted(CompactedItem {
|
||||
replacement_history: Some(items),
|
||||
..
|
||||
}) => {
|
||||
let Some(Value::Array(values)) = payload.get_mut("replacement_history") else {
|
||||
return;
|
||||
};
|
||||
attach_all_response_item_ids(values, items);
|
||||
}
|
||||
Self::SessionMeta(_)
|
||||
| Self::Compacted(CompactedItem {
|
||||
replacement_history: None,
|
||||
..
|
||||
})
|
||||
| Self::TurnContext(_)
|
||||
| Self::EventMsg(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
||||
pub struct CompactedItem {
|
||||
pub message: String,
|
||||
@@ -4085,6 +4121,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_inter_agent_communication_model_input_uses_source_message_id() {
|
||||
let communication = InterAgentCommunication::new_encrypted(
|
||||
AgentPath::root(),
|
||||
AgentPath::root().join("reviewer").expect("recipient path"),
|
||||
Vec::new(),
|
||||
"encrypted-payload".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
let expected = ResponseItem::AgentMessage {
|
||||
id: Some("amsg_stable".to_string()),
|
||||
author: "/root".to_string(),
|
||||
recipient: "/root/reviewer".to_string(),
|
||||
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: "encrypted-payload".to_string(),
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
communication.to_model_input_item(Some("msg_stable")),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
communication.to_model_input_item(/*source_message_id*/ None),
|
||||
ResponseItem::AgentMessage {
|
||||
id: None,
|
||||
author: "/root".to_string(),
|
||||
recipient: "/root/reviewer".to_string(),
|
||||
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: "encrypted-payload".to_string(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_source_from_startup_arg_normalizes_custom_values() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user