Add text element metadata to types (#9235)

Initial type tweaking PR to make the diff of
https://github.com/openai/codex/pull/9116 smaller

This should not change any behavior, just adds some fields to types
This commit is contained in:
charley-oai 2026-01-14 16:41:50 -08:00 committed by GitHub
parent 2a68b74b9b
commit 4a9c2bcc5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 483 additions and 41 deletions

View file

@ -197,6 +197,8 @@ impl ThreadHistoryBuilder {
if !payload.message.trim().is_empty() {
content.push(UserInput::Text {
text: payload.message.clone(),
// TODO: Thread text element ranges into thread history. Empty keeps old behavior.
text_elements: Vec::new(),
});
}
if let Some(images) = &payload.images {
@ -244,6 +246,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "First turn".into(),
images: Some(vec!["https://example.com/one.png".into()]),
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Hi there".into(),
@ -257,6 +261,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Second turn".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Reply two".into(),
@ -277,6 +283,7 @@ mod tests {
content: vec![
UserInput::Text {
text: "First turn".into(),
text_elements: Vec::new(),
},
UserInput::Image {
url: "https://example.com/one.png".into(),
@ -308,7 +315,8 @@ mod tests {
ThreadItem::UserMessage {
id: "item-4".into(),
content: vec![UserInput::Text {
text: "Second turn".into()
text: "Second turn".into(),
text_elements: Vec::new(),
}],
}
);
@ -327,6 +335,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Turn start".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "first summary".into(),
@ -371,6 +381,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Please do the thing".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Working...".into(),
@ -381,6 +393,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Let's try again".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "Second attempt complete.".into(),
@ -398,7 +412,8 @@ mod tests {
ThreadItem::UserMessage {
id: "item-1".into(),
content: vec![UserInput::Text {
text: "Please do the thing".into()
text: "Please do the thing".into(),
text_elements: Vec::new(),
}],
}
);
@ -418,7 +433,8 @@ mod tests {
ThreadItem::UserMessage {
id: "item-3".into(),
content: vec![UserInput::Text {
text: "Let's try again".into()
text: "Let's try again".into(),
text_elements: Vec::new(),
}],
}
);
@ -437,6 +453,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "First".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "A1".into(),
@ -444,6 +462,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Second".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "A2".into(),
@ -452,6 +472,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Third".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "A3".into(),
@ -469,6 +491,7 @@ mod tests {
id: "item-1".into(),
content: vec![UserInput::Text {
text: "First".into(),
text_elements: Vec::new(),
}],
},
ThreadItem::AgentMessage {
@ -486,6 +509,7 @@ mod tests {
id: "item-3".into(),
content: vec![UserInput::Text {
text: "Third".into(),
text_elements: Vec::new(),
}],
},
ThreadItem::AgentMessage {
@ -504,6 +528,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "One".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "A1".into(),
@ -511,6 +537,8 @@ mod tests {
EventMsg::UserMessage(UserMessageEvent {
message: "Two".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "A2".into(),

View file

@ -1543,21 +1543,55 @@ pub struct TurnInterruptParams {
pub struct TurnInterruptResponse {}
// User input types
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ByteRange {
pub start: usize,
pub end: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct TextElement {
/// Byte range in the parent `text` buffer that this element occupies.
pub byte_range: ByteRange,
/// Optional human-readable placeholder for the element, displayed in the UI.
pub placeholder: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum UserInput {
Text { text: String },
Image { url: String },
LocalImage { path: PathBuf },
Skill { name: String, path: PathBuf },
Text {
text: String,
/// UI-defined spans within `text` used to render or persist special elements.
#[serde(default)]
text_elements: Vec<TextElement>,
},
Image {
url: String,
},
LocalImage {
path: PathBuf,
},
Skill {
name: String,
path: PathBuf,
},
}
impl UserInput {
pub fn into_core(self) -> CoreUserInput {
match self {
UserInput::Text { text } => CoreUserInput::Text { text },
UserInput::Text { text, .. } => CoreUserInput::Text {
text,
// TODO: Thread text element ranges into v2 inputs. Empty keeps old behavior.
text_elements: Vec::new(),
},
UserInput::Image { url } => CoreUserInput::Image { image_url: url },
UserInput::LocalImage { path } => CoreUserInput::LocalImage { path },
UserInput::Skill { name, path } => CoreUserInput::Skill { name, path },
@ -1568,7 +1602,11 @@ impl UserInput {
impl From<CoreUserInput> for UserInput {
fn from(value: CoreUserInput) -> Self {
match value {
CoreUserInput::Text { text } => UserInput::Text { text },
CoreUserInput::Text { text, .. } => UserInput::Text {
text,
// TODO: Thread text element ranges from core into v2 inputs.
text_elements: Vec::new(),
},
CoreUserInput::Image { image_url } => UserInput::Image { url: image_url },
CoreUserInput::LocalImage { path } => UserInput::LocalImage { path },
CoreUserInput::Skill { name, path } => UserInput::Skill { name, path },
@ -2160,6 +2198,7 @@ mod tests {
content: vec![
CoreUserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
},
CoreUserInput::Image {
image_url: "https://example.com/image.png".to_string(),
@ -2181,6 +2220,7 @@ mod tests {
content: vec![
UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
},
UserInput::Image {
url: "https://example.com/image.png".to_string(),

View file

@ -256,7 +256,11 @@ fn send_message_v2_with_policies(
println!("< thread/start response: {thread_response:?}");
let mut turn_params = TurnStartParams {
thread_id: thread_response.thread.id.clone(),
input: vec![V2UserInput::Text { text: user_message }],
input: vec![V2UserInput::Text {
text: user_message,
// Plain text conversion has no UI element ranges.
text_elements: Vec::new(),
}],
..Default::default()
};
turn_params.approval_policy = approval_policy;
@ -288,6 +292,7 @@ fn send_follow_up_v2(
thread_id: thread_response.thread.id.clone(),
input: vec![V2UserInput::Text {
text: first_message,
text_elements: Vec::new(),
}],
..Default::default()
};
@ -299,6 +304,7 @@ fn send_follow_up_v2(
thread_id: thread_response.thread.id.clone(),
input: vec![V2UserInput::Text {
text: follow_up_message,
text_elements: Vec::new(),
}],
..Default::default()
};

View file

@ -3125,7 +3125,11 @@ impl CodexMessageProcessor {
let mapped_items: Vec<CoreInputItem> = items
.into_iter()
.map(|item| match item {
WireInputItem::Text { text } => CoreInputItem::Text { text },
WireInputItem::Text { text } => CoreInputItem::Text {
text,
// TODO: Thread text element ranges into v1 input handling.
text_elements: Vec::new(),
},
WireInputItem::Image { image_url } => CoreInputItem::Image { image_url },
WireInputItem::LocalImage { path } => CoreInputItem::LocalImage { path },
})
@ -3171,7 +3175,11 @@ impl CodexMessageProcessor {
let mapped_items: Vec<CoreInputItem> = items
.into_iter()
.map(|item| match item {
WireInputItem::Text { text } => CoreInputItem::Text { text },
WireInputItem::Text { text } => CoreInputItem::Text {
text,
// TODO: Thread text element ranges into v1 input handling.
text_elements: Vec::new(),
},
WireInputItem::Image { image_url } => CoreInputItem::Image { image_url },
WireInputItem::LocalImage { path } => CoreInputItem::LocalImage { path },
})
@ -3333,6 +3341,7 @@ impl CodexMessageProcessor {
id: turn_id.clone(),
content: vec![V2UserInput::Text {
text: display_text.to_string(),
text_elements: Vec::new(),
}],
}]
};

View file

@ -61,6 +61,7 @@ async fn turn_start_accepts_output_schema_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
output_schema: Some(output_schema.clone()),
..Default::default()
@ -142,6 +143,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
output_schema: Some(output_schema.clone()),
..Default::default()
@ -183,6 +185,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello again".to_string(),
text_elements: Vec::new(),
}],
output_schema: None,
..Default::default()

View file

@ -95,7 +95,8 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
assert_eq!(
content,
&vec![UserInput::Text {
text: preview.to_string()
text: preview.to_string(),
text_elements: Vec::new(),
}]
);
}

View file

@ -118,7 +118,8 @@ async fn thread_resume_returns_rollout_history() -> Result<()> {
assert_eq!(
content,
&vec![UserInput::Text {
text: preview.to_string()
text: preview.to_string(),
text_elements: Vec::new(),
}]
);
}

View file

@ -57,6 +57,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: first_text.to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
@ -77,6 +78,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Second".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
@ -115,7 +117,8 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()
assert_eq!(
content,
&vec![V2UserInput::Text {
text: first_text.to_string()
text: first_text.to_string(),
text_elements: Vec::new(),
}]
);
}
@ -143,7 +146,8 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()
assert_eq!(
content,
&vec![V2UserInput::Text {
text: first_text.to_string()
text: first_text.to_string(),
text_elements: Vec::new(),
}]
);
}

View file

@ -73,6 +73,7 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run sleep".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(working_directory.clone()),
..Default::default()

View file

@ -80,6 +80,7 @@ async fn turn_start_sends_originator_header() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
@ -149,6 +150,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
@ -181,6 +183,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Second".to_string(),
text_elements: Vec::new(),
}],
model: Some("mock-model-override".to_string()),
..Default::default()
@ -331,6 +334,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run python".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
@ -376,6 +380,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run python again".to_string(),
text_elements: Vec::new(),
}],
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
@ -452,6 +457,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run python".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
@ -600,6 +606,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "first turn".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(first_cwd.clone()),
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
@ -633,6 +640,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(second_cwd.clone()),
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
@ -733,6 +741,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
@ -910,6 +919,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "apply patch 1".into(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
@ -986,6 +996,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "apply patch 2".into(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
@ -1083,6 +1094,7 @@ async fn turn_start_file_change_approval_decline_v2() -> Result<()> {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "apply patch".into(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
@ -1230,6 +1242,7 @@ unified_exec = true
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run a command".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})

View file

@ -56,7 +56,11 @@ impl AgentControl {
.send_op(
agent_id,
Op::UserInput {
items: vec![UserInput::Text { text: prompt }],
items: vec![UserInput::Text {
text: prompt,
// Plain text conversion has no UI element ranges.
text_elements: Vec::new(),
}],
final_output_json_schema: None,
},
)
@ -321,6 +325,7 @@ mod tests {
Op::UserInput {
items: vec![UserInput::Text {
text: "hello from tests".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
},
@ -351,6 +356,7 @@ mod tests {
Op::UserInput {
items: vec![UserInput::Text {
text: "spawned".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
},

View file

@ -2260,6 +2260,7 @@ mod handlers {
Arc::clone(&turn_context),
vec![UserInput::Text {
text: turn_context.compact_prompt().to_string(),
text_elements: Vec::new(),
}],
CompactTask,
)
@ -2470,6 +2471,7 @@ async fn spawn_review_thread(
// Seed the child task with the review prompt as the initial user message.
let input: Vec<UserInput> = vec![UserInput::Text {
text: review_prompt,
text_elements: Vec::new(),
}];
let tc = Arc::new(review_turn_context);
sess.spawn_task(tc.clone(), input, ReviewTask::new()).await;
@ -3964,6 +3966,7 @@ mod tests {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
sess.spawn_task(
Arc::clone(&tc),
@ -3993,6 +3996,7 @@ mod tests {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
sess.spawn_task(
Arc::clone(&tc),
@ -4019,6 +4023,7 @@ mod tests {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
text: "start review".to_string(),
text_elements: Vec::new(),
}];
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
.await;

View file

@ -44,7 +44,11 @@ pub(crate) async fn run_inline_auto_compact_task(
turn_context: Arc<TurnContext>,
) {
let prompt = turn_context.compact_prompt().to_string();
let input = vec![UserInput::Text { text: prompt }];
let input = vec![UserInput::Text {
text: prompt,
// Plain text conversion has no UI element ranges.
text_elements: Vec::new(),
}];
run_compact_task_inner(sess, turn_context, input).await;
}

View file

@ -50,7 +50,11 @@ fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
if is_session_prefix(text) || is_user_shell_command_text(text) {
return None;
}
content.push(UserInput::Text { text: text.clone() });
content.push(UserInput::Text {
text: text.clone(),
// Plain text conversion has no UI element ranges.
text_elements: Vec::new(),
});
}
ContentItem::InputImage { image_url } => {
content.push(UserInput::Image {
@ -179,6 +183,7 @@ mod tests {
let expected_content = vec![
UserInput::Text {
text: "Hello world".to_string(),
text_elements: Vec::new(),
},
UserInput::Image { image_url: img1 },
UserInput::Image { image_url: img2 },
@ -218,7 +223,10 @@ mod tests {
TurnItem::UserMessage(user) => {
let expected_content = vec![
UserInput::Image { image_url },
UserInput::Text { text: user_text },
UserInput::Text {
text: user_text,
text_elements: Vec::new(),
},
];
assert_eq!(user.content, expected_content);
}
@ -255,7 +263,10 @@ mod tests {
TurnItem::UserMessage(user) => {
let expected_content = vec![
UserInput::Image { image_url },
UserInput::Text { text: user_text },
UserInput::Text {
text: user_text,
text_elements: Vec::new(),
},
];
assert_eq!(user.content, expected_content);
}

View file

@ -603,6 +603,8 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "hello".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
})),
};
writeln!(file, "{}", serde_json::to_string(&user_event_line)?)?;

View file

@ -270,6 +270,7 @@ impl TestCodex {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: prompt.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: self.cwd.path().to_path_buf(),

View file

@ -48,6 +48,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "start sleep".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -101,6 +102,7 @@ async fn interrupt_tool_records_history_entries() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "start history recording".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -118,6 +120,7 @@ async fn interrupt_tool_records_history_entries() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "follow up".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -302,6 +302,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "rename without content change".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -888,6 +889,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "apply via shell heredoc with cd".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -965,6 +967,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "apply patch via shell".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1112,6 +1115,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "emit diff".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1172,6 +1176,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "rename with change".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1240,6 +1245,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "aggregate diffs".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1308,6 +1314,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "apply patch twice with failure".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),

View file

@ -492,6 +492,7 @@ async fn submit_turn(
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: prompt.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),

View file

@ -289,6 +289,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -387,6 +388,7 @@ async fn includes_conversation_id_and_model_headers_in_request() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -440,6 +442,7 @@ async fn includes_base_instructions_override_in_request() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -496,6 +499,7 @@ async fn chatgpt_auth_sends_correct_request() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -588,6 +592,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -629,6 +634,7 @@ async fn includes_user_instructions_message_in_request() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -708,6 +714,7 @@ async fn skills_append_to_instructions() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -760,6 +767,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -797,6 +805,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -832,6 +841,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -871,6 +881,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -910,6 +921,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -943,6 +955,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -983,6 +996,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1022,6 +1036,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1077,6 +1092,7 @@ async fn includes_developer_instructions_message_in_request() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1326,6 +1342,7 @@ async fn token_count_includes_rate_limits_snapshot() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1484,6 +1501,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1554,6 +1572,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "seed turn".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1565,6 +1584,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "trigger context window".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1685,6 +1705,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1768,6 +1789,7 @@ async fn env_var_overrides_loaded_auth() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1840,7 +1862,10 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
// Turn 1: user sends U1; wait for completion.
codex
.submit(Op::UserInput {
items: vec![UserInput::Text { text: "U1".into() }],
items: vec![UserInput::Text {
text: "U1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await
@ -1850,7 +1875,10 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
// Turn 2: user sends U2; wait for completion.
codex
.submit(Op::UserInput {
items: vec![UserInput::Text { text: "U2".into() }],
items: vec![UserInput::Text {
text: "U2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await
@ -1860,7 +1888,10 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
// Turn 3: user sends U3; wait for completion.
codex
.submit(Op::UserInput {
items: vec![UserInput::Text { text: "U3".into() }],
items: vec![UserInput::Text {
text: "U3".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await

View file

@ -159,6 +159,7 @@ async fn summarize_context_three_requests_and_instructions() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello world".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -180,6 +181,7 @@ async fn summarize_context_three_requests_and_instructions() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: THIRD_USER_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -573,6 +575,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: user_message.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1049,6 +1052,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: FIRST_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1061,6 +1065,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: SECOND_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1073,6 +1078,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: POST_AUTO_USER_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1273,6 +1279,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: follow_up_user.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: resumed.cwd.path().to_path_buf(),
@ -1382,6 +1389,7 @@ async fn auto_compact_persists_rollout_entries() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: FIRST_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1393,6 +1401,7 @@ async fn auto_compact_persists_rollout_entries() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: SECOND_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1404,6 +1413,7 @@ async fn auto_compact_persists_rollout_entries() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: POST_AUTO_USER_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1496,6 +1506,7 @@ async fn manual_compact_retries_after_context_window_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "first turn".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1629,6 +1640,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: first_user_message.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1643,6 +1655,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: second_user_message.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1657,6 +1670,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: final_user_message.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1835,7 +1849,10 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
for user in [MULTI_AUTO_MSG, follow_up_user, final_user] {
codex
.submit(Op::UserInput {
items: vec![UserInput::Text { text: user.into() }],
items: vec![UserInput::Text {
text: user.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await
@ -1948,6 +1965,7 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: FUNCTION_CALL_LIMIT_MSG.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1960,6 +1978,7 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: follow_up_user.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -2075,7 +2094,10 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
{
codex
.submit(Op::UserInput {
items: vec![UserInput::Text { text: user.into() }],
items: vec![UserInput::Text {
text: user.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await

View file

@ -73,6 +73,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello remote compact".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -86,6 +87,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "after compact".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -192,6 +194,7 @@ async fn remote_compact_runs_automatically() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello remote compact".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -265,6 +268,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "needs compaction".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -983,7 +983,10 @@ async fn start_test_conversation(
async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
conversation
.submit(Op::UserInput {
items: vec![UserInput::Text { text: text.into() }],
items: vec![UserInput::Text {
text: text.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await

View file

@ -72,6 +72,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run shell command".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),

View file

@ -70,6 +70,7 @@ async fn fork_thread_twice_drops_to_first_message() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: text.to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -117,6 +117,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
},
UserInput::Text {
text: "pasted image".to_string(),
text_elements: Vec::new(),
},
],
final_output_json_schema: None,
@ -194,6 +195,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
},
UserInput::Text {
text: "dropped image".to_string(),
text_elements: Vec::new(),
},
],
final_output_json_schema: None,

View file

@ -42,6 +42,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: (vec![UserInput::Text {
text: "please inspect sample.txt".into(),
text_elements: Vec::new(),
}]),
final_output_json_schema: None,
})
@ -69,12 +70,14 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
started_item.content,
vec![UserInput::Text {
text: "please inspect sample.txt".into(),
text_elements: Vec::new(),
}]
);
assert_eq!(
completed_item.content,
vec![UserInput::Text {
text: "please inspect sample.txt".into(),
text_elements: Vec::new(),
}]
);
Ok(())
@ -99,6 +102,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "please summarize results".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -156,6 +160,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "explain your reasoning".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -215,6 +220,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "find the weather".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -268,6 +274,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "please stream text".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -334,6 +341,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "reason through it".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -392,6 +400,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "show raw reasoning".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -76,6 +76,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello world".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?),
cwd: cwd.path().to_path_buf(),

View file

@ -87,7 +87,10 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text { text: "hi".into() }],
items: vec![UserInput::Text {
text: "hi".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: codex_core::protocol::AskForApproval::Never,

View file

@ -100,6 +100,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please run a tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),

View file

@ -45,6 +45,7 @@ async fn responses_api_emits_api_request_event() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -87,6 +88,7 @@ async fn process_sse_emits_tracing_for_output_item() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -126,6 +128,7 @@ async fn process_sse_emits_failed_event_on_parse_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -166,6 +169,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed()
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -226,6 +230,7 @@ async fn process_sse_failed_event_records_response_error_message() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -284,6 +289,7 @@ async fn process_sse_failed_event_logs_parse_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -329,6 +335,7 @@ async fn process_sse_failed_event_logs_missing_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -383,6 +390,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -434,6 +442,7 @@ async fn process_sse_emits_completed_telemetry() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -502,6 +511,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -567,6 +577,7 @@ async fn record_responses_sets_span_fields_for_response_events() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -647,6 +658,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -715,6 +727,7 @@ async fn handle_response_item_records_tool_result_for_function_call() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -793,6 +806,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_missing_ids()
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -855,6 +869,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -960,6 +975,7 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1009,6 +1025,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "approved".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1068,6 +1085,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision()
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "persist".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1127,6 +1145,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "retry".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1186,6 +1205,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "deny".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1245,6 +1265,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "persist".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -1305,6 +1326,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "deny".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -100,6 +100,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "first prompt".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -115,6 +116,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "second prompt".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -61,6 +61,7 @@ async fn permissions_message_sent_once_on_start() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -93,6 +94,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -114,6 +116,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -152,6 +155,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -162,6 +166,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -203,6 +208,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -226,6 +232,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -238,6 +245,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "after resume".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -276,6 +284,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -299,6 +308,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -319,6 +329,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "after resume".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -346,6 +357,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "after fork".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -393,6 +405,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -108,6 +108,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -118,6 +119,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -180,6 +182,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -190,6 +193,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -241,6 +245,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -251,6 +256,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -316,6 +322,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -345,6 +352,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -406,6 +414,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "first message".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -517,6 +526,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -536,6 +546,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: new_cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
@ -627,6 +638,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
approval_policy: default_approval_policy,
@ -643,6 +655,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
approval_policy: default_approval_policy,
@ -720,6 +733,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
approval_policy: default_approval_policy,
@ -736,6 +750,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
approval_policy: AskForApproval::Never,

View file

@ -43,6 +43,7 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "quota?".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -165,6 +165,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run call".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -369,6 +370,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello remote".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),

View file

@ -39,6 +39,7 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "compress me".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -82,6 +83,7 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() ->
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "do not compress".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -36,6 +36,7 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "Record some messages".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -89,6 +90,7 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()>
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "Record reasoning messages".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -707,6 +707,7 @@ async fn review_history_surfaces_in_parent_session() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: followup.clone(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -108,6 +108,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
@ -244,6 +245,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp image tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
@ -438,6 +440,7 @@ async fn stdio_image_completions_round_trip() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp image tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
@ -580,6 +583,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
@ -733,6 +737,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp streamable http echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
@ -918,6 +923,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp streamable http oauth echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),

View file

@ -71,6 +71,7 @@ async fn run_snapshot_command(command: &str) -> Result<SnapshotRun> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run unified exec with shell snapshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd,
@ -147,6 +148,7 @@ async fn run_shell_command_snapshot(command: &str) -> Result<SnapshotRun> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run shell_command with shell snapshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd,
@ -284,6 +286,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "apply patch via shell_command with snapshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.clone(),

View file

@ -64,6 +64,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
items: vec![
UserInput::Text {
text: "please use $demo".to_string(),
text_elements: Vec::new(),
},
UserInput::Skill {
name: "demo".to_string(),

View file

@ -88,6 +88,7 @@ async fn continue_after_stream_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "first message".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
@ -106,6 +107,7 @@ async fn continue_after_stream_error() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "follow up".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -95,6 +95,7 @@ async fn retries_on_early_close() {
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -82,6 +82,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()>
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please run the shell command".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -148,6 +149,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please update the plan".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -224,6 +226,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please update the plan".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -312,6 +315,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please apply a patch".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -408,6 +412,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please apply a patch".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),

View file

@ -38,6 +38,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: prompt.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
@ -353,6 +354,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "stream delayed completion".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),

View file

@ -536,6 +536,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp image tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),

View file

@ -200,6 +200,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "apply patch via unified exec".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd,
@ -326,6 +327,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "emit begin event".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -401,6 +403,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run relative workdir test".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -479,6 +482,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run workdir test".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -569,6 +573,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "emit end event".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -641,6 +646,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "emit delta".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -714,6 +720,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "exercise full unified exec lifecycle".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -840,6 +847,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()>
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "stdin delta".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -973,6 +981,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "delayed terminal interaction output".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1130,6 +1139,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check poll event behavior".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1225,6 +1235,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run metadata test".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1340,6 +1351,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check default pipe mode".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1427,6 +1439,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check tty enabled".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1505,6 +1518,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "watch early exit timing".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1633,6 +1647,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "test write_stdin exit behavior".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1798,6 +1813,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "end on exit".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1872,6 +1888,7 @@ async fn unified_exec_closes_long_running_session_at_turn_end() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "close unified exec processes on turn end".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -1990,6 +2007,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "run unified exec".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2122,6 +2140,7 @@ PY
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "exercise lag handling".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2233,6 +2252,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check timeout".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2326,6 +2346,7 @@ PY
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "summarize large output".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2404,6 +2425,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "summarize large output".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2506,6 +2528,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "start python under seatbelt".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2598,6 +2621,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "summarize large output".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -2728,6 +2752,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "fill session cache".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),

View file

@ -60,6 +60,7 @@ echo -n "${@: -1}" > $(dirname "${0}")/notify.txt"#,
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello world".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})

View file

@ -171,6 +171,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please add the screenshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -301,6 +302,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please attach the folder".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -373,6 +375,7 @@ async fn view_image_tool_placeholder_for_non_image_files() -> anyhow::Result<()>
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please use the view_image tool to read the json file".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
@ -464,6 +467,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please attach the missing image".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),

View file

@ -184,7 +184,11 @@ impl AppServerClient {
request_id: request_id.clone(),
params: TurnStartParams {
thread_id: thread_id.to_string(),
input: vec![UserInput::Text { text }],
input: vec![UserInput::Text {
text,
// Plain text conversion has no UI element ranges.
text_elements: Vec::new(),
}],
..Default::default()
},
};

View file

@ -360,6 +360,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
.collect();
items.push(UserInput::Text {
text: prompt_text.clone(),
text_elements: Vec::new(),
});
let output_schema = load_output_schema(output_schema_path.clone());
(
@ -378,6 +379,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
.collect();
items.push(UserInput::Text {
text: prompt_text.clone(),
text_elements: Vec::new(),
});
let output_schema = load_output_schema(output_schema_path);
(

View file

@ -116,6 +116,7 @@ pub async fn run_codex_tool_session(
op: Op::UserInput {
items: vec![UserInput::Text {
text: initial_prompt.clone(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
},
@ -158,7 +159,11 @@ pub async fn run_codex_tool_session_reply(
.insert(request_id.clone(), thread_id);
if let Err(e) = thread
.submit(Op::UserInput {
items: vec![UserInput::Text { text: prompt }],
items: vec![UserInput::Text {
text: prompt,
// Plain text conversion has no UI element ranges.
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await

View file

@ -324,7 +324,7 @@ impl OtelManager {
let prompt = items
.iter()
.flat_map(|item| match item {
UserInput::Text { text } => Some(text.as_str()),
UserInput::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<String>();

View file

@ -65,6 +65,10 @@ impl UserMessageItem {
EventMsg::UserMessage(UserMessageEvent {
message: self.message(),
images: Some(self.image_urls()),
// TODO: Thread text element ranges into legacy user message events.
text_elements: Vec::new(),
// TODO: Thread local image paths into legacy user message events.
local_images: Vec::new(),
})
}
@ -72,7 +76,7 @@ impl UserMessageItem {
self.content
.iter()
.map(|c| match c {
UserInput::Text { text } => text.clone(),
UserInput::Text { text, .. } => text.clone(),
_ => String::new(),
})
.collect::<Vec<String>>()

View file

@ -550,7 +550,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
content: items
.into_iter()
.flat_map(|c| match c {
UserInput::Text { text } => vec![ContentItem::InputText { text }],
UserInput::Text { text, .. } => vec![ContentItem::InputText { text }],
UserInput::Image { image_url } => vec![
ContentItem::InputText {
text: image_open_tag_text(),

View file

@ -1255,8 +1255,19 @@ pub struct AgentMessageEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct UserMessageEvent {
pub message: String,
/// Image URLs sourced from `UserInput::Image`. These are safe
/// to replay in legacy UI history events and correspond to images sent to
/// the model.
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<String>>,
/// Local file paths sourced from `UserInput::LocalImage`. These are kept so
/// the UI can reattach images when editing history, and should not be sent
/// to the model or treated as API-ready URLs.
#[serde(default)]
pub local_images: Vec<std::path::PathBuf>,
/// UI-defined spans within `message` used to render or persist special elements.
#[serde(default)]
pub text_elements: Vec<crate::user_input::TextElement>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@ -2298,6 +2309,48 @@ mod tests {
Ok(())
}
#[test]
fn user_input_text_serializes_empty_text_elements() -> Result<()> {
let input = UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
};
let json_input = serde_json::to_value(input)?;
assert_eq!(
json_input,
json!({
"type": "text",
"text": "hello",
"text_elements": [],
})
);
Ok(())
}
#[test]
fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
let event = UserMessageEvent {
message: "hello".to_string(),
images: None,
local_images: Vec::new(),
text_elements: Vec::new(),
};
let json_event = serde_json::to_value(event)?;
assert_eq!(
json_event,
json!({
"message": "hello",
"local_images": [],
"text_elements": [],
})
);
Ok(())
}
/// Serialize Event to verify that its JSON representation has the expected
/// amount of nesting.
#[test]

View file

@ -10,17 +10,19 @@ use ts_rs::TS;
pub enum UserInput {
Text {
text: String,
/// UI-defined spans within `text` that should be treated as special elements.
/// These are byte ranges into the UTF-8 `text` buffer and are used to render
/// or persist rich input markers (e.g., image placeholders) across history
/// and resume without mutating the literal text.
#[serde(default)]
text_elements: Vec<TextElement>,
},
/// Preencoded data: URI image.
Image {
image_url: String,
},
Image { image_url: String },
/// Local image path provided by the user. This will be converted to an
/// `Image` variant (base64 data URL) during request serialization.
LocalImage {
path: std::path::PathBuf,
},
LocalImage { path: std::path::PathBuf },
/// Skill selected by the user (name + path to SKILL.md).
Skill {
@ -28,3 +30,28 @@ pub enum UserInput {
path: std::path::PathBuf,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)]
pub struct TextElement {
/// Byte range in the parent `text` buffer that this element occupies.
pub byte_range: ByteRange,
/// Optional human-readable placeholder for the element, displayed in the UI.
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
pub struct ByteRange {
/// Start byte offset (inclusive) within the UTF-8 text buffer.
pub start: usize,
/// End byte offset (exclusive) within the UTF-8 text buffer.
pub end: usize,
}
impl From<std::ops::Range<usize>> for ByteRange {
fn from(range: std::ops::Range<usize>) -> Self {
Self {
start: range.start,
end: range.end,
}
}
}

View file

@ -2266,7 +2266,11 @@ impl ChatWidget {
}
if !text.is_empty() {
items.push(UserInput::Text { text: text.clone() });
// TODO: Thread text element ranges from the composer input. Empty keeps old behavior.
items.push(UserInput::Text {
text: text.clone(),
text_elements: Vec::new(),
});
}
if let Some(skills) = self.bottom_pane.skills() {

View file

@ -143,6 +143,8 @@ async fn resumed_initial_messages_render_history() {
EventMsg::UserMessage(UserMessageEvent {
message: "hello from user".to_string(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(),

View file

@ -2017,7 +2017,11 @@ impl ChatWidget {
}
if !text.is_empty() {
items.push(UserInput::Text { text: text.clone() });
// TODO: Thread text element ranges from the composer input. Empty keeps old behavior.
items.push(UserInput::Text {
text: text.clone(),
text_elements: Vec::new(),
});
}
if let Some(skills) = self.bottom_pane.skills() {

View file

@ -132,6 +132,8 @@ async fn resumed_initial_messages_render_history() {
EventMsg::UserMessage(UserMessageEvent {
message: "hello from user".to_string(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(),