mirror of
https://github.com/openai/codex.git
synced 2026-11-11 14:32:25 +00:00
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:
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));
|
||||
}
|
||||
-2
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user