mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
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:
@@ -32,6 +32,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
upgrade: preset.upgrade.as_ref().map(Into::into),
|
||||
model_messages: Some(ModelMessages {
|
||||
apps: None,
|
||||
plugins: None,
|
||||
content_filter_guidance: None,
|
||||
persistent_instructions: None,
|
||||
tools: None,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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(¬ice));
|
||||
|
||||
// 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(¬ice));
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
@@ -17,11 +17,13 @@ pub(crate) fn build_plugin_injections(
|
||||
mcp_tools: &[ToolInfo],
|
||||
available_connectors: &[connectors::AppInfo],
|
||||
features: &Features,
|
||||
model_messages: codex_prompts::ResolvedModelMessages<'_>,
|
||||
) -> Vec<ResponseItem> {
|
||||
if mentioned_plugins.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let messages = model_messages.selected_plugin();
|
||||
// Turn each explicit plugin mention with available capabilities into a developer hint.
|
||||
mentioned_plugins
|
||||
.iter()
|
||||
@@ -57,6 +59,7 @@ pub(crate) fn build_plugin_injections(
|
||||
&available_mcp_servers,
|
||||
&available_apps,
|
||||
features,
|
||||
&messages,
|
||||
)
|
||||
.map(PluginInstructions::new)
|
||||
.map(ContextualUserFragment::into)
|
||||
|
||||
@@ -5,17 +5,20 @@ use crate::context::ContextualUserFragment;
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_prompts::ResolvedSelectedPluginMessages;
|
||||
#[cfg(test)]
|
||||
use codex_protocol_test_support::ContentItemTestExt;
|
||||
use codex_utils_string::take_bytes_at_char_boundary;
|
||||
|
||||
const MAX_EXPLICIT_PLUGIN_INSTRUCTIONS_BYTES: usize = 4 * 1024;
|
||||
const TRUNCATED_PLUGIN_INSTRUCTIONS_SUFFIX: &str =
|
||||
"\n- Additional plugin capabilities omitted to fit the context limit.";
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn render_plugins_section(plugins: &[PluginCapabilitySummary]) -> Option<String> {
|
||||
(!plugins.is_empty()).then(|| AvailablePluginsInstructions.render().expect_text())
|
||||
(!plugins.is_empty()).then(|| {
|
||||
AvailablePluginsInstructions::default()
|
||||
.render()
|
||||
.expect_text()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_explicit_plugin_instructions(
|
||||
@@ -23,6 +26,7 @@ pub(crate) fn render_explicit_plugin_instructions(
|
||||
available_mcp_servers: &[String],
|
||||
available_apps: &[String],
|
||||
features: &Features,
|
||||
messages: &ResolvedSelectedPluginMessages,
|
||||
) -> Option<String> {
|
||||
if features.enabled(Feature::SimplifiedSelectionHints) {
|
||||
return (plugin.has_skills || !available_apps.is_empty() || !available_mcp_servers.is_empty())
|
||||
@@ -37,20 +41,14 @@ pub(crate) fn render_explicit_plugin_instructions(
|
||||
});
|
||||
}
|
||||
|
||||
let mut lines = vec![format!(
|
||||
"Capabilities from the `{}` plugin:",
|
||||
plugin.display_name
|
||||
)];
|
||||
let mut lines = vec![
|
||||
messages
|
||||
.intro
|
||||
.replace("{{ plugin_name }}", &plugin.display_name),
|
||||
];
|
||||
|
||||
if !available_apps.is_empty() {
|
||||
lines.push(
|
||||
concat!(
|
||||
"- For the user request that explicitly selected this plugin, and only for that ",
|
||||
"request, if `tool_search` is available and an app from this plugin may help, ",
|
||||
"search for its tools before falling back to unrelated or built-in tools."
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
lines.push(messages.search_tools.clone());
|
||||
}
|
||||
|
||||
if plugin.has_skills {
|
||||
@@ -58,42 +56,55 @@ pub(crate) fn render_explicit_plugin_instructions(
|
||||
.plugin_namespace
|
||||
.as_deref()
|
||||
.unwrap_or(plugin.display_name.as_str());
|
||||
lines.push(format!(
|
||||
"- Skills from this plugin are prefixed with `{skill_namespace}:`."
|
||||
));
|
||||
lines.push(
|
||||
messages
|
||||
.skill_namespace
|
||||
.replace("{{ skill_namespace }}", skill_namespace),
|
||||
);
|
||||
}
|
||||
|
||||
if !available_apps.is_empty() {
|
||||
lines.push(format!(
|
||||
"- Apps from this plugin available in this session: {}.",
|
||||
available_apps
|
||||
.iter()
|
||||
.map(|app| format!("`{app}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
lines.push(
|
||||
messages.apps.replace(
|
||||
"{{ apps }}",
|
||||
&available_apps
|
||||
.iter()
|
||||
.map(|app| format!("`{app}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if !available_mcp_servers.is_empty() {
|
||||
lines.push(format!(
|
||||
"- MCP servers from this plugin available in this session: {}.",
|
||||
available_mcp_servers
|
||||
.iter()
|
||||
.map(|server| format!("`{server}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
lines.push(
|
||||
messages.mcp_servers.replace(
|
||||
"{{ mcp_servers }}",
|
||||
&available_mcp_servers
|
||||
.iter()
|
||||
.map(|server| format!("`{server}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if lines.len() == 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
lines.push("Use these plugin-associated capabilities to help solve the task.".to_string());
|
||||
lines.push(messages.closing.clone());
|
||||
|
||||
// The separator belongs to the harness; preserve the catalog's exact bytes after it.
|
||||
let omitted = if messages.omitted.starts_with('\n') || messages.omitted.starts_with("\r\n") {
|
||||
messages.omitted.clone()
|
||||
} else {
|
||||
format!("\n{}", messages.omitted)
|
||||
};
|
||||
|
||||
Some(bound_explicit_plugin_instructions(
|
||||
lines.join("\n"),
|
||||
TRUNCATED_PLUGIN_INSTRUCTIONS_SUFFIX,
|
||||
&omitted,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ fn explicit_plugin_instructions_use_manifest_namespace_for_skills() {
|
||||
&[],
|
||||
&[],
|
||||
&Features::default(),
|
||||
&codex_prompts::ResolvedModelMessages::bundled().selected_plugin(),
|
||||
)
|
||||
.expect("skill capability should render");
|
||||
|
||||
@@ -55,6 +56,7 @@ fn explicit_plugin_instructions_search_available_apps_before_fallback() {
|
||||
&[],
|
||||
&["Adobe".to_string()],
|
||||
&Features::default(),
|
||||
&codex_prompts::ResolvedModelMessages::bundled().selected_plugin(),
|
||||
)
|
||||
.expect("app capability should render");
|
||||
|
||||
@@ -88,6 +90,7 @@ fn explicit_plugin_instructions_are_bounded() {
|
||||
&servers,
|
||||
&apps,
|
||||
&Features::default(),
|
||||
&codex_prompts::ResolvedModelMessages::bundled().selected_plugin(),
|
||||
)
|
||||
.expect("MCP capability should render");
|
||||
|
||||
@@ -96,5 +99,59 @@ fn explicit_plugin_instructions_are_bounded() {
|
||||
assert!(rendered.contains("if `tool_search` is available"));
|
||||
assert!(rendered.contains("Skills from this plugin"));
|
||||
assert!(rendered.contains("`app-0`"));
|
||||
assert!(rendered.ends_with(TRUNCATED_PLUGIN_INSTRUCTIONS_SUFFIX));
|
||||
assert!(
|
||||
rendered.ends_with(
|
||||
&codex_prompts::ResolvedModelMessages::bundled()
|
||||
.selected_plugin()
|
||||
.omitted
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_plugin_templates_insert_runtime_facts_once() {
|
||||
let mut messages = codex_prompts::ResolvedModelMessages::bundled().selected_plugin();
|
||||
messages.intro = "Selected {{ plugin_name }}.".into();
|
||||
messages.search_tools = "Search wording {{literal}}.\r\n".into();
|
||||
messages.skill_namespace = "Skills {{ skill_namespace }}.".into();
|
||||
messages.apps = "Apps {{ apps }}.".into();
|
||||
messages.mcp_servers = "Servers {{ mcp_servers }}.".into();
|
||||
messages.closing = "Closing {{literal}}.".into();
|
||||
let rendered = render_explicit_plugin_instructions(
|
||||
&PluginCapabilitySummary {
|
||||
display_name: "Name {{ apps }}".into(),
|
||||
plugin_namespace: Some("namespace{{ plugin_name }}".into()),
|
||||
has_skills: true,
|
||||
..PluginCapabilitySummary::default()
|
||||
},
|
||||
&["server{{ apps }}".into()],
|
||||
&["app{{ mcp_servers }}".into()],
|
||||
&Features::default(),
|
||||
&messages,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"Selected Name {{ apps }}.\nSearch wording {{literal}}.\r\n\nSkills namespace{{ plugin_name }}.\nApps `app{{ mcp_servers }}`.\nServers `server{{ apps }}`.\nClosing {{literal}}."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_plugin_omission_keeps_utf8_and_the_final_item_bound() {
|
||||
let mut messages = codex_prompts::ResolvedModelMessages::bundled().selected_plugin();
|
||||
messages.omitted = "é".repeat(250);
|
||||
let rendered = render_explicit_plugin_instructions(
|
||||
&PluginCapabilitySummary {
|
||||
display_name: "plugin".into(),
|
||||
..Default::default()
|
||||
},
|
||||
&[],
|
||||
&["🦀".repeat(2_000)],
|
||||
&Features::default(),
|
||||
&messages,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rendered.len() <= MAX_EXPLICIT_PLUGIN_INSTRUCTIONS_BYTES);
|
||||
assert!(rendered.ends_with(&format!("\n{}", messages.omitted)));
|
||||
assert!(rendered.contains("🦀"));
|
||||
}
|
||||
|
||||
@@ -4983,7 +4983,13 @@ impl Session {
|
||||
if next_item.is_none()
|
||||
&& let Some(recommended_plugins) = recommended_plugin_candidates
|
||||
.as_deref()
|
||||
.and_then(RecommendedPluginsInstructions::from_plugins)
|
||||
.and_then(|plugins| {
|
||||
RecommendedPluginsInstructions::from_plugins(
|
||||
plugins,
|
||||
&ResolvedModelMessages::from_model(&step_context.settings.model_info)
|
||||
.plugin_recommendations(),
|
||||
)
|
||||
})
|
||||
{
|
||||
developer_sections.push(recommended_plugins.render_fragment());
|
||||
}
|
||||
|
||||
@@ -1271,6 +1271,7 @@ async fn build_skills_and_plugins(
|
||||
mcp_tools,
|
||||
&available_connectors,
|
||||
&turn_context.config.features,
|
||||
codex_prompts::ResolvedModelMessages::from_model(&step_context.settings.model_info),
|
||||
);
|
||||
let mut explicitly_enabled_connectors = collect_explicit_app_ids(user_input);
|
||||
explicitly_enabled_connectors.extend(skill_connector_ids);
|
||||
|
||||
@@ -342,9 +342,15 @@ impl Session {
|
||||
));
|
||||
let plugins_usage_instructions_available =
|
||||
step_context.mcp.plugins_available() && model_info.include_plugin_usage_instructions;
|
||||
world_state.add_section(PluginsInstructionsState::new(
|
||||
plugins_usage_instructions_available,
|
||||
));
|
||||
let plugins_state = {
|
||||
let state = self.state.lock().await;
|
||||
PluginsInstructionsState::new(
|
||||
plugins_usage_instructions_available,
|
||||
model_messages.generic_plugin(),
|
||||
state.history.raw_items(),
|
||||
)
|
||||
};
|
||||
world_state.add_section(plugins_state);
|
||||
if turn_context
|
||||
.config
|
||||
.features
|
||||
|
||||
@@ -41,6 +41,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent(
|
||||
let mut model = model_info_from_slug(model_slug);
|
||||
model.model_messages = Some(ModelMessages {
|
||||
apps: None,
|
||||
plugins: None,
|
||||
content_filter_guidance: None,
|
||||
persistent_instructions: None,
|
||||
tools: None,
|
||||
|
||||
@@ -75,6 +75,7 @@ fn model_with_collaboration_messages(
|
||||
let mut model = model_info_from_slug(slug);
|
||||
let model_messages = model.model_messages.get_or_insert(ModelMessages {
|
||||
apps: None,
|
||||
plugins: None,
|
||||
content_filter_guidance: None,
|
||||
persistent_instructions: None,
|
||||
tools: None,
|
||||
|
||||
@@ -644,6 +644,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
upgrade: None,
|
||||
model_messages: Some(ModelMessages {
|
||||
apps: None,
|
||||
plugins: None,
|
||||
content_filter_guidance: None,
|
||||
persistent_instructions: None,
|
||||
tools: None,
|
||||
|
||||
@@ -609,6 +609,143 @@ async fn startup_recommendations_use_developer_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case("custom"; "custom")]
|
||||
#[test_case("absent"; "absent")]
|
||||
#[test_case("null"; "null")]
|
||||
#[test_case("empty"; "empty")]
|
||||
#[test_case("blank"; "blank")]
|
||||
#[test_case("oversized"; "oversized")]
|
||||
#[test_case("disabled"; "disabled")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn catalog_plugin_recommendations_use_receiving_model(case: &str) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let apps = AppsTestServer::mount(&server).await?;
|
||||
mount_recommendations(
|
||||
&server,
|
||||
ResponseTemplate::new(200).set_body_json(json!({
|
||||
"enabled": true,
|
||||
"plugins": [{"id": "plugin_github", "name": "github", "display_name": "GitHub"}],
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let custom = " Catalog recommendations: {{literal}}.\r\n";
|
||||
let recommendations = match case {
|
||||
"custom" | "disabled" => json!({"intro": custom}),
|
||||
"absent" => json!({}),
|
||||
"null" => json!({"intro": null}),
|
||||
"empty" => json!({"intro": ""}),
|
||||
"blank" => json!({"intro": " \n"}),
|
||||
"oversized" => json!({"intro": "x".repeat(501)}),
|
||||
_ => unreachable!("declared test case"),
|
||||
};
|
||||
let enabled = case != "disabled";
|
||||
let test = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
configure_apps_without_search_tool(config, &apps.chatgpt_base_url);
|
||||
config.features.disable(Feature::ToolSuggest).unwrap();
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::RecommendedPlugins, enabled)
|
||||
.unwrap();
|
||||
let catalog = config.model_catalog.as_mut().unwrap();
|
||||
let startup_model = catalog
|
||||
.models
|
||||
.iter_mut()
|
||||
.find(|model| model.slug == "gpt-5.5")
|
||||
.unwrap();
|
||||
startup_model.model_messages = Some(serde_json::from_value(json!({
|
||||
"plugins": {"recommendations": {"intro": "Stale startup model recommendations."}},
|
||||
})).unwrap());
|
||||
let mut receiving_model = startup_model.clone();
|
||||
receiving_model.slug = "recommendations-receiving".to_string();
|
||||
receiving_model.model_messages = Some(
|
||||
serde_json::from_value(json!({
|
||||
"plugins": {"recommendations": recommendations},
|
||||
}))
|
||||
.unwrap(),
|
||||
);
|
||||
catalog.models.push(receiving_model);
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![ev_completed("recommendations-first")]),
|
||||
sse(vec![ev_completed("recommendations-followup")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
test.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "Suggest a plugin for this task.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
model: Some("recommendations-receiving".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
test.submit_text_turn("Continue with the same recommendations.")
|
||||
.await?;
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let expected_intro = if case == "custom" {
|
||||
custom
|
||||
} else {
|
||||
"Here is a list of plugins that are available but not installed."
|
||||
};
|
||||
let expected = if enabled {
|
||||
vec![format!(
|
||||
"<recommended_plugins>\n{expected_intro}\n\n- GitHub (github@openai-curated-remote)\n</recommended_plugins>"
|
||||
)]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
for request in &requests {
|
||||
assert_eq!(
|
||||
request.body_json()["model"],
|
||||
json!("recommendations-receiving")
|
||||
);
|
||||
let actual = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<recommended_plugins>"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"the receiving model owns wording; runtime recommendation identities remain unchanged and appear once"
|
||||
);
|
||||
assert!(!request.body_contains_text("Stale startup model recommendations."));
|
||||
assert!(
|
||||
!request
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.any(|text| text.contains("<recommended_plugins>"))
|
||||
);
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
format!("catalog_plugin_recommendations_{case}"),
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.",
|
||||
&requests,
|
||||
&ContextSnapshotOptions::default()
|
||||
.rewrite_known_segments()
|
||||
.include_request_settings(),
|
||||
)
|
||||
);
|
||||
test.codex.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mcp_discovery_overlaps_endpoint_plugin_recommendations() -> Result<()> {
|
||||
skip_if_wine_exec!(
|
||||
|
||||
@@ -19,6 +19,7 @@ use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::types::McpServerConfig;
|
||||
@@ -350,6 +351,27 @@ fn configure_scenario_catalog(config: &mut Config) {
|
||||
config.cloud_skill_enabled = false;
|
||||
}
|
||||
|
||||
fn configure_catalog_plugin_scenario(config: &mut Config) {
|
||||
configure_scenario_catalog(config);
|
||||
config.workspace_roots = vec![config.cwd.clone()];
|
||||
let stack = &config.config_layer_stack;
|
||||
let fixture_config = stack.effective_config();
|
||||
// Keep the fixture's plugin and App policy while avoiding discovery of the real
|
||||
// $HOME/.agents/skills, which a User layer adds alongside the fixture's skills.
|
||||
config.config_layer_stack = ConfigLayerStack::new(
|
||||
vec![ConfigLayerEntry::new(
|
||||
ConfigLayerSource::System {
|
||||
file: config.codex_home.join("config.toml"),
|
||||
},
|
||||
fixture_config,
|
||||
)],
|
||||
stack.requirements().clone(),
|
||||
stack.requirements_toml().clone(),
|
||||
)
|
||||
.expect("isolated plugin scenario config")
|
||||
.with_cloud_config_binding(stack.cloud_config_binding().cloned());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn admitted_prompt_id_reaches_responses() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -3901,3 +3923,525 @@ async fn unshared_apps_catalog_across_turns() -> Result<()> {
|
||||
insta::assert_snapshot!("unshared_apps_catalog_across_turns", snapshot);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn catalog_plugin_guidance_follows_model_and_survives_resume() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
write_scenario_capabilities(&home)?;
|
||||
let configure = |config: &mut Config| {
|
||||
configure_catalog_plugin_scenario(config);
|
||||
let catalog = config.model_catalog.as_mut().unwrap();
|
||||
let base = catalog
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.slug == "gpt-5.5")
|
||||
.unwrap()
|
||||
.clone();
|
||||
for (slug, generic, enabled) in [
|
||||
("plugins-default", json!({}), true),
|
||||
(
|
||||
"plugins-a",
|
||||
json!({"intro": "Catalog intro A: {{literal}}.", "usage_instructions": "Catalog usage A.\nKeep this line."}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"plugins-b",
|
||||
json!({"intro": "Catalog intro B.", "usage_instructions": "Catalog usage B."}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"plugins-empty",
|
||||
json!({"intro": "", "usage_instructions": " \n"}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"plugins-invalid",
|
||||
json!({"intro": "x".repeat(501), "usage_instructions": "y".repeat(2001)}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"plugins-null",
|
||||
json!({"intro": null, "usage_instructions": null}),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"plugins-off",
|
||||
json!({"intro": "Disabled catalog intro.", "usage_instructions": "Disabled catalog usage."}),
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let mut model = base.clone();
|
||||
model.slug = slug.to_string();
|
||||
model.include_plugin_usage_instructions = enabled;
|
||||
model.model_messages =
|
||||
Some(serde_json::from_value(json!({"plugins": {"generic": generic}})).unwrap());
|
||||
catalog.models.push(model);
|
||||
}
|
||||
};
|
||||
let mut builder = test_codex()
|
||||
.with_model("plugins-default")
|
||||
.with_home(home)
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_extensions(skills_extensions())
|
||||
.with_config(configure);
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
(0..10)
|
||||
.map(|i| sse(vec![ev_completed(&format!("plugins-{i}"))]))
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
test.submit_text_turn("Use the bundled plugin guidance.")
|
||||
.await?;
|
||||
for model in ["plugins-a", "plugins-a", "plugins-b"] {
|
||||
test.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![text("Continue with the selected model.")])
|
||||
.with_thread_settings(codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
model: Some(model.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
builder = builder.with_config(configure).with_model("plugins-b");
|
||||
let resumed = builder.restart_with_auto_env(&server, &test).await?;
|
||||
resumed
|
||||
.submit_text_turn("Continue after restarting with the same guidance.")
|
||||
.await?;
|
||||
for model in [
|
||||
"plugins-empty",
|
||||
"plugins-invalid",
|
||||
"plugins-null",
|
||||
"plugins-off",
|
||||
"plugins-a",
|
||||
] {
|
||||
resumed
|
||||
.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![text("Continue with the selected model.")])
|
||||
.with_thread_settings(codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
model: Some(model.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
wait_for_event(&resumed.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), 10);
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| request.body_json()["model"].clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
json!("plugins-default"),
|
||||
json!("plugins-a"),
|
||||
json!("plugins-a"),
|
||||
json!("plugins-b"),
|
||||
json!("plugins-b"),
|
||||
json!("plugins-empty"),
|
||||
json!("plugins-invalid"),
|
||||
json!("plugins-null"),
|
||||
json!("plugins-off"),
|
||||
json!("plugins-a"),
|
||||
]
|
||||
);
|
||||
let guidance = requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<plugins_instructions>"))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let bundled = concat!(
|
||||
"<plugins_instructions>\n## Plugins\n",
|
||||
"A plugin is a local bundle of skills, MCP servers, and apps.\n",
|
||||
"### How to use plugins\n",
|
||||
"- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.\n",
|
||||
"- 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.\n",
|
||||
"- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.\n",
|
||||
"- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.\n",
|
||||
"- 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.\n",
|
||||
"- 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.\n",
|
||||
"</plugins_instructions>",
|
||||
);
|
||||
let catalog_a = "<plugins_instructions>\n## Plugins\nCatalog intro A: {{literal}}.\n### How to use plugins\nCatalog usage A.\nKeep this line.\n</plugins_instructions>";
|
||||
let replacement_prefix = "<plugins_instructions>\nThese plugin usage instructions replace all previously provided plugin usage instructions.\n\n\n";
|
||||
let replaced_a = catalog_a.replacen("<plugins_instructions>\n", replacement_prefix, 1);
|
||||
let replaced_bundled = bundled.replacen("<plugins_instructions>\n", replacement_prefix, 1);
|
||||
assert_eq!(guidance[0], vec![bundled]);
|
||||
assert_eq!(guidance[1].last(), Some(&replaced_a));
|
||||
assert_eq!(guidance[2], guidance[1]);
|
||||
assert_eq!(
|
||||
guidance[3].last().map(String::as_str),
|
||||
Some(concat!(
|
||||
"<plugins_instructions>\n",
|
||||
"These plugin usage instructions replace all previously provided plugin usage instructions.\n\n\n",
|
||||
"## Plugins\nCatalog intro B.\n### How to use plugins\nCatalog usage B.\n</plugins_instructions>",
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
guidance[4], guidance[3],
|
||||
"resume must not duplicate retained custom guidance"
|
||||
);
|
||||
assert_eq!(guidance[5].last(), Some(&replaced_bundled));
|
||||
assert_eq!(
|
||||
guidance[6], guidance[5],
|
||||
"oversized fields fall back without appending unchanged guidance"
|
||||
);
|
||||
assert_eq!(
|
||||
guidance[7], guidance[5],
|
||||
"null fields preserve bundled guidance"
|
||||
);
|
||||
assert_eq!(
|
||||
guidance[8].last().map(String::as_str),
|
||||
Some(
|
||||
"<plugins_instructions>\nThe previously provided plugin usage instructions no longer apply.\n</plugins_instructions>"
|
||||
)
|
||||
);
|
||||
assert_eq!(guidance[9].last().map(String::as_str), Some(catalog_a));
|
||||
for pair in guidance.windows(2) {
|
||||
assert!(
|
||||
pair[1].starts_with(&pair[0]),
|
||||
"prior plugin guidance must remain verbatim in history"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
guidance[9]
|
||||
.iter()
|
||||
.filter(|text| text.contains("Catalog intro A: {{literal}}."))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(guidance.iter().flatten().all(|text| text.len() <= 4096));
|
||||
for request in &requests {
|
||||
assert!(!request.body_contains_text("Disabled catalog intro."));
|
||||
assert!(
|
||||
!request
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.any(|text| text.contains("<plugins_instructions>"))
|
||||
);
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
"catalog_plugin_guidance_follows_model_and_survives_resume",
|
||||
context_snapshot::format_labeled_requests_snapshot(
|
||||
"First and final requests show generic plugin guidance and the complete append-only history. All ten intermediate model, wording, fallback, availability and resume states are asserted in the test.",
|
||||
&[
|
||||
("Initial bundled guidance", &requests[0]),
|
||||
("Final retained history", &requests[9])
|
||||
],
|
||||
&ContextSnapshotOptions::default().include_request_settings(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(true, false; "apps_enabled")]
|
||||
#[test_case::test_case(false, false; "apps_disabled")]
|
||||
#[test_case::test_case(true, true; "simplified_selection")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn catalog_selected_plugin_wording_preserves_capability_policy(
|
||||
apps_enabled: bool,
|
||||
simplified_selection: bool,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
core_test_support::skip_if_remote!(Ok(()), "plugin MCP fixture uses a host-local executable");
|
||||
let server = start_mock_server().await;
|
||||
let apps = core_test_support::apps_test_server::AppsTestServer::mount(&server).await?;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
write_scenario_capabilities(&home)?;
|
||||
let plugin_root = home.path().join("plugins/cache/test/notes/local");
|
||||
fs::write(
|
||||
plugin_root.join(".mcp.json"),
|
||||
json!({"mcpServers": {"notes": {"command": stdio_server_bin()?, "cwd": "."}}}).to_string(),
|
||||
)?;
|
||||
fs::write(
|
||||
plugin_root.join(".app.json"),
|
||||
json!({"apps": {"notes_app": {"id": "calendar"}}}).to_string(),
|
||||
)?;
|
||||
let config_path = home.path().join("config.toml");
|
||||
fs::write(
|
||||
&config_path,
|
||||
format!(
|
||||
"{}\n[apps.calendar]\nenabled = {apps_enabled}\n",
|
||||
fs::read_to_string(&config_path)?
|
||||
),
|
||||
)?;
|
||||
let test = test_codex()
|
||||
.with_model("selected-custom")
|
||||
.with_home(home)
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_extensions(skills_extensions())
|
||||
.with_config(move |config| {
|
||||
configure_catalog_plugin_scenario(config);
|
||||
config.chatgpt_base_url = apps.chatgpt_base_url;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
.expect("enable Apps for selected-plugin scenario");
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::SimplifiedSelectionHints, simplified_selection)
|
||||
.expect("configure selected-plugin hint mode");
|
||||
let catalog = config
|
||||
.model_catalog
|
||||
.as_mut()
|
||||
.expect("scenario model catalog");
|
||||
let base = catalog
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.slug == "gpt-5.5")
|
||||
.expect("bundled scenario base model")
|
||||
.clone();
|
||||
for (slug, selected) in [
|
||||
(
|
||||
"selected-custom",
|
||||
json!({
|
||||
"intro": "Catalog selected {{ plugin_name }}.",
|
||||
"search_tools": "Catalog search for this explicit request.",
|
||||
"skill_namespace": "Catalog skills {{ skill_namespace }}.",
|
||||
"apps": "Catalog apps {{ apps }}.",
|
||||
"mcp_servers": "Catalog MCP {{ mcp_servers }}.",
|
||||
"closing": "Catalog closing.",
|
||||
"omitted": "Catalog omitted.",
|
||||
}),
|
||||
),
|
||||
(
|
||||
"selected-fallback",
|
||||
json!({
|
||||
"intro": "Invalid intro without the required name.",
|
||||
"search_tools": "Catalog search from the receiving model.",
|
||||
"skill_namespace": "Invalid {{ unknown }}.",
|
||||
"apps": " \n",
|
||||
"mcp_servers": "{{ mcp_servers }} and {{ mcp_servers }}",
|
||||
"closing": "x".repeat(501),
|
||||
"omitted": null,
|
||||
}),
|
||||
),
|
||||
] {
|
||||
let mut model = base.clone();
|
||||
model.slug = slug.to_string();
|
||||
model.include_plugin_usage_instructions = false;
|
||||
model.supports_search_tool = false;
|
||||
model.model_messages = Some(
|
||||
serde_json::from_value(json!({"plugins": {"selected": selected}}))
|
||||
.expect("selected-plugin model messages"),
|
||||
);
|
||||
catalog.models.push(model);
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
wait_for_mcp_server(&test.codex, "notes").await?;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
(0..4)
|
||||
.map(|i| sse(vec![ev_completed(&format!("selected-{i}"))]))
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
test.submit_text_turn("List the available capabilities.")
|
||||
.await?;
|
||||
for (model, input) in [
|
||||
(
|
||||
"selected-custom",
|
||||
vec![text("Use the selected plugin."), plugin("notes")],
|
||||
),
|
||||
(
|
||||
"selected-custom",
|
||||
vec![text("Continue without a plugin selection.")],
|
||||
),
|
||||
(
|
||||
"selected-fallback",
|
||||
vec![text("Use the selected plugin again."), plugin("notes")],
|
||||
),
|
||||
] {
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(input).with_thread_settings(
|
||||
codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
model: Some(model.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), 4);
|
||||
assert_eq!(requests[3].body_json()["model"], json!("selected-fallback"));
|
||||
let selected = requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| {
|
||||
text.starts_with("Catalog selected ")
|
||||
|| text.starts_with("Capabilities from ")
|
||||
|| text.starts_with("The user selected ")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let custom = if simplified_selection {
|
||||
"The user selected the `notes` plugin. Use its available skills and tools to complete this request."
|
||||
} else if apps_enabled {
|
||||
"Catalog selected notes.\nCatalog search for this explicit request.\nCatalog skills notes.\nCatalog apps `Calendar`.\nCatalog MCP `notes`.\nCatalog closing."
|
||||
} else {
|
||||
"Catalog selected notes.\nCatalog skills notes.\nCatalog MCP `notes`.\nCatalog closing."
|
||||
};
|
||||
let fallback = if simplified_selection {
|
||||
custom
|
||||
} else if apps_enabled {
|
||||
concat!(
|
||||
"Capabilities from the `notes` plugin:\n",
|
||||
"Catalog search from the receiving model.\n",
|
||||
"- Skills from this plugin are prefixed with `notes:`.\n",
|
||||
"- Apps from this plugin available in this session: `Calendar`.\n",
|
||||
"- MCP servers from this plugin available in this session: `notes`.\n",
|
||||
"Use these plugin-associated capabilities to help solve the task.",
|
||||
)
|
||||
} else {
|
||||
concat!(
|
||||
"Capabilities from the `notes` plugin:\n",
|
||||
"- Skills from this plugin are prefixed with `notes:`.\n",
|
||||
"- MCP servers from this plugin available in this session: `notes`.\n",
|
||||
"Use these plugin-associated capabilities to help solve the task.",
|
||||
)
|
||||
};
|
||||
assert!(
|
||||
selected[0].is_empty(),
|
||||
"catalog wording must not imply an explicit selection"
|
||||
);
|
||||
assert_eq!(selected[1], vec![custom]);
|
||||
assert_eq!(
|
||||
selected[2], selected[1],
|
||||
"an implicit follow-up must not repeat the selected-plugin hint"
|
||||
);
|
||||
assert_eq!(selected[3], vec![custom, fallback]);
|
||||
assert!(
|
||||
requests[1].body_contains_text("This tool is part of plugin `notes`."),
|
||||
"catalog wording must preserve tool provenance"
|
||||
);
|
||||
for request in &requests {
|
||||
assert!(
|
||||
!request
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.any(|text| text.contains("Catalog selected notes."))
|
||||
);
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
format!(
|
||||
"catalog_selected_plugin_wording_apps_{apps_enabled}_simplified_{simplified_selection}"
|
||||
),
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.",
|
||||
&requests,
|
||||
&ContextSnapshotOptions::default().include_request_settings(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn catalog_selected_plugin_omission_bounds_runtime_capabilities() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let apps = core_test_support::apps_test_server::AppsTestServer::mount_with_connector_name(
|
||||
&server,
|
||||
&"Calendar café ".repeat(400),
|
||||
)
|
||||
.await?;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
write_scenario_capabilities(&home)?;
|
||||
fs::write(
|
||||
home.path().join("plugins/cache/test/notes/local/.app.json"),
|
||||
json!({"apps": {"notes_app": {"id": "calendar"}}}).to_string(),
|
||||
)?;
|
||||
let test = test_codex()
|
||||
.with_model("gpt-5.5")
|
||||
.with_home(home)
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_extensions(skills_extensions())
|
||||
.with_config(move |config| {
|
||||
configure_catalog_plugin_scenario(config);
|
||||
config.chatgpt_base_url = apps.chatgpt_base_url;
|
||||
config.features.enable(Feature::Apps).unwrap();
|
||||
config
|
||||
.features
|
||||
.disable(Feature::SimplifiedSelectionHints)
|
||||
.unwrap();
|
||||
let model = config
|
||||
.model_catalog
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.models
|
||||
.iter_mut()
|
||||
.find(|model| model.slug == "gpt-5.5")
|
||||
.unwrap();
|
||||
model.include_plugin_usage_instructions = false;
|
||||
model.model_messages = Some(
|
||||
serde_json::from_value(json!({"plugins": {"selected": {
|
||||
"intro": "Catalog selected {{ plugin_name }}.",
|
||||
"apps": "Catalog apps {{ apps }}.",
|
||||
"omitted": "Catalog capability list omitted here.",
|
||||
}}}))
|
||||
.unwrap(),
|
||||
);
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
let mock = mount_sse_sequence(&server, vec![sse(vec![ev_completed("bounded-plugin")])]).await;
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![
|
||||
text("Use the selected plugin."),
|
||||
plugin("notes"),
|
||||
]))
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
let request = mock.single_request();
|
||||
let selected = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("Catalog selected notes."))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert!(selected[0].contains("Catalog apps `Calendar café "));
|
||||
assert!(selected[0].ends_with("\nCatalog capability list omitted here."));
|
||||
assert!(
|
||||
selected[0].len() <= 4096,
|
||||
"the bound applies after runtime capability substitution"
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
"catalog_selected_plugin_omission_bounds_runtime_capabilities",
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"A catalog omission suffix is reserved within the final selected-plugin bound after substituting a long Unicode runtime capability name.",
|
||||
&mock.requests(),
|
||||
&ContextSnapshotOptions::default().include_request_settings(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Here is a list of plugins that are available but not installed.
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Here is a list of plugins that are available but not installed.
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Catalog recommendations: {{literal}}.
|
||||
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Here is a list of plugins that are available but not installed.
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Here is a list of plugins that are available but not installed.
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
|
||||
---
|
||||
Scenario: The first receiving model controls recommendation wording without changing feature eligibility, developer role, runtime plugin identities or retained-history deduplication.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "recommendations-receiving"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=62414ae13aabdb67):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=9aecf0b528e2fee4
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Here is a list of plugins that are available but not installed.
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
Suggest a plugin for this task.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Continue with the same recommendations.
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_labeled_requests_snapshot(\"First and final requests show generic plugin guidance and the complete append-only history. All ten intermediate model, wording, fallback, availability and resume states are asserted in the test.\",\n&[(\"Initial bundled guidance\", &requests[0]),\n(\"Final retained history\", &requests[9])],\n&ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: First and final requests show generic plugin guidance and the complete append-only history. All ten intermediate model, wording, fallback, availability and resume states are asserted in the test.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "plugins-default"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (7; hash=994c9baf5d9a36a0):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=b573ed69c0b0e6c7
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn; Initial bundled guidance) --
|
||||
00:message/developer[4]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <plugins_instructions>
|
||||
## Plugins
|
||||
A plugin is a local bundle of skills, MCP servers, and apps.
|
||||
### How to use plugins
|
||||
- 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-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
</plugins_instructions>
|
||||
[04] <tools>
|
||||
Deferred tool namespaces:
|
||||
- multi_agent_v1: Tools for spawning and managing sub-agents.
|
||||
</tools>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
Use the bundled plugin guidance.
|
||||
|
||||
## Window 2 (after request 1)
|
||||
Settings: relative to window 1
|
||||
model: "plugins-a"
|
||||
tools (10; hash=df5c5997ff843305):
|
||||
+ function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
+ function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
+ function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
-- request 2 (turn; Final retained history) --
|
||||
00:message/developer[4]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <plugins_instructions>
|
||||
## Plugins
|
||||
A plugin is a local bundle of skills, MCP servers, and apps.
|
||||
### How to use plugins
|
||||
- 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-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
</plugins_instructions>
|
||||
[04] <tools>
|
||||
Deferred tool namespaces:
|
||||
- multi_agent_v1: Tools for spawning and managing sub-agents.
|
||||
</tools>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
Use the bundled plugin guidance.
|
||||
03:message/developer:
|
||||
<plugins_instructions>
|
||||
These plugin usage instructions replace all previously provided plugin usage instructions.
|
||||
|
||||
|
||||
## Plugins
|
||||
Catalog intro A: {{literal}}.
|
||||
### How to use plugins
|
||||
Catalog usage A.
|
||||
Keep this line.
|
||||
</plugins_instructions>
|
||||
04:message/user:
|
||||
Continue with the selected model.
|
||||
05:message/user:
|
||||
Continue with the selected model.
|
||||
06:message/developer:
|
||||
<plugins_instructions>
|
||||
These plugin usage instructions replace all previously provided plugin usage instructions.
|
||||
|
||||
|
||||
## Plugins
|
||||
Catalog intro B.
|
||||
### How to use plugins
|
||||
Catalog usage B.
|
||||
</plugins_instructions>
|
||||
07:message/user:
|
||||
Continue with the selected model.
|
||||
08:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD 2></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD 2></root></workspace_roots><permission_profile type="managed">...ntry></file_system></permission_profile></filesystem> [hash=8fc9d186c1133657]
|
||||
</environment_context>
|
||||
09:message/user:
|
||||
Continue after restarting with the same guidance.
|
||||
10:message/developer:
|
||||
<plugins_instructions>
|
||||
These plugin usage instructions replace all previously provided plugin usage instructions.
|
||||
|
||||
|
||||
## Plugins
|
||||
A plugin is a local bundle of skills, MCP servers, and apps.
|
||||
### How to use plugins
|
||||
- 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-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
</plugins_instructions>
|
||||
11:message/user:
|
||||
Continue with the selected model.
|
||||
12:message/user:
|
||||
Continue with the selected model.
|
||||
13:message/user:
|
||||
Continue with the selected model.
|
||||
14:message/developer:
|
||||
<plugins_instructions>
|
||||
The previously provided plugin usage instructions no longer apply.
|
||||
</plugins_instructions>
|
||||
15:message/user:
|
||||
Continue with the selected model.
|
||||
16:message/developer:
|
||||
<plugins_instructions>
|
||||
## Plugins
|
||||
Catalog intro A: {{literal}}.
|
||||
### How to use plugins
|
||||
Catalog usage A.
|
||||
Keep this line.
|
||||
</plugins_instructions>
|
||||
17:message/user:
|
||||
Continue with the selected model.
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"A catalog omission suffix is reserved within the final selected-plugin bound after substituting a long Unicode runtime capability name.\",\n&mock.requests(),\n&ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: A catalog omission suffix is reserved within the final selected-plugin bound after substituting a long Unicode runtime capability name.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "gpt-5.5"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (10; hash=df5c5997ff843305):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=b573ed69c0b0e6c7
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[4]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <apps_instructions>
|
||||
## Apps (Connectors)
|
||||
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connect...long as the context suggests usage of available apps. [hash=8c2ddeb1adaf185d]
|
||||
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
</apps_instructions>
|
||||
[04] <tools>
|
||||
Deferred tool namespaces:
|
||||
- mcp__codex_apps__calendar_caf__calendar_caf__calendar_caf__calendar_caf__calendar_caf__calendar_caf__calendar_caf: Plan events and manage your calendar.
|
||||
- multi_agent_v1: Tools for spawning and managing sub-agents.
|
||||
</tools>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
Use the selected plugin.
|
||||
03:message/developer:
|
||||
Catalog selected notes.
|
||||
- For the user request that explicitly selected this plugin, and only for that request, if `tool_search`...s before falling back to unrelated or built-in tools. [hash=a8711ebc98aa4e05]
|
||||
- Skills from this plugin are prefixed with `notes:`.
|
||||
Catalog apps `Calendar café Calendar café Calendar café Calendar café Calendar café Calendar café Calend...dar café Calendar café Calendar café Calendar café Ca [hash=b7ebb1f4aabcf30e]
|
||||
Catalog capability list omitted here.
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.\",\n&requests, &ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "selected-custom"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (11; hash=70f020489861129c):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__notes: Use these tools to exercise the rmcp test server.; hash=508dce78d8cc5d3b
|
||||
- function/client_capabilities
|
||||
- function/cwd
|
||||
- function/echo
|
||||
- function/echo_tool
|
||||
- function/encrypted_output
|
||||
- function/image
|
||||
- function/image_scenario
|
||||
- function/sandbox_meta
|
||||
- function/sync
|
||||
- function/sync_readonly
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[2]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
List the available capabilities.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Use the selected plugin.
|
||||
04:message/developer:
|
||||
Catalog selected notes.
|
||||
Catalog skills notes.
|
||||
Catalog MCP `notes`.
|
||||
Catalog closing.
|
||||
-- request 3 (turn) --
|
||||
05:message/user:
|
||||
Continue without a plugin selection.
|
||||
|
||||
## Window 2 (after request 3)
|
||||
Settings: relative to window 1
|
||||
model: "selected-fallback"
|
||||
-- request 4 (turn) --
|
||||
00:message/developer[2]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
List the available capabilities.
|
||||
03:message/user:
|
||||
Use the selected plugin.
|
||||
04:message/developer:
|
||||
Catalog selected notes.
|
||||
Catalog skills notes.
|
||||
Catalog MCP `notes`.
|
||||
Catalog closing.
|
||||
05:message/user:
|
||||
Continue without a plugin selection.
|
||||
06:message/user:
|
||||
Use the selected plugin again.
|
||||
07:message/developer:
|
||||
Capabilities from the `notes` plugin:
|
||||
- Skills from this plugin are prefixed with `notes:`.
|
||||
- MCP servers from this plugin available in this session: `notes`.
|
||||
Use these plugin-associated capabilities to help solve the task.
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.\",\n&requests, &ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "selected-custom"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (12; hash=222ce6d65d94fe37):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__notes: Use these tools to exercise the rmcp test server.; hash=508dce78d8cc5d3b
|
||||
- function/client_capabilities
|
||||
- function/cwd
|
||||
- function/echo
|
||||
- function/echo_tool
|
||||
- function/encrypted_output
|
||||
- function/image
|
||||
- function/image_scenario
|
||||
- function/sandbox_meta
|
||||
- function/sync
|
||||
- function/sync_readonly
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=1ab035994e88edae
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <apps_instructions>
|
||||
## Apps (Connectors)
|
||||
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connect...long as the context suggests usage of available apps. [hash=8c2ddeb1adaf185d]
|
||||
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
</apps_instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
List the available capabilities.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Use the selected plugin.
|
||||
04:message/developer:
|
||||
Catalog selected notes.
|
||||
Catalog search for this explicit request.
|
||||
Catalog skills notes.
|
||||
Catalog apps `Calendar`.
|
||||
Catalog MCP `notes`.
|
||||
Catalog closing.
|
||||
-- request 3 (turn) --
|
||||
05:message/user:
|
||||
Continue without a plugin selection.
|
||||
|
||||
## Window 2 (after request 3)
|
||||
Settings: relative to window 1
|
||||
model: "selected-fallback"
|
||||
-- request 4 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <apps_instructions>
|
||||
## Apps (Connectors)
|
||||
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connect...long as the context suggests usage of available apps. [hash=8c2ddeb1adaf185d]
|
||||
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
</apps_instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
List the available capabilities.
|
||||
03:message/user:
|
||||
Use the selected plugin.
|
||||
04:message/developer:
|
||||
Catalog selected notes.
|
||||
Catalog search for this explicit request.
|
||||
Catalog skills notes.
|
||||
Catalog apps `Calendar`.
|
||||
Catalog MCP `notes`.
|
||||
Catalog closing.
|
||||
05:message/user:
|
||||
Continue without a plugin selection.
|
||||
06:message/user:
|
||||
Use the selected plugin again.
|
||||
07:message/developer:
|
||||
Capabilities from the `notes` plugin:
|
||||
Catalog search from the receiving model.
|
||||
- Skills from this plugin are prefixed with `notes:`.
|
||||
- Apps from this plugin available in this session: `Calendar`.
|
||||
- MCP servers from this plugin available in this session: `notes`.
|
||||
Use these plugin-associated capabilities to help solve the task.
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.\",\n&requests, &ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Selected-plugin wording follows the receiving model while live skills, MCP, App policy, explicit selection eligibility and simplified selection dispatch remain authoritative.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "selected-custom"
|
||||
parallel_tool_calls: true
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"effort":"medium"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
tools (12; hash=222ce6d65d94fe37):
|
||||
- function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction.; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=c04108e51a8548c5
|
||||
- function/write_stdin: Writes characters to an existing unified exec session and returns recent output.; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=7fb62d8a3e2df4e2
|
||||
- function/list_mcp_resources: Lists resources provided by MCP servers. Resources allow servers to share data that provides cont...; args=[cursor, server]; hash=6792edaff13561bd
|
||||
- function/list_mcp_resource_templates: Lists resource templates provided by MCP servers. Parameterized resource templates allow servers ...; args=[cursor, server]; hash=60f17c90ef7a4ebb
|
||||
- function/read_mcp_resource: Read a specific resource from an MCP server given the server name and resource URI.; args=[server, uri]; hash=24246896b39008ae
|
||||
- function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6
|
||||
- custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=dea9f12b0111a4ba
|
||||
- function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=3b766d5bdec5d50e
|
||||
- namespace/multi_agent_v1: Tools for spawning and managing sub-agents.; hash=2327142bda177c50
|
||||
- function/close_agent
|
||||
- function/resume_agent
|
||||
- function/send_input
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
- namespace/mcp__notes: Use these tools to exercise the rmcp test server.; hash=508dce78d8cc5d3b
|
||||
- function/client_capabilities
|
||||
- function/cwd
|
||||
- function/echo
|
||||
- function/echo_tool
|
||||
- function/encrypted_output
|
||||
- function/image
|
||||
- function/image_scenario
|
||||
- function/sandbox_meta
|
||||
- function/sync
|
||||
- function/sync_readonly
|
||||
- namespace/mcp__codex_apps__calendar: Plan events and manage your calendar.; hash=1ab035994e88edae
|
||||
- function/_create_event
|
||||
- function/_extract_text
|
||||
- function/_list_events
|
||||
- web_search; hash=f103e327c6870a46
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <apps_instructions>
|
||||
## Apps (Connectors)
|
||||
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connect...long as the context suggests usage of available apps. [hash=8c2ddeb1adaf185d]
|
||||
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
</apps_instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
List the available capabilities.
|
||||
-- request 2 (turn) --
|
||||
03:message/user:
|
||||
Use the selected plugin.
|
||||
04:message/developer:
|
||||
The user selected the `notes` plugin. Use its available skills and tools to complete this request.
|
||||
-- request 3 (turn) --
|
||||
05:message/user:
|
||||
Continue without a plugin selection.
|
||||
|
||||
## Window 2 (after request 3)
|
||||
Settings: relative to window 1
|
||||
model: "selected-fallback"
|
||||
-- request 4 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<SKILLS_ROOT>`
|
||||
- `r1` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- final-check: Review a final brief (file: r0/final-check/SKILL.md)
|
||||
<OMITTED 14 LINES; ~497 TOKENS; hash=1553c68bf4173825>
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruc...Do not load unrelated references, scripts, or assets. [hash=d6eeb13522175402]
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
|
||||
</skills_instructions>
|
||||
[02] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 44 LINES; ~790 TOKENS; hash=3a8900343e968d61>
|
||||
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
</permissions instructions>
|
||||
[03] <apps_instructions>
|
||||
## Apps (Connectors)
|
||||
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connect...long as the context suggests usage of available apps. [hash=8c2ddeb1adaf185d]
|
||||
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
</apps_instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="managed"><f...ntry></file_system></permission_profile></filesystem> [hash=e8c1de60307d0a3d]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
List the available capabilities.
|
||||
03:message/user:
|
||||
Use the selected plugin.
|
||||
04:message/developer:
|
||||
The user selected the `notes` plugin. Use its available skills and tools to complete this request.
|
||||
05:message/user:
|
||||
Continue without a plugin selection.
|
||||
06:message/user:
|
||||
Use the selected plugin again.
|
||||
07:message/developer:
|
||||
The user selected the `notes` plugin. Use its available skills and tools to complete this request.
|
||||
@@ -94,6 +94,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
|
||||
apps: Some(AppsMessages {
|
||||
usage_instructions: Some("Catalog Apps instructions.".to_string()),
|
||||
}),
|
||||
plugins: None,
|
||||
content_filter_guidance: None,
|
||||
persistent_instructions: Some(persistent_instructions.to_string()),
|
||||
tools: Some(ToolMessages {
|
||||
|
||||
@@ -17,10 +17,13 @@ pub use guardian_instructions::render_guardian_rejection;
|
||||
pub use model_instructions::render_model_instructions;
|
||||
pub use model_messages::ResolvedAutoReviewMessages;
|
||||
pub use model_messages::ResolvedCollaborationModeMessages;
|
||||
pub use model_messages::ResolvedGenericPluginMessages;
|
||||
pub use model_messages::ResolvedIncrementalToolMessages;
|
||||
pub use model_messages::ResolvedMessage;
|
||||
pub use model_messages::ResolvedModelMessages;
|
||||
pub use model_messages::ResolvedMultiAgentMessages;
|
||||
pub use model_messages::ResolvedPluginRecommendationMessages;
|
||||
pub use model_messages::ResolvedSelectedPluginMessages;
|
||||
pub use multi_agent_instructions::MultiAgentRoleInstructions;
|
||||
pub use permissions_instructions::ApprovalPromptContext;
|
||||
pub use permissions_instructions::PermissionsInstructions;
|
||||
|
||||
@@ -18,11 +18,15 @@ mod guardian;
|
||||
mod incremental_tools;
|
||||
mod multi_agent;
|
||||
pub(crate) mod permissions;
|
||||
mod plugins;
|
||||
|
||||
pub use collaboration::ResolvedCollaborationModeMessages;
|
||||
pub use guardian::ResolvedAutoReviewMessages;
|
||||
pub use incremental_tools::ResolvedIncrementalToolMessages;
|
||||
pub use multi_agent::ResolvedMultiAgentMessages;
|
||||
pub use plugins::ResolvedGenericPluginMessages;
|
||||
pub use plugins::ResolvedPluginRecommendationMessages;
|
||||
pub use plugins::ResolvedSelectedPluginMessages;
|
||||
|
||||
/// Text together with whether it was supplied by the catalog, even when it equals the default.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -279,3 +283,7 @@ impl<'a> ResolvedModelMessages<'a> {
|
||||
.unwrap_or(CONTENT_FILTER_GUIDANCE)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_messages/plugins_tests.rs"]
|
||||
mod plugins_tests;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
//! Resolves bounded plugin wording while leaving capability facts and eligibility to callers.
|
||||
//! Missing, blank, oversized, or invalid fields independently retain their bundled wording.
|
||||
|
||||
use super::ResolvedMessage;
|
||||
use super::ResolvedModelMessages;
|
||||
use codex_protocol::openai_models::PluginMessages;
|
||||
|
||||
const MAX_WORDING_BYTES: usize = 500;
|
||||
const MAX_USAGE_BYTES: usize = 2_000;
|
||||
const GENERIC_INTRO: &str = "A plugin is a local bundle of skills, MCP servers, and apps.";
|
||||
const GENERIC_USAGE: &str = 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."###;
|
||||
const SELECTED_INTRO: &str = "Capabilities from the `{{ plugin_name }}` plugin:";
|
||||
const SEARCH_TOOLS: &str = concat!(
|
||||
"- For the user request that explicitly selected this plugin, and only for that ",
|
||||
"request, if `tool_search` is available and an app from this plugin may help, ",
|
||||
"search for its tools before falling back to unrelated or built-in tools."
|
||||
);
|
||||
const SKILL_NAMESPACE: &str =
|
||||
"- Skills from this plugin are prefixed with `{{ skill_namespace }}:`.";
|
||||
const APPS: &str = "- Apps from this plugin available in this session: {{ apps }}.";
|
||||
const MCP_SERVERS: &str =
|
||||
"- MCP servers from this plugin available in this session: {{ mcp_servers }}.";
|
||||
const CLOSING: &str = "Use these plugin-associated capabilities to help solve the task.";
|
||||
const OMITTED: &str = "\n- Additional plugin capabilities omitted to fit the context limit.";
|
||||
const RECOMMENDATIONS_INTRO: &str =
|
||||
"Here is a list of plugins that are available but not installed.";
|
||||
|
||||
/// Generic wording and its source, retained by the usage-guidance lifecycle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedGenericPluginMessages {
|
||||
pub intro: String,
|
||||
pub usage_instructions: String,
|
||||
pub catalog_override: bool,
|
||||
}
|
||||
|
||||
/// Validated templates retain one required placeholder for each harness-owned fact.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedSelectedPluginMessages {
|
||||
pub intro: String,
|
||||
pub search_tools: String,
|
||||
pub skill_namespace: String,
|
||||
pub apps: String,
|
||||
pub mcp_servers: String,
|
||||
pub closing: String,
|
||||
pub omitted: String,
|
||||
}
|
||||
|
||||
/// Bounded introduction independent of recommendation eligibility and plugin identities.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedPluginRecommendationMessages {
|
||||
pub intro: String,
|
||||
}
|
||||
|
||||
impl<'a> ResolvedModelMessages<'a> {
|
||||
fn plugin_messages(self) -> Option<&'a PluginMessages> {
|
||||
self.catalog_messages?.plugins.as_ref()
|
||||
}
|
||||
|
||||
/// Resolves the generic plugin guidance used in the conversation's world state.
|
||||
pub fn generic_plugin(self) -> ResolvedGenericPluginMessages {
|
||||
let generic = self
|
||||
.plugin_messages()
|
||||
.and_then(|messages| messages.generic.as_ref());
|
||||
let intro = literal(
|
||||
generic.and_then(|messages| messages.intro.as_deref()),
|
||||
GENERIC_INTRO,
|
||||
MAX_WORDING_BYTES,
|
||||
);
|
||||
let usage = literal(
|
||||
generic.and_then(|messages| messages.usage_instructions.as_deref()),
|
||||
GENERIC_USAGE,
|
||||
MAX_USAGE_BYTES,
|
||||
);
|
||||
ResolvedGenericPluginMessages {
|
||||
intro: intro.text().to_owned(),
|
||||
usage_instructions: usage.text().to_owned(),
|
||||
catalog_override: intro.catalog_override().is_some()
|
||||
|| usage.catalog_override().is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the wording surrounding an explicitly selected plugin's capabilities.
|
||||
pub fn selected_plugin(self) -> ResolvedSelectedPluginMessages {
|
||||
let selected = self
|
||||
.plugin_messages()
|
||||
.and_then(|messages| messages.selected.as_ref());
|
||||
ResolvedSelectedPluginMessages {
|
||||
intro: template(
|
||||
selected.and_then(|messages| messages.intro.as_deref()),
|
||||
SELECTED_INTRO,
|
||||
"{{ plugin_name }}",
|
||||
),
|
||||
search_tools: literal(
|
||||
selected.and_then(|messages| messages.search_tools.as_deref()),
|
||||
SEARCH_TOOLS,
|
||||
MAX_WORDING_BYTES,
|
||||
)
|
||||
.text()
|
||||
.to_owned(),
|
||||
skill_namespace: template(
|
||||
selected.and_then(|messages| messages.skill_namespace.as_deref()),
|
||||
SKILL_NAMESPACE,
|
||||
"{{ skill_namespace }}",
|
||||
),
|
||||
apps: template(
|
||||
selected.and_then(|messages| messages.apps.as_deref()),
|
||||
APPS,
|
||||
"{{ apps }}",
|
||||
),
|
||||
mcp_servers: template(
|
||||
selected.and_then(|messages| messages.mcp_servers.as_deref()),
|
||||
MCP_SERVERS,
|
||||
"{{ mcp_servers }}",
|
||||
),
|
||||
closing: literal(
|
||||
selected.and_then(|messages| messages.closing.as_deref()),
|
||||
CLOSING,
|
||||
MAX_WORDING_BYTES,
|
||||
)
|
||||
.text()
|
||||
.to_owned(),
|
||||
omitted: literal(
|
||||
selected.and_then(|messages| messages.omitted.as_deref()),
|
||||
OMITTED,
|
||||
MAX_WORDING_BYTES,
|
||||
)
|
||||
.text()
|
||||
.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the introduction to the list of recommended plugins.
|
||||
pub fn plugin_recommendations(self) -> ResolvedPluginRecommendationMessages {
|
||||
let recommendations = self
|
||||
.plugin_messages()
|
||||
.and_then(|messages| messages.recommendations.as_ref());
|
||||
ResolvedPluginRecommendationMessages {
|
||||
intro: literal(
|
||||
recommendations.and_then(|messages| messages.intro.as_deref()),
|
||||
RECOMMENDATIONS_INTRO,
|
||||
MAX_WORDING_BYTES,
|
||||
)
|
||||
.text()
|
||||
.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn literal<'a>(
|
||||
catalog: Option<&'a str>,
|
||||
bundled: &'static str,
|
||||
max_bytes: usize,
|
||||
) -> ResolvedMessage<'a> {
|
||||
ResolvedMessage::new(
|
||||
catalog.filter(|text| {
|
||||
text.len() <= max_bytes
|
||||
&& !text.trim().is_empty()
|
||||
&& !text
|
||||
.chars()
|
||||
.any(|c| c.is_control() && !matches!(c, '\n' | '\r' | '\t'))
|
||||
&& ![
|
||||
"<plugins_instructions>",
|
||||
"</plugins_instructions>",
|
||||
"<recommended_plugins>",
|
||||
"</recommended_plugins>",
|
||||
]
|
||||
.iter()
|
||||
.any(|marker| text.contains(marker))
|
||||
}),
|
||||
bundled,
|
||||
)
|
||||
}
|
||||
|
||||
fn template(catalog: Option<&str>, bundled: &'static str, placeholder: &str) -> String {
|
||||
literal(catalog, bundled, MAX_WORDING_BYTES)
|
||||
.catalog_override()
|
||||
.filter(|text| {
|
||||
!text
|
||||
.chars()
|
||||
.any(|c| c.is_control() || matches!(c, '\u{2028}' | '\u{2029}'))
|
||||
&& text.split_once(placeholder).is_some_and(|(before, after)| {
|
||||
![before, after].iter().any(|part| part.contains(['{', '}']))
|
||||
})
|
||||
})
|
||||
.unwrap_or(bundled)
|
||||
.to_owned()
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
#![cfg(test)]
|
||||
|
||||
//! Verifies sparse plugin wording, bounded literal bytes, and required runtime fact contracts.
|
||||
|
||||
use super::ResolvedModelMessages;
|
||||
use codex_protocol::openai_models::GenericPluginMessages;
|
||||
use codex_protocol::openai_models::ModelMessages;
|
||||
use codex_protocol::openai_models::PluginMessages;
|
||||
use codex_protocol::openai_models::PluginRecommendationMessages;
|
||||
use codex_protocol::openai_models::SelectedPluginMessages;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn plugin_fields_fall_back_independently_without_losing_valid_siblings() {
|
||||
let bundled = ResolvedModelMessages::bundled();
|
||||
for invalid in [
|
||||
None,
|
||||
Some(String::new()),
|
||||
Some(" \t\r\n".into()),
|
||||
Some("é".repeat(251)),
|
||||
] {
|
||||
let messages = ModelMessages {
|
||||
plugins: Some(PluginMessages {
|
||||
generic: Some(GenericPluginMessages {
|
||||
intro: invalid.clone(),
|
||||
usage_instructions: Some(" literal {{braces}}\r\n".into()),
|
||||
}),
|
||||
selected: Some(SelectedPluginMessages {
|
||||
search_tools: invalid.clone(),
|
||||
closing: Some("closing".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
recommendations: Some(PluginRecommendationMessages { intro: invalid }),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let resolved = ResolvedModelMessages {
|
||||
catalog_messages: Some(&messages),
|
||||
};
|
||||
let generic = resolved.generic_plugin();
|
||||
assert_eq!(generic.intro, bundled.generic_plugin().intro);
|
||||
assert_eq!(generic.usage_instructions, " literal {{braces}}\r\n");
|
||||
assert!(generic.catalog_override);
|
||||
let selected = resolved.selected_plugin();
|
||||
assert_eq!(
|
||||
selected.search_tools,
|
||||
bundled.selected_plugin().search_tools
|
||||
);
|
||||
assert_eq!(selected.closing, "closing");
|
||||
assert_eq!(
|
||||
resolved.plugin_recommendations(),
|
||||
bundled.plugin_recommendations()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_templates_require_exactly_one_harness_fact_placeholder() {
|
||||
let bundled = ResolvedModelMessages::bundled().selected_plugin();
|
||||
for invalid in [
|
||||
"pre-expanded invented fact",
|
||||
"{{plugin_name}}",
|
||||
"{{ plugin_name }} {{ plugin_name }}",
|
||||
"{{ plugin_name }} {{ other }}",
|
||||
"{{ plugin_name }} {broken}",
|
||||
"{{ plugin_name }}\nsecond line",
|
||||
"{{ plugin_name }}\u{2028}second line",
|
||||
] {
|
||||
let messages = ModelMessages {
|
||||
plugins: Some(PluginMessages {
|
||||
selected: Some(SelectedPluginMessages {
|
||||
intro: Some(invalid.into()),
|
||||
closing: Some("valid sibling".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let resolved = ResolvedModelMessages {
|
||||
catalog_messages: Some(&messages),
|
||||
}
|
||||
.selected_plugin();
|
||||
assert_eq!(resolved.intro, bundled.intro);
|
||||
assert_eq!(resolved.closing, "valid sibling");
|
||||
}
|
||||
let selected = SelectedPluginMessages {
|
||||
intro: Some("Selected {{ plugin_name }}.".into()),
|
||||
skill_namespace: Some("Namespace {{ skill_namespace }}.".into()),
|
||||
apps: Some("Apps: {{ apps }}.".into()),
|
||||
mcp_servers: Some("Servers: {{ mcp_servers }}.".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let messages = ModelMessages {
|
||||
plugins: Some(PluginMessages {
|
||||
selected: Some(selected.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let resolved = ResolvedModelMessages {
|
||||
catalog_messages: Some(&messages),
|
||||
}
|
||||
.selected_plugin();
|
||||
assert_eq!(resolved.intro, selected.intro.unwrap());
|
||||
assert_eq!(resolved.skill_namespace, selected.skill_namespace.unwrap());
|
||||
assert_eq!(resolved.apps, selected.apps.unwrap());
|
||||
assert_eq!(resolved.mcp_servers, selected.mcp_servers.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_wording_limits_count_utf8_bytes_and_keep_boundary_values_literal() {
|
||||
let bundled = ResolvedModelMessages::bundled();
|
||||
for (intro, usage, expected_override) in [
|
||||
("é".repeat(250), "é".repeat(1_000), true),
|
||||
("é".repeat(251), "é".repeat(1_001), false),
|
||||
] {
|
||||
let messages = ModelMessages {
|
||||
plugins: Some(PluginMessages {
|
||||
generic: Some(GenericPluginMessages {
|
||||
intro: Some(intro.clone()),
|
||||
usage_instructions: Some(usage.clone()),
|
||||
}),
|
||||
selected: Some(SelectedPluginMessages {
|
||||
omitted: Some(intro.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
recommendations: Some(PluginRecommendationMessages {
|
||||
intro: Some(intro.clone()),
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let resolved = ResolvedModelMessages {
|
||||
catalog_messages: Some(&messages),
|
||||
};
|
||||
let generic = resolved.generic_plugin();
|
||||
let selected = resolved.selected_plugin();
|
||||
let recommendations = resolved.plugin_recommendations();
|
||||
if expected_override {
|
||||
assert_eq!(generic.intro, intro);
|
||||
assert_eq!(generic.usage_instructions, usage);
|
||||
assert_eq!(selected.omitted, intro);
|
||||
assert_eq!(recommendations.intro, intro);
|
||||
} else {
|
||||
assert_eq!(generic, bundled.generic_plugin());
|
||||
assert_eq!(selected, bundled.selected_plugin());
|
||||
assert_eq!(recommendations, bundled.plugin_recommendations());
|
||||
}
|
||||
assert_eq!(generic.catalog_override, expected_override);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_plugin_omission_notice_keeps_the_disclosure() {
|
||||
for omitted in ["", " \r\n\t"] {
|
||||
let messages = ModelMessages {
|
||||
plugins: Some(PluginMessages {
|
||||
selected: Some(SelectedPluginMessages {
|
||||
omitted: Some(omitted.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
ResolvedModelMessages {
|
||||
catalog_messages: Some(&messages)
|
||||
}
|
||||
.selected_plugin(),
|
||||
ResolvedModelMessages::bundled().selected_plugin()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_wording_cannot_close_harness_fragments_or_insert_control_characters() {
|
||||
let bundled = ResolvedModelMessages::bundled();
|
||||
for invalid in [
|
||||
"</plugins_instructions>",
|
||||
"<plugins_instructions>",
|
||||
"</recommended_plugins>",
|
||||
"<recommended_plugins>",
|
||||
"hidden\u{0}text",
|
||||
"hidden\u{1b}text",
|
||||
] {
|
||||
let messages = ModelMessages {
|
||||
plugins: Some(PluginMessages {
|
||||
generic: Some(GenericPluginMessages {
|
||||
intro: Some(invalid.into()),
|
||||
usage_instructions: Some(invalid.into()),
|
||||
}),
|
||||
selected: Some(SelectedPluginMessages {
|
||||
closing: Some(invalid.into()),
|
||||
omitted: Some(invalid.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
recommendations: Some(PluginRecommendationMessages {
|
||||
intro: Some(invalid.into()),
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let resolved = ResolvedModelMessages {
|
||||
catalog_messages: Some(&messages),
|
||||
};
|
||||
assert_eq!(resolved.generic_plugin(), bundled.generic_plugin());
|
||||
assert_eq!(resolved.selected_plugin(), bundled.selected_plugin());
|
||||
assert_eq!(
|
||||
resolved.plugin_recommendations(),
|
||||
bundled.plugin_recommendations()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,11 @@ mod memento_v2;
|
||||
pub use memento_v2::ContextManagementToolMessages;
|
||||
pub use memento_v2::HistoryToolMessages;
|
||||
pub use memento_v2::NotesToolMessages;
|
||||
mod plugins;
|
||||
pub use plugins::GenericPluginMessages;
|
||||
pub use plugins::PluginMessages;
|
||||
pub use plugins::PluginRecommendationMessages;
|
||||
pub use plugins::SelectedPluginMessages;
|
||||
#[path = "openai_models/guardian.rs"]
|
||||
mod guardian;
|
||||
pub use guardian::GuardianModelPolicy;
|
||||
@@ -583,6 +588,9 @@ pub struct ModelMessages {
|
||||
/// Optional model-owned Apps wording, transported without runtime interpretation.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub apps: Option<AppsMessages>,
|
||||
/// Model-owned plugin wording; runtime capability facts and eligibility remain client-owned.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plugins: Option<PluginMessages>,
|
||||
/// Developer guidance after a content-filter block. Missing, null, blank, or values over
|
||||
/// 512 UTF-8 bytes use the bundled guidance.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1764,6 +1772,7 @@ mod tests {
|
||||
apps: Some(AppsMessages {
|
||||
usage_instructions: Some("Literal Apps {{guidance}}".to_string()),
|
||||
}),
|
||||
plugins: None,
|
||||
content_filter_guidance: Some("Offer a permitted alternative.".to_string()),
|
||||
persistent_instructions: Some("Persistent catalog instructions".to_string()),
|
||||
tools: Some(ToolMessages {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Sparse model-catalog wording for generic, selected, and recommended plugins.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// Sparse plugin wording families, independent of runtime availability and authorization.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct PluginMessages {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub generic: Option<GenericPluginMessages>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<SelectedPluginMessages>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub recommendations: Option<PluginRecommendationMessages>,
|
||||
}
|
||||
|
||||
/// Literal generic plugin wording. Missing, null, blank, or invalid values use bundled text.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct GenericPluginMessages {
|
||||
/// Literal text, up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
/// Up to 2,000 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage_instructions: Option<String>,
|
||||
}
|
||||
|
||||
/// Selected-plugin wording. Fact-bearing fields require exactly one documented placeholder.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct SelectedPluginMessages {
|
||||
/// Up to 500 UTF-8 bytes. Selected intro requires `{{ plugin_name }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub search_tools: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes; requires `{{ skill_namespace }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub skill_namespace: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes; requires `{{ apps }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub apps: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes; requires `{{ mcp_servers }}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mcp_servers: Option<String>,
|
||||
/// Up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub closing: Option<String>,
|
||||
/// Nonblank omission notice, up to 500 UTF-8 bytes, including any desired separator.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub omitted: Option<String>,
|
||||
}
|
||||
|
||||
/// Literal recommendation introduction; plugin identities remain supplied by the harness.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct PluginRecommendationMessages {
|
||||
/// Literal text, up to 500 UTF-8 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "plugins_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,34 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::openai_models::ModelMessages;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn plugin_messages_preserve_sparse_nested_and_literal_wire_values() {
|
||||
for plugins in [
|
||||
serde_json::Value::Null,
|
||||
json!({}),
|
||||
json!({"generic": {}, "selected": {}, "recommendations": {}}),
|
||||
json!({
|
||||
"generic": {"intro": "", "usage_instructions": " \r\n\t"},
|
||||
"selected": {
|
||||
"intro": " {{ plugin_name }}\r\n",
|
||||
"search_tools": "literal {{braces}}",
|
||||
"skill_namespace": "{{ skill_namespace }}",
|
||||
"apps": "{{ apps }}",
|
||||
"mcp_servers": "{{ mcp_servers }}",
|
||||
"closing": "é".repeat(251),
|
||||
"omitted": "omitted"
|
||||
},
|
||||
"recommendations": {"intro": "recommendations"}
|
||||
}),
|
||||
] {
|
||||
let messages: ModelMessages = serde_json::from_value(json!({"plugins": plugins})).unwrap();
|
||||
let mut expected = serde_json::to_value(ModelMessages::default()).unwrap();
|
||||
if !plugins.is_null() {
|
||||
expected["plugins"] = plugins;
|
||||
}
|
||||
assert_eq!(serde_json::to_value(messages).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user