mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add user input client ids (#24653)
## Summary Adds an optional `clientId` field to app-server v2 `UserInput` and carries it through the core `UserInput` model so clients can correlate echoed user input items without relying on payload equality. ## Details - Adds `client_id: Option<String>` to core `UserInput` variants. - Exposes the v2 app-server field as `clientId` on the wire and in generated TypeScript. - Preserves the id when converting between app-server v2 and core protocol types. - Regenerates app-server schema fixtures. ## Validation - `just fmt` - `just write-app-server-schema` - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-protocol` - `just fix -p codex-app-server-protocol` - `just fix -p codex-protocol` - `git diff --check`
This commit is contained in:
@@ -56,6 +56,9 @@ pub enum TurnItem {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
pub struct UserMessageItem {
|
||||
pub id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub client_id: Option<String>,
|
||||
pub content: Vec<UserInput>,
|
||||
}
|
||||
|
||||
@@ -237,6 +240,7 @@ impl UserMessageItem {
|
||||
pub fn new(content: &[UserInput]) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
client_id: None,
|
||||
content: content.to_vec(),
|
||||
}
|
||||
}
|
||||
@@ -245,6 +249,7 @@ impl UserMessageItem {
|
||||
// Legacy user-message events flatten only text inputs into `message` and
|
||||
// rebase text element ranges onto that concatenated text.
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: self.client_id.clone(),
|
||||
message: self.message(),
|
||||
images: Some(self.image_urls()),
|
||||
image_details: self.image_details(),
|
||||
@@ -272,6 +277,7 @@ impl UserMessageItem {
|
||||
if let UserInput::Text {
|
||||
text,
|
||||
text_elements,
|
||||
..
|
||||
} = input
|
||||
{
|
||||
// Text element ranges are relative to each text chunk; offset them so they align
|
||||
|
||||
@@ -1237,7 +1237,9 @@ impl From<Vec<UserInput>> for ResponseInputItem {
|
||||
.into_iter()
|
||||
.flat_map(|c| match c {
|
||||
UserInput::Text { text, .. } => vec![ContentItem::InputText { text }],
|
||||
UserInput::Image { image_url, detail } => {
|
||||
UserInput::Image {
|
||||
image_url, detail, ..
|
||||
} => {
|
||||
image_index += 1;
|
||||
let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL);
|
||||
vec![ContentItem::InputImage {
|
||||
@@ -1245,7 +1247,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
|
||||
detail: Some(detail),
|
||||
}]
|
||||
}
|
||||
UserInput::LocalImage { path, detail } => {
|
||||
UserInput::LocalImage { path, detail, .. } => {
|
||||
image_index += 1;
|
||||
let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL);
|
||||
match std::fs::read(&path) {
|
||||
|
||||
@@ -129,6 +129,9 @@ pub struct Submission {
|
||||
pub id: String,
|
||||
/// Payload
|
||||
pub op: Op,
|
||||
/// Client-provided id for the user message represented by `Op::UserInput`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub client_user_message_id: Option<String>,
|
||||
/// Optional W3C trace carrier propagated across async submission handoffs.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trace: Option<W3cTraceContext>,
|
||||
@@ -2164,6 +2167,8 @@ pub struct AgentMessageEvent {
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct UserMessageEvent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub client_id: Option<String>,
|
||||
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
|
||||
@@ -5088,6 +5093,7 @@ mod tests {
|
||||
#[test]
|
||||
fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
|
||||
let event = UserMessageEvent {
|
||||
client_id: None,
|
||||
message: "hello".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
@@ -5133,7 +5139,7 @@ mod tests {
|
||||
#[test]
|
||||
fn user_message_item_legacy_event_preserves_image_details() {
|
||||
let local_path = PathBuf::from("/tmp/local.png");
|
||||
let item = UserMessageItem::new(&[
|
||||
let mut item = UserMessageItem::new(&[
|
||||
crate::user_input::UserInput::Image {
|
||||
image_url: "https://example.com/first.png".to_string(),
|
||||
detail: Some(ImageDetail::Original),
|
||||
@@ -5147,6 +5153,7 @@ mod tests {
|
||||
detail: Some(ImageDetail::Original),
|
||||
},
|
||||
]);
|
||||
item.client_id = Some("client-message-1".to_string());
|
||||
|
||||
let EventMsg::UserMessage(event) = item.as_legacy_event() else {
|
||||
panic!("expected user message event");
|
||||
@@ -5159,6 +5166,7 @@ mod tests {
|
||||
"https://example.com/second.png".to_string(),
|
||||
])
|
||||
);
|
||||
assert_eq!(event.client_id, Some("client-message-1".to_string()));
|
||||
assert_eq!(event.image_details, vec![Some(ImageDetail::Original)]);
|
||||
assert_eq!(event.local_images, vec![local_path]);
|
||||
assert_eq!(event.local_image_details, vec![Some(ImageDetail::Original)]);
|
||||
|
||||
Reference in New Issue
Block a user