mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## 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 `...`.
106 lines
3.5 KiB
Rust
106 lines
3.5 KiB
Rust
use std::borrow::Cow;
|
|
|
|
use codex_utils_string::take_bytes_at_char_boundary;
|
|
|
|
use crate::catalog::SkillCatalog;
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::catalog::SkillSourceKind;
|
|
use crate::fragments::AvailableSkillsInstructions;
|
|
|
|
const MAX_AVAILABLE_SKILLS_BYTES: usize = 8_000;
|
|
const MAX_MAIN_PROMPT_BYTES: usize = 8_000;
|
|
const MAX_CATALOG_SKILL_DESCRIPTION_CHARS: usize = 1_024;
|
|
const TRUNCATED_SKILL_DESCRIPTION_SUFFIX: &str = "...";
|
|
pub(crate) const MAX_SKILL_NAME_BYTES: usize = 256;
|
|
pub(crate) const MAX_SKILL_PATH_BYTES: usize = 1_024;
|
|
|
|
#[tracing::instrument(
|
|
level = "trace",
|
|
skip_all,
|
|
fields(catalog_entry_count = catalog.entries.len())
|
|
)]
|
|
pub(crate) fn available_skills_fragment(
|
|
catalog: &SkillCatalog,
|
|
) -> Option<AvailableSkillsInstructions> {
|
|
let mut total_bytes = 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 description = truncate_catalog_skill_description(description);
|
|
let line = render_skill_line(entry, description.as_ref());
|
|
let next_bytes = total_bytes.saturating_add(line.len());
|
|
if next_bytes > MAX_AVAILABLE_SKILLS_BYTES {
|
|
omitted = omitted.saturating_add(1);
|
|
continue;
|
|
}
|
|
total_bytes = next_bytes;
|
|
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(AvailableSkillsInstructions::from_skill_lines(skill_lines))
|
|
}
|
|
|
|
pub(crate) fn truncate_catalog_skill_description(description: &str) -> Cow<'_, str> {
|
|
if description
|
|
.char_indices()
|
|
.nth(MAX_CATALOG_SKILL_DESCRIPTION_CHARS)
|
|
.is_none()
|
|
{
|
|
return Cow::Borrowed(description);
|
|
}
|
|
|
|
let prefix_chars = MAX_CATALOG_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)
|
|
}
|
|
|
|
fn render_skill_line(entry: &SkillCatalogEntry, description: &str) -> String {
|
|
let locator_kind = match &entry.authority.kind {
|
|
SkillSourceKind::Host => "file",
|
|
SkillSourceKind::Executor => "environment resource",
|
|
SkillSourceKind::Orchestrator => "orchestrator resource",
|
|
SkillSourceKind::Custom(_) => "custom resource",
|
|
};
|
|
let name = entry.name.as_str();
|
|
let path = entry.rendered_path();
|
|
if description.is_empty() {
|
|
format!("- {name}: ({locator_kind}: {path})")
|
|
} else {
|
|
format!("- {name}: {description} ({locator_kind}: {path})")
|
|
}
|
|
}
|
|
|
|
pub(crate) fn truncate_main_prompt_contents(contents: &str) -> (String, bool) {
|
|
truncate_utf8_to_bytes(contents, MAX_MAIN_PROMPT_BYTES)
|
|
}
|
|
|
|
pub(crate) fn truncate_utf8_to_bytes(contents: &str, max_bytes: usize) -> (String, bool) {
|
|
let truncated = take_bytes_at_char_boundary(contents, max_bytes);
|
|
(truncated.to_string(), truncated.len() < contents.len())
|
|
}
|