mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Add developer instructions for persistent mode (#41050)
## What changed - Add bundled proactivity and follow-up guidance when `ReasoningEffort::Persistent` is selected. - Allow model metadata to override or disable the guidance with `persistent_instructions`. - Track the instructions as world state so effort and model changes replace or retire stale guidance without duplicating unchanged context. - Tailor approval guidance to `send_user_message_async` availability, rebuild it for forked agents and rolled-back history, and exclude Guardian sessions. ## Testing - Cover default, overridden, disabled, replaced, and retired instructions. - Cover model changes, history transitions, agent forks, Guardian sessions, and asynchronous-message tool availability. GitOrigin-RevId: 78e05d0335a70b385bd069b45edcd4ba570cef91
This commit is contained in:
@@ -10,6 +10,7 @@ mod model;
|
||||
mod multi_agent_mode;
|
||||
mod multi_agent_usage_hint;
|
||||
mod permissions;
|
||||
mod persistent_mode;
|
||||
mod personality;
|
||||
mod plugins_instructions;
|
||||
mod realtime;
|
||||
@@ -49,6 +50,7 @@ pub(crate) use model::ModelInstructionsState;
|
||||
pub(crate) use multi_agent_mode::MultiAgentModeState;
|
||||
pub(crate) use multi_agent_usage_hint::MultiAgentUsageHintState;
|
||||
pub(crate) use permissions::PermissionsState;
|
||||
pub(crate) use persistent_mode::PersistentModeState;
|
||||
pub(crate) use personality::PersonalityState;
|
||||
pub(crate) use plugins_instructions::PluginsInstructionsState;
|
||||
pub(crate) use realtime::RealtimeState;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! Keeps persistent-mode developer instructions current without repeating unchanged context.
|
||||
//! Mode changes retire prior instructions; missing catalog values use the bundled default.
|
||||
|
||||
use super::PreviousSectionState;
|
||||
use super::WorldStateHash;
|
||||
use super::WorldStateSection;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use codex_protocol::models::ContentItemKind;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
const DEFAULT_INSTRUCTIONS: &str = include_str!("../../../templates/persistent_mode.md");
|
||||
const REPLACEMENT_NOTICE: &str = "These persistent-mode instructions replace all previously provided persistent-mode instructions.";
|
||||
const REMOVAL_NOTICE: &str =
|
||||
"The previously provided persistent-mode instructions no longer apply.";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PersistentModeState {
|
||||
instructions: String,
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for PersistentModeState {
|
||||
fn content_kind(&self) -> ContentItemKind {
|
||||
ContentItemKind("persistent_mode.instructions".to_string())
|
||||
}
|
||||
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
("<persistent_mode>", "</persistent_mode>")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!("\n{}\n", self.instructions)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct PersistentModeSnapshot {
|
||||
// Keep an object: JSON null would delete the section and lose its disabled state.
|
||||
instructions: Option<WorldStateHash>,
|
||||
}
|
||||
|
||||
impl PersistentModeState {
|
||||
pub(crate) fn new(
|
||||
reasoning_effort: Option<&ReasoningEffort>,
|
||||
catalog_instructions: Option<&str>,
|
||||
send_user_message_async_available: bool,
|
||||
) -> Self {
|
||||
let instructions = if reasoning_effort == Some(&ReasoningEffort::Persistent) {
|
||||
catalog_instructions
|
||||
.unwrap_or(DEFAULT_INSTRUCTIONS)
|
||||
.trim()
|
||||
.replace(
|
||||
"{{ approval_request_channel }}",
|
||||
if send_user_message_async_available {
|
||||
" via functions.send_user_message_async"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
Self { instructions }
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldStateSection for PersistentModeState {
|
||||
const ID: &'static str = "persistent_mode";
|
||||
type Snapshot = PersistentModeSnapshot;
|
||||
|
||||
fn snapshot(&self) -> Self::Snapshot {
|
||||
PersistentModeSnapshot {
|
||||
instructions: (!self.instructions.is_empty())
|
||||
.then(|| WorldStateHash::from_fragment(self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
|
||||
role == "developer" && Self::matches_text(text)
|
||||
}
|
||||
|
||||
fn has_retained_fragment_matcher() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn matches_retained_fragment(role: &str, text: &str) -> bool {
|
||||
Self::matches_legacy_fragment(role, text)
|
||||
}
|
||||
|
||||
fn render_diff(
|
||||
&self,
|
||||
previous: PreviousSectionState<'_, Self::Snapshot>,
|
||||
) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
if matches!(previous, PreviousSectionState::Known(previous) if previous == &self.snapshot())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let previous_had_instructions = match previous {
|
||||
PreviousSectionState::Absent => false,
|
||||
PreviousSectionState::Unknown => true,
|
||||
PreviousSectionState::Known(previous) => previous.instructions.is_some(),
|
||||
};
|
||||
let instructions = match (self.instructions.as_str(), previous_had_instructions) {
|
||||
("", false) => return None,
|
||||
("", true) => REMOVAL_NOTICE.to_string(),
|
||||
(instructions, true) => format!("{REPLACEMENT_NOTICE}\n\n{instructions}"),
|
||||
(instructions, false) => instructions.to_string(),
|
||||
};
|
||||
Some(Box::new(Self { instructions }))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "persistent_mode_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Covers effort selection, catalog overrides, and persistent-context transitions.
|
||||
|
||||
use super::*;
|
||||
use crate::context::world_state::WorldState;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn persistent_instructions_follow_effort_and_catalog_updates_without_duplicates() {
|
||||
let mut history = Vec::new();
|
||||
let mut previous = None;
|
||||
let persistent = Some(ReasoningEffort::Persistent);
|
||||
let medium = Some(ReasoningEffort::Medium);
|
||||
let replacement = format!("{REPLACEMENT_NOTICE}\n\nupdated instructions");
|
||||
|
||||
for (effort, instructions, expected) in [
|
||||
(None, None, None),
|
||||
(
|
||||
persistent.clone(),
|
||||
Some("instructions"),
|
||||
Some("instructions"),
|
||||
),
|
||||
(persistent.clone(), Some("instructions"), None),
|
||||
(
|
||||
persistent.clone(),
|
||||
Some("updated instructions"),
|
||||
Some(replacement.as_str()),
|
||||
),
|
||||
(persistent.clone(), Some(""), Some(REMOVAL_NOTICE)),
|
||||
(persistent.clone(), Some(""), None),
|
||||
(persistent, Some("instructions"), Some("instructions")),
|
||||
(medium.clone(), None, Some(REMOVAL_NOTICE)),
|
||||
(medium, None, None),
|
||||
] {
|
||||
let mut world_state = WorldState::default();
|
||||
world_state.add_section(PersistentModeState::new(
|
||||
effort.as_ref(),
|
||||
instructions,
|
||||
/*send_user_message_async_available*/ false,
|
||||
));
|
||||
let updates = world_state
|
||||
.render_history_diff(previous.as_ref(), &history)
|
||||
.into_iter()
|
||||
.map(ContextualUserFragment::into_boxed_response_item)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
updates,
|
||||
expected
|
||||
.map(|instructions| {
|
||||
ContextualUserFragment::into(PersistentModeState {
|
||||
instructions: instructions.to_string(),
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
history.extend(updates);
|
||||
previous = Some(world_state.snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_persistent_instructions_are_replaced_or_retired_without_a_snapshot() {
|
||||
let retained = ContextualUserFragment::into(PersistentModeState {
|
||||
instructions: "previous instructions".to_string(),
|
||||
});
|
||||
for (effort, expected) in [
|
||||
(
|
||||
ReasoningEffort::Persistent,
|
||||
format!("{REPLACEMENT_NOTICE}\n\ncurrent instructions"),
|
||||
),
|
||||
(ReasoningEffort::Medium, REMOVAL_NOTICE.to_string()),
|
||||
] {
|
||||
let mut world_state = WorldState::default();
|
||||
world_state.add_section(PersistentModeState::new(
|
||||
Some(&effort),
|
||||
Some("current instructions"),
|
||||
/*send_user_message_async_available*/ false,
|
||||
));
|
||||
assert_eq!(
|
||||
world_state
|
||||
.render_history_diff(/*previous*/ None, std::slice::from_ref(&retained))
|
||||
.into_iter()
|
||||
.map(ContextualUserFragment::into_boxed_response_item)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![ContextualUserFragment::into(PersistentModeState {
|
||||
instructions: expected,
|
||||
})]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user