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
4 changes: 4 additions & 0 deletions codex-rs/config/src/skills_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub struct SkillsConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_instructions: Option<bool>,

/// Whether the automatic skills instructions include guidance to announce skill usage.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub announce_usage: Option<bool>,

#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<SkillConfig>,
}
Expand Down
66 changes: 62 additions & 4 deletions codex-rs/core-skills/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub const SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX: &str =
"Exceeded skills context budget. All skill descriptions were removed and";
pub const SKILLS_INTRO_WITH_ABSOLUTE_PATHS: &str = "A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.";
pub const SKILLS_INTRO_WITH_ALIASES: &str = "A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table.";
pub const SKILLS_ANNOUNCEMENT_GUIDANCE_LINE: &str = " - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.";
pub const SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS: &str = r###"- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
Expand Down Expand Up @@ -59,7 +60,11 @@ pub const SKILLS_HOW_TO_USE_WITH_ALIASES: &str = r###"- Discovery: The list abov
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."###;

pub fn render_available_skills_body(skill_root_lines: &[String], skill_lines: &[String]) -> String {
pub fn render_available_skills_body(
skill_root_lines: &[String],
skill_lines: &[String],
announce_usage: bool,
) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push("## Skills".to_string());
if skill_root_lines.is_empty() {
Expand All @@ -78,11 +83,23 @@ pub fn render_available_skills_body(skill_root_lines: &[String], skill_lines: &[
} else {
SKILLS_HOW_TO_USE_WITH_ALIASES
};
lines.push(how_to_use.to_string());
lines.push(render_how_to_use(how_to_use, announce_usage));

format!("\n{}\n", lines.join("\n"))
}

fn render_how_to_use(how_to_use: &str, announce_usage: bool) -> String {
if announce_usage {
return how_to_use.to_string();
}

how_to_use
.lines()
.filter(|line| *line != SKILLS_ANNOUNCEMENT_GUIDANCE_LINE)
.collect::<Vec<_>>()
.join("\n")
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkillMetadataBudget {
Tokens(usize),
Expand Down Expand Up @@ -786,8 +803,13 @@ fn aliased_metadata_overhead_cost(
skill_root_lines: &[String],
) -> usize {
let empty_skill_lines: &[String] = &[];
let absolute_body = render_available_skills_body(&[], empty_skill_lines);
let aliased_body = render_available_skills_body(skill_root_lines, empty_skill_lines);
let absolute_body =
render_available_skills_body(&[], empty_skill_lines, /*announce_usage*/ true);
let aliased_body = render_available_skills_body(
skill_root_lines,
empty_skill_lines,
/*announce_usage*/ true,
);
budget
.cost(&aliased_body)
.saturating_sub(budget.cost(&absolute_body))
Expand Down Expand Up @@ -1006,6 +1028,42 @@ mod tests {
);
}

#[test]
fn render_body_includes_announcement_guidance_when_enabled() {
let skill_lines = vec!["- example: test skill (file: /tmp/example/SKILL.md)".to_string()];

let absolute =
render_available_skills_body(&[], &skill_lines, /*announce_usage*/ true);
let aliased = render_available_skills_body(
&["- `r0` = `/tmp`".to_string()],
&skill_lines,
/*announce_usage*/ true,
);

assert!(absolute.contains(SKILLS_ANNOUNCEMENT_GUIDANCE_LINE));
assert!(aliased.contains(SKILLS_ANNOUNCEMENT_GUIDANCE_LINE));
assert!(absolute.contains(SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS));
assert!(aliased.contains(SKILLS_HOW_TO_USE_WITH_ALIASES));
}

#[test]
fn render_body_omits_only_announcement_guidance_when_disabled() {
let skill_lines = vec!["- example: test skill (file: /tmp/example/SKILL.md)".to_string()];
let skill_root_lines = vec!["- `r0` = `/tmp`".to_string()];

for roots in [&[][..], skill_root_lines.as_slice()] {
let with_guidance =
render_available_skills_body(roots, &skill_lines, /*announce_usage*/ true);
let without_guidance =
render_available_skills_body(roots, &skill_lines, /*announce_usage*/ false);
let expected =
with_guidance.replace(&format!("\n{SKILLS_ANNOUNCEMENT_GUIDANCE_LINE}"), "");

assert_eq!(without_guidance, expected);
assert!(!without_guidance.contains(SKILLS_ANNOUNCEMENT_GUIDANCE_LINE));
}
}

#[test]
fn budgeted_rendering_truncates_descriptions_equally_before_omitting_skills() {
let alpha = make_skill_with_description("alpha-skill", SkillScope::Repo, "abcdef");
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2483,6 +2483,10 @@
"SkillsConfig": {
"additionalProperties": false,
"properties": {
"announce_usage": {
"description": "Whether the automatic skills instructions include guidance to announce skill usage.",
"type": "boolean"
},
"bundled": {
"$ref": "#/definitions/BundledSkillsConfig"
},
Expand Down
41 changes: 41 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ fn parses_bundled_skills_config() {
r#"
[skills]
include_instructions = false
announce_usage = false

[skills.bundled]
enabled = false
Expand All @@ -380,6 +381,7 @@ enabled = false
Some(SkillsConfig {
bundled: Some(BundledSkillsConfig { enabled: false }),
include_instructions: Some(false),
announce_usage: Some(false),
config: Vec::new(),
})
);
Expand Down Expand Up @@ -7805,6 +7807,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
include_apps_instructions: true,
include_collaboration_mode_instructions: true,
include_skill_instructions: true,
skill_announce_usage: true,
include_environment_context: true,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
Expand Down Expand Up @@ -8259,6 +8262,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
include_apps_instructions: true,
include_collaboration_mode_instructions: true,
include_skill_instructions: true,
skill_announce_usage: true,
include_environment_context: true,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
Expand Down Expand Up @@ -8427,6 +8431,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
include_apps_instructions: true,
include_collaboration_mode_instructions: true,
include_skill_instructions: true,
skill_announce_usage: true,
include_environment_context: true,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
Expand Down Expand Up @@ -8580,6 +8585,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
include_apps_instructions: true,
include_collaboration_mode_instructions: true,
include_skill_instructions: true,
skill_announce_usage: true,
include_environment_context: true,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
Expand Down Expand Up @@ -9936,6 +9942,41 @@ include_environment_context = true
Ok(())
}

#[tokio::test]
async fn skill_announce_usage_defaults_to_enabled() -> std::io::Result<()> {
let codex_home = TempDir::new()?;

let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;

assert!(config.skill_announce_usage);
Ok(())
}

#[tokio::test]
async fn skill_announce_usage_can_be_disabled() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[skills]
announce_usage = false
"#,
)?;

let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;

assert!(config.include_skill_instructions);
assert!(!config.skill_announce_usage);
Ok(())
}

#[tokio::test]
async fn approvals_reviewer_stays_manual_only_when_guardian_feature_is_enabled()
-> std::io::Result<()> {
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,9 @@ pub struct Config {
/// Whether to inject the `<skills_instructions>` developer block.
pub include_skill_instructions: bool,

/// Whether the automatic skills instructions include guidance to announce skill usage.
pub skill_announce_usage: bool,

/// Whether to inject the `<environment_context>` user block.
pub include_environment_context: bool,

Expand Down Expand Up @@ -3186,6 +3189,11 @@ impl Config {
.as_ref()
.and_then(|skills| skills.include_instructions)
.unwrap_or(true);
let skill_announce_usage = cfg
.skills
.as_ref()
.and_then(|skills| skills.announce_usage)
.unwrap_or(true);
let include_environment_context = config_profile
.include_environment_context
.or(cfg.include_environment_context)
Expand Down Expand Up @@ -3411,6 +3419,7 @@ impl Config {
include_apps_instructions,
include_collaboration_mode_instructions,
include_skill_instructions,
skill_announce_usage,
include_environment_context,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
Expand Down
12 changes: 9 additions & 3 deletions codex-rs/core/src/context/available_skills_instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ use super::ContextualUserFragment;
pub(crate) struct AvailableSkillsInstructions {
skill_root_lines: Vec<String>,
skill_lines: Vec<String>,
announce_usage: bool,
}

impl From<AvailableSkills> for AvailableSkillsInstructions {
fn from(available_skills: AvailableSkills) -> Self {
impl AvailableSkillsInstructions {
pub(crate) fn new(available_skills: AvailableSkills, announce_usage: bool) -> Self {
Self {
skill_root_lines: available_skills.skill_root_lines,
skill_lines: available_skills.skill_lines,
announce_usage,
}
}
}
Expand All @@ -34,6 +36,10 @@ impl ContextualUserFragment for AvailableSkillsInstructions {
}

fn body(&self) -> String {
render_available_skills_body(&self.skill_root_lines, &self.skill_lines)
render_available_skills_body(
&self.skill_root_lines,
&self.skill_lines,
self.announce_usage,
)
}
}
13 changes: 9 additions & 4 deletions codex-rs/core/src/session/config_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,9 @@ fn save_config_resolved_fields(
agents.job_max_runtime_seconds = config.agent_job_max_runtime_seconds;
agents.interrupt_message = Some(config.agent_interrupt_message_enabled);

lock_config
.skills
.get_or_insert_with(Default::default)
.include_instructions = Some(config.include_skill_instructions);
let skills = lock_config.skills.get_or_insert_with(Default::default);
skills.include_instructions = Some(config.include_skill_instructions);
skills.announce_usage = Some(config.skill_announce_usage);

Ok(())
}
Expand Down Expand Up @@ -224,6 +223,12 @@ mod tests {
assert_eq!(lock.instructions, Some(sc.base_instructions.clone()));
assert_eq!(lock.developer_instructions, sc.developer_instructions);
assert_eq!(lock.compact_prompt, sc.compact_prompt);
assert_eq!(
lock.skills
.as_ref()
.and_then(|skills| skills.announce_usage),
Some(sc.original_config_do_not_use.skill_announce_usage)
);
assert_eq!(lock.model, Some(sc.collaboration_mode.model().to_string()));
assert_eq!(
lock.model_reasoning_effort,
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2772,7 +2772,10 @@ impl Session {
);
if let Some(available_skills) = available_skills {
let warning_message = available_skills.warning_message.clone();
let skills_instructions = AvailableSkillsInstructions::from(available_skills);
let skills_instructions = AvailableSkillsInstructions::new(
available_skills,
turn_context.config.skill_announce_usage,
);
if let Some(warning_message) = warning_message {
self.send_event_raw(Event {
id: String::new(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/thread-manager-sample/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
include_apps_instructions: false,
include_collaboration_mode_instructions: false,
include_skill_instructions: false,
skill_announce_usage: false,
include_environment_context: false,
compact_prompt: None,
notify: None,
Expand Down