Merge branch 'main' into compaction-count-plumbing

This commit is contained in:
canvrno-oai
2026-07-08 15:14:52 -07:00
committed by GitHub
329 changed files with 11515 additions and 3534 deletions
@@ -31,6 +31,11 @@ pub enum AuthMode {
#[ts(rename = "chatgptAuthTokens")]
#[strum(serialize = "chatgptAuthTokens")]
ChatgptAuthTokens,
/// Backend auth supplied as request headers.
#[serde(rename = "headers")]
#[ts(rename = "headers")]
#[strum(serialize = "headers")]
Headers,
/// Programmatic Codex auth backed by a registered Agent Identity.
#[serde(rename = "agentIdentity")]
#[ts(rename = "agentIdentity")]
@@ -53,7 +58,7 @@ impl AuthMode {
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
Self::ApiKey | Self::Headers | Self::AgentIdentity | Self::BedrockApiKey => false,
}
}
@@ -62,6 +67,7 @@ impl AuthMode {
match self {
Self::Chatgpt
| Self::ChatgptAuthTokens
| Self::Headers
| Self::AgentIdentity
| Self::PersonalAccessToken => true,
Self::ApiKey | Self::BedrockApiKey => false,
@@ -2721,7 +2727,9 @@ mod tests {
let request = ClientRequest::LoginAccount {
request_id: RequestId::Integer(3),
params: v2::LoginAccountParams::Chatgpt {
app_brand: None,
codex_streamlined_login: false,
use_hosted_login_success_page: false,
},
};
assert_eq!(
@@ -2729,7 +2737,8 @@ mod tests {
"method": "account/login/start",
"id": 3,
"params": {
"type": "chatgpt"
"type": "chatgpt",
"appBrand": null
}
}),
serde_json::to_value(&request)?,
@@ -2742,7 +2751,9 @@ mod tests {
let request = ClientRequest::LoginAccount {
request_id: RequestId::Integer(3),
params: v2::LoginAccountParams::Chatgpt {
app_brand: None,
codex_streamlined_login: true,
use_hosted_login_success_page: false,
},
};
assert_eq!(
@@ -2751,6 +2762,7 @@ mod tests {
"id": 3,
"params": {
"type": "chatgpt",
"appBrand": null,
"codexStreamlinedLogin": true
}
}),
@@ -2759,6 +2771,32 @@ mod tests {
Ok(())
}
#[test]
fn serialize_account_login_chatgpt_with_hosted_success_page() -> Result<()> {
let request = ClientRequest::LoginAccount {
request_id: RequestId::Integer(3),
params: v2::LoginAccountParams::Chatgpt {
app_brand: Some(v2::LoginAppBrand::Chatgpt),
codex_streamlined_login: true,
use_hosted_login_success_page: true,
},
};
assert_eq!(
json!({
"method": "account/login/start",
"id": 3,
"params": {
"type": "chatgpt",
"appBrand": "chatgpt",
"codexStreamlinedLogin": true,
"useHostedLoginSuccessPage": true
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_account_login_chatgpt_device_code() -> Result<()> {
let request = ClientRequest::LoginAccount {
@@ -33,6 +33,9 @@ use codex_protocol::protocol::GuardianAssessmentAction;
use codex_protocol::protocol::GuardianAssessmentEvent;
use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::PatchApplyEndEvent;
use codex_protocol::protocol::ReviewOutputEvent;
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
use codex_protocol::review_format::render_review_output_text;
use codex_shell_command::parse_command::parse_command;
use codex_shell_command::parse_command::shlex_join;
use codex_utils_path_uri::PathConvention;
@@ -41,6 +44,12 @@ use std::collections::HashMap;
use std::path::PathBuf;
use tracing::warn;
pub(crate) fn review_output_text(output: Option<&ReviewOutputEvent>) -> String {
output
.map(render_review_output_text)
.unwrap_or_else(|| REVIEW_FALLBACK_MESSAGE.to_string())
}
pub fn build_file_change_approval_request_item(
payload: &ApplyPatchApprovalRequestEvent,
) -> ThreadItem {
@@ -4,6 +4,7 @@ use crate::protocol::item_builders::build_file_change_approval_request_item;
use crate::protocol::item_builders::build_file_change_begin_item;
use crate::protocol::item_builders::build_file_change_end_item;
use crate::protocol::item_builders::build_item_from_guardian_event;
use crate::protocol::item_builders::review_output_text;
use crate::protocol::v2::CollabAgentState;
use crate::protocol::v2::CollabAgentTool;
use crate::protocol::v2::CollabAgentToolCallStatus;
@@ -21,7 +22,11 @@ use crate::protocol::v2::TurnError;
use crate::protocol::v2::TurnItemsView;
use crate::protocol::v2::TurnStatus;
use crate::protocol::v2::UserInput;
#[cfg(test)]
use crate::protocol::v2::WebSearchAction;
use crate::protocol::v2::WebSearchItem;
use crate::protocol::v2::web_search_action_from_core;
use codex_extension_items::image_generation::ImageGenerationItem;
use codex_protocol::items::parse_hook_prompt_message;
use codex_protocol::models::MessagePhase;
use codex_protocol::protocol::AgentReasoningEvent;
@@ -45,7 +50,6 @@ use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::McpToolCallEndEvent;
use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::PatchApplyEndEvent;
use codex_protocol::protocol::ReviewOutputEvent;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::TurnAbortedEvent;
@@ -55,6 +59,8 @@ use codex_protocol::protocol::UserMessageEvent;
use codex_protocol::protocol::ViewImageToolCallEvent;
use codex_protocol::protocol::WebSearchBeginEvent;
use codex_protocol::protocol::WebSearchEndEvent;
#[cfg(test)]
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
use std::collections::HashMap;
use tracing::warn;
use uuid::Uuid;
@@ -586,15 +592,23 @@ impl ThreadHistoryBuilder {
turn_id: &str,
item: &codex_protocol::items::TurnItem,
) {
let is_review_mode_item = matches!(
item,
codex_protocol::items::TurnItem::EnteredReviewMode(_)
| codex_protocol::items::TurnItem::ExitedReviewMode(_)
);
let should_upsert = match item {
codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(),
codex_protocol::items::TurnItem::Sleep(_)
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::CommandExecution(_)
| codex_protocol::items::TurnItem::DynamicToolCall(_)
| codex_protocol::items::TurnItem::CollabAgentToolCall(_)
| codex_protocol::items::TurnItem::SubAgentActivity(_) => true,
| codex_protocol::items::TurnItem::SubAgentActivity(_)
| codex_protocol::items::TurnItem::Extension(_)
| codex_protocol::items::TurnItem::EnteredReviewMode(_)
| codex_protocol::items::TurnItem::ExitedReviewMode(_) => true,
codex_protocol::items::TurnItem::UserMessage(_)
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::AgentMessage(_)
| codex_protocol::items::TurnItem::Reasoning(_)
| codex_protocol::items::TurnItem::WebSearch(_)
@@ -606,25 +620,30 @@ impl ThreadHistoryBuilder {
};
if should_upsert {
self.upsert_item_in_turn_id(turn_id, ThreadItem::from(item.clone()));
let item = ThreadItem::from(item.clone());
if is_review_mode_item {
self.upsert_review_mode_item(Some(turn_id), item);
} else {
self.upsert_item_in_turn_id(turn_id, item);
}
}
}
fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent) {
let item = ThreadItem::WebSearch {
let item = ThreadItem::WebSearch(WebSearchItem {
id: payload.call_id.clone(),
query: String::new(),
action: None,
};
});
self.upsert_item_in_current_turn(item);
}
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) {
let item = ThreadItem::WebSearch {
let item = ThreadItem::WebSearch(WebSearchItem {
id: payload.call_id.clone(),
query: payload.query.clone(),
action: Some(WebSearchAction::from(payload.action.clone())),
};
action: Some(web_search_action_from_core(payload.action.clone())),
});
self.upsert_item_in_current_turn(item);
}
@@ -827,24 +846,24 @@ impl ThreadHistoryBuilder {
}
fn handle_image_generation_begin(&mut self, payload: &ImageGenerationBeginEvent) {
let item = ThreadItem::ImageGeneration {
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
id: payload.call_id.clone(),
status: String::new(),
revised_prompt: None,
result: String::new(),
saved_path: None,
};
});
self.upsert_item_in_current_turn(item);
}
fn handle_image_generation_end(&mut self, payload: &ImageGenerationEndEvent) {
let item = ThreadItem::ImageGeneration {
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
id: payload.call_id.clone(),
status: payload.status.clone(),
revised_prompt: payload.revised_prompt.clone(),
result: payload.result.clone(),
saved_path: payload.saved_path.clone(),
};
});
self.upsert_item_in_current_turn(item);
}
@@ -1103,26 +1122,55 @@ impl ThreadHistoryBuilder {
self.push_item_in_current_turn(ThreadItem::ContextCompaction { id });
}
fn handle_entered_review_mode(&mut self, payload: &codex_protocol::protocol::ReviewRequest) {
fn handle_entered_review_mode(
&mut self,
payload: &codex_protocol::protocol::EnteredReviewModeEvent,
) {
let review = payload
.user_facing_hint
.clone()
.unwrap_or_else(|| "Review requested.".to_string());
let id = self.next_item_id();
self.push_item_in_current_turn(ThreadItem::EnteredReviewMode { id, review });
let id = payload
.item_id
.clone()
.unwrap_or_else(|| self.next_item_id());
self.upsert_review_mode_item(
payload.turn_id.as_deref(),
ThreadItem::EnteredReviewMode { id, review },
);
}
fn handle_exited_review_mode(
&mut self,
payload: &codex_protocol::protocol::ExitedReviewModeEvent,
) {
let review = payload
.review_output
let review = review_output_text(payload.review_output.as_ref());
let id = payload
.item_id
.clone()
.unwrap_or_else(|| self.next_item_id());
self.upsert_review_mode_item(
payload.turn_id.as_deref(),
ThreadItem::ExitedReviewMode { id, review },
);
}
fn upsert_review_mode_item(&mut self, turn_id: Option<&str>, item: ThreadItem) {
let Some(turn_id) = turn_id else {
self.upsert_item_in_current_turn(item);
return;
};
let current_turn_matches = self
.current_turn
.as_ref()
.map(render_review_output_text)
.unwrap_or_else(|| REVIEW_FALLBACK_MESSAGE.to_string());
let id = self.next_item_id();
self.push_item_in_current_turn(ThreadItem::ExitedReviewMode { id, review });
.is_some_and(|turn| turn.id == turn_id);
if !current_turn_matches && !self.turns.iter().any(|turn| turn.id == turn_id) {
self.finish_current_turn();
let turn = self.new_turn(Some(turn_id.to_string()));
self.record_changed_pending_turn(&turn);
self.current_turn = Some(turn);
}
self.upsert_item_in_turn_id(turn_id, item);
}
fn handle_error(&mut self, payload: &ErrorEvent) {
@@ -1438,17 +1486,6 @@ impl ThreadHistoryBuilder {
}
}
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
let explanation = output.overall_explanation.trim();
if explanation.is_empty() {
REVIEW_FALLBACK_MESSAGE.to_string()
} else {
explanation.to_string()
}
}
fn convert_dynamic_tool_content_items(
items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem],
) -> Vec<DynamicToolCallOutputContentItem> {
@@ -1548,10 +1585,13 @@ impl From<&PendingTurn> for Turn {
mod tests {
use super::*;
use crate::protocol::v2::CommandExecutionSource;
use codex_extension_items::ExtensionItem as CoreExtensionItem;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
use codex_protocol::items::CommandExecutionItem as CoreCommandExecutionItem;
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
use codex_protocol::items::EnteredReviewModeItem as CoreEnteredReviewModeItem;
use codex_protocol::items::ExitedReviewModeItem as CoreExitedReviewModeItem;
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
use codex_protocol::items::SleepItem as CoreSleepItem;
use codex_protocol::items::TurnItem as CoreTurnItem;
@@ -1569,12 +1609,15 @@ mod tests {
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::CompactedItem;
use codex_protocol::protocol::DynamicToolCallResponseEvent;
use codex_protocol::protocol::EnteredReviewModeEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExitedReviewModeEvent;
use codex_protocol::protocol::ItemStartedEvent;
use codex_protocol::protocol::McpInvocation;
use codex_protocol::protocol::McpToolCallEndEvent;
use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortedEvent;
@@ -1699,6 +1742,111 @@ mod tests {
);
}
#[test]
fn review_mode_events_replay_persisted_ids() {
let events = vec![
EventMsg::EnteredReviewMode(EnteredReviewModeEvent {
target: ReviewTarget::Custom {
instructions: "review this".into(),
},
user_facing_hint: Some("Review requested.".into()),
turn_id: Some("turn-1".into()),
item_id: Some("entered-review".into()),
}),
EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
turn_id: Some("turn-1".into()),
item_id: Some("exited-review".into()),
review_output: None,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
}),
];
let mut builder = ThreadHistoryBuilder::new();
for event in &events {
builder.handle_event(event);
}
let turns = builder.finish();
assert_eq!(turns[0].id, "turn-1");
assert_eq!(
turns[0].items,
vec![
ThreadItem::EnteredReviewMode {
id: "entered-review".into(),
review: "Review requested.".into(),
},
ThreadItem::ExitedReviewMode {
id: "exited-review".into(),
review: REVIEW_FALLBACK_MESSAGE.into(),
},
]
);
}
#[test]
fn review_mode_items_replay_without_turn_started() {
let thread_id = ThreadId::new();
let entered = CoreTurnItem::EnteredReviewMode(CoreEnteredReviewModeItem {
id: "entered-review".into(),
target: ReviewTarget::Custom {
instructions: "review this".into(),
},
user_facing_hint: "Review requested.".into(),
});
let exited = CoreTurnItem::ExitedReviewMode(CoreExitedReviewModeItem {
id: "exited-review".into(),
review_output: None,
});
let events = vec![
EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id,
turn_id: "turn-1".into(),
item: entered,
completed_at_ms: 0,
}),
EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id,
turn_id: "turn-1".into(),
item: exited,
completed_at_ms: 0,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
}),
];
let mut builder = ThreadHistoryBuilder::new();
for event in &events {
builder.handle_event(event);
}
let turns = builder.finish();
assert_eq!(turns[0].id, "turn-1");
assert_eq!(
turns[0].items,
vec![
ThreadItem::EnteredReviewMode {
id: "entered-review".into(),
review: "Review requested.".into(),
},
ThreadItem::ExitedReviewMode {
id: "exited-review".into(),
review: REVIEW_FALLBACK_MESSAGE.into(),
},
]
);
}
#[test]
fn rebuilds_user_message_image_details_from_legacy_events() {
let local_path = PathBuf::from("/tmp/local.png");
@@ -1846,6 +1994,60 @@ mod tests {
);
}
#[test]
fn rebuilds_extension_image_generation_item_from_persisted_completion() {
let turn_id = "turn-1";
let thread_id = ThreadId::new();
let saved_path = test_path_buf("/tmp/image-1.png").abs();
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
trace_id: None,
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id,
turn_id: turn_id.to_string(),
item: CoreTurnItem::Extension(CoreExtensionItem::ImageGeneration(
ImageGenerationItem {
id: "image-1".to_string(),
status: "completed".to_string(),
revised_prompt: Some("A blue square".to_string()),
result: "cG5n".to_string(),
saved_path: Some(saved_path.clone()),
},
)),
completed_at_ms: 1_000,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
}),
];
let items = events
.into_iter()
.map(RolloutItem::EventMsg)
.collect::<Vec<_>>();
let turns = build_turns_from_rollout_items(&items);
assert_eq!(
turns[0].items,
vec![ThreadItem::ImageGeneration(ImageGenerationItem {
id: "image-1".to_string(),
status: "completed".to_string(),
revised_prompt: Some("A blue square".to_string()),
result: "cG5n".to_string(),
saved_path: Some(saved_path),
})]
);
}
#[test]
fn rebuilds_command_execution_item_from_persisted_completion() {
let turn_id = "turn-1";
@@ -2058,13 +2260,13 @@ mod tests {
text_elements: Vec::new(),
}],
},
ThreadItem::ImageGeneration {
ThreadItem::ImageGeneration(ImageGenerationItem {
id: "ig_123".into(),
status: "completed".into(),
revised_prompt: Some("final prompt".into()),
result: "Zm9v".into(),
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
},
}),
],
}
);
@@ -2492,14 +2694,14 @@ mod tests {
assert_eq!(turns[0].items.len(), 4);
assert_eq!(
turns[0].items[1],
ThreadItem::WebSearch {
ThreadItem::WebSearch(WebSearchItem {
id: "search-1".into(),
query: "codex".into(),
action: Some(WebSearchAction::Search {
query: Some("codex".into()),
queries: None,
}),
}
})
);
assert_eq!(
turns[0].items[2],
@@ -3825,6 +4027,37 @@ mod tests {
);
}
#[test]
fn canonical_hook_prompt_completion_updates_turn_history() {
let hook_prompt = CoreTurnItem::HookPrompt(codex_protocol::items::HookPromptItem {
id: "hook-prompt-1".into(),
fragments: vec![CoreHookPromptFragment::from_single_hook(
"Retry with tests.",
"hook-run-1",
)],
});
let expected_item = ThreadItem::from(hook_prompt.clone());
let mut builder = ThreadHistoryBuilder::new();
builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
trace_id: None,
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}));
builder.handle_event(&EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-a".into(),
item: hook_prompt,
completed_at_ms: 0,
}));
assert_eq!(
builder.active_turn_snapshot().expect("active turn").items,
vec![expected_item]
);
}
#[test]
fn ignores_plain_user_response_items_in_rollout_replay() {
let items = vec![
@@ -3872,7 +4105,6 @@ mod tests {
..Default::default()
}),
));
assert_eq!(
changes,
ThreadHistoryChangeSet {
@@ -3919,20 +4151,19 @@ mod tests {
},
}),
));
assert_eq!(
changes,
ThreadHistoryChangeSet {
changed_items: vec![ThreadHistoryItemChange {
turn_id: "rollout-0".into(),
item: ThreadItem::WebSearch {
item: ThreadItem::WebSearch(WebSearchItem {
id: "search-1".into(),
query: "codex".into(),
action: Some(WebSearchAction::Search {
query: Some("codex".into()),
queries: None,
}),
},
}),
}],
changed_turns: Vec::new(),
removed_turn_ids: Vec::new(),
@@ -3954,7 +4185,6 @@ mod tests {
text: "raw content".into(),
}),
));
assert_eq!(
changes,
ThreadHistoryChangeSet {
@@ -4054,20 +4284,19 @@ mod tests {
},
})),
]);
assert_eq!(
changes,
ThreadHistoryChangeSet {
changed_items: vec![ThreadHistoryItemChange {
turn_id: "rollout-0".into(),
item: ThreadItem::WebSearch {
item: ThreadItem::WebSearch(WebSearchItem {
id: "search-1".into(),
query: "codex".into(),
action: Some(WebSearchAction::Search {
query: Some("codex".into()),
queries: None,
}),
},
}),
}],
changed_turns: vec![ThreadHistoryTurnChange {
turn_id: "rollout-0".into(),
@@ -78,6 +78,11 @@ pub enum LoginAccountParams {
Chatgpt {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
codex_streamlined_login: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
use_hosted_login_success_page: bool,
#[serde(default)]
#[ts(optional = nullable)]
app_brand: Option<LoginAppBrand>,
},
#[serde(rename = "chatgptDeviceCode")]
#[ts(rename = "chatgptDeviceCode")]
@@ -102,6 +107,16 @@ pub enum LoginAccountParams {
},
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[ts(rename_all = "lowercase")]
#[ts(export_to = "v2/")]
pub enum LoginAppBrand {
#[default]
Codex,
Chatgpt,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
@@ -10,7 +10,12 @@ use super::UserInput;
use super::shared::v2_enum_from_core;
use crate::protocol::item_builders::command_actions_for_path_uri;
use crate::protocol::item_builders::convert_patch_changes;
use crate::protocol::item_builders::review_output_text;
use codex_experimental_api_macros::ExperimentalApi;
use codex_extension_items::ExtensionItem;
pub use codex_extension_items::image_generation::ImageGenerationItem;
pub use codex_extension_items::web_search::WebSearchAction;
pub use codex_extension_items::web_search::WebSearchItem;
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
@@ -246,7 +251,10 @@ pub enum ThreadItem {
#[ts(rename_all = "camelCase")]
/// EXPERIMENTAL - proposed plan item content. The completed plan item is
/// authoritative and may not match the concatenation of `PlanDelta` text.
Plan { id: String, text: String },
Plan {
id: String,
text: String,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Reasoning {
@@ -353,13 +361,7 @@ pub enum ThreadItem {
agent_thread_id: String,
agent_path: String,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
WebSearch {
id: String,
query: String,
action: Option<WebSearchAction>,
},
WebSearch(WebSearchItem),
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
ImageView {
@@ -373,26 +375,24 @@ pub enum ThreadItem {
#[ts(type = "number")]
duration_ms: u64,
},
ImageGeneration(ImageGenerationItem),
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
ImageGeneration {
EnteredReviewMode {
id: String,
status: String,
revised_prompt: Option<String>,
result: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
saved_path: Option<AbsolutePathBuf>,
review: String,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
EnteredReviewMode { id: String, review: String },
ExitedReviewMode {
id: String,
review: String,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
ExitedReviewMode { id: String, review: String },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
ContextCompaction { id: String },
ContextCompaction {
id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
@@ -429,13 +429,13 @@ impl ThreadItem {
| ThreadItem::DynamicToolCall { id, .. }
| ThreadItem::CollabAgentToolCall { id, .. }
| ThreadItem::SubAgentActivity { id, .. }
| ThreadItem::WebSearch { id, .. }
| ThreadItem::ImageView { id, .. }
| ThreadItem::Sleep { id, .. }
| ThreadItem::ImageGeneration { id, .. }
| ThreadItem::EnteredReviewMode { id, .. }
| ThreadItem::ExitedReviewMode { id, .. }
| ThreadItem::ContextCompaction { id, .. } => id,
ThreadItem::WebSearch(item) => &item.id,
ThreadItem::ImageGeneration(item) => &item.id,
}
}
}
@@ -783,40 +783,20 @@ impl TryFrom<GuardianApprovalReviewAction> for CoreGuardianAssessmentAction {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum WebSearchAction {
Search {
query: Option<String>,
queries: Option<Vec<String>>,
},
OpenPage {
url: Option<String>,
},
FindInPage {
url: Option<String>,
pattern: Option<String>,
},
#[serde(other)]
Other,
}
impl From<codex_protocol::models::WebSearchAction> for WebSearchAction {
fn from(value: codex_protocol::models::WebSearchAction) -> Self {
match value {
codex_protocol::models::WebSearchAction::Search { query, queries } => {
WebSearchAction::Search { query, queries }
}
codex_protocol::models::WebSearchAction::OpenPage { url } => {
WebSearchAction::OpenPage { url }
}
codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => {
WebSearchAction::FindInPage { url, pattern }
}
codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other,
pub(crate) fn web_search_action_from_core(
value: codex_protocol::models::WebSearchAction,
) -> WebSearchAction {
match value {
codex_protocol::models::WebSearchAction::Search { query, queries } => {
WebSearchAction::Search { query, queries }
}
codex_protocol::models::WebSearchAction::OpenPage { url } => {
WebSearchAction::OpenPage { url }
}
codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => {
WebSearchAction::FindInPage { url, pattern }
}
codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other,
}
}
@@ -918,11 +898,11 @@ impl From<CoreTurnItem> for ThreadItem {
agent_thread_id: activity.agent_thread_id.to_string(),
agent_path: String::from(activity.agent_path),
},
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch(WebSearchItem {
id: search.id,
query: search.query,
action: Some(WebSearchAction::from(search.action)),
},
action: Some(web_search_action_from_core(search.action)),
}),
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
id: image.id,
path: image.path.into(),
@@ -931,12 +911,26 @@ impl From<CoreTurnItem> for ThreadItem {
id: sleep.id,
duration_ms: sleep.duration_ms,
},
CoreTurnItem::ImageGeneration(image) => ThreadItem::ImageGeneration {
id: image.id,
status: image.status,
revised_prompt: image.revised_prompt,
result: image.result,
saved_path: image.saved_path,
CoreTurnItem::Extension(extension) => match extension {
ExtensionItem::ImageGeneration(item) => ThreadItem::ImageGeneration(item),
ExtensionItem::WebSearch(item) => ThreadItem::WebSearch(item),
},
CoreTurnItem::ImageGeneration(image) => {
ThreadItem::ImageGeneration(ImageGenerationItem {
id: image.id,
status: image.status,
revised_prompt: image.revised_prompt,
result: image.result,
saved_path: image.saved_path,
})
}
CoreTurnItem::EnteredReviewMode(review) => ThreadItem::EnteredReviewMode {
id: review.id,
review: review.user_facing_hint,
},
CoreTurnItem::ExitedReviewMode(review) => ThreadItem::ExitedReviewMode {
id: review.id,
review: review_output_text(review.review_output.as_ref()),
},
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
id: file_change.id,
@@ -19,7 +19,7 @@ use codex_protocol::items::ReasoningItem;
use codex_protocol::items::SubAgentActivityItem;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::items::WebSearchItem as CoreWebSearchItem;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
@@ -2770,7 +2770,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
}
);
let search_item = TurnItem::WebSearch(WebSearchItem {
let search_item = TurnItem::WebSearch(CoreWebSearchItem {
id: "search-1".to_string(),
query: "docs".to_string(),
action: CoreWebSearchAction::Search {
@@ -2779,16 +2779,24 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
},
});
let expected_search_item = WebSearchItem {
id: "search-1".to_string(),
query: "docs".to_string(),
action: Some(WebSearchAction::Search {
query: Some("docs".to_string()),
queries: None,
}),
};
assert_eq!(
ThreadItem::from(search_item),
ThreadItem::WebSearch {
id: "search-1".to_string(),
query: "docs".to_string(),
action: Some(WebSearchAction::Search {
query: Some("docs".to_string()),
queries: None,
}),
}
ThreadItem::WebSearch(expected_search_item.clone())
);
assert_eq!(
ThreadItem::from(TurnItem::Extension(
codex_extension_items::ExtensionItem::WebSearch(expected_search_item.clone()),
)),
ThreadItem::WebSearch(expected_search_item)
);
let image_view_item = TurnItem::ImageView(ImageViewItem {