Support model catalog overrides for plugin guidance (#58894)

## What changed

- Add `model_messages.plugins` overrides for generic usage guidance, selected-plugin hints, and recommendation introductions, with independent fallback to bundled wording for missing or invalid fields.
- Validate text limits and required template placeholders while keeping plugin identities, available capabilities, and eligibility controlled by the client.
- Track generic guidance changes across model switches and session resume, emitting replacement or retirement notices and supporting legacy snapshots and retained history.

## Testing

Add unit and scenario coverage for sparse overrides, literal text preservation, template validation, UTF-8 size bounds, capability filtering, recommendation fallback, and guidance recovery after resume.

GitOrigin-RevId: 148b658c2d8490c9d0dc208addbee1be5342a5f5
This commit is contained in:
goodman-oai
2026-11-02 14:06:17 +00:00
committed by copyberry
parent 1e8b536dc7
commit aef7d276a4
39 changed files with 3326 additions and 107 deletions
@@ -7,7 +7,23 @@ use codex_protocol::models::ContentItemKind;
use codex_protocol::models::input_text;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AvailablePluginsInstructions;
pub(crate) struct AvailablePluginsInstructions(pub(crate) String);
impl From<codex_prompts::ResolvedGenericPluginMessages> for AvailablePluginsInstructions {
fn from(messages: codex_prompts::ResolvedGenericPluginMessages) -> Self {
let intro = messages.intro;
let usage = messages.usage_instructions;
Self(format!(
"\n## Plugins\n{intro}\n### How to use plugins\n{usage}\n"
))
}
}
impl Default for AvailablePluginsInstructions {
fn default() -> Self {
Self::from(codex_prompts::ResolvedModelMessages::bundled().generic_plugin())
}
}
impl ContextualUserFragment for AvailablePluginsInstructions {
fn content_kind(&self) -> ContentItemKind {
@@ -30,22 +46,6 @@ impl ContextualUserFragment for AvailablePluginsInstructions {
}
fn body(&self) -> ContentItem {
let mut lines = vec![
"## Plugins".to_string(),
"A plugin is a local bundle of skills, MCP servers, and apps.".to_string(),
];
lines.push("### How to use plugins".to_string());
lines.push(
r###"- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as `mcp__server__tool`; use tool provenance to tell which plugin they come from.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associated skills, MCP tools, and apps exposed elsewhere in this turn.
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback."###
.to_string(),
);
input_text(format!("\n{}\n", lines.join("\n")))
input_text(self.0.clone())
}
}
@@ -4,21 +4,28 @@ use codex_protocol::models::ContentItemKind;
use codex_protocol::models::input_text;
use codex_tools::DiscoverableTool;
const RECOMMENDED_PLUGINS_INTRO: &str =
"Here is a list of plugins that are available but not installed.";
// Endpoint candidates cap display names at 64 Unicode characters and slugs at 64 ASCII
// characters. With the curated marketplace suffix, 50 rows, a 500-byte introduction and
// fragment markers, the final item is at most 17,946 UTF-8 bytes. Revisit this bound if
// upstream identity limits or the marketplace suffix change; never truncate these facts.
const MAX_RECOMMENDED_PLUGINS: usize = 50;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RecommendedPluginsInstructions {
plugins: Vec<DiscoverableTool>,
intro: String,
}
impl RecommendedPluginsInstructions {
pub(crate) fn from_plugins(plugins: &[DiscoverableTool]) -> Option<Self> {
pub(crate) fn from_plugins(
plugins: &[DiscoverableTool],
messages: &codex_prompts::ResolvedPluginRecommendationMessages,
) -> Option<Self> {
if plugins.is_empty() {
return None;
}
Some(Self {
intro: messages.intro.clone(),
plugins: plugins
.iter()
.take(MAX_RECOMMENDED_PLUGINS)
@@ -52,6 +59,11 @@ impl ContextualUserFragment for RecommendedPluginsInstructions {
.map(|plugin| format!("- {} ({})", plugin.name(), plugin.id()))
.collect::<Vec<_>>()
.join("\n");
input_text(format!("\n{RECOMMENDED_PLUGINS_INTRO}\n\n{plugins}\n"))
let intro = &self.intro;
input_text(format!("\n{intro}\n\n{plugins}\n"))
}
}
#[cfg(test)]
#[path = "recommended_plugins_instructions_tests.rs"]
mod tests;
@@ -0,0 +1,48 @@
#![cfg(test)]
//! Checks final recommendation bounds without truncating runtime identities.
use super::*;
use codex_protocol_test_support::ContentItemTestExt;
use codex_tools::DiscoverablePluginInfo;
use pretty_assertions::assert_eq;
#[test]
fn recommendation_wording_keeps_runtime_identities_within_the_derived_final_bound() {
// Endpoint recommendations validate names at 64 chars. Display names permit four-byte
// characters; config IDs use the ASCII slug and the curated marketplace suffix.
let name = "🦀".repeat(64);
let id = format!("{}@openai-curated-remote", "a".repeat(64));
let plugins = vec![
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
id: id.clone(),
remote_plugin_id: None,
name: name.clone(),
description: None,
has_skills: true,
mcp_server_names: vec![],
app_connector_ids: vec![],
}));
51
];
let messages = codex_prompts::ResolvedPluginRecommendationMessages {
intro: "é".repeat(250),
};
let fragment = RecommendedPluginsInstructions::from_plugins(&plugins, &messages).unwrap();
let rendered = fragment.render().expect_text();
let rows = vec![format!("- {name} ({id})"); 50].join("\n");
assert_eq!(
rendered,
format!(
"<recommended_plugins>\n{}\n\n{rows}\n</recommended_plugins>",
messages.intro
)
);
assert_eq!(rendered.len(), 17_946);
assert_eq!(fragment.role(), "developer");
assert_eq!(
fragment.content_kind(),
ContentItemKind("plugins.recommendations".into())
);
assert!(RecommendedPluginsInstructions::from_plugins(&[], &messages).is_none());
}
+34 -11
View File
@@ -426,6 +426,34 @@ impl WorldStateSnapshot {
self.sections.into_iter().collect()
}
fn section(&self, id: &str) -> Option<&Value> {
let snapshot = self.sections.get(id)?;
if id == PluginsInstructionsState::ID
&& snapshot == &Value::Bool(false)
&& let Some(retired) = self
.sections
.get(plugins_instructions::RETIRED_CHECKPOINT_ID)
&& plugins_instructions::is_retired_checkpoint(retired)
{
return Some(retired);
}
Some(snapshot)
}
fn insert_section(&mut self, id: &str, mut snapshot: Value) {
if id == PluginsInstructionsState::ID
&& plugins_instructions::is_retired_checkpoint(&snapshot)
{
// Keep the original section readable by clients whose snapshot type is bool.
self.sections.insert(
plugins_instructions::RETIRED_CHECKPOINT_ID.to_string(),
snapshot,
);
snapshot = Value::Bool(false);
}
self.sections.insert(id.to_string(), snapshot);
}
/// Returns the RFC 7386 merge patch that advances `previous` to `self`.
pub(crate) fn merge_patch_from(&self, previous: &Self) -> Option<Map<String, Value>> {
let mut patch = Map::new();
@@ -475,7 +503,7 @@ impl WorldState {
pub(crate) fn add_section<S: WorldStateSection>(&mut self, section: S) {
let id = S::ID;
assert!(
!self.sections.contains_key(id),
id != plugins_instructions::RETIRED_CHECKPOINT_ID && !self.sections.contains_key(id),
"duplicate world-state section ID: {id}"
);
self.sections.insert(id, Box::new(section));
@@ -484,7 +512,7 @@ impl WorldState {
pub(crate) fn add_extension_section(&mut self, section: WorldStateSectionContribution) {
let id = section.id();
assert!(
!self.sections.contains_key(id),
id != plugins_instructions::RETIRED_CHECKPOINT_ID && !self.sections.contains_key(id),
"duplicate world-state section ID: {id}"
);
let section = Box::new(ExtensionWorldStateSection(section));
@@ -509,7 +537,7 @@ impl WorldState {
items: impl IntoIterator<Item = &'a ResponseItem> + Clone,
) -> (WorldStateSnapshot, Vec<WorldStateUpdate>) {
self.render_with(previous, |id, section| {
if let Some(previous) = previous.and_then(|previous| previous.sections.get(id)) {
if let Some(previous) = previous.and_then(|previous| previous.section(id)) {
if section.has_retained_fragment_matcher()
&& !has_retained_fragment(items.clone(), section)
{
@@ -536,17 +564,12 @@ impl WorldState {
let prior = previous(id, section.as_ref());
let (section_snapshot, section_updates) = section.render_diff(prior);
// A skipped snapshot retains stored state even if history required a fresh render.
let section_snapshot = section_snapshot.or_else(|| {
stored
.and_then(|snapshot| snapshot.sections.get(*id))
.cloned()
});
let section_snapshot = section_snapshot
.or_else(|| stored.and_then(|snapshot| snapshot.section(id)).cloned());
if let Some(section_snapshot) = section_snapshot
&& !section_snapshot.is_null()
{
snapshot
.sections
.insert((*id).to_string(), section_snapshot);
snapshot.insert_section(id, section_snapshot);
}
updates.extend(section_updates);
}
@@ -1,25 +1,82 @@
//! Tracks literal generic plugin wording across catalog changes, legacy snapshots, and retained history.
//! Bundled-only availability changes retain their previous silent-removal behavior.
use super::PreviousSectionState;
use super::SectionTransition;
use super::WorldStateHash;
use super::WorldStateSection;
use super::WorldStateUpdate;
use crate::context::AvailablePluginsInstructions;
use crate::context::ContextualUserFragment;
use codex_prompts::ResolvedGenericPluginMessages;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
/// Whether generic plugin usage guidance should be visible to the model.
#[derive(Clone, Copy, Debug, Default)]
const REPLACEMENT_PREFIX: &str = "\nThese plugin usage instructions replace all previously provided plugin usage instructions.\n\n";
const REMOVAL_BODY: &str = "\nThe previously provided plugin usage instructions no longer apply.\n";
// Only a catalog retirement needs this extra checkpoint to distinguish it from the
// legacy behavior of silently disabling bundled guidance. Older clients read the primary bool.
pub(super) const RETIRED_CHECKPOINT_ID: &str = "plugins_instructions_catalog_retired";
pub(super) fn is_retired_checkpoint(snapshot: &Value) -> bool {
snapshot.get("instructions").is_none() && snapshot.get("catalog") == Some(&Value::Bool(true))
}
#[derive(Clone, Debug)]
pub(crate) struct PluginsInstructionsState {
available: bool,
resolved: ResolvedGenericPluginMessages,
retained_body: Option<String>,
}
/// Older clients read only availability. Keep writing false when guidance is disabled.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum PluginsInstructionsSnapshot {
Current {
instructions: Option<WorldStateHash>,
catalog: bool,
},
Legacy(bool),
}
impl PluginsInstructionsState {
pub(crate) fn new(available: bool) -> Self {
Self { available }
pub(crate) fn new<'a>(
available: bool,
resolved: ResolvedGenericPluginMessages,
history: impl DoubleEndedIterator<Item = &'a ResponseItem>,
) -> Self {
let (open, close) = AvailablePluginsInstructions::type_markers();
let retained_body = history.rev().find_map(|item| match item {
ResponseItem::Message { role, content, .. } if role == "developer" => {
content.iter().rev().find_map(|content| match content {
ContentItem::InputText { text }
if AvailablePluginsInstructions::matches_text(text) =>
{
let text = text.trim();
text.get(open.len()..text.len() - close.len())
.map(str::to_owned)
}
_ => None,
})
}
_ => None,
});
Self {
available,
resolved,
retained_body,
}
}
}
impl WorldStateSection for PluginsInstructionsState {
const ID: &'static str = "plugins_instructions";
type Snapshot = bool;
type Snapshot = PluginsInstructionsSnapshot;
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
role == "developer" && AvailablePluginsInstructions::matches_text(text)
@@ -37,17 +94,76 @@ impl WorldStateSection for PluginsInstructionsState {
&self,
previous: PreviousSectionState<'_, Self::Snapshot>,
) -> SectionTransition<Self::Snapshot> {
let current = self.available;
if !self.available
|| matches!(previous, PreviousSectionState::Known(previous) if *previous)
|| matches!(previous, PreviousSectionState::Unknown)
use PluginsInstructionsSnapshot::Current;
use PluginsInstructionsSnapshot::Legacy;
// JSON hashing preserves CRLF and all other literal bytes, unlike fragment hashing.
let fingerprint =
|text: &str| WorldStateHash::from_json(&serde_json::Value::String(text.to_string()));
let rendered = self
.available
.then(|| AvailablePluginsInstructions::from(self.resolved.clone()).0);
let instructions = rendered.as_deref();
let current_hash = instructions.map(fingerprint);
// Catalog selection can retire retained guidance even while availability is gated off.
let catalog = self.resolved.catalog_override;
let bundled = AvailablePluginsInstructions::default().0;
let (mut previous_hash, mut previous_catalog) = match previous {
PreviousSectionState::Absent | PreviousSectionState::Known(Legacy(false)) => {
(None, false)
}
PreviousSectionState::Unknown | PreviousSectionState::Known(Legacy(true)) => {
(Some(fingerprint(&bundled)), false)
}
PreviousSectionState::Known(Current {
instructions,
catalog,
}) => (instructions.clone(), *catalog),
};
if !matches!(previous, PreviousSectionState::Absent)
&& let Some(body) = self.retained_body.as_deref()
{
// Catalog fields occur inside the "## Plugins" body, after any transition framing.
let retained = (body != REMOVAL_BODY)
.then(|| body.strip_prefix(REPLACEMENT_PREFIX).unwrap_or(body));
let retained_catalog = retained != Some(bundled.as_str());
// A legacy false snapshot can coexist with retained catalog guidance, while a
// bundled-only disablement deliberately leaves the old bundled text in history.
if previous_hash.is_some() || previous_catalog || catalog || retained_catalog {
previous_hash = retained.map(fingerprint);
previous_catalog |= retained_catalog;
}
}
let current = if self.available {
Current {
instructions: current_hash.clone(),
catalog,
}
} else if previous_catalog || (catalog && previous_hash.is_some()) {
Current {
instructions: None,
catalog: true,
}
} else {
Legacy(false)
};
if previous_hash == current_hash {
return (Some(current), Vec::new());
}
let body = match instructions {
Some(text) if previous_hash.is_some() => format!("{REPLACEMENT_PREFIX}{text}"),
Some(text) => text.to_string(),
None if previous_hash.is_some() && (previous_catalog || catalog) => {
REMOVAL_BODY.to_string()
}
None => return (Some(current), Vec::new()),
};
// The 500/2,000-byte field allowances leave room for all framing below 4 KiB.
(
Some(current),
vec![WorldStateUpdate::fragment(AvailablePluginsInstructions)],
vec![WorldStateUpdate::fragment(AvailablePluginsInstructions(
body,
))],
)
}
}
@@ -1,9 +1,34 @@
//! Covers generic plugin wording transitions and recovery from retained developer history.
#![cfg(test)]
use super::*;
use crate::context::ContextualUserFragment;
use crate::context::world_state::PreviousSectionState;
use crate::context::world_state::WorldState;
use crate::context::world_state::WorldStateSnapshot;
use crate::context::world_state::test_support::render_section_cases;
use codex_prompts::ResolvedGenericPluginMessages;
use codex_prompts::ResolvedModelMessages;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use pretty_assertions::assert_eq;
use serde_json::json;
fn resolve(intro: Option<&str>, usage: Option<&str>) -> ResolvedGenericPluginMessages {
let mut model = codex_models_manager::model_info::model_info_from_slug("gpt-5.5");
model.model_messages = Some(
serde_json::from_value(json!({"plugins": {"generic": {
"intro": intro,
"usage_instructions": usage,
}}}))
.unwrap(),
);
ResolvedModelMessages::from_model(&model).generic_plugin()
}
fn expected_body(intro: &str, usage: &str) -> String {
format!("\n## Plugins\n{intro}\n### How to use plugins\n{usage}\n")
}
#[test]
fn snapshots() {
@@ -11,8 +36,14 @@ fn snapshots() {
use PreviousSectionState::Known;
use PreviousSectionState::Unknown;
let unavailable = PluginsInstructionsState::new(/*available*/ false);
let available = PluginsInstructionsState::new(/*available*/ true);
let bundled = ResolvedModelMessages::bundled().generic_plugin();
let unavailable = PluginsInstructionsState::new(
/*available*/ false,
bundled.clone(),
std::iter::empty(),
);
let available =
PluginsInstructionsState::new(/*available*/ true, bundled, std::iter::empty());
insta::assert_snapshot!(render_section_cases(&[
(Absent, Absent),
@@ -28,9 +59,14 @@ fn snapshots() {
#[test]
fn legacy_guidance_is_not_injected_again() {
let mut world_state = super::super::WorldState::default();
world_state.add_section(PluginsInstructionsState::new(/*available*/ true));
let legacy: ResponseItem = ContextualUserFragment::into(AvailablePluginsInstructions);
let legacy: ResponseItem =
ContextualUserFragment::into(AvailablePluginsInstructions::default());
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
/*available*/ true,
ResolvedModelMessages::bundled().generic_plugin(),
std::iter::once(&legacy),
));
assert!(
world_state
@@ -42,22 +78,487 @@ fn legacy_guidance_is_not_injected_again() {
#[test]
fn persisted_guidance_is_restored_only_when_missing_from_history() {
let mut world_state = super::super::WorldState::default();
world_state.add_section(PluginsInstructionsState::new(/*available*/ true));
let snapshot = world_state.render_full().0;
let retained: ResponseItem = ContextualUserFragment::into(AvailablePluginsInstructions);
let retained: ResponseItem =
ContextualUserFragment::into(AvailablePluginsInstructions::default());
for history in [vec![], vec![retained]] {
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
/*available*/ true,
ResolvedModelMessages::bundled().generic_plugin(),
history.iter(),
));
let snapshot = world_state.render_full().0;
assert_eq!(
world_state
.render_history_fragment_diff(Some(&snapshot), &history)
.1
.len(),
usize::from(history.is_empty())
);
}
}
#[test]
fn catalog_text_equal_to_bundled_changes_provenance_without_appending_guidance() {
let bundled = ResolvedModelMessages::bundled().generic_plugin();
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
/*available*/ true,
bundled.clone(),
std::iter::empty(),
));
let (mut previous, initial) =
world_state.render_history_fragment_diff(/*previous*/ None, &[]);
let history = initial
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
assert_eq!(
world_state
.render_history_fragment_diff(Some(&snapshot), &[])
history,
vec![ContextualUserFragment::into(
AvailablePluginsInstructions::default()
)]
);
let fingerprint =
serde_json::to_value(&previous).unwrap()["plugins_instructions"]["instructions"].clone();
assert!(!fingerprint.is_null());
for (resolved, expected_catalog) in [
(
resolve(Some(&bundled.intro), Some(&bundled.usage_instructions)),
true,
),
(
resolve(Some(&bundled.intro), Some(&bundled.usage_instructions)),
true,
),
(resolve(/*intro*/ None, /*usage*/ None), false),
] {
assert_eq!(resolved.catalog_override, expected_catalog);
let mut current = WorldState::default();
current.add_section(PluginsInstructionsState::new(
/*available*/ true,
resolved,
history.iter(),
));
let (snapshot, updates) = current.render_history_fragment_diff(Some(&previous), &history);
assert_eq!(
updates
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>(),
Vec::<ResponseItem>::new()
);
assert_eq!(
serde_json::to_value(&snapshot).unwrap(),
json!({"plugins_instructions": {"instructions": fingerprint, "catalog": expected_catalog}})
);
previous = snapshot;
}
}
#[test]
fn catalog_guidance_changes_falls_back_and_retires_without_rewriting_history() {
let bundled = AvailablePluginsInstructions::default().0;
let body_a = expected_body("Intro A", "Usage A");
let body_b = expected_body("Intro B", "Usage B");
let replacement_a = format!("{REPLACEMENT_PREFIX}{body_a}");
let replacement_b = format!("{REPLACEMENT_PREFIX}{body_b}");
let fallback = format!("{REPLACEMENT_PREFIX}{bundled}");
let oversized_intro = "x".repeat(501);
let oversized_usage = "y".repeat(2_001);
let mut history = Vec::new();
let mut previous = None;
for (available, intro, usage, expected) in [
(
true,
Some("Intro A"),
Some("Usage A"),
Some(body_a.as_str()),
),
(true, Some("Intro A"), Some("Usage A"), None),
(
true,
Some("Intro B"),
Some("Usage B"),
Some(replacement_b.as_str()),
),
(true, Some(""), Some(" \n"), Some(fallback.as_str())),
(
true,
Some(oversized_intro.as_str()),
Some(oversized_usage.as_str()),
None,
),
(true, None, None, None),
// Bundled-only availability changes retain the legacy quiet removal.
(false, None, None, None),
(true, None, None, Some(bundled.as_str())),
// Valid catalog intent still retires bundled text when availability is off.
(false, Some("Intro B"), Some("Usage B"), Some(REMOVAL_BODY)),
(false, Some("Intro B"), Some("Usage B"), None),
(
true,
Some("Intro B"),
Some("Usage B"),
Some(body_b.as_str()),
),
(false, Some("Intro B"), Some("Usage B"), Some(REMOVAL_BODY)),
// Empty plugin fields return to the bundled body, rather than clearing it.
(true, Some(""), Some(""), Some(bundled.as_str())),
(
true,
Some("Intro A"),
Some("Usage A"),
Some(replacement_a.as_str()),
),
] {
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
available,
resolve(intro, usage),
history.iter(),
));
let (snapshot, fragments) =
world_state.render_history_fragment_diff(previous.as_ref(), &history);
let updates = fragments
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
assert_eq!(
updates,
expected
.map(
|body| ContextualUserFragment::into(AvailablePluginsInstructions(
body.to_string()
))
)
.into_iter()
.collect::<Vec<_>>()
);
if !available {
// A previous client's boolean reader must restore bundled guidance once plugins
// become available again, including after a catalog retirement notice.
let mut replayed = previous.clone().unwrap_or_default();
if let Some(patch) = snapshot.merge_patch_from(&replayed) {
replayed.apply_merge_patch(&patch);
}
let availability: bool = serde_json::from_value(
replayed.into_object()[PluginsInstructionsState::ID].clone(),
)
.expect("a previous client can read the persisted plugin availability");
assert!(!availability);
}
history.extend(updates);
previous = Some(snapshot);
}
}
#[test]
fn a_lost_catalog_retirement_is_repaired_after_switching_to_a_bundled_model() {
fn render(
available: bool,
intro: Option<&str>,
history: &[ResponseItem],
previous: &WorldStateSnapshot,
) -> (WorldStateSnapshot, Vec<ResponseItem>) {
let mut world = WorldState::default();
world.add_section(PluginsInstructionsState::new(
available,
resolve(intro, /*usage*/ None),
history.iter(),
));
let (snapshot, updates) = world.render_history_fragment_diff(Some(previous), history);
(
snapshot,
updates
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect(),
)
}
let bundled = ContextualUserFragment::into(AvailablePluginsInstructions::default());
let stale_history = std::slice::from_ref(&bundled);
let previous: WorldStateSnapshot =
serde_json::from_value(json!({"plugins_instructions": true})).unwrap();
let (retired, updates) = render(
/*available*/ false,
Some("Catalog intro"),
stale_history,
&previous,
);
let notice =
ContextualUserFragment::into(AvailablePluginsInstructions(REMOVAL_BODY.to_string()));
assert_eq!(updates.as_slice(), std::slice::from_ref(&notice));
// A boolean reader can still restore its bundled guidance, and a reader with the
// original object snapshot can repair the same lost retirement.
let mut saved = previous;
saved.apply_merge_patch(&retired.merge_patch_from(&saved).unwrap());
assert_eq!(
serde_json::to_value(&saved).unwrap(),
json!({
"plugins_instructions": false,
"plugins_instructions_catalog_retired": {"catalog": true}
})
);
let previous_format: WorldStateSnapshot =
serde_json::from_value(json!({"plugins_instructions": {"catalog": true}})).unwrap();
for checkpoint in [&saved, &previous_format] {
let (repaired, updates) = render(
/*available*/ false,
/*intro*/ None,
stale_history,
checkpoint,
);
assert_eq!(repaired, retired);
assert_eq!(updates.as_slice(), std::slice::from_ref(&notice));
}
let repaired_history = [bundled.clone(), notice];
let (restored, updates) = render(
/*available*/ true,
/*intro*/ None,
&repaired_history,
&saved,
);
assert_eq!(updates, [bundled]);
saved.apply_merge_patch(&restored.merge_patch_from(&saved).unwrap());
assert!(!saved.into_object().contains_key(RETIRED_CHECKPOINT_ID));
}
#[test]
fn legacy_false_snapshots_do_not_hide_retained_catalog_guidance() {
let bundled = AvailablePluginsInstructions::default().0;
let custom = expected_body("Catalog intro", "Catalog usage");
let replaced_custom = format!("{REPLACEMENT_PREFIX}{custom}");
let replaced_bundled = format!("{REPLACEMENT_PREFIX}{bundled}");
let snapshot: WorldStateSnapshot =
serde_json::from_value(json!({"plugins_instructions": false})).unwrap();
for (retained_body, available, intro, usage, expected) in [
(
custom.as_str(),
true,
Some("Catalog intro"),
Some("Catalog usage"),
None,
),
(
replaced_custom.as_str(),
true,
Some("Catalog intro"),
Some("Catalog usage"),
None,
),
(
custom.as_str(),
true,
None,
None,
Some(replaced_bundled.as_str()),
),
(
replaced_custom.as_str(),
false,
None,
None,
Some(REMOVAL_BODY),
),
(
bundled.as_str(),
false,
Some("Catalog intro"),
Some("Catalog usage"),
Some(REMOVAL_BODY),
),
(bundled.as_str(), false, None, None, None),
(bundled.as_str(), true, None, None, Some(bundled.as_str())),
] {
let retained: ResponseItem =
ContextualUserFragment::into(AvailablePluginsInstructions(retained_body.to_string()));
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
available,
resolve(intro, usage),
std::iter::once(&retained),
));
let updates = world_state
.render_history_fragment_diff(Some(&snapshot), std::slice::from_ref(&retained))
.1
.len(),
1
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
assert_eq!(
updates,
expected
.map(
|body| ContextualUserFragment::into(AvailablePluginsInstructions(
body.to_string()
))
)
.into_iter()
.collect::<Vec<_>>()
);
}
}
#[test]
fn retained_guidance_recovers_snapshots_and_repairs_a_missing_latest_update() {
let mut history = Vec::new();
let mut previous = None;
for (available, intro, usage) in [
(true, "Intro A", "Usage A"),
(true, "Intro B", "Usage B"),
(false, "Intro B", "Usage B"),
(true, "Intro B", "Usage B\r\n"),
(true, "Intro B", "Usage B\n"),
// Literal transition-like text inside catalog fields must remain literal on replay.
(
true,
"These plugin usage instructions replace all previously provided plugin usage instructions.",
"\nThese plugin usage instructions replace all previously provided plugin usage instructions.\n\nUsage C",
),
(true, "", ""),
] {
let resolved = resolve(Some(intro), Some(usage));
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
available,
resolved.clone(),
history.iter(),
));
let (snapshot, fragments) =
world_state.render_history_fragment_diff(previous.as_ref(), &history);
let updates = fragments
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
assert_eq!(
updates.len(),
1,
"a byte change or availability transition must emit exactly one update"
);
for saved in [
None,
Some(json!({"plugins_instructions": true})),
Some(serde_json::to_value(&snapshot).unwrap()),
] {
let saved: Option<WorldStateSnapshot> =
saved.map(|value| serde_json::from_value(value).unwrap());
let mut resumed = WorldState::default();
resumed.add_section(PluginsInstructionsState::new(
available,
resolved.clone(),
updates.iter(),
));
assert!(
resumed
.render_history_fragment_diff(saved.as_ref(), &updates)
.1
.is_empty(),
"retained current wording must not be duplicated after resume"
);
}
if !history.is_empty() {
assert_eq!(
world_state
.render_history_fragment_diff(Some(&snapshot), &history)
.1
.len(),
1,
"an older retained marker must not suppress repair of a lost newest update"
);
}
history.extend(updates);
previous = Some(snapshot);
}
}
#[test]
fn retained_plugin_guidance_must_be_a_complete_developer_fragment() {
let body = expected_body("Catalog intro", "Catalog usage");
for (role, quoted, expected_count) in [
("developer", false, 0),
("user", false, 1),
("assistant", false, 1),
("developer", true, 1),
] {
let mut retained: ResponseItem =
ContextualUserFragment::into(AvailablePluginsInstructions(body.clone()));
let ResponseItem::Message {
role: retained_role,
content,
..
} = &mut retained
else {
panic!("plugin instructions should render as a message");
};
*retained_role = role.to_string();
if quoted {
let ContentItem::InputText { text } = &mut content[0] else {
panic!("plugin instructions should render as input text");
};
*text = format!("Quoted example: {text}");
}
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
/*available*/ true,
resolve(Some("Catalog intro"), Some("Catalog usage")),
std::iter::once(&retained),
));
let snapshot = world_state.render_full().0;
let updates = world_state
.render_history_fragment_diff(Some(&snapshot), std::slice::from_ref(&retained))
.1;
assert_eq!(
updates.len(),
expected_count,
"role={role}, quoted={quoted}"
);
for update in updates {
assert_eq!(
(update.role(), update.content_kind().0),
("developer", "plugins.usage_instructions".to_string())
);
assert_eq!(
update.into_boxed_response_item(),
ContextualUserFragment::into(AvailablePluginsInstructions(body.clone()))
);
}
}
}
#[test]
fn catalog_fields_at_utf8_limits_fit_final_framing_without_truncation() {
let intro = "é".repeat(250);
let usage = "\u{10ffff}".repeat(500);
let body = expected_body(&intro, &usage);
let retained: ResponseItem = ContextualUserFragment::into(AvailablePluginsInstructions(
expected_body("Old intro", "Old usage"),
));
let mut world_state = WorldState::default();
world_state.add_section(PluginsInstructionsState::new(
/*available*/ true,
resolve(Some(&intro), Some(&usage)),
std::iter::once(&retained),
));
let (_, updates) = world_state
.render_history_fragment_diff(/*previous*/ None, std::slice::from_ref(&retained));
assert_eq!(updates.len(), 1);
let update = &updates[0];
assert_eq!(
(update.role(), update.content_kind().0),
("developer", "plugins.usage_instructions".to_string())
);
let ContentItem::InputText { text } = update.render() else {
panic!("plugin instructions should render as input text");
};
assert_eq!(
text,
format!("<plugins_instructions>{REPLACEMENT_PREFIX}{body}</plugins_instructions>")
);
assert!(
world_state
.render_history_fragment_diff(Some(&snapshot), &[retained])
.1
.is_empty()
text.len() <= 4096,
"the final bound includes markers and transition framing"
);
}
@@ -8,7 +8,7 @@ None
Absent -> false
None
Absent -> true (role - developer)
Absent -> {"catalog":false,"instructions":"d3f660b06d428f262fffddce3d2b11f390d8b1da"} (role - developer)
<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps.
@@ -21,7 +21,7 @@ A plugin is a local bundle of skills, MCP servers, and apps.
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>
false -> true (role - developer)
false -> {"catalog":false,"instructions":"d3f660b06d428f262fffddce3d2b11f390d8b1da"} (role - developer)
<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps.
@@ -34,14 +34,14 @@ A plugin is a local bundle of skills, MCP servers, and apps.
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>
true -> true
{"catalog":false,"instructions":"d3f660b06d428f262fffddce3d2b11f390d8b1da"} -> {"catalog":false,"instructions":"d3f660b06d428f262fffddce3d2b11f390d8b1da"}
None
true -> false
{"catalog":false,"instructions":"d3f660b06d428f262fffddce3d2b11f390d8b1da"} -> false
None
Unknown -> false
None
Unknown -> true
Unknown -> {"catalog":false,"instructions":"d3f660b06d428f262fffddce3d2b11f390d8b1da"}
None