mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Preserve skill descriptions outside model context (#29006)
## Why Skill descriptions are used in model-visible lists: the default available-skills catalog that supports implicit selection, and the on-demand `skills.list` tool response used to discover orchestrator skills. A single overlong description should not consume a disproportionate share of either list. Enforcing the 1024-character limit while loading or migrating skills is the wrong boundary: it rejects otherwise-valid skills and discards metadata that non-model consumers and full skill reads may need. Skill metadata and `SKILL.md` content should remain intact; the cap belongs at model-visible list rendering boundaries. ## What changed - Preserve full `description` and `metadata.short-description` values when loading skills. - Preserve full external-agent command descriptions during `source-command-*` migration instead of skipping commands solely because their descriptions exceed 1024 characters. - Preserve full normalized orchestrator descriptions in the underlying skills catalog. - Cap each description at 1024 Unicode characters when rendering the default available-skills context in `codex-core-skills` and `codex-skills-extension`. - Apply the same cap when serializing descriptions in the model-visible `skills.list` response. - Render truncated descriptions as 1021 original characters plus `...`. - Leave explicit `$skill` injection, `skills.read`, underlying metadata, and on-disk `SKILL.md` files unchanged and full-fidelity. ## Implicit skill selection Codex injects a bounded catalog containing each implicitly allowed skill's name, description, and source locator, together with instructions to use a skill when the task clearly matches its description. The model makes that semantic choice; after selecting a skill, it reads the full `SKILL.md` from its filesystem or provider resource. Explicit `$skill` mentions remain a separate path that injects the full skill instructions. For orchestrator skills, `skills.list` provides bounded discovery metadata before `skills.read` returns the full selected resource. ## Test plan - `just test -p codex-core-skills` - `just test -p codex-skills-extension` - `just test -p codex-external-agent-migration` The focused regressions verify that overlong metadata is preserved at load and migration boundaries while default available-skills rendering and `skills.list` output produce the 1021-character prefix plus `...`.
This commit is contained in:
1 parent
abd901770e
commit
64bdeed9f7
8 files changed
+263
-34
No files matched your search
@@ -1,3 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Component;
|
||||
@@ -16,6 +17,8 @@ use codex_utils_output_truncation::approx_token_count;
|
||||
|
||||
const DEFAULT_SKILL_METADATA_CHAR_BUDGET: usize = 8_000;
|
||||
const SKILL_METADATA_CONTEXT_WINDOW_PERCENT: usize = 2;
|
||||
const MAX_DEFAULT_CONTEXT_SKILL_DESCRIPTION_CHARS: usize = 1_024;
|
||||
const TRUNCATED_SKILL_DESCRIPTION_SUFFIX: &str = "...";
|
||||
const SKILL_DESCRIPTION_TRUNCATION_WARNING_THRESHOLD_CHARS: usize = 100;
|
||||
const APPROX_BYTES_PER_TOKEN: usize = 4;
|
||||
pub const SKILL_DESCRIPTION_TRUNCATED_WARNING: &str = "Skill descriptions were shortened to fit the skills context budget. Codex can still see every skill, but some descriptions are shorter. Disable unused skills or plugins to leave more room for the rest.";
|
||||
@@ -446,7 +449,7 @@ impl SkillRenderReport {
|
||||
|
||||
struct SkillLine<'a> {
|
||||
name: &'a str,
|
||||
description: &'a str,
|
||||
description: Cow<'a, str>,
|
||||
path: String,
|
||||
}
|
||||
|
||||
@@ -485,9 +488,10 @@ impl<'a> SkillLine<'a> {
|
||||
}
|
||||
|
||||
fn with_path(skill: &'a SkillMetadata, path: String) -> Self {
|
||||
let description = truncate_default_context_skill_description(skill.description.as_str());
|
||||
Self {
|
||||
name: skill.name.as_str(),
|
||||
description: skill.description.as_str(),
|
||||
description,
|
||||
path,
|
||||
}
|
||||
}
|
||||
@@ -505,7 +509,7 @@ impl<'a> SkillLine<'a> {
|
||||
}
|
||||
|
||||
fn render_full(&self) -> String {
|
||||
self.render_with_description(self.description)
|
||||
self.render_with_description(self.description.as_ref())
|
||||
}
|
||||
|
||||
fn render_minimum(&self) -> String {
|
||||
@@ -524,7 +528,7 @@ impl<'a> SkillLine<'a> {
|
||||
format!("- {}: (file: {})", self.name, self.path)
|
||||
} else {
|
||||
let end = self.rendered_description_prefix_len(description_chars);
|
||||
let description = &self.description[..end];
|
||||
let description = &self.description.as_ref()[..end];
|
||||
format!("- {}: {} (file: {})", self.name, description, self.path)
|
||||
}
|
||||
}
|
||||
@@ -538,6 +542,26 @@ impl<'a> SkillLine<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_default_context_skill_description(description: &str) -> Cow<'_, str> {
|
||||
if description
|
||||
.char_indices()
|
||||
.nth(MAX_DEFAULT_CONTEXT_SKILL_DESCRIPTION_CHARS)
|
||||
.is_none()
|
||||
{
|
||||
return Cow::Borrowed(description);
|
||||
}
|
||||
|
||||
let prefix_chars = MAX_DEFAULT_CONTEXT_SKILL_DESCRIPTION_CHARS
|
||||
.saturating_sub(TRUNCATED_SKILL_DESCRIPTION_SUFFIX.chars().count());
|
||||
let prefix_end = description
|
||||
.char_indices()
|
||||
.nth(prefix_chars)
|
||||
.map_or(description.len(), |(index, _)| index);
|
||||
let mut truncated = description[..prefix_end].to_string();
|
||||
truncated.push_str(TRUNCATED_SKILL_DESCRIPTION_SUFFIX);
|
||||
Cow::Owned(truncated)
|
||||
}
|
||||
|
||||
impl<'a> DescriptionBudgetLine<'a> {
|
||||
fn new(line: &'a SkillLine<'a>, budget: SkillMetadataBudget) -> Self {
|
||||
let minimum_line = line.render_minimum();
|
||||
@@ -1030,6 +1054,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_context_caps_descriptions_without_mutating_metadata() {
|
||||
let description = "\u{1F4A1}".repeat(MAX_DEFAULT_CONTEXT_SKILL_DESCRIPTION_CHARS + 1);
|
||||
let skill = make_skill_with_description("long-skill", SkillScope::Repo, &description);
|
||||
let expected_description = "\u{1F4A1}".repeat(
|
||||
MAX_DEFAULT_CONTEXT_SKILL_DESCRIPTION_CHARS
|
||||
- TRUNCATED_SKILL_DESCRIPTION_SUFFIX.chars().count(),
|
||||
) + TRUNCATED_SKILL_DESCRIPTION_SUFFIX;
|
||||
|
||||
let rendered = build_available_skills_from_metadata(
|
||||
std::slice::from_ref(&skill),
|
||||
SkillMetadataBudget::Characters(usize::MAX),
|
||||
)
|
||||
.expect("skill should render");
|
||||
|
||||
assert_eq!(skill.description, description);
|
||||
assert_eq!(
|
||||
rendered.skill_lines,
|
||||
vec![expected_skill_line(&skill, &expected_description)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budgeted_rendering_truncates_descriptions_equally_before_omitting_skills() {
|
||||
let alpha = make_skill_with_description("alpha-skill", SkillScope::Repo, "abcdef");
|
||||
|
||||
Reference in new issue
Block a user