mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
chore: clarify plugin + app copy in model instructions (#14541)
- clarify app mentions are in user messages - clarify what it means for tools to be provided via `codex_apps` MCP - add plugin descriptions (with basic sanitization) to top-level `## Plugins` section alongside the corresponding plugin names - explain that skills from plugins are prefixed with `plugin_name:` in top-level `##Plugins` section changes to more logically organize `Apps`, `Skills`, and `Plugins` instructions will be in a separate PR, as that shuffles dev + user instructions in ways that change tests broadly. ### Tests confirmed in local rollout, some new tests.
This commit is contained in:
committed by
GitHub
Unverified
parent
59b588b8ec
commit
9f2da5a9ce
@@ -66,6 +66,7 @@ const DEFAULT_APP_CONFIG_FILE: &str = ".app.json";
|
||||
const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated";
|
||||
const REMOTE_PLUGIN_SYNC_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AppConnectorId(pub String);
|
||||
@@ -191,7 +192,7 @@ impl PluginCapabilitySummary {
|
||||
.manifest_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| plugin.config_name.clone()),
|
||||
description: plugin.manifest_description.clone(),
|
||||
description: prompt_safe_plugin_description(plugin.manifest_description.as_deref()),
|
||||
has_skills: !plugin.skill_roots.is_empty(),
|
||||
mcp_server_names,
|
||||
app_connector_ids: plugin.apps.clone(),
|
||||
@@ -213,6 +214,23 @@ impl PluginCapabilitySummary {
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_safe_plugin_description(description: Option<&str>) -> Option<String> {
|
||||
let description = description?
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if description.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
description
|
||||
.chars()
|
||||
.take(MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PluginLoadOutcome {
|
||||
plugins: Vec<LoadedPlugin>,
|
||||
|
||||
@@ -270,6 +270,73 @@ fn plugin_telemetry_metadata_uses_default_mcp_config_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_summary_sanitizes_plugin_descriptions_to_one_line() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/sample/local");
|
||||
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "sample",
|
||||
"description": "Plugin that\n includes the sample\tserver"
|
||||
}"#,
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join("skills/sample-search/SKILL.md"),
|
||||
"---\nname: sample-search\ndescription: search sample data\n---\n",
|
||||
);
|
||||
|
||||
let outcome = load_plugins_from_config(&plugin_config_toml(true, true), codex_home.path());
|
||||
|
||||
assert_eq!(
|
||||
outcome.plugins[0].manifest_description.as_deref(),
|
||||
Some("Plugin that\n includes the sample\tserver")
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.capability_summaries()[0].description.as_deref(),
|
||||
Some("Plugin that includes the sample server")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_summary_truncates_overlong_plugin_descriptions() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/sample/local");
|
||||
let too_long = "x".repeat(MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN + 1);
|
||||
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
&format!(
|
||||
r#"{{
|
||||
"name": "sample",
|
||||
"description": "{too_long}"
|
||||
}}"#
|
||||
),
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join("skills/sample-search/SKILL.md"),
|
||||
"---\nname: sample-search\ndescription: search sample data\n---\n",
|
||||
);
|
||||
|
||||
let outcome = load_plugins_from_config(&plugin_config_toml(true, true), codex_home.path());
|
||||
|
||||
assert_eq!(
|
||||
outcome.plugins[0].manifest_description.as_deref(),
|
||||
Some(too_long.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.capability_summaries()[0].description,
|
||||
Some("x".repeat(MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
@@ -14,12 +14,16 @@ pub(crate) fn render_plugins_section(plugins: &[PluginCapabilitySummary]) -> Opt
|
||||
lines.extend(
|
||||
plugins
|
||||
.iter()
|
||||
.map(|plugin| format!("- `{}`", plugin.display_name)),
|
||||
.map(|plugin| match plugin.description.as_deref() {
|
||||
Some(description) => format!("- `{}`: {description}", plugin.display_name),
|
||||
None => format!("- `{}`", plugin.display_name),
|
||||
}),
|
||||
);
|
||||
|
||||
lines.push("### How to use plugins".to_string());
|
||||
lines.push(
|
||||
r###"- Discovery: The list above is the plugins available in this session.
|
||||
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
|
||||
- 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.
|
||||
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
|
||||
|
||||
@@ -5,3 +5,19 @@ use pretty_assertions::assert_eq;
|
||||
fn render_plugins_section_returns_none_for_empty_plugins() {
|
||||
assert_eq!(render_plugins_section(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_plugins_section_includes_descriptions_and_skill_naming_guidance() {
|
||||
let rendered = render_plugins_section(&[PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
display_name: "sample".to_string(),
|
||||
description: Some("inspect sample data".to_string()),
|
||||
has_skills: true,
|
||||
..PluginCapabilitySummary::default()
|
||||
}])
|
||||
.expect("plugin section should render");
|
||||
|
||||
let expected = "## Plugins\nA plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.\n### Available plugins\n- `sample`: inspect sample data\n### How to use plugins\n- Discovery: The list above is the plugins available in this session.\n- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.\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- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.\n- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.";
|
||||
|
||||
assert_eq!(rendered, expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user