mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Route terminal Codex output to realtime
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::endpoint::realtime_websocket::methods_common::conversation_item_creat
|
||||
use crate::endpoint::realtime_websocket::methods_common::normalized_session_mode;
|
||||
use crate::endpoint::realtime_websocket::methods_common::session_update_session;
|
||||
use crate::endpoint::realtime_websocket::methods_common::websocket_intent;
|
||||
use crate::endpoint::realtime_websocket::protocol::ConversationRole;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeAudioFrame;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeEvent;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeEventParser;
|
||||
@@ -241,6 +242,16 @@ impl RealtimeWebsocketConnection {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_conversation_handoff_append(
|
||||
&self,
|
||||
handoff_id: Option<String>,
|
||||
output_text: String,
|
||||
) -> Result<(), ApiError> {
|
||||
self.writer
|
||||
.send_conversation_handoff_append(handoff_id, output_text)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn close(&self) -> Result<(), ApiError> {
|
||||
self.writer.close().await
|
||||
}
|
||||
@@ -287,8 +298,24 @@ impl RealtimeWebsocketWriter {
|
||||
}
|
||||
|
||||
pub async fn send_conversation_item_create(&self, text: String) -> Result<(), ApiError> {
|
||||
self.send_json(&conversation_item_create_message(self.event_parser, text))
|
||||
.await
|
||||
self.send_json(&conversation_item_create_message(
|
||||
self.event_parser,
|
||||
ConversationRole::User,
|
||||
text,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_conversation_developer_item_create(
|
||||
&self,
|
||||
text: String,
|
||||
) -> Result<(), ApiError> {
|
||||
self.send_json(&conversation_item_create_message(
|
||||
self.event_parser,
|
||||
ConversationRole::Developer,
|
||||
text,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_conversation_function_call_output(
|
||||
@@ -304,6 +331,20 @@ impl RealtimeWebsocketWriter {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_conversation_handoff_append(
|
||||
&self,
|
||||
handoff_id: Option<String>,
|
||||
output_text: String,
|
||||
) -> Result<(), ApiError> {
|
||||
self.send_json(
|
||||
&crate::endpoint::realtime_websocket::methods_v1::conversation_handoff_append_message(
|
||||
handoff_id,
|
||||
output_text,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_response_create(&self) -> Result<(), ApiError> {
|
||||
self.send_json(&RealtimeOutboundMessage::ResponseCreate)
|
||||
.await
|
||||
@@ -1611,11 +1652,12 @@ 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["output_text"],
|
||||
"\"Agent Final Message\":\n\nhello from background agent"
|
||||
fourth_json,
|
||||
json!({
|
||||
"type": "conversation.handoff.append",
|
||||
"output_text": "hello from background agent"
|
||||
})
|
||||
);
|
||||
|
||||
ws.send(Message::Text(
|
||||
@@ -1738,8 +1780,8 @@ mod tests {
|
||||
.await
|
||||
.expect("send item");
|
||||
connection
|
||||
.send_conversation_function_call_output(
|
||||
"handoff_1".to_string(),
|
||||
.send_conversation_handoff_append(
|
||||
/*handoff_id*/ None,
|
||||
"hello from background agent".to_string(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::endpoint::realtime_websocket::methods_v2::conversation_function_call_
|
||||
use crate::endpoint::realtime_websocket::methods_v2::conversation_item_create_message as v2_conversation_item_create_message;
|
||||
use crate::endpoint::realtime_websocket::methods_v2::session_update_session as v2_session_update_session;
|
||||
use crate::endpoint::realtime_websocket::methods_v2::websocket_intent as v2_websocket_intent;
|
||||
use crate::endpoint::realtime_websocket::protocol::ConversationRole;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeEventParser;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeOutboundMessage;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeOutputModality;
|
||||
@@ -18,7 +19,6 @@ use serde_json::Value;
|
||||
use serde_json::to_value;
|
||||
|
||||
pub(super) const REALTIME_AUDIO_SAMPLE_RATE: u32 = 24_000;
|
||||
const AGENT_FINAL_MESSAGE_PREFIX: &str = "\"Agent Final Message\":\n\n";
|
||||
|
||||
pub(super) fn normalized_session_mode(
|
||||
event_parser: RealtimeEventParser,
|
||||
@@ -32,11 +32,12 @@ pub(super) fn normalized_session_mode(
|
||||
|
||||
pub(super) fn conversation_item_create_message(
|
||||
event_parser: RealtimeEventParser,
|
||||
role: ConversationRole,
|
||||
text: String,
|
||||
) -> RealtimeOutboundMessage {
|
||||
match event_parser {
|
||||
RealtimeEventParser::V1 => v1_conversation_item_create_message(text),
|
||||
RealtimeEventParser::RealtimeV2 => v2_conversation_item_create_message(text),
|
||||
RealtimeEventParser::V1 => v1_conversation_item_create_message(role, text),
|
||||
RealtimeEventParser::RealtimeV2 => v2_conversation_item_create_message(role, text),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +47,9 @@ pub(super) fn conversation_function_call_output_message(
|
||||
output_text: String,
|
||||
) -> RealtimeOutboundMessage {
|
||||
match event_parser {
|
||||
RealtimeEventParser::V1 => v1_conversation_handoff_append_message(
|
||||
call_id,
|
||||
format!("{AGENT_FINAL_MESSAGE_PREFIX}{output_text}"),
|
||||
),
|
||||
RealtimeEventParser::V1 => {
|
||||
v1_conversation_handoff_append_message(Some(call_id), output_text)
|
||||
}
|
||||
RealtimeEventParser::RealtimeV2 => {
|
||||
v2_conversation_function_call_output_message(call_id, output_text)
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ use crate::endpoint::realtime_websocket::protocol::SessionAudioOutput;
|
||||
use crate::endpoint::realtime_websocket::protocol::SessionType;
|
||||
use crate::endpoint::realtime_websocket::protocol::SessionUpdateSession;
|
||||
|
||||
pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutboundMessage {
|
||||
pub(super) fn conversation_item_create_message(
|
||||
role: ConversationRole,
|
||||
text: String,
|
||||
) -> RealtimeOutboundMessage {
|
||||
RealtimeOutboundMessage::ConversationItemCreate {
|
||||
item: ConversationItemPayload::Message(ConversationMessageItem {
|
||||
r#type: ConversationItemType::Message,
|
||||
role: ConversationRole::User,
|
||||
role,
|
||||
content: vec![ConversationItemContent {
|
||||
r#type: ConversationContentType::InputText,
|
||||
text,
|
||||
@@ -29,7 +32,7 @@ pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutbound
|
||||
}
|
||||
|
||||
pub(super) fn conversation_handoff_append_message(
|
||||
handoff_id: String,
|
||||
handoff_id: Option<String>,
|
||||
output_text: String,
|
||||
) -> RealtimeOutboundMessage {
|
||||
RealtimeOutboundMessage::ConversationHandoffAppend {
|
||||
|
||||
@@ -36,11 +36,14 @@ const REALTIME_V2_SILENCE_TOOL_NAME: &str = "remain_silent";
|
||||
const REALTIME_V2_SILENCE_TOOL_DESCRIPTION: &str = "Call this when the best response is to say nothing. Use it instead of speaking after hidden system/control messages, after background agent updates in silent modes, or whenever acknowledging aloud would be distracting. This tool has no user-visible effect.";
|
||||
const REALTIME_V2_INPUT_TRANSCRIPTION_MODEL: &str = "gpt-4o-mini-transcribe";
|
||||
|
||||
pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutboundMessage {
|
||||
pub(super) fn conversation_item_create_message(
|
||||
role: ConversationRole,
|
||||
text: String,
|
||||
) -> RealtimeOutboundMessage {
|
||||
RealtimeOutboundMessage::ConversationItemCreate {
|
||||
item: ConversationItemPayload::Message(ConversationMessageItem {
|
||||
r#type: ConversationItemType::Message,
|
||||
role: ConversationRole::User,
|
||||
role,
|
||||
content: vec![ConversationItemContent {
|
||||
r#type: ConversationContentType::InputText,
|
||||
text,
|
||||
|
||||
@@ -38,7 +38,8 @@ pub(super) enum RealtimeOutboundMessage {
|
||||
InputAudioBufferAppend { audio: String },
|
||||
#[serde(rename = "conversation.handoff.append")]
|
||||
ConversationHandoffAppend {
|
||||
handoff_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
handoff_id: Option<String>,
|
||||
output_text: String,
|
||||
},
|
||||
#[serde(rename = "response.create")]
|
||||
@@ -171,6 +172,7 @@ pub(super) enum ConversationItemType {
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(super) enum ConversationRole {
|
||||
Developer,
|
||||
User,
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,21 @@ pub(super) fn parse_transcript_done_event(
|
||||
.map(|text| RealtimeTranscriptDone { text })
|
||||
}
|
||||
|
||||
pub(super) fn parse_response_event_response_id(parsed: &Value) -> Option<String> {
|
||||
parsed
|
||||
.get("response")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|response| response.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
parsed
|
||||
.get("response_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn parse_error_event(parsed: &Value) -> Option<RealtimeEvent> {
|
||||
parsed
|
||||
.get("message")
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_error_event;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_realtime_payload;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_response_event_response_id;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_session_updated_event;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_delta_event;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_done_event;
|
||||
use codex_protocol::protocol::RealtimeAudioFrame;
|
||||
use codex_protocol::protocol::RealtimeEvent;
|
||||
use codex_protocol::protocol::RealtimeHandoffRequested;
|
||||
use codex_protocol::protocol::RealtimeResponseCancelled;
|
||||
use codex_protocol::protocol::RealtimeResponseCreated;
|
||||
use codex_protocol::protocol::RealtimeResponseDone;
|
||||
use serde_json::Value;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -62,6 +66,17 @@ pub(super) fn parse_realtime_event_v1(payload: &str) -> Option<RealtimeEvent> {
|
||||
.cloned()
|
||||
.map(RealtimeEvent::ConversationItemAdded),
|
||||
"conversation.item.done" => parse_conversation_item_done_event(&parsed),
|
||||
"response.created" => Some(RealtimeEvent::ResponseCreated(RealtimeResponseCreated {
|
||||
response_id: parse_response_event_response_id(&parsed),
|
||||
})),
|
||||
"response.cancelled" => Some(RealtimeEvent::ResponseCancelled(
|
||||
RealtimeResponseCancelled {
|
||||
response_id: parse_response_event_response_id(&parsed),
|
||||
},
|
||||
)),
|
||||
"response.done" => Some(RealtimeEvent::ResponseDone(RealtimeResponseDone {
|
||||
response_id: parse_response_event_response_id(&parsed),
|
||||
})),
|
||||
"conversation.handoff.requested" => {
|
||||
let handoff_id = parsed
|
||||
.get("handoff_id")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_error_event;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_realtime_payload;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_response_event_response_id;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_session_updated_event;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_delta_event;
|
||||
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_done_event;
|
||||
@@ -78,21 +79,6 @@ pub(super) fn parse_realtime_event_v2(payload: &str) -> Option<RealtimeEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_response_event_response_id(parsed: &Value) -> Option<String> {
|
||||
parsed
|
||||
.get("response")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|response| response.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
parsed
|
||||
.get("response_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_output_audio_delta_event(parsed: &Value) -> Option<RealtimeEvent> {
|
||||
let data = parsed
|
||||
.get("delta")
|
||||
|
||||
Reference in New Issue
Block a user