mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Move host skill prompt injection into the skills extension (#37503)
## What changed - Add `HostSkillsSnapshot::load_skill_prompts` so the skills extension owns reading and rendering selected host skills. - Keep explicit invocation telemetry, unreadable-skill warnings, plugin prompt ordering, and suppression of host prompts superseded by provider prompts. - Remove the legacy prompt injection and fragment implementation from `codex-core-skills`. ## Testing - Cover prompt truncation without an installed skills extension, skill/plugin ordering, selective provider supersession, unreadable skills, and contextual fragment detection. GitOrigin-RevId: 9c8b84ad54b90d174abc287ab49cc5a231e9e1aa
This commit is contained in:
1 parent
8e4b10446e
commit
beac16cccd
22 files changed
+553
-299
No files matched your search
@@ -2,8 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::HostSkillsSnapshot;
|
||||
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
|
||||
use codex_core_skills::injection::InjectedHostSkillPrompts;
|
||||
use crate::InjectedHostSkillPrompts;
|
||||
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
||||
use codex_exec_server::FileSystemSandboxContext;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
@@ -61,6 +60,7 @@ use crate::selection::collect_explicit_skill_mentions;
|
||||
use crate::shadow_selection_experiment::ShadowSelectionExperiment;
|
||||
use crate::sources::SkillProviders;
|
||||
use crate::state::ExecutorSkillsStepState;
|
||||
use crate::state::HostSkillsCatalogInWorldState;
|
||||
use crate::state::HostSkillsStepState;
|
||||
use crate::state::SkillsSessionState;
|
||||
use crate::state::SkillsThreadState;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_extension_api::ContextualUserFragment;
|
||||
use codex_skills::SkillMetadata;
|
||||
use codex_skills::normalize_skill_path;
|
||||
|
||||
use crate::HostSkillsSnapshot;
|
||||
use crate::fragments::SkillInstructions;
|
||||
use crate::render::truncate_main_prompt_contents;
|
||||
|
||||
/// Host skill prompts already supplied or superseded by an extension.
|
||||
///
|
||||
/// Core preserves its host skill invocation lifecycle while avoiding duplicate
|
||||
/// prompts and retaining executor/orchestrator skill precedence.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct InjectedHostSkillPrompts {
|
||||
paths: HashSet<String>,
|
||||
}
|
||||
|
||||
impl InjectedHostSkillPrompts {
|
||||
pub fn insert_path(&mut self, path: impl Into<String>) {
|
||||
let path = path.into();
|
||||
self.paths.insert(normalize_host_skill_path(&path));
|
||||
self.paths.insert(path);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.paths.is_empty()
|
||||
}
|
||||
|
||||
pub fn contains_path(&self, path: &str) -> bool {
|
||||
self.paths.contains(path) || self.paths.contains(&normalize_host_skill_path(path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt fragments and read outcomes for a set of selected host skills.
|
||||
pub struct HostSkillPrompts {
|
||||
pub fragments: Vec<Box<dyn ContextualUserFragment + Send>>,
|
||||
pub injected: Vec<SkillMetadata>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
fn normalize_host_skill_path(path: &str) -> String {
|
||||
normalize_skill_path(path).replace('\\', "/")
|
||||
}
|
||||
|
||||
impl HostSkillsSnapshot {
|
||||
/// Reads selected host skills and builds their model-visible prompt fragments.
|
||||
///
|
||||
/// Core calls this directly, including for hosts without an installed skills extension.
|
||||
#[tracing::instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(selected_skill_count = selected_skills.len())
|
||||
)]
|
||||
pub async fn load_skill_prompts(&self, selected_skills: &[SkillMetadata]) -> HostSkillPrompts {
|
||||
let mut prompts = HostSkillPrompts {
|
||||
fragments: Vec::with_capacity(selected_skills.len()),
|
||||
injected: Vec::with_capacity(selected_skills.len()),
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
|
||||
for skill in selected_skills {
|
||||
match self.read_skill_text(skill).await {
|
||||
Ok(contents) => {
|
||||
let (contents, truncated) = if self.outcome().is_agent_plugin_skill(skill) {
|
||||
truncate_main_prompt_contents(&contents)
|
||||
} else {
|
||||
(contents, false)
|
||||
};
|
||||
if truncated {
|
||||
prompts.warnings.push(format!(
|
||||
"Skill `{}` exceeded the main prompt context limit and was truncated.",
|
||||
skill.name
|
||||
));
|
||||
}
|
||||
prompts.fragments.push(Box::new(SkillInstructions {
|
||||
name: skill.name.clone(),
|
||||
path: skill.path_to_skills_md.to_string_lossy().into_owned(),
|
||||
contents,
|
||||
resource_access: None,
|
||||
}));
|
||||
prompts.injected.push(skill.clone());
|
||||
}
|
||||
Err(err) => {
|
||||
prompts.warnings.push(format!(
|
||||
"Failed to load skill {} at {}: {err:#}",
|
||||
skill.name,
|
||||
skill.path_to_skills_md.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompts
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod dynamic_skill_selector;
|
||||
mod extension;
|
||||
mod fragments;
|
||||
mod host_aliases;
|
||||
mod host_prompt;
|
||||
mod host_roots;
|
||||
mod host_service;
|
||||
mod host_snapshot;
|
||||
@@ -26,6 +27,8 @@ pub use config::SkillsExtensionConfig;
|
||||
pub use extension::install;
|
||||
pub use extension::install_with_providers;
|
||||
pub use extension::install_with_providers_and_metrics;
|
||||
pub use host_prompt::HostSkillPrompts;
|
||||
pub use host_prompt::InjectedHostSkillPrompts;
|
||||
pub use host_service::HostSkillsLoadInput;
|
||||
pub use host_service::HostSkillsService;
|
||||
pub use host_service::bundled_skills_enabled_from_stack;
|
||||
@@ -37,3 +40,10 @@ pub use provider::OrchestratorSkillProvider;
|
||||
pub use provider::SkillProvider;
|
||||
pub use sources::SkillProviderSource;
|
||||
pub use sources::SkillProviders;
|
||||
|
||||
/// Recognizes persisted explicit skill prompts without exposing their fragment implementation.
|
||||
pub fn is_skill_prompt_fragment(text: &str) -> bool {
|
||||
<fragments::SkillInstructions as codex_extension_api::ContextualUserFragment>::matches_text(
|
||||
text,
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,16 @@ use crate::catalog_prompt::render_available_skills_body;
|
||||
use crate::loader::HostSkillRoot;
|
||||
use crate::loader::load_and_merge_host_skill_roots;
|
||||
|
||||
#[test]
|
||||
fn skill_prompt_contents_are_bounded_at_utf8_boundaries() {
|
||||
let contents = format!("{}é", "a".repeat(MAX_SKILL_PROMPT_BYTES - 1));
|
||||
|
||||
let (bounded, truncated) = truncate_main_prompt_contents(&contents);
|
||||
|
||||
assert_eq!(bounded.len(), MAX_SKILL_PROMPT_BYTES - 1);
|
||||
assert_eq!(truncated, true);
|
||||
}
|
||||
|
||||
fn entry(name: &str, description: &str, short_description: Option<&str>) -> SkillCatalogEntry {
|
||||
entry_with_path(
|
||||
name,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_core_skills::injection::extract_tool_mentions;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_skills::extract_tool_mentions;
|
||||
|
||||
use crate::catalog::SkillAuthority;
|
||||
use crate::catalog::SkillCatalog;
|
||||
|
||||
@@ -384,6 +384,9 @@ pub(crate) struct SkillsTurnState {
|
||||
pub(crate) main_prompts_injected: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct HostSkillsCatalogInWorldState;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ExecutorSkillsStepState(pub(crate) SkillCatalog);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
|
||||
use codex_extension_api::ContextualUserFragment;
|
||||
use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionWarning;
|
||||
@@ -22,6 +21,7 @@ use crate::render_observability::record_catalog_render;
|
||||
use crate::sources::SkillProviders;
|
||||
use crate::state::EmittedCatalogBudgetWarnings;
|
||||
use crate::state::ExecutorSkillsStepState;
|
||||
use crate::state::HostSkillsCatalogInWorldState;
|
||||
use crate::state::HostSkillsStepState;
|
||||
use crate::state::SkillsSessionState;
|
||||
use crate::state::SkillsThreadState;
|
||||
|
||||
Reference in new issue
Block a user