Allow custom Codex instructions for realtime transitions (#36408)

## What changed

- Add optional `realtimeStartInstructions` and `realtimeEndInstructions` fields to `thread/realtime/start`.
- Apply the instructions when Codex enters and leaves realtime mode, while preserving the existing defaults when either field is omitted.
- Limit each field to 8,192 estimated tokens.

## Testing

- Cover custom entry and exit instructions across realtime state transitions.
- Cover token-limit validation for both fields.

GitOrigin-RevId: b9cfa1ff22e084d8a6b2ed56bd27d95ffdd6b6bd
This commit is contained in:
guinness-oai authored and copyberry committed 2026-07-31 23:51:33 +00:00
1 parent 64b2a3008e
commit 670f69416b
18 files changed
+395 -33

No files matched your search

@@ -5,13 +5,17 @@ use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RealtimeEndInstructions {
reason: String,
instructions: Option<String>,
}
impl RealtimeEndInstructions {
pub(crate) fn new(reason: impl Into<String>) -> Self {
pub(crate) fn new() -> Self {
Self { instructions: None }
}
pub(crate) fn with_instructions(instructions: impl Into<String>) -> Self {
Self {
reason: reason.into(),
instructions: Some(instructions.into()),
}
}
}
@@ -32,11 +36,11 @@ impl ContextualUserFragment for RealtimeEndInstructions {
)
}
fn matches_text(text: &str) -> bool {
text.contains(END_INSTRUCTIONS.trim())
}
fn body(&self) -> String {
format!("\n{}\n\nReason: {}\n", END_INSTRUCTIONS.trim(), self.reason)
let instructions = self
.instructions
.as_deref()
.unwrap_or_else(|| END_INSTRUCTIONS.trim());
format!("\n{instructions}\n")
}
}
@@ -12,6 +12,7 @@ use serde::Serialize;
pub(crate) struct RealtimeState {
snapshot: RealtimeSnapshot,
start_instructions: Option<String>,
end_instructions: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
@@ -20,10 +21,15 @@ pub(crate) struct RealtimeSnapshot {
}
impl RealtimeState {
pub(crate) fn new(active: bool, start_instructions: Option<&str>) -> Self {
pub(crate) fn new(
active: bool,
start_instructions: Option<&str>,
end_instructions: Option<&str>,
) -> Self {
Self {
snapshot: RealtimeSnapshot { active },
start_instructions: start_instructions.map(str::to_string),
end_instructions: end_instructions.map(str::to_string),
}
}
@@ -37,7 +43,12 @@ impl RealtimeState {
fn render_transition(&self, previous_active: bool) -> Option<Box<dyn ContextualUserFragment>> {
match (previous_active, self.snapshot.active) {
(false, true) => Some(self.render_start()),
(true, false) => Some(Box::new(RealtimeEndInstructions::new("inactive"))),
(true, false) => Some(match self.end_instructions.as_deref() {
Some(instructions) => {
Box::new(RealtimeEndInstructions::with_instructions(instructions))
}
None => Box::new(RealtimeEndInstructions::new()),
}),
(false, false) | (true, true) => None,
}
}
@@ -52,9 +63,7 @@ impl WorldStateSection for RealtimeState {
}
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
role == "developer"
&& RealtimeStartInstructions::matches_text(text)
&& !RealtimeEndInstructions::matches_text(text)
role == "developer" && RealtimeStartInstructions::matches_text(text)
}
fn has_retained_fragment_matcher() -> bool {
@@ -2,7 +2,7 @@ use super::super::test_support::render_section_cases;
use super::*;
fn state(active: bool, start_instructions: Option<&str>) -> RealtimeState {
RealtimeState::new(active, start_instructions)
RealtimeState::new(active, start_instructions, /*end_instructions*/ None)
}
#[test]
@@ -34,10 +34,10 @@ fn snapshots() {
}
#[test]
fn retained_fragment_matcher_only_matches_starts() {
fn retained_fragment_matcher_matches_realtime_fragments() {
let start = RealtimeStartWithInstructions::new("custom instructions").render();
let end = RealtimeEndInstructions::new("inactive").render();
let end = RealtimeEndInstructions::new().render();
assert!(RealtimeState::matches_legacy_fragment("developer", &start));
assert!(!RealtimeState::matches_legacy_fragment("developer", &end));
assert!(RealtimeState::matches_legacy_fragment("developer", &end));
}
@@ -50,8 +50,6 @@ None
Realtime conversation ended.
Subsequent user input will return to typed text rather than transcript-style text. Do not assume recognition errors or missing punctuation once realtime has ended. Resume normal chat behavior.
Reason: inactive
</realtime_conversation>
Unknown -> {"active":true} (role - developer)