Skip to content

Commit 55777bf

Browse files
committed
update skill loading + add tests.
1 parent ece3915 commit 55777bf

File tree

19 files changed

+391
-85
lines changed

19 files changed

+391
-85
lines changed

codex-rs/core/src/codex.rs

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ use crate::protocol::ReviewDecision;
100100
use crate::protocol::SandboxCommandAssessment;
101101
use crate::protocol::SandboxPolicy;
102102
use crate::protocol::SessionConfiguredEvent;
103+
use crate::protocol::SkillErrorInfo;
104+
use crate::protocol::SkillInfo;
105+
use crate::protocol::SkillLoadOutcomeInfo;
103106
use crate::protocol::StreamErrorEvent;
104107
use crate::protocol::Submission;
105108
use crate::protocol::TokenCountEvent;
@@ -111,9 +114,10 @@ use crate::rollout::RolloutRecorder;
111114
use crate::rollout::RolloutRecorderParams;
112115
use crate::rollout::map_session_init_error;
113116
use crate::shell;
117+
use crate::shell_snapshot::ShellSnapshot;
118+
use crate::skills::SkillLoadOutcome;
114119
use crate::skills::SkillMetadata;
115120
use crate::skills::load_skills;
116-
use crate::shell_snapshot::ShellSnapshot;
117121
use crate::state::ActiveTurn;
118122
use crate::state::SessionServices;
119123
use crate::state::SessionState;
@@ -195,9 +199,18 @@ impl Codex {
195199
}
196200
}
197201

198-
let user_instructions =
199-
get_user_instructions(&config, loaded_skills.as_ref().map(|o| o.skills.as_slice()))
200-
.await;
202+
let skills_outcome = loaded_skills.clone();
203+
204+
let user_instructions = get_user_instructions(
205+
&config,
206+
skills_outcome.as_ref().and_then(|outcome| {
207+
outcome
208+
.errors
209+
.is_empty()
210+
.then_some(outcome.skills.as_slice())
211+
}),
212+
)
213+
.await;
201214

202215
let exec_policy = load_exec_policy_for_features(&config.features, &config.codex_home)
203216
.await
@@ -225,7 +238,6 @@ impl Codex {
225238

226239
// Generate a unique ID for the lifetime of this Codex session.
227240
let session_source_clone = session_configuration.session_source.clone();
228-
let skills_cache = loaded_skills.as_ref().map(|o| o.skills.clone());
229241

230242
let session = Session::new(
231243
session_configuration,
@@ -235,7 +247,7 @@ impl Codex {
235247
tx_event.clone(),
236248
conversation_history,
237249
session_source_clone,
238-
skills_cache,
250+
skills_outcome.clone(),
239251
)
240252
.await
241253
.map_err(|e| {
@@ -501,7 +513,7 @@ impl Session {
501513
tx_event: Sender<Event>,
502514
initial_history: InitialHistory,
503515
session_source: SessionSource,
504-
loaded_skills: Option<Vec<SkillMetadata>>,
516+
skills: Option<SkillLoadOutcome>,
505517
) -> anyhow::Result<Arc<Self>> {
506518
debug!(
507519
"Configuring session: model={}; provider={:?}",
@@ -608,7 +620,7 @@ impl Session {
608620
.await
609621
.map(Arc::new);
610622
}
611-
let state = SessionState::new(session_configuration.clone(), loaded_skills);
623+
let state = SessionState::new(session_configuration.clone(), skills.clone());
612624

613625
let services = SessionServices {
614626
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())),
@@ -637,6 +649,7 @@ impl Session {
637649
// Dispatch the SessionConfiguredEvent first and then report any errors.
638650
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
639651
let initial_messages = initial_history.get_event_msgs();
652+
let skill_load_outcome = skill_load_outcome_for_client(skills.as_ref());
640653

641654
let events = std::iter::once(Event {
642655
id: INITIAL_SUBMIT_ID.to_owned(),
@@ -651,6 +664,7 @@ impl Session {
651664
history_log_id,
652665
history_entry_count,
653666
initial_messages,
667+
skill_load_outcome,
654668
rollout_path,
655669
}),
656670
})
@@ -1341,7 +1355,11 @@ impl Session {
13411355

13421356
let skills = {
13431357
let state = self.state.lock().await;
1344-
state.skills.clone().unwrap_or_default()
1358+
state
1359+
.skills
1360+
.as_ref()
1361+
.map(|outcome| outcome.skills.clone())
1362+
.unwrap_or_default()
13451363
};
13461364

13471365
let mentioned_skills = collect_explicit_skill_mentions(user_input, &skills);
@@ -2093,30 +2111,43 @@ fn collect_explicit_skill_mentions(
20932111
inputs: &[UserInput],
20942112
skills: &[SkillMetadata],
20952113
) -> Vec<SkillMetadata> {
2096-
let mut full_text: Vec<String> = Vec::new();
2097-
for input in inputs {
2098-
if let UserInput::Text { text } = input {
2099-
full_text.push(text.clone());
2100-
}
2101-
}
2102-
let combined = full_text.join(" ");
2114+
let mut selected: Vec<SkillMetadata> = Vec::new();
21032115
let mut seen: HashSet<String> = HashSet::new();
2104-
let mut matches: Vec<SkillMetadata> = Vec::new();
21052116

2106-
for skill in skills {
2107-
let name = skill.name.clone();
2108-
if seen.contains(&name) {
2109-
continue;
2110-
}
2111-
let needle = format!("${name}");
2112-
let hit = combined.contains(&needle);
2113-
if hit {
2114-
seen.insert(name);
2115-
matches.push(skill.clone());
2117+
for input in inputs {
2118+
if let UserInput::Skill { name, path } = input
2119+
&& seen.insert(name.clone())
2120+
&& let Some(skill) = skills.iter().find(|s| s.name == *name && s.path == *path)
2121+
{
2122+
selected.push(skill.clone());
21162123
}
21172124
}
21182125

2119-
matches
2126+
selected
2127+
}
2128+
2129+
fn skill_load_outcome_for_client(
2130+
outcome: Option<&SkillLoadOutcome>,
2131+
) -> Option<SkillLoadOutcomeInfo> {
2132+
outcome.map(|outcome| SkillLoadOutcomeInfo {
2133+
skills: outcome
2134+
.skills
2135+
.iter()
2136+
.map(|skill| SkillInfo {
2137+
name: skill.name.clone(),
2138+
description: skill.description.clone(),
2139+
path: skill.path.clone(),
2140+
})
2141+
.collect(),
2142+
errors: outcome
2143+
.errors
2144+
.iter()
2145+
.map(|err| SkillErrorInfo {
2146+
path: err.path.clone(),
2147+
message: err.message.clone(),
2148+
})
2149+
.collect(),
2150+
})
21202151
}
21212152

21222153
/// Takes a user message as input and runs a loop where, at each turn, the model

codex-rs/core/src/event_mapping.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,22 @@ mod tests {
201201
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
202202
}],
203203
},
204-
ResponseItem::Message {
205-
id: None,
206-
role: "user".to_string(),
207-
content: vec![ContentItem::InputText {
208-
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
209-
}],
210-
},
211-
];
204+
ResponseItem::Message {
205+
id: None,
206+
role: "user".to_string(),
207+
content: vec![ContentItem::InputText {
208+
text: "<SKILL name=\"demo\" path=\"skills/demo/SKILL.md\">\nbody\n</SKILL>"
209+
.to_string(),
210+
}],
211+
},
212+
ResponseItem::Message {
213+
id: None,
214+
role: "user".to_string(),
215+
content: vec![ContentItem::InputText {
216+
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
217+
}],
218+
},
219+
];
212220

213221
for item in items {
214222
let turn_item = parse_turn_item(&item);

codex-rs/core/src/project_doc.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ mod tests {
239239
use super::*;
240240
use crate::config::ConfigOverrides;
241241
use crate::config::ConfigToml;
242+
use crate::skills::load_skills;
242243
use std::fs;
243244
use std::path::PathBuf;
244245
use tempfile::TempDir;
@@ -505,9 +506,13 @@ mod tests {
505506
"extract from pdfs",
506507
);
507508

508-
let res = get_user_instructions(&cfg, None)
509-
.await
510-
.expect("instructions expected");
509+
let skills = load_skills(&cfg);
510+
let res = get_user_instructions(
511+
&cfg,
512+
skills.errors.is_empty().then_some(skills.skills.as_slice()),
513+
)
514+
.await
515+
.expect("instructions expected");
511516
let expected_path = dunce::canonicalize(
512517
cfg.codex_home
513518
.join("skills/pdf-processing/SKILL.md")
@@ -528,9 +533,13 @@ mod tests {
528533
let cfg = make_config(&tmp, 4096, None);
529534
create_skill(cfg.codex_home.clone(), "linting", "run clippy");
530535

531-
let res = get_user_instructions(&cfg, None)
532-
.await
533-
.expect("instructions expected");
536+
let skills = load_skills(&cfg);
537+
let res = get_user_instructions(
538+
&cfg,
539+
skills.errors.is_empty().then_some(skills.skills.as_slice()),
540+
)
541+
.await
542+
.expect("instructions expected");
534543
let expected_path =
535544
dunce::canonicalize(cfg.codex_home.join("skills/linting/SKILL.md").as_path())
536545
.unwrap_or_else(|_| cfg.codex_home.join("skills/linting/SKILL.md"));

codex-rs/core/src/state/session.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ use crate::context_manager::ContextManager;
77
use crate::protocol::RateLimitSnapshot;
88
use crate::protocol::TokenUsage;
99
use crate::protocol::TokenUsageInfo;
10-
use crate::skills::SkillMetadata;
10+
use crate::skills::SkillLoadOutcome;
1111
use crate::truncate::TruncationPolicy;
1212

1313
/// Persistent, session-scoped state previously stored directly on `Session`.
1414
pub(crate) struct SessionState {
1515
pub(crate) session_configuration: SessionConfiguration,
1616
pub(crate) history: ContextManager,
1717
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
18-
pub(crate) skills: Option<Vec<SkillMetadata>>,
18+
pub(crate) skills: Option<SkillLoadOutcome>,
1919
}
2020

2121
impl SessionState {
2222
/// Create a new session state mirroring previous `State::default()` semantics.
2323
pub(crate) fn new(
2424
session_configuration: SessionConfiguration,
25-
skills: Option<Vec<SkillMetadata>>,
25+
skills: Option<SkillLoadOutcome>,
2626
) -> Self {
2727
let history = ContextManager::new();
2828
Self {

codex-rs/core/src/user_instructions.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use codex_protocol::models::ResponseItem;
66

77
pub const USER_INSTRUCTIONS_OPEN_TAG_LEGACY: &str = "<user_instructions>";
88
pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for ";
9-
pub const SKILL_INSTRUCTIONS_PREFIX: &str = "# SKILL.md instructions for ";
9+
pub const SKILL_INSTRUCTIONS_PREFIX: &str = "<SKILL";
1010

1111
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1212
#[serde(rename = "user_instructions", rename_all = "snake_case")]
@@ -67,7 +67,7 @@ impl From<SkillInstructions> for ResponseItem {
6767
role: "user".to_string(),
6868
content: vec![ContentItem::InputText {
6969
text: format!(
70-
"{SKILL_INSTRUCTIONS_PREFIX}{name}\nPath: {path}\n\n<SKILL>\n{contents}\n</SKILL>",
70+
"<SKILL name=\"{name}\" path=\"{path}\">\n{contents}\n</SKILL>",
7171
name = si.name,
7272
path = si.path,
7373
contents = si.contents
@@ -108,6 +108,7 @@ impl From<DeveloperInstructions> for ResponseItem {
108108
#[cfg(test)]
109109
mod tests {
110110
use super::*;
111+
use pretty_assertions::assert_eq;
111112

112113
#[test]
113114
fn test_user_instructions() {
@@ -151,4 +152,44 @@ mod tests {
151152
}
152153
]));
153154
}
155+
156+
#[test]
157+
fn test_skill_instructions() {
158+
let skill_instructions = SkillInstructions {
159+
name: "demo-skill".to_string(),
160+
path: "skills/demo/SKILL.md".to_string(),
161+
contents: "body".to_string(),
162+
};
163+
let response_item: ResponseItem = skill_instructions.into();
164+
165+
let ResponseItem::Message { role, content, .. } = response_item else {
166+
panic!("expected ResponseItem::Message");
167+
};
168+
169+
assert_eq!(role, "user");
170+
171+
let [ContentItem::InputText { text }] = content.as_slice() else {
172+
panic!("expected one InputText content item");
173+
};
174+
175+
assert_eq!(
176+
text,
177+
"<SKILL name=\"demo-skill\" path=\"skills/demo/SKILL.md\">\nbody\n</SKILL>",
178+
);
179+
}
180+
181+
#[test]
182+
fn test_is_skill_instructions() {
183+
assert!(SkillInstructions::is_skill_instructions(&[
184+
ContentItem::InputText {
185+
text: "<SKILL name=\"demo-skill\" path=\"skills/demo/SKILL.md\">\nbody\n</SKILL>"
186+
.to_string(),
187+
}
188+
]));
189+
assert!(!SkillInstructions::is_skill_instructions(&[
190+
ContentItem::InputText {
191+
text: "regular text".to_string(),
192+
}
193+
]));
194+
}
154195
}

0 commit comments

Comments
 (0)