mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Add structured asynchronous user input requests (#42178)
## What changed - Replace `send_user_message_async` with `request_user_input_async`, accepting one or more questions with optional suggested answers while allowing the turn to continue. - Attach structured question metadata to asynchronous agent messages while retaining readable fallback text, and preserve it through app-server events, thread history, and generated schemas. - Continue enabling the new tool for model catalogs that advertise either the old or new tool name. ## Testing - Cover tool registration, question validation, emitted items, continued turn execution, history preservation, and backwards-compatible deserialization. GitOrigin-RevId: ffc49aebde2e854c3c50a122aa52805fec3fc6b3
This commit is contained in:
@@ -497,6 +497,7 @@ impl ThreadHistoryBuilder {
|
||||
phase: payload.phase.clone(),
|
||||
memory_citation: payload.memory_citation.clone().map(Into::into),
|
||||
delivery: payload.delivery,
|
||||
questions: payload.questions.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1657,6 +1658,7 @@ impl From<&PendingTurn> for Turn {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::AgentMessageDelivery;
|
||||
use crate::protocol::v2::AsyncUserInputQuestion;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_extension_items::ExtensionItem as CoreExtensionItem;
|
||||
use codex_extension_items::sleep::SleepItem as CoreSleepItem;
|
||||
@@ -1759,6 +1761,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "thinking".into(),
|
||||
@@ -1779,6 +1782,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1818,6 +1822,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1852,6 +1857,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2422,12 +2428,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_agent_message_phase_and_delivery_in_history() {
|
||||
fn preserves_agent_message_phase_delivery_and_questions_in_history() {
|
||||
let questions = vec![
|
||||
AsyncUserInputQuestion {
|
||||
title: "Which environment?".into(),
|
||||
options: Some(vec!["Staging".into(), "Production".into()]),
|
||||
},
|
||||
AsyncUserInputQuestion {
|
||||
title: "Anything else?".into(),
|
||||
options: None,
|
||||
},
|
||||
];
|
||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Final reply".into(),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(questions.clone()),
|
||||
})];
|
||||
|
||||
let items = events
|
||||
@@ -2444,6 +2461,7 @@ mod tests {
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(questions),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2554,6 +2572,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "second summary".into(),
|
||||
@@ -2603,6 +2622,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
@@ -2624,6 +2644,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2656,6 +2677,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2681,6 +2703,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2701,6 +2724,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2715,6 +2739,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
@@ -2730,6 +2755,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2761,6 +2787,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2781,6 +2808,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2802,6 +2830,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2816,6 +2845,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
|
||||
];
|
||||
@@ -3849,6 +3879,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
@@ -4028,6 +4059,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -4295,6 +4327,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
|
||||
@@ -125,6 +125,7 @@ fn projects_completed_canonical_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
});
|
||||
|
||||
let user_changes = project(item_completed(thread_id, "turn-1", user_item.clone()));
|
||||
|
||||
@@ -26,6 +26,7 @@ use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianA
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
pub use codex_protocol::items::AgentMessageDelivery;
|
||||
pub use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
|
||||
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
|
||||
@@ -255,6 +256,8 @@ pub enum ThreadItem {
|
||||
memory_citation: Option<MemoryCitation>,
|
||||
#[serde(default)]
|
||||
delivery: Option<AgentMessageDelivery>,
|
||||
#[serde(default)]
|
||||
questions: Option<Vec<AsyncUserInputQuestion>>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -881,6 +884,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
phase: agent.phase,
|
||||
memory_citation: agent.memory_citation.map(Into::into),
|
||||
delivery: agent.delivery,
|
||||
questions: agent.questions,
|
||||
}
|
||||
}
|
||||
CoreTurnItem::FunctionCallOutput(output) => ThreadItem::FunctionCallOutput {
|
||||
|
||||
@@ -3071,6 +3071,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -3081,6 +3082,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3100,6 +3102,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
rollout_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
questions: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -3118,9 +3121,42 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
thread_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
|
||||
let async_item = ThreadItem::from(TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: "async-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "Which?".to_string(),
|
||||
}],
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(vec![AsyncUserInputQuestion {
|
||||
title: "Which?".to_string(),
|
||||
options: None,
|
||||
}]),
|
||||
}));
|
||||
assert_eq!(
|
||||
serde_json::to_value(&async_item).unwrap(),
|
||||
json!({
|
||||
"type": "agentMessage", "id": "async-1", "text": "Which?", "phase": "final_answer",
|
||||
"memoryCitation": null, "delivery": "async", "questions": [{"title": "Which?", "options": null}]
|
||||
})
|
||||
);
|
||||
let old_item: ThreadItem = serde_json::from_value(json!({
|
||||
"type": "agentMessage", "id": "old-1", "text": "An old message"
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
old_item,
|
||||
ThreadItem::AgentMessage {
|
||||
questions: None,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
let reasoning_item = TurnItem::Reasoning(ReasoningItem {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary_text: vec!["line one".to_string(), "line two".to_string()],
|
||||
|
||||
Reference in New Issue
Block a user