Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/schema/typescript/v2/index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1772,6 +1772,7 @@ mod tests {
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::CodexResponseHandoffMode;
use codex_protocol::protocol::ConversationTextRole;
use codex_protocol::protocol::RealtimeConversationVersion;
use codex_protocol::protocol::RealtimeOutputModality;
use codex_protocol::protocol::RealtimeVoice;
Expand Down Expand Up @@ -3403,6 +3404,16 @@ mod tests {
model: Some("realtime-treatment-model".to_string()),
output_modality: RealtimeOutputModality::Audio,
include_startup_context: Some(false),
initial_items: Some(vec![
v2::ThreadRealtimeInitialItem {
role: ConversationTextRole::Developer,
text: "Remember this.".to_string(),
},
v2::ThreadRealtimeInitialItem {
role: ConversationTextRole::Assistant,
text: "Understood.".to_string(),
},
]),
prompt: Some(Some("You are on a call".to_string())),
realtime_session_id: Some("sess_456".to_string()),
transport: None,
Expand All @@ -3424,6 +3435,16 @@ mod tests {
"model": "realtime-treatment-model",
"outputModality": "audio",
"includeStartupContext": false,
"initialItems": [
{
"role": "developer",
"text": "Remember this."
},
{
"role": "assistant",
"text": "Understood."
}
],
"prompt": "You are on a call",
"realtimeSessionId": "sess_456",
"transport": null,
Expand All @@ -3450,6 +3471,7 @@ mod tests {
model: None,
output_modality: RealtimeOutputModality::Audio,
include_startup_context: None,
initial_items: None,
prompt: None,
realtime_session_id: None,
transport: None,
Expand All @@ -3471,6 +3493,7 @@ mod tests {
"model": null,
"outputModality": "audio",
"includeStartupContext": null,
"initialItems": null,
"realtimeSessionId": null,
"transport": null,
"version": null,
Expand All @@ -3492,6 +3515,7 @@ mod tests {
model: None,
output_modality: RealtimeOutputModality::Audio,
include_startup_context: None,
initial_items: None,
prompt: Some(None),
realtime_session_id: None,
transport: None,
Expand All @@ -3513,6 +3537,7 @@ mod tests {
"model": null,
"outputModality": "audio",
"includeStartupContext": null,
"initialItems": null,
"prompt": null,
"realtimeSessionId": null,
"transport": null,
Expand Down Expand Up @@ -3734,6 +3759,7 @@ mod tests {
model: None,
output_modality: RealtimeOutputModality::Audio,
include_startup_context: None,
initial_items: None,
prompt: Some(Some("You are on a call".to_string())),
realtime_session_id: None,
transport: None,
Expand Down
14 changes: 14 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/realtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ pub struct ThreadRealtimeStartParams {
/// Set to false to start without Codex's startup context. Omitted or null includes it.
#[ts(optional = nullable)]
pub include_startup_context: Option<bool>,
/// Adds complete role-bearing text items to the initial Frameless Bidi session history.
/// This is only supported by realtime V3 and is sent during session startup. Requests are
/// limited to 128 items and 8,192 estimated text tokens in total.
#[ts(optional = nullable)]
pub initial_items: Option<Vec<ThreadRealtimeInitialItem>>,
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
Expand All @@ -115,6 +120,15 @@ pub struct ThreadRealtimeStartParams {
pub voice: Option<RealtimeVoice>,
}

/// EXPERIMENTAL - role-bearing text item included when a realtime V3 session starts.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeInitialItem {
pub role: ConversationTextRole,
pub text: String,
}

/// EXPERIMENTAL - transport used by thread realtime.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4375,3 +4375,14 @@ fn realtime_append_text_defaults_role_to_user() {
}
);
}

#[test]
fn realtime_start_omitted_initial_items_remain_none() {
let params = serde_json::from_value::<ThreadRealtimeStartParams>(json!({
"threadId": "thread_123",
"outputModality": "audio",
}))
.expect("params should deserialize");

assert_eq!(params.initial_items, None);
}
28 changes: 27 additions & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Example with notification opt-out:
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, and pass `includeStartupContext: false` to omit Codex's generated startup context. Version `"v1"` uses legacy Bidi `conversation.handoff.*`, `"v2"` uses the Realtime Voice API, and `"v3"` preserves V1 Codex Voice behavior while using Frameless Bidi `delegation.*`. For V3 automatic Codex text, `codexResponseHandoffMode` accepts `"thinking"` (the default; all output uses channel-less thinking appends), `"commentary"` (all output uses the commentary channel), or `"bemTags"` (the raw BEM envelope selects the API channel: BEM `analysis` and `commentary` use `commentary`, while BEM `final` and unparsable output use `speakable`). The BEM envelope remains in the appended text for the frontend model to interpret. V1 and V2 ignore this setting. V3 handoffs do not prepend the legacy `"Agent Final Message"` label. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a Bidi WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. Conversation `version: "v2"` requests remain unsupported for WebRTC.
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, optionally pass `model` and `version` to override configured realtime selection for this session only, pass `includeStartupContext: false` to omit Codex's generated startup context, and optionally pass `initialItems` to seed V3 with complete role-bearing text messages at session creation. Version `"v1"` uses legacy Bidi `conversation.handoff.*`, `"v2"` uses the Realtime Voice API, and `"v3"` preserves V1 Codex Voice behavior while using Frameless Bidi `delegation.*`. For V3 automatic Codex text, `codexResponseHandoffMode` accepts `"thinking"` (the default; all output uses channel-less thinking appends), `"commentary"` (all output uses the commentary channel), or `"bemTags"` (the raw BEM envelope selects the API channel: BEM `analysis` and `commentary` use `commentary`, while BEM `final` and unparsable output use `speakable`). The BEM envelope remains in the appended text for the frontend model to interpret. V1 and V2 ignore this setting. V3 handoffs do not prepend the legacy `"Agent Final Message"` label. Pass `clientManagedHandoffs: true` to disable automatic Codex response delivery so only the client's explicit append calls produce handoffs. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a Bidi WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. Conversation `version: "v2"` requests remain unsupported for WebRTC.
- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`.
- `thread/realtime/appendText` — append text input to the active realtime session with a required `role` of `user`, `developer`, or `assistant` (experimental); returns `{}`. Older clients that omit `role` default to `user`.
- `thread/realtime/appendSpeech` — append text that the realtime model should speak to the user (experimental); returns `{}`.
Expand Down Expand Up @@ -943,6 +943,32 @@ only. WebRTC uses AVAS and supports legacy Bidi `"v1"` or Frameless Bidi
`"v3"`; Realtime Voice `"v2"` is rejected for WebRTC.
Pass `includeStartupContext: false` to skip Codex's startup context for this
session while still using the selected backend prompt.
For V3, clients may pass `initialItems` to seed the session with complete text
messages before live input begins:

```json
{
"initialItems": [
{
"role": "developer",
"text": "Relevant user memory: prefers concise technical answers."
},
{
"role": "user",
"text": "Continue from the prior discussion."
}
]
}
```

Each item requires a `role` of `"user"`, `"developer"`, or `"assistant"` and a
`text` string. Core serializes these as Frameless Bidi `session.initial_items`
during the initial session bootstrap (including WebRTC call creation).
Requests are limited to 128 items, 8,192 estimated text tokens per item, and
8,192 estimated text tokens across all items.
Omitting `initialItems`, or passing an empty list, preserves the previous
session payload and startup behavior. V1 and V2 reject non-empty
`initialItems`.
Pass `clientManagedHandoffs: true` to suppress automatic Codex response handoffs
and items. The client can then choose which updates to deliver with
`thread/realtime/appendText` or `thread/realtime/appendSpeech`.
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/app-server/src/request_processors/turn_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,15 @@ impl TurnRequestProcessor {
model: params.model,
output_modality: params.output_modality,
include_startup_context: params.include_startup_context.unwrap_or(true),
initial_items: params
.initial_items
.unwrap_or_default()
.into_iter()
.map(|item| ConversationTextParams {
text: item.text,
role: item.role,
})
.collect(),
prompt: params.prompt,
realtime_session_id: params.realtime_session_id,
transport: params.transport.map(|transport| match transport {
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/tests/suite/v2/experimental_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
model: None,
output_modality: RealtimeOutputModality::Audio,
include_startup_context: None,
initial_items: None,
prompt: Some(Some("hello".to_string())),
realtime_session_id: None,
transport: None,
Expand Down Expand Up @@ -227,6 +228,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
model: None,
output_modality: RealtimeOutputModality::Audio,
include_startup_context: None,
initial_items: None,
prompt: Some(Some("hello".to_string())),
realtime_session_id: None,
transport: Some(ThreadRealtimeStartTransport::Webrtc {
Expand Down
Loading
Loading