mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Implement v1 skills extension prompt injection (#26167)
## Why The skills extension needs a real turn-time path before host, executor, or remote skills can be routed through it. The previous code was mostly a placeholder catalog/provider sketch, so there was no bounded available-skills fragment, no source-owned `SKILL.md` read, and no place for warnings or per-turn selection state to live. This PR makes `ext/skills` the authority-preserving flow for listing candidate skills and injecting only explicitly selected main prompts, without adding more of that logic to `codex-core`. ## What changed - Expands catalog entries with `main_prompt`, display path, short description, dependency metadata, enabled/prompt visibility flags, and authority/package-aware read requests. - Replaces the placeholder `providers/*` modules with `SkillProviderSource` and `SkillProviders`, routing list/read/search calls by source kind and surfacing provider failures as warnings. - Adds bounded available-skills rendering and `SKILL.md` main-prompt truncation before the fragments enter model context. - Resolves explicit skill selections from structured `UserInput::Skill`, skill-file mentions, `skill://...` paths, and plain `$skill` text mentions, then reads selected prompts through their owning provider. - Stores mutable per-thread skills config and per-turn catalog/selection/warning state. - Adds `install_with_providers` so tests and future host wiring can supply concrete providers. ## Testing - Not run locally. - Added `codex-rs/ext/skills/tests/skills_extension.rs` coverage for available-catalog injection, selected prompt injection through the owning provider, and prompt-hidden skills that remain invokable.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
use codex_core_skills::render_available_skills_body;
|
||||
use codex_extension_api::ContextualUserFragment;
|
||||
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
|
||||
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
|
||||
|
||||
use crate::catalog::SkillCatalog;
|
||||
|
||||
const MAX_AVAILABLE_SKILLS_CHARS: usize = 8_000;
|
||||
const MAX_MAIN_PROMPT_CHARS: usize = 40_000;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct AvailableSkillsFragment {
|
||||
body: String,
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for AvailableSkillsFragment {
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
self.body.clone()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
(SKILLS_INSTRUCTIONS_OPEN_TAG, SKILLS_INSTRUCTIONS_CLOSE_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn available_skills_fragment(catalog: &SkillCatalog) -> Option<AvailableSkillsFragment> {
|
||||
let mut total_chars = 0usize;
|
||||
let mut omitted = 0usize;
|
||||
let mut skill_lines = Vec::new();
|
||||
|
||||
for entry in catalog
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.enabled && entry.prompt_visible)
|
||||
{
|
||||
let description = entry
|
||||
.short_description
|
||||
.as_deref()
|
||||
.unwrap_or(entry.description.as_str());
|
||||
let line = render_skill_line(entry.name.as_str(), description, entry.rendered_path());
|
||||
let next_chars = total_chars.saturating_add(line.chars().count());
|
||||
if next_chars > MAX_AVAILABLE_SKILLS_CHARS {
|
||||
omitted = omitted.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
total_chars = next_chars;
|
||||
skill_lines.push(line);
|
||||
}
|
||||
|
||||
if skill_lines.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if omitted > 0 {
|
||||
let skill_word = if omitted == 1 { "skill" } else { "skills" };
|
||||
skill_lines.push(format!(
|
||||
"- {omitted} additional {skill_word} omitted from this bounded skills list."
|
||||
));
|
||||
}
|
||||
|
||||
Some(AvailableSkillsFragment {
|
||||
body: render_available_skills_body(&[], &skill_lines),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_skill_line(name: &str, description: &str, path: &str) -> String {
|
||||
if description.is_empty() {
|
||||
format!("- {name}: (file: {path})")
|
||||
} else {
|
||||
format!("- {name}: {description} (file: {path})")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_main_prompt_contents(contents: &str) -> (String, bool) {
|
||||
let mut chars = 0usize;
|
||||
for (index, _) in contents.char_indices() {
|
||||
if chars == MAX_MAIN_PROMPT_CHARS {
|
||||
return (contents[..index].to_string(), true);
|
||||
}
|
||||
chars = chars.saturating_add(1);
|
||||
}
|
||||
(contents.to_string(), false)
|
||||
}
|
||||
Reference in New Issue
Block a user