mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Support assistant realtime append text (#28836)
## Why
Frontend realtime voice continuity needs to replay a tiny
previous-session overlap as actual conversation items, including
assistant text. The app-server `thread/realtime/appendText` API already
carries a role through to the Rust realtime websocket layer, but the
shared role enum only accepted `user` and `developer`.
## What Changed
- Added `assistant` to `ConversationTextRole` and regenerated the
app-server schema/type fixtures.
- Added `output_text` as a realtime conversation content type.
- Updated realtime websocket item creation so assistant appendText emits
`content: [{ type: "output_text", text }]`, while user and developer
continue to emit `input_text`.
- Updated app-server docs and tests to cover assistant appendText
alongside the existing developer role behavior.
## Validation
- `just write-app-server-schema`
- `just fmt` (first sandboxed attempt failed because `uv` could not
access `~/.cache/uv`; reran with filesystem access and passed)
- `just test -p codex-api` passed: 126/126
- `just test -p codex-app-server-protocol` passed: 239/239, including
generated JSON/TypeScript fixture checks
- `just test -p codex-app-server` was started locally but stopped per
request after unrelated local sandbox/Seatbelt failures (`sandbox-exec:
sandbox_apply: Operation not permitted`) and one missing local `codex`
binary failure; CI should be faster and more authoritative for the full
suite.
This commit is contained in:
committed by
GitHub
Unverified
parent
683bd170dc
commit
e922f46a0f
@@ -1637,10 +1637,29 @@ mod tests {
|
||||
.into_text()
|
||||
.expect("text");
|
||||
let fourth_json: Value = serde_json::from_str(&fourth).expect("json");
|
||||
assert_eq!(fourth_json["type"], "conversation.handoff.append");
|
||||
assert_eq!(fourth_json["handoff_id"], "handoff_1");
|
||||
assert_eq!(fourth_json["type"], "conversation.item.create");
|
||||
assert_eq!(fourth_json["item"]["role"], "assistant");
|
||||
assert_eq!(
|
||||
fourth_json["output_text"],
|
||||
fourth_json["item"]["content"][0]["type"],
|
||||
Value::String("output_text".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
fourth_json["item"]["content"][0]["text"],
|
||||
Value::String("assistant context".to_string())
|
||||
);
|
||||
|
||||
let fifth = ws
|
||||
.next()
|
||||
.await
|
||||
.expect("fifth msg")
|
||||
.expect("fifth msg ok")
|
||||
.into_text()
|
||||
.expect("text");
|
||||
let fifth_json: Value = serde_json::from_str(&fifth).expect("json");
|
||||
assert_eq!(fifth_json["type"], "conversation.handoff.append");
|
||||
assert_eq!(fifth_json["handoff_id"], "handoff_1");
|
||||
assert_eq!(
|
||||
fifth_json["output_text"],
|
||||
"\"Agent Final Message\":\n\nhello from background agent"
|
||||
);
|
||||
|
||||
@@ -1766,6 +1785,13 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("send item");
|
||||
connection
|
||||
.send_conversation_item_create(
|
||||
"assistant context".to_string(),
|
||||
ConversationTextRole::Assistant,
|
||||
)
|
||||
.await
|
||||
.expect("send assistant item");
|
||||
connection
|
||||
.send_conversation_function_call_output(
|
||||
"handoff_1".to_string(),
|
||||
@@ -1988,16 +2014,35 @@ mod tests {
|
||||
.expect("text");
|
||||
let third_json: Value = serde_json::from_str(&third).expect("json");
|
||||
assert_eq!(third_json["type"], "conversation.item.create");
|
||||
assert_eq!(third_json["item"]["role"], "assistant");
|
||||
assert_eq!(
|
||||
third_json["item"]["type"],
|
||||
third_json["item"]["content"][0]["type"],
|
||||
Value::String("output_text".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
third_json["item"]["content"][0]["text"],
|
||||
Value::String("assistant context".to_string())
|
||||
);
|
||||
|
||||
let fourth = ws
|
||||
.next()
|
||||
.await
|
||||
.expect("fourth msg")
|
||||
.expect("fourth msg ok")
|
||||
.into_text()
|
||||
.expect("text");
|
||||
let fourth_json: Value = serde_json::from_str(&fourth).expect("json");
|
||||
assert_eq!(fourth_json["type"], "conversation.item.create");
|
||||
assert_eq!(
|
||||
fourth_json["item"]["type"],
|
||||
Value::String("function_call_output".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
third_json["item"]["call_id"],
|
||||
fourth_json["item"]["call_id"],
|
||||
Value::String("call_1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
third_json["item"]["output"],
|
||||
fourth_json["item"]["output"],
|
||||
Value::String("delegated result".to_string())
|
||||
);
|
||||
});
|
||||
@@ -2054,6 +2099,13 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("send text item");
|
||||
connection
|
||||
.send_conversation_item_create(
|
||||
"assistant context".to_string(),
|
||||
ConversationTextRole::Assistant,
|
||||
)
|
||||
.await
|
||||
.expect("send assistant item");
|
||||
connection
|
||||
.send_conversation_function_call_output(
|
||||
"call_1".to_string(),
|
||||
|
||||
@@ -19,12 +19,19 @@ pub(super) fn conversation_item_create_message(
|
||||
text: String,
|
||||
role: ConversationTextRole,
|
||||
) -> RealtimeOutboundMessage {
|
||||
let content_type = match role {
|
||||
ConversationTextRole::Assistant => ConversationContentType::OutputText,
|
||||
ConversationTextRole::User | ConversationTextRole::Developer => {
|
||||
ConversationContentType::InputText
|
||||
}
|
||||
};
|
||||
|
||||
RealtimeOutboundMessage::ConversationItemCreate {
|
||||
item: ConversationItemPayload::Message(ConversationMessageItem {
|
||||
r#type: ConversationItemType::Message,
|
||||
role,
|
||||
content: vec![ConversationItemContent {
|
||||
r#type: ConversationContentType::InputText,
|
||||
r#type: content_type,
|
||||
text,
|
||||
}],
|
||||
}),
|
||||
|
||||
@@ -40,12 +40,19 @@ pub(super) fn conversation_item_create_message(
|
||||
text: String,
|
||||
role: ConversationTextRole,
|
||||
) -> RealtimeOutboundMessage {
|
||||
let content_type = match role {
|
||||
ConversationTextRole::Assistant => ConversationContentType::OutputText,
|
||||
ConversationTextRole::User | ConversationTextRole::Developer => {
|
||||
ConversationContentType::InputText
|
||||
}
|
||||
};
|
||||
|
||||
RealtimeOutboundMessage::ConversationItemCreate {
|
||||
item: ConversationItemPayload::Message(ConversationMessageItem {
|
||||
r#type: ConversationItemType::Message,
|
||||
role,
|
||||
content: vec![ConversationItemContent {
|
||||
r#type: ConversationContentType::InputText,
|
||||
r#type: content_type,
|
||||
text,
|
||||
}],
|
||||
}),
|
||||
|
||||
@@ -195,6 +195,7 @@ pub(super) struct ConversationItemContent {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(super) enum ConversationContentType {
|
||||
InputText,
|
||||
OutputText,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
Reference in New Issue
Block a user