[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:
guinness-oai authored and GitHub committed 2026-06-17 20:57:13 -07:00
1 parent 683bd170dc
commit e922f46a0f
11 files changed
+123 -18

No files matched your search

@@ -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(),