mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why `ext/skills` currently depends on `codex-core` for two host concerns: reading the concrete `Config` type and borrowing core-owned model-context fragment types. That coupling prevents the extension from being assembled independently above core and leaves context that belongs to the skills feature owned by core. This stacked PR introduces the host boundary needed for the broader extension migration while intentionally preserving existing skills behavior. It is stacked on #27404. ## What changed - Adds a small public `SkillsExtensionConfig` view and makes skills installation generic over the host config type. - Requires the host to map its config into that view; app-server supplies the current `Config` values. - Moves the available-skills and selected-skill context fragment implementations into `ext/skills`, preserving their roles, markers, and rendered bytes. - Removes the direct `codex-core` dependency from `codex-skills-extension`. - Keeps local discovery, invocation, side effects, and the `codex-core-skills` compatibility types unchanged for later staged PRs. ## Behavior This adds no capability and is intended to have no user-visible or model-visible behavior change. The install API and ownership boundary change internally; emitted skills context remains byte-for-byte compatible. ## Validation - Updates the skills extension integration coverage to use a host-owned test config. - Asserts the complete rendered catalog and selected-skill fragments, including their roles and markers. - `just bazel-lock-check` - Rust tests and Clippy were not run locally per request; CI will run them.
76 lines
2.5 KiB
Rust
76 lines
2.5 KiB
Rust
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;
|
|
pub(crate) const MAX_SKILL_NAME_BYTES: usize = 256;
|
|
pub(crate) const MAX_SKILL_PATH_BYTES: usize = 1_024;
|
|
|
|
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 line = render_skill_line(entry, description);
|
|
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))
|
|
}
|
|
|
|
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())
|
|
}
|