add @plugin mentions (#13510)

## Note-- added plugin mentions via @, but that conflicts with file
mentions

depends and builds upon #13433.

- introduces explicit `@plugin` mentions. this injects the plugin's mcp
servers, app names, and skill name format into turn context as a dev
message.
- we do not yet have UI for these mentions, so we currently parse raw
text (as opposed to skills and apps which have UI chips, autocomplete,
etc.) this depends on a `plugins/list` app-server endpoint we can feed
the UI with, which is upcoming
- also annotate mcp and app tool descriptions with the plugin(s) they
come from. this gives the model a first class way of understanding what
tools come from which plugins, which will help implicit invocation.

### Tests
Added and updated tests, unit and integration. Also confirmed locally a
raw `@plugin` injects the dev message, and the model knows about its
apps, mcps, and skills.
This commit is contained in:
sayan-oai
2026-03-05 16:03:39 -08:00
committed by GitHub
parent 1ed542bf31
commit 4e77ea0ec7
24 changed files with 1067 additions and 181 deletions

View File

@@ -0,0 +1,58 @@
use std::collections::BTreeSet;
use std::collections::HashMap;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseItem;
use crate::connectors;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_connection_manager::ToolInfo;
use crate::plugins::PluginCapabilitySummary;
use crate::plugins::render_explicit_plugin_instructions;
pub(crate) fn build_plugin_injections(
mentioned_plugins: &[PluginCapabilitySummary],
mcp_tools: &HashMap<String, ToolInfo>,
available_connectors: &[connectors::AppInfo],
) -> Vec<ResponseItem> {
if mentioned_plugins.is_empty() {
return Vec::new();
}
// Turn each explicit @plugin mention into a developer hint that points the
// model at the plugin's visible MCP servers, enabled apps, and skill prefix.
mentioned_plugins
.iter()
.filter_map(|plugin| {
let available_mcp_servers = mcp_tools
.values()
.filter(|tool| {
tool.server_name != CODEX_APPS_MCP_SERVER_NAME
&& tool
.plugin_display_names
.iter()
.any(|plugin_name| plugin_name == &plugin.display_name)
})
.map(|tool| tool.server_name.clone())
.collect::<BTreeSet<String>>()
.into_iter()
.collect::<Vec<_>>();
let available_apps = available_connectors
.iter()
.filter(|connector| {
connector.is_enabled
&& connector
.plugin_display_names
.iter()
.any(|plugin_name| plugin_name == &plugin.display_name)
})
.map(connectors::connector_display_label)
.collect::<BTreeSet<String>>()
.into_iter()
.collect::<Vec<_>>();
render_explicit_plugin_instructions(plugin, &available_mcp_servers, &available_apps)
.map(DeveloperInstructions::new)
.map(ResponseItem::from)
})
.collect()
}

View File

@@ -198,11 +198,6 @@ impl PluginsManager {
force_reload: bool,
) -> PluginLoadOutcome {
if !plugins_feature_enabled_from_stack(config_layer_stack) {
let mut cache = match self.cache_by_cwd.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
cache.insert(cwd.to_path_buf(), PluginLoadOutcome::default());
return PluginLoadOutcome::default();
}

View File

@@ -1,9 +1,11 @@
mod injection;
mod manager;
mod manifest;
mod marketplace;
mod render;
mod store;
pub(crate) use injection::build_plugin_injections;
pub use manager::AppConnectorId;
pub use manager::LoadedPlugin;
pub use manager::PluginCapabilitySummary;
@@ -14,6 +16,7 @@ pub use manager::PluginsManager;
pub(crate) use manager::plugin_namespace_for_skill_path;
pub(crate) use manifest::load_plugin_manifest;
pub(crate) use manifest::plugin_manifest_name;
pub(crate) use render::render_explicit_plugin_instructions;
pub(crate) use render::render_plugins_section;
pub use store::PluginId;
pub use store::PluginInstallResult;

View File

@@ -30,6 +30,54 @@ pub(crate) fn render_plugins_section(plugins: &[PluginCapabilitySummary]) -> Opt
Some(lines.join("\n"))
}
pub(crate) fn render_explicit_plugin_instructions(
plugin: &PluginCapabilitySummary,
available_mcp_servers: &[String],
available_apps: &[String],
) -> Option<String> {
let mut lines = vec![format!(
"Capabilities from the `{}` plugin:",
plugin.display_name
)];
if plugin.has_skills {
lines.push(format!(
"- Skills from this plugin are prefixed with `{}:`.",
plugin.display_name
));
}
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(", ")
));
}
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(", ")
));
}
if lines.len() == 1 {
return None;
}
lines.push("Use these plugin-associated capabilities to help solve the task.".to_string());
Some(lines.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;