mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00: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 `...`.
114 lines
3.3 KiB
Rust
114 lines
3.3 KiB
Rust
use codex_extension_api::ToolCall;
|
|
use codex_extension_api::ToolExecutor;
|
|
use codex_extension_api::ToolExecutorFuture;
|
|
use codex_extension_api::ToolName;
|
|
use codex_extension_api::ToolSpec;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::render::truncate_catalog_skill_description;
|
|
use crate::render::truncate_utf8_to_bytes;
|
|
|
|
use super::MAX_HANDLE_BYTES;
|
|
use super::SkillToolAuthority;
|
|
use super::SkillToolContext;
|
|
use super::external_json_output;
|
|
use super::is_bounded_handle;
|
|
use super::parse_args;
|
|
use super::skill_function_tool;
|
|
use super::skill_tool_name;
|
|
|
|
const TOOL_NAME: &str = "list";
|
|
const MAX_WARNINGS: usize = 4;
|
|
const MAX_WARNING_BYTES: usize = 256;
|
|
|
|
#[derive(Deserialize, JsonSchema)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct ListArgs {
|
|
authority: SkillToolAuthority,
|
|
}
|
|
|
|
#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]
|
|
#[schemars(deny_unknown_fields)]
|
|
struct ListedSkill {
|
|
authority: SkillToolAuthority,
|
|
package: String,
|
|
name: String,
|
|
description: String,
|
|
main_resource: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]
|
|
#[schemars(deny_unknown_fields)]
|
|
struct ListResponse {
|
|
skills: Vec<ListedSkill>,
|
|
warnings: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct ListTool {
|
|
pub(super) context: SkillToolContext,
|
|
}
|
|
|
|
impl ToolExecutor<ToolCall> for ListTool {
|
|
fn tool_name(&self) -> ToolName {
|
|
skill_tool_name(TOOL_NAME)
|
|
}
|
|
|
|
fn spec(&self) -> ToolSpec {
|
|
skill_function_tool::<ListArgs, ListResponse>(
|
|
TOOL_NAME,
|
|
"List enabled skills owned by the requested authority. Only orchestrator-owned skills are currently supported. Returns the opaque package and main-resource handles required by skills.read.",
|
|
)
|
|
}
|
|
|
|
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_> {
|
|
Box::pin(async move {
|
|
let args: ListArgs = parse_args(&call)?;
|
|
let authority = args.authority.into_authority();
|
|
let catalog = self.context.catalog(&call.turn_id, args.authority).await;
|
|
let response = ListResponse {
|
|
skills: catalog
|
|
.entries
|
|
.into_iter()
|
|
.filter(|entry| entry.enabled && entry.authority == authority)
|
|
.filter_map(listed_skill)
|
|
.collect(),
|
|
warnings: bounded_warnings(catalog.warnings),
|
|
};
|
|
|
|
external_json_output(&response)
|
|
})
|
|
}
|
|
}
|
|
|
|
fn listed_skill(entry: SkillCatalogEntry) -> Option<ListedSkill> {
|
|
let authority = SkillToolAuthority::from_authority(&entry.authority)?;
|
|
if !is_bounded_handle(&entry.id.0, MAX_HANDLE_BYTES)
|
|
|| !is_bounded_handle(entry.main_prompt.as_str(), MAX_HANDLE_BYTES)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
Some(ListedSkill {
|
|
authority,
|
|
package: entry.id.0,
|
|
name: entry.name,
|
|
description: truncate_catalog_skill_description(&entry.description).into_owned(),
|
|
main_resource: entry.main_prompt.as_str().to_string(),
|
|
})
|
|
}
|
|
|
|
fn bounded_warnings(warnings: Vec<String>) -> Vec<String> {
|
|
warnings
|
|
.into_iter()
|
|
.take(MAX_WARNINGS)
|
|
.map(|warning| {
|
|
let (warning, _) = truncate_utf8_to_bytes(&warning, MAX_WARNING_BYTES);
|
|
warning
|
|
})
|
|
.collect()
|
|
}
|