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:
brianz-openai
2026-08-27 04:58:21 +00:00
committed by copyberry
parent 72c96598c6
commit f1433fc71f
24 changed files with 565 additions and 9 deletions
+19 -3
View File
@@ -532,6 +532,10 @@ impl ModelInfo {
/// When variables are present but incomplete, missing values render as empty strings.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ModelMessages {
/// Additional developer instructions for persistent mode. Missing or null uses the built-in
/// instructions; an empty string disables them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persistent_instructions: Option<String>,
pub instructions_template: Option<String>,
pub instructions_variables: Option<ModelInstructionsVariables>,
pub approvals: Option<ApprovalMessages>,
@@ -761,6 +765,7 @@ where
.is_none()
{
let messages = model.model_messages.get_or_insert(ModelMessages {
persistent_instructions: None,
instructions_template: None,
instructions_variables: None,
approvals: None,
@@ -959,13 +964,15 @@ mod tests {
#[test]
fn model_messages_deserialize_without_optional_sections() {
let messages: ModelMessages =
from_str(r#"{"instructions_template":null,"instructions_variables":null}"#)
.expect("model messages should deserialize");
let messages: ModelMessages = from_str(
r#"{"instructions_template":null,"instructions_variables":null,"persistent_instructions":null}"#,
)
.expect("model messages should deserialize");
assert_eq!(
messages,
ModelMessages {
persistent_instructions: None,
instructions_template: None,
instructions_variables: None,
approvals: None,
@@ -1111,6 +1118,7 @@ mod tests {
assert_eq!(
messages,
ModelMessages {
persistent_instructions: None,
instructions_template: None,
instructions_variables: None,
approvals: None,
@@ -1202,6 +1210,7 @@ mod tests {
#[test]
fn get_model_instructions_uses_template_when_placeholder_present() {
let model = test_model(Some(ModelMessages {
persistent_instructions: None,
instructions_template: Some("Hello {{ personality }}".to_string()),
instructions_variables: Some(personality_variables()),
approvals: None,
@@ -1221,6 +1230,7 @@ mod tests {
#[test]
fn get_model_instructions_strips_placeholder_with_incomplete_variables() {
let model = test_model(Some(ModelMessages {
persistent_instructions: None,
instructions_template: Some("Hello\n{{ personality }}".to_string()),
instructions_variables: Some(ModelInstructionsVariables {
personality_default: None,
@@ -1245,6 +1255,7 @@ mod tests {
);
let model_no_personality = test_model(Some(ModelMessages {
persistent_instructions: None,
instructions_template: Some("Hello\n{{ personality }}".to_string()),
instructions_variables: Some(ModelInstructionsVariables {
personality_default: None,
@@ -1280,6 +1291,7 @@ mod tests {
#[test]
fn get_model_instructions_is_empty_when_template_is_missing() {
let model = test_model(Some(ModelMessages {
persistent_instructions: None,
instructions_template: None,
instructions_variables: Some(ModelInstructionsVariables {
personality_default: None,
@@ -1325,6 +1337,7 @@ mod tests {
assert_eq!(
model.model_messages,
Some(ModelMessages {
persistent_instructions: None,
instructions_template: Some("legacy instructions".to_string()),
instructions_variables: None,
approvals: None,
@@ -1373,6 +1386,7 @@ mod tests {
fn models_response_serializes_rendered_legacy_base_instructions() {
let response = ModelsResponse {
models: vec![test_model(Some(ModelMessages {
persistent_instructions: None,
instructions_template: Some("before {{ personality }} after".to_string()),
instructions_variables: Some(ModelInstructionsVariables {
personality_default: Some("default".to_string()),
@@ -1400,6 +1414,7 @@ mod tests {
#[test]
fn models_response_prefers_template_and_preserves_message_siblings() {
let messages = ModelMessages {
persistent_instructions: Some("Persistent catalog instructions".to_string()),
instructions_template: None,
instructions_variables: None,
approvals: Some(ApprovalMessages {
@@ -1450,6 +1465,7 @@ mod tests {
assert_eq!(response.models[0].model_messages, Some(expected_messages));
let canonical_messages = ModelMessages {
persistent_instructions: Some(String::new()),
instructions_template: Some("canonical instructions".to_string()),
instructions_variables: None,
approvals: None,