Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ const SKILLS_LIST_CWD_CONCURRENCY: usize = 5;

fn skills_to_info(
skills: &[codex_core::skills::SkillMetadata],
disabled_paths: &HashSet<AbsolutePathBuf>,
disabled_paths: &HashSet<codex_exec_server::EnvironmentPathRef>,
) -> Vec<codex_app_server_protocol::SkillMetadata> {
skills
.iter()
.map(|skill| {
let enabled = !disabled_paths.contains(&skill.path_to_skills_md);
let enabled = !disabled_paths.contains(&skill.source_path);
codex_app_server_protocol::SkillMetadata {
name: skill.name.clone(),
description: skill.description.clone(),
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/core-plugins/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,10 @@ pub async fn load_plugin_skills(
.into_iter()
.filter(|skill| skill.matches_product_restriction_for_product(restriction_product))
.collect::<Vec<_>>();
let disabled_skill_paths = resolve_disabled_skill_paths(&skills, skill_config_rules);
let disabled_skill_paths = resolve_disabled_skill_paths(&skills, skill_config_rules)
.into_iter()
.map(|path| path.path().clone())
.collect();

ResolvedPluginSkills {
skills,
Expand Down
25 changes: 16 additions & 9 deletions codex-rs/core-skills/src/config_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
use codex_config::SkillConfig;
use codex_config::SkillsConfig;
use codex_exec_server::EnvironmentPathRef;
use codex_utils_absolute_path::AbsolutePathBuf;
use tracing::warn;

Expand Down Expand Up @@ -71,28 +72,34 @@ pub fn skill_config_rules_from_stack(config_layer_stack: &ConfigLayerStack) -> S
pub fn resolve_disabled_skill_paths(
skills: &[SkillMetadata],
rules: &SkillConfigRules,
) -> HashSet<AbsolutePathBuf> {
) -> HashSet<EnvironmentPathRef> {
let mut disabled_paths = HashSet::new();

for entry in &rules.entries {
match &entry.selector {
SkillConfigRuleSelector::Path(path) => {
if entry.enabled {
disabled_paths.remove(path);
} else {
disabled_paths.insert(path.clone());
for source_path in skills
.iter()
.filter(|skill| skill.path_to_skills_md == *path)
.map(|skill| skill.source_path.clone())
{
if entry.enabled {
disabled_paths.remove(&source_path);
} else {
disabled_paths.insert(source_path);
}
}
}
SkillConfigRuleSelector::Name(name) => {
for path in skills
for source_path in skills
.iter()
.filter(|skill| skill.name == *name)
.map(|skill| skill.path_to_skills_md.clone())
.map(|skill| skill.source_path.clone())
{
if entry.enabled {
disabled_paths.remove(&path);
disabled_paths.remove(&source_path);
} else {
disabled_paths.insert(path);
disabled_paths.insert(source_path);
}
}
}
Expand Down
141 changes: 108 additions & 33 deletions codex-rs/core-skills/src/injection.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;

use crate::SkillLoadOutcome;
use crate::SkillMetadata;
use crate::build_skill_name_counts;
use codex_analytics::AnalyticsEventsClient;
use codex_analytics::InvocationType;
use codex_analytics::SkillInvocation;
use codex_analytics::TrackEventsContext;
use codex_exec_server::LOCAL_FS;
use codex_exec_server::EnvironmentPathRef;
use codex_otel::SessionTelemetry;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL;
use std::collections::HashMap;
use std::collections::HashSet;

#[derive(Debug, Default)]
pub struct SkillInjections {
Expand All @@ -30,7 +28,7 @@ pub struct SkillInjection {

pub async fn build_skill_injections(
mentioned_skills: &[SkillMetadata],
loaded_skills: Option<&SkillLoadOutcome>,
_loaded_skills: Option<&SkillLoadOutcome>,
otel: Option<&SessionTelemetry>,
analytics_client: &AnalyticsEventsClient,
tracking: TrackEventsContext,
Expand All @@ -46,13 +44,7 @@ pub async fn build_skill_injections(
let mut invocations = Vec::new();

for skill in mentioned_skills {
let fs = loaded_skills
.and_then(|outcome| outcome.file_system_for_skill(skill))
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
match fs
.read_file_text(&skill.path_to_skills_md, /*sandbox*/ None)
.await
{
match skill.source_path.read_to_string(/*sandbox*/ None).await {
Ok(contents) => {
emit_skill_injected_metric(otel, skill, "ok");
invocations.push(SkillInvocation {
Expand Down Expand Up @@ -106,7 +98,8 @@ fn emit_skill_injected_metric(
/// Structured `UserInput::Skill` selections are resolved first by path against
/// enabled skills. Text inputs are then scanned to extract `$skill-name` tokens, and we
/// iterate `skills` in their existing order to preserve prior ordering semantics.
/// Explicit links are resolved by path and plain names are only used when the match
/// Explicit links are resolved by path, using the linked name only when duplicate
/// filesystem-bound copies share the same raw path. Plain names are only used when the match
/// is unambiguous.
///
/// Complexity: `O(T + (N_s + N_t) * S)` time, `O(S + M)` space, where:
Expand All @@ -115,7 +108,7 @@ fn emit_skill_injected_metric(
pub fn collect_explicit_skill_mentions(
inputs: &[UserInput],
skills: &[SkillMetadata],
disabled_paths: &HashSet<AbsolutePathBuf>,
disabled_paths: &HashSet<EnvironmentPathRef>,
connector_slug_counts: &HashMap<String, usize>,
) -> Vec<SkillMetadata> {
let skill_name_counts = build_skill_name_counts(skills, disabled_paths).0;
Expand All @@ -128,7 +121,7 @@ pub fn collect_explicit_skill_mentions(
};
let mut selected: Vec<SkillMetadata> = Vec::new();
let mut seen_names: HashSet<String> = HashSet::new();
let mut seen_paths: HashSet<AbsolutePathBuf> = HashSet::new();
let mut seen_paths: HashSet<EnvironmentPathRef> = HashSet::new();
let mut blocked_plain_names: HashSet<String> = HashSet::new();

for input in inputs {
Expand All @@ -138,18 +131,23 @@ pub fn collect_explicit_skill_mentions(
continue;
};

if selection_context.disabled_paths.contains(&path) || seen_paths.contains(&path) {
continue;
}

if let Some(skill) = selection_context
let matching_skills = selection_context
.skills
.iter()
.find(|skill| skill.path_to_skills_md == path)
.filter(|skill| {
skill.name == *name
&& skill.path_to_skills_md == path
&& !selection_context
.disabled_paths
.contains(&skill.source_path)
})
.collect::<Vec<_>>();
if let [skill] = matching_skills.as_slice()
&& !seen_paths.contains(&skill.source_path)
{
seen_paths.insert(skill.path_to_skills_md.clone());
seen_paths.insert(skill.source_path.clone());
seen_names.insert(skill.name.clone());
selected.push(skill.clone());
selected.push((*skill).clone());
}
}
}
Expand All @@ -173,7 +171,7 @@ pub fn collect_explicit_skill_mentions(

struct SkillSelectionContext<'a> {
skills: &'a [SkillMetadata],
disabled_paths: &'a HashSet<AbsolutePathBuf>,
disabled_paths: &'a HashSet<EnvironmentPathRef>,
skill_name_counts: &'a HashMap<String, usize>,
connector_slug_counts: &'a HashMap<String, usize>,
}
Expand All @@ -182,6 +180,7 @@ pub struct ToolMentions<'a> {
names: HashSet<&'a str>,
paths: HashSet<&'a str>,
plain_names: HashSet<&'a str>,
linked_mentions: HashSet<LinkedToolMention<'a>>,
}

impl<'a> ToolMentions<'a> {
Expand All @@ -196,6 +195,16 @@ impl<'a> ToolMentions<'a> {
pub fn paths(&self) -> impl Iterator<Item = &'a str> + '_ {
self.paths.iter().copied()
}

fn linked_mentions(&self) -> impl Iterator<Item = LinkedToolMention<'a>> + '_ {
self.linked_mentions.iter().copied()
}
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct LinkedToolMention<'a> {
name: &'a str,
path: &'a str,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -260,6 +269,7 @@ pub fn extract_tool_mentions_with_sigil(text: &str, sigil: char) -> ToolMentions
let mut mentioned_names: HashSet<&str> = HashSet::new();
let mut mentioned_paths: HashSet<&str> = HashSet::new();
let mut plain_names: HashSet<&str> = HashSet::new();
let mut linked_mentions: HashSet<LinkedToolMention<'_>> = HashSet::new();

let mut index = 0;
while index < text_bytes.len() {
Expand All @@ -276,6 +286,7 @@ pub fn extract_tool_mentions_with_sigil(text: &str, sigil: char) -> ToolMentions
mentioned_names.insert(name);
}
mentioned_paths.insert(path);
linked_mentions.insert(LinkedToolMention { name, path });
}
index = end_index;
continue;
Expand Down Expand Up @@ -315,6 +326,7 @@ pub fn extract_tool_mentions_with_sigil(text: &str, sigil: char) -> ToolMentions
names: mentioned_names,
paths: mentioned_paths,
plain_names,
linked_mentions,
}
}

Expand All @@ -324,7 +336,7 @@ fn select_skills_from_mentions(
blocked_plain_names: &HashSet<String>,
mentions: &ToolMentions<'_>,
seen_names: &mut HashSet<String>,
seen_paths: &mut HashSet<AbsolutePathBuf>,
seen_paths: &mut HashSet<EnvironmentPathRef>,
selected: &mut Vec<SkillMetadata>,
) {
if mentions.is_empty() {
Expand All @@ -341,19 +353,82 @@ fn select_skills_from_mentions(
})
.map(normalize_skill_path)
.collect();
let mention_skill_links: HashSet<LinkedToolMention<'_>> = mentions
.linked_mentions()
.filter(|mention| {
!matches!(
tool_kind_for_path(mention.path),
ToolMentionKind::App | ToolMentionKind::Mcp | ToolMentionKind::Plugin
)
})
.map(|mention| LinkedToolMention {
name: mention.name,
path: normalize_skill_path(mention.path),
})
.collect();

let mention_skill_path_counts = selection_context
.skills
.iter()
.filter(|skill| {
!selection_context
.disabled_paths
.contains(&skill.source_path)
})
.filter_map(|skill| {
let path = skill.path_to_skills_md.to_string_lossy();
mention_skill_paths
.contains(path.as_ref())
.then_some(path.into_owned())
})
.fold(HashMap::new(), |mut counts, path| {
*counts.entry(path).or_insert(0usize) += 1;
counts
});
let mention_skill_link_counts = selection_context
.skills
.iter()
.filter(|skill| {
!selection_context
.disabled_paths
.contains(&skill.source_path)
})
.filter_map(|skill| {
let path = skill.path_to_skills_md.to_string_lossy();
mention_skill_links
.contains(&LinkedToolMention {
name: skill.name.as_str(),
path: path.as_ref(),
})
.then_some((skill.name.clone(), path.into_owned()))
})
.fold(HashMap::new(), |mut counts, link| {
*counts.entry(link).or_insert(0usize) += 1;
counts
});

for skill in selection_context.skills {
if selection_context
.disabled_paths
.contains(&skill.path_to_skills_md)
|| seen_paths.contains(&skill.path_to_skills_md)
.contains(&skill.source_path)
|| seen_paths.contains(&skill.source_path)
{
continue;
}

let path_str = skill.path_to_skills_md.to_string_lossy();
if mention_skill_paths.contains(path_str.as_ref()) {
seen_paths.insert(skill.path_to_skills_md.clone());
let path = path_str.as_ref();
let linked_mention = LinkedToolMention {
name: skill.name.as_str(),
path,
};
let linked_mention_key = (skill.name.clone(), path.to_string());
if mention_skill_paths.contains(path)
&& (mention_skill_path_counts.get(path) == Some(&1)
|| (mention_skill_links.contains(&linked_mention)
&& mention_skill_link_counts.get(&linked_mention_key) == Some(&1)))
{
seen_paths.insert(skill.source_path.clone());
seen_names.insert(skill.name.clone());
selected.push(skill.clone());
}
Expand All @@ -362,8 +437,8 @@ fn select_skills_from_mentions(
for skill in selection_context.skills {
if selection_context
.disabled_paths
.contains(&skill.path_to_skills_md)
|| seen_paths.contains(&skill.path_to_skills_md)
.contains(&skill.source_path)
|| seen_paths.contains(&skill.source_path)
{
continue;
}
Expand All @@ -390,7 +465,7 @@ fn select_skills_from_mentions(
}

if seen_names.insert(skill.name.clone()) {
seen_paths.insert(skill.path_to_skills_md.clone());
seen_paths.insert(skill.source_path.clone());
selected.push(skill.clone());
}
}
Expand Down
Loading
Loading