Unify explicit plugin guidance with context builders

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-25 00:00:55 -07:00
co-authored by Codex
parent fac2f41559
commit 8b05d1abeb
18 changed files with 247 additions and 110 deletions
+35 -62
View File
@@ -258,7 +258,9 @@ use crate::mentions::collect_explicit_app_ids;
use crate::mentions::collect_explicit_plugin_mentions;
use crate::mentions::collect_tool_mentions_from_messages;
use crate::network_policy_decision::execpolicy_network_rule_amendment;
use crate::plugins::ExplicitPluginInstructionsContext;
use crate::plugins::PluginsManager;
use crate::plugins::build_explicit_plugin_instructions_context;
use crate::plugins::build_plugin_developer_sections;
use crate::plugins::render_plugins_section;
use crate::project_doc::get_user_instructions;
@@ -2570,12 +2572,14 @@ impl Session {
.await
}
/// `user_input` is only consulted for explicit plugin mentions on this turn.
/// `explicit_plugin_instructions` carries the already-resolved turn-local plugin mention
/// context from the current user input so this builder can render that guidance without
/// re-listing MCP tools.
async fn build_settings_update_items(
&self,
reference_context_item: Option<&TurnContextItem>,
current_context: &TurnContext,
user_input: &[UserInput],
explicit_plugin_instructions: &ExplicitPluginInstructionsContext,
) -> Vec<ResponseItem> {
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
@@ -2587,9 +2591,6 @@ impl Session {
};
let shell = self.user_shell();
let exec_policy = self.services.exec_policy.current();
let additional_developer_sections = self
.build_explicit_plugin_developer_sections(current_context, user_input)
.await;
crate::context_manager::updates::build_settings_update_items(
reference_context_item,
previous_turn_settings.as_ref(),
@@ -2597,48 +2598,10 @@ impl Session {
shell.as_ref(),
exec_policy.as_ref(),
self.features.enabled(Feature::Personality),
additional_developer_sections,
explicit_plugin_instructions,
)
}
async fn build_explicit_plugin_developer_sections(
&self,
turn_context: &TurnContext,
user_input: &[UserInput],
) -> Vec<String> {
if user_input.is_empty() {
return Vec::new();
}
let loaded_plugins = self
.services
.plugins_manager
.plugins_for_config(&turn_context.config);
let mentioned_plugins =
collect_explicit_plugin_mentions(user_input, loaded_plugins.capability_summaries());
if mentioned_plugins.is_empty() {
return Vec::new();
}
let mcp_tools = self
.services
.mcp_connection_manager
.read()
.await
.list_all_tools()
.await;
let available_connectors = if turn_context.apps_enabled() {
let connectors = connectors::merge_plugin_apps_with_accessible(
loaded_plugins.effective_apps(),
connectors::accessible_connectors_from_mcp_tools(&mcp_tools),
);
connectors::with_app_enabled_state(connectors, &turn_context.config)
} else {
Vec::new()
};
build_plugin_developer_sections(&mentioned_plugins, &mcp_tools, &available_connectors)
}
/// Persist the event to rollout and send it to clients.
pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) {
let legacy_source = msg.clone();
@@ -3475,13 +3438,14 @@ impl Session {
}
}
/// `maybe_user_input` is only consulted for explicit plugin mentions on this turn.
/// Pass `None` when no raw current-turn user input is available, such as compaction callers
/// that are rebuilding prompt context from flattened history alone.
/// `explicit_plugin_instructions` is optional because compaction/rebuild callers do not have
/// the raw current-turn user input needed to resolve explicit plugin mentions. When present,
/// it carries the already-resolved plugin/tool/app context for turn-local plugin guidance in
/// the canonical developer envelope.
pub(crate) async fn build_initial_context(
&self,
turn_context: &TurnContext,
maybe_user_input: Option<&[UserInput]>,
explicit_plugin_instructions: Option<&ExplicitPluginInstructionsContext>,
) -> Vec<ResponseItem> {
let mut developer_sections = Vec::<String>::with_capacity(8);
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
@@ -3602,11 +3566,10 @@ impl Session {
{
developer_sections.push(plugin_section);
}
if let Some(user_input) = maybe_user_input {
developer_sections.extend(
self.build_explicit_plugin_developer_sections(turn_context, user_input)
.await,
);
if let Some(explicit_plugin_instructions) = explicit_plugin_instructions {
developer_sections.extend(build_plugin_developer_sections(
explicit_plugin_instructions,
));
}
if turn_context.features.enabled(Feature::CodexGitCommit)
&& let Some(commit_message_instruction) = commit_message_trailer_instruction(
@@ -3696,26 +3659,27 @@ impl Session {
/// reinjects full initial context into replacement history. Other non-regular tasks
/// intentionally do not update the baseline.
///
/// `user_input` is only used to derive explicit plugin-mention guidance for the canonical
/// developer envelope on this turn.
/// `explicit_plugin_instructions` carries the current turn's already-resolved explicit
/// plugin-mention context so whichever canonical builder path runs can render that guidance
/// exactly once without re-listing MCP tools.
pub(crate) async fn record_context_updates_and_set_reference_context_item(
&self,
turn_context: &TurnContext,
user_input: &[UserInput],
explicit_plugin_instructions: &ExplicitPluginInstructionsContext,
) {
let reference_context_item = {
let state = self.state.lock().await;
state.reference_context_item()
};
let context_items = if reference_context_item.is_none() {
self.build_initial_context(turn_context, Some(user_input))
self.build_initial_context(turn_context, Some(explicit_plugin_instructions))
.await
} else {
// Steady-state path: append only context diffs to minimize token overhead.
self.build_settings_update_items(
reference_context_item.as_ref(),
turn_context,
user_input,
explicit_plugin_instructions,
)
.await
};
@@ -5595,8 +5559,9 @@ pub(crate) async fn run_turn(
.plugins_manager
.plugins_for_config(&turn_context.config);
// Structured plugin:// mentions are resolved from the current session's
// enabled plugins here for telemetry/tool selection; the canonical context
// builders derive any explicit plugin guidance from this turn's user input.
// enabled plugins here for telemetry/tool selection. The canonical context
// builders then render explicit plugin guidance from the already-resolved
// per-turn plugin context built below.
let mentioned_plugins =
collect_explicit_plugin_mentions(&input, loaded_plugins.capability_summaries());
let mcp_tools = if turn_context.apps_enabled() || !mentioned_plugins.is_empty() {
@@ -5686,9 +5651,17 @@ pub(crate) async fn run_turn(
.iter()
.filter_map(crate::plugins::PluginCapabilitySummary::telemetry_metadata)
.collect::<Vec<_>>();
let explicit_plugin_instructions = build_explicit_plugin_instructions_context(
&mentioned_plugins,
&mcp_tools,
&available_connectors,
);
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref(), &input)
.await;
sess.record_context_updates_and_set_reference_context_item(
turn_context.as_ref(),
&explicit_plugin_instructions,
)
.await;
let mut explicitly_enabled_connectors = collect_explicit_app_ids(&input);
explicitly_enabled_connectors.extend(collect_explicit_app_ids_from_skill_items(
+70 -29
View File
@@ -12,6 +12,7 @@ use crate::exec::ExecToolCallOutput;
use crate::function_tool::FunctionCallError;
use crate::mcp_connection_manager::ToolInfo;
use crate::models_manager::model_info;
use crate::plugins::ExplicitPluginInstructionsContext;
use crate::shell::default_user_shell;
use crate::tools::format_exec_output_str;
@@ -957,18 +958,24 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
assert_eq!(expected, history_before_seed.raw_items());
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
expected.extend(
session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await,
);
let history_after_seed = session.clone_history().await;
assert_eq!(expected, history_after_seed.raw_items());
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
let history_after_second_seed = session.clone_history().await;
assert_eq!(
@@ -1343,7 +1350,7 @@ async fn thread_rollback_drops_last_turn_from_history() {
let rollout_path = attach_rollout_recorder(&sess).await;
let initial_context = sess
.build_initial_context(tc.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(tc.as_ref(), /*explicit_plugin_instructions*/ None)
.await;
let turn_1 = vec![
user_message("turn 1 user"),
@@ -1409,7 +1416,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns()
attach_rollout_recorder(&sess).await;
let initial_context = sess
.build_initial_context(tc.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(tc.as_ref(), /*explicit_plugin_instructions*/ None)
.await;
let turn_1 = vec![user_message("turn 1 user")];
let mut full_history = Vec::new();
@@ -1437,7 +1444,7 @@ async fn thread_rollback_fails_without_persisted_rollout_path() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = sess
.build_initial_context(tc.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(tc.as_ref(), /*explicit_plugin_instructions*/ None)
.await;
sess.record_into_history(&initial_context, tc.as_ref())
.await;
@@ -1756,7 +1763,7 @@ async fn thread_rollback_fails_when_turn_in_progress() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = sess
.build_initial_context(tc.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(tc.as_ref(), /*explicit_plugin_instructions*/ None)
.await;
sess.record_into_history(&initial_context, tc.as_ref())
.await;
@@ -1779,7 +1786,7 @@ async fn thread_rollback_fails_when_num_turns_is_zero() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let initial_context = sess
.build_initial_context(tc.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(tc.as_ref(), /*explicit_plugin_instructions*/ None)
.await;
sess.record_into_history(&initial_context, tc.as_ref())
.await;
@@ -3713,7 +3720,11 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
let reference_context_item = previous_context.to_turn_context_item();
let update_items = session
.build_settings_update_items(Some(&reference_context_item), &current_context, &[])
.build_settings_update_items(
Some(&reference_context_item),
&current_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
let environment_update = update_items
@@ -3748,7 +3759,11 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
let reference_context_item = previous_context.to_turn_context_item();
let update_items = session
.build_settings_update_items(Some(&reference_context_item), &current_context, &[])
.build_settings_update_items(
Some(&reference_context_item),
&current_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
let environment_update = update_items
@@ -3783,7 +3798,7 @@ async fn build_settings_update_items_emits_realtime_start_when_session_becomes_l
.build_settings_update_items(
Some(&previous_context.to_turn_context_item()),
&current_context,
&[],
&ExplicitPluginInstructionsContext::default(),
)
.await;
@@ -3812,7 +3827,7 @@ async fn build_settings_update_items_emits_realtime_end_when_session_stops_being
.build_settings_update_items(
Some(&previous_context.to_turn_context_item()),
&current_context,
&[],
&ExplicitPluginInstructionsContext::default(),
)
.await;
@@ -3846,7 +3861,11 @@ async fn build_settings_update_items_uses_previous_turn_settings_for_realtime_en
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let update_items = session
.build_settings_update_items(Some(&previous_context_item), &current_context, &[])
.build_settings_update_items(
Some(&previous_context_item),
&current_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
let developer_texts = developer_input_texts(&update_items);
@@ -3864,7 +3883,7 @@ async fn build_initial_context_uses_previous_realtime_state() {
turn_context.realtime_active = true;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let developer_texts = developer_input_texts(&initial_context);
assert!(
@@ -3880,7 +3899,7 @@ async fn build_initial_context_uses_previous_realtime_state() {
state.set_reference_context_item(Some(previous_context_item));
}
let resumed_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let resumed_developer_texts = developer_input_texts(&resumed_context);
assert!(
@@ -3907,7 +3926,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
.await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let developer_texts = developer_input_texts(&initial_context);
assert!(
@@ -3923,7 +3942,7 @@ async fn build_initial_context_omits_default_image_save_location_without_image_h
let (session, turn_context) = make_session_and_context().await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let developer_texts = developer_input_texts(&initial_context);
@@ -4038,7 +4057,7 @@ async fn build_initial_context_uses_previous_turn_settings_for_realtime_end() {
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let developer_texts = developer_input_texts(&initial_context);
assert!(
@@ -4062,7 +4081,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let developer_texts = developer_input_texts(&initial_context);
assert!(
@@ -4078,11 +4097,14 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
{
let (session, turn_context) = make_session_and_context().await;
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
let history = session.clone_history().await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
assert_eq!(history.raw_items().to_vec(), initial_context);
@@ -4111,7 +4133,10 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
.record_into_history(std::slice::from_ref(&compacted_summary), &turn_context)
.await;
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
{
let mut state = session.state.lock().await;
@@ -4122,14 +4147,17 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
.await;
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
let history = session.clone_history().await;
let mut expected_history = vec![compacted_summary];
expected_history.extend(
session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await,
);
assert_eq!(history.raw_items().to_vec(), expected_history);
@@ -4175,12 +4203,19 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
}
let update_items = session
.build_settings_update_items(Some(&previous_context_item), &turn_context, &[])
.build_settings_update_items(
Some(&previous_context_item),
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
assert_eq!(update_items, Vec::new());
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
assert_eq!(
@@ -4226,7 +4261,7 @@ async fn build_initial_context_prepends_model_switch_message() {
.set_previous_turn_settings(Some(previous_turn_settings))
.await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let ResponseItem::Message { role, content, .. } = &initial_context[0] else {
@@ -4295,7 +4330,10 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
}))
.await;
session
.record_context_updates_and_set_reference_context_item(&turn_context, &[])
.record_context_updates_and_set_reference_context_item(
&turn_context,
&ExplicitPluginInstructionsContext::default(),
)
.await;
session.ensure_rollout_materialized().await;
session.flush_rollout().await;
@@ -4908,7 +4946,10 @@ async fn sample_rollout(
// personality_spec) matches reconstruction.
let reconstruction_turn = session.new_default_turn().await;
let mut initial_context = session
.build_initial_context(reconstruction_turn.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(
reconstruction_turn.as_ref(),
/*explicit_plugin_instructions*/ None,
)
.await;
// Ensure personality_spec is present when Personality is enabled, so expected matches
// what reconstruction produces (build_initial_context may omit it when baked into model).
+4 -1
View File
@@ -201,7 +201,10 @@ async fn run_compact_task_inner(
InitialContextInjection::BeforeLastUserMessage
) {
let initial_context = sess
.build_initial_context(turn_context.as_ref(), /*maybe_user_input*/ None)
.build_initial_context(
turn_context.as_ref(),
/*explicit_plugin_instructions*/ None,
)
.await;
new_history =
insert_initial_context_before_last_real_user_or_summary(new_history, initial_context);
+1 -1
View File
@@ -178,7 +178,7 @@ pub(crate) async fn process_compacted_history(
initial_context_injection,
InitialContextInjection::BeforeLastUserMessage
) {
sess.build_initial_context(turn_context, /*maybe_user_input*/ None)
sess.build_initial_context(turn_context, /*explicit_plugin_instructions*/ None)
.await
} else {
Vec::new()
+1 -1
View File
@@ -10,7 +10,7 @@ async fn process_compacted_history_with_test_session(
.set_previous_turn_settings(previous_turn_settings.cloned())
.await;
let initial_context = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
let refreshed = crate::compact_remote::process_compacted_history(
&session,
@@ -954,6 +954,44 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() {
);
}
#[test]
fn drop_last_n_user_turns_trims_explicit_plugin_guidance_above_rolled_back_turn() {
let items = vec![
assistant_msg("session prefix item"),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
developer_msg(
"<explicit_plugin_instructions>\nCapabilities from the `sample` plugin:\n- Apps from this plugin available in this session: `Google Calendar`.\nUse these plugin-associated capabilities to help solve the task.\n</explicit_plugin_instructions>",
),
user_input_text_msg(
"<environment_context><cwd>PRETURN_CONTEXT_DIFF_CWD</cwd></environment_context>",
),
user_input_text_msg("turn 2 user"),
assistant_msg("turn 2 assistant"),
];
let modalities = default_input_modalities();
let mut history = create_history_with_items(items);
let reference_context_item = reference_context_item();
history.set_reference_context_item(Some(reference_context_item.clone()));
history.drop_last_n_user_turns(1);
assert_eq!(
history.clone().for_prompt(&modalities),
vec![
assistant_msg("session prefix item"),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
]
);
assert_eq!(
serde_json::to_value(history.reference_context_item())
.expect("serialize retained reference context item"),
serde_json::to_value(Some(reference_context_item))
.expect("serialize expected reference context item")
);
}
#[test]
fn drop_last_n_user_turns_clears_reference_context_for_mixed_developer_context_bundles() {
let items = vec![
+6 -2
View File
@@ -1,6 +1,8 @@
use crate::codex::PreviousTurnSettings;
use crate::codex::TurnContext;
use crate::environment_context::EnvironmentContext;
use crate::plugins::ExplicitPluginInstructionsContext;
use crate::plugins::build_plugin_developer_sections;
use crate::shell::Shell;
use codex_execpolicy::Policy;
use codex_features::Feature;
@@ -192,7 +194,7 @@ pub(crate) fn build_settings_update_items(
shell: &Shell,
exec_policy: &Policy,
personality_feature_enabled: bool,
additional_developer_sections: Vec<String>,
explicit_plugin_instructions: &ExplicitPluginInstructionsContext,
) -> Vec<ResponseItem> {
// TODO(ccunningham): build_settings_update_items still does not cover every
// model-visible item emitted by build_initial_context. Persist the remaining
@@ -211,7 +213,9 @@ pub(crate) fn build_settings_update_items(
.into_iter()
.flatten()
.map(DeveloperInstructions::into_text)
.chain(additional_developer_sections)
.chain(build_plugin_developer_sections(
explicit_plugin_instructions,
))
.collect::<Vec<_>>();
let mut items = Vec::with_capacity(2);
+2
View File
@@ -15,6 +15,7 @@ use codex_protocol::models::is_image_open_tag_text;
use codex_protocol::models::is_local_image_close_tag_text;
use codex_protocol::models::is_local_image_open_tag_text;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_protocol::protocol::EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
use codex_protocol::user_input::UserInput;
use tracing::warn;
@@ -28,6 +29,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
"<permissions instructions>",
"<model_switch>",
COLLABORATION_MODE_OPEN_TAG,
EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG,
REALTIME_CONVERSATION_OPEN_TAG,
"<personality_spec>",
];
+51 -8
View File
@@ -7,20 +7,34 @@ use crate::mcp_connection_manager::ToolInfo;
use crate::plugins::PluginCapabilitySummary;
use crate::plugins::render_explicit_plugin_instructions;
pub(crate) fn build_plugin_developer_sections(
/// Turn-local data needed to render explicit plugin-mention guidance inside the
/// canonical pre-user developer envelope.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct ExplicitPluginInstructionsContext {
entries: Vec<ExplicitPluginInstructionsEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ExplicitPluginInstructionsEntry {
plugin: PluginCapabilitySummary,
available_mcp_servers: Vec<String>,
available_apps: Vec<String>,
}
/// Capture the turn-local plugin/tool/app ingredients needed to render explicit plugin guidance
/// later in the canonical context builders, without re-listing MCP tools.
pub(crate) fn build_explicit_plugin_instructions_context(
mentioned_plugins: &[PluginCapabilitySummary],
mcp_tools: &HashMap<String, ToolInfo>,
available_connectors: &[connectors::AppInfo],
) -> Vec<String> {
) -> ExplicitPluginInstructionsContext {
if mentioned_plugins.is_empty() {
return Vec::new();
return ExplicitPluginInstructionsContext::default();
}
// Turn each explicit plugin mention into developer-message sections that
// can be folded into the canonical pre-user developer envelope for this turn.
mentioned_plugins
let entries = mentioned_plugins
.iter()
.filter_map(|plugin| {
.map(|plugin| {
let available_mcp_servers = mcp_tools
.values()
.filter(|tool| {
@@ -47,7 +61,36 @@ pub(crate) fn build_plugin_developer_sections(
.collect::<BTreeSet<String>>()
.into_iter()
.collect::<Vec<_>>();
render_explicit_plugin_instructions(plugin, &available_mcp_servers, &available_apps)
ExplicitPluginInstructionsEntry {
plugin: plugin.clone(),
available_mcp_servers,
available_apps,
}
})
.collect();
ExplicitPluginInstructionsContext { entries }
}
/// Render explicit plugin-mention guidance from the already-resolved per-turn plugin context.
///
/// The live turn path builds `ExplicitPluginInstructionsContext` once from the current turn's
/// plugin/tool/app inventory, then whichever canonical context builder runs uses this renderer.
pub(crate) fn build_plugin_developer_sections(
explicit_plugin_instructions: &ExplicitPluginInstructionsContext,
) -> Vec<String> {
// Turn each explicit plugin mention into developer-message sections that
// can be folded into the canonical pre-user developer envelope for this turn.
explicit_plugin_instructions
.entries
.iter()
.filter_map(|entry| {
render_explicit_plugin_instructions(
&entry.plugin,
&entry.available_mcp_servers,
&entry.available_apps,
)
})
.collect()
}
+2
View File
@@ -12,6 +12,8 @@ pub(crate) mod test_support;
mod toggles;
pub(crate) use discoverable::list_tool_suggest_discoverable_plugins;
pub(crate) use injection::ExplicitPluginInstructionsContext;
pub(crate) use injection::build_explicit_plugin_instructions_context;
pub(crate) use injection::build_plugin_developer_sections;
pub use manager::AppConnectorId;
pub use manager::ConfiguredMarketplace;
+6 -1
View File
@@ -1,4 +1,6 @@
use crate::plugins::PluginCapabilitySummary;
use codex_protocol::protocol::EXPLICIT_PLUGIN_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
@@ -84,7 +86,10 @@ pub(crate) fn render_explicit_plugin_instructions(
lines.push("Use these plugin-associated capabilities to help solve the task.".to_string());
Some(lines.join("\n"))
let body = lines.join("\n");
Some(format!(
"{EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG}\n{body}\n{EXPLICIT_PLUGIN_INSTRUCTIONS_CLOSE_TAG}"
))
}
#[cfg(test)]
+19
View File
@@ -21,3 +21,22 @@ fn render_plugins_section_includes_descriptions_and_skill_naming_guidance() {
assert_eq!(rendered, expected);
}
#[test]
fn render_explicit_plugin_instructions_wraps_turn_local_guidance_in_stable_tag() {
let rendered = render_explicit_plugin_instructions(
&PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
has_skills: true,
..PluginCapabilitySummary::default()
},
&[],
&["Google Calendar".to_string()],
)
.expect("explicit plugin instructions should render");
let expected = "<explicit_plugin_instructions>\nCapabilities from the `sample` plugin:\n- Skills from this plugin are prefixed with `sample:`.\n- Apps from this plugin available in this session: `Google Calendar`.\nUse these plugin-associated capabilities to help solve the task.\n</explicit_plugin_instructions>";
assert_eq!(rendered, expected);
}
+1 -1
View File
@@ -196,7 +196,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() {
async fn ignores_session_prefix_messages_when_truncating() {
let (session, turn_context) = make_session_and_context().await;
let mut items = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
items.push(user_msg("feature request"));
items.push(assistant_msg("ack"));
@@ -125,7 +125,7 @@ fn truncates_rollout_from_start_applies_thread_rollback_markers() {
async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
let (session, turn_context) = make_session_and_context().await;
let mut items = session
.build_initial_context(&turn_context, /*maybe_user_input*/ None)
.build_initial_context(&turn_context, /*explicit_plugin_instructions*/ None)
.await;
items.push(user_msg("feature request"));
items.push(assistant_msg("ack"));
@@ -4,6 +4,7 @@ use std::sync::OnceLock;
use crate::responses::ResponsesRequest;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
@@ -281,6 +282,9 @@ fn canonicalize_snapshot_text(text: &str) -> String {
if text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG) {
return "<PLUGINS_INSTRUCTIONS>".to_string();
}
if text.starts_with(EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG) {
return "<EXPLICIT_PLUGIN_INSTRUCTIONS>".to_string();
}
if text.starts_with("# AGENTS.md instructions for ") {
return "<AGENTS_MD>".to_string();
}
+3 -2
View File
@@ -8,6 +8,7 @@ use std::time::Instant;
use anyhow::Result;
use codex_core::CodexAuth;
use codex_features::Feature;
use codex_protocol::protocol::EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
@@ -325,14 +326,14 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
assert!(
developer_messages
.iter()
.any(|text| text.contains("Capabilities from the `sample` plugin:")),
.any(|text| text.contains(EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG)),
"expected plugin mention guidance in developer messages: {developer_messages:?}"
);
let user_messages = request.message_input_texts("user");
assert!(
user_messages
.iter()
.all(|text| !text.contains("Capabilities from the `sample` plugin:")),
.all(|text| !text.contains(EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG)),
"expected plugin mention guidance to stay out of user messages: {user_messages:?}"
);
assert!(
@@ -7,6 +7,6 @@ Scenario: Explicit plugin mention request layout
## Plugin Mention Request
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] Capabilities from the `sample` plugin:\n- Skills from this plugin are prefixed with `sample:`.\n- Apps from this plugin available in this session: `Google Calendar`.\nUse these plugin-associated capabilities to help solv...
[02] <EXPLICIT_PLUGIN_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:help me inspect the sample plugin
+2
View File
@@ -95,6 +95,8 @@ pub const SKILLS_INSTRUCTIONS_OPEN_TAG: &str = "<skills_instructions>";
pub const SKILLS_INSTRUCTIONS_CLOSE_TAG: &str = "</skills_instructions>";
pub const PLUGINS_INSTRUCTIONS_OPEN_TAG: &str = "<plugins_instructions>";
pub const PLUGINS_INSTRUCTIONS_CLOSE_TAG: &str = "</plugins_instructions>";
pub const EXPLICIT_PLUGIN_INSTRUCTIONS_OPEN_TAG: &str = "<explicit_plugin_instructions>";
pub const EXPLICIT_PLUGIN_INSTRUCTIONS_CLOSE_TAG: &str = "</explicit_plugin_instructions>";
pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";