mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
## What changed - Move host skill discovery, snapshot caching, and configuration handling from `codex-core-skills` to `codex-skills-extension`. - Rename `SkillsService` and `SkillsLoadInput` to `HostSkillsService` and `HostSkillsLoadInput` to distinguish the host implementation from other skill providers. - Keep shared loading primitives and skill outcome modeling in `codex-core-skills`. GitOrigin-RevId: d81a21791d2ef8c066e157e7e538b8cb7ee4c24b
149 lines
4.9 KiB
Rust
149 lines
4.9 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use codex_core_skills::SkillLoadOutcome;
|
|
use codex_skills::SkillMetadata;
|
|
|
|
use crate::catalog::SkillAuthority;
|
|
use crate::catalog::SkillCatalog;
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::catalog::SkillPackageId;
|
|
use crate::catalog::SkillProviderError;
|
|
use crate::catalog::SkillReadResult;
|
|
use crate::catalog::SkillResourceId;
|
|
use crate::catalog::SkillSearchResult;
|
|
use crate::catalog::SkillSourceKind;
|
|
use crate::provider::SkillListQuery;
|
|
use crate::provider::SkillProvider;
|
|
use crate::provider::SkillProviderFuture;
|
|
use crate::provider::SkillReadRequest;
|
|
use crate::provider::SkillSearchRequest;
|
|
|
|
const HOST_AUTHORITY_ID: &str = "host";
|
|
|
|
/// Host-owned skill provider backed by an immutable service snapshot.
|
|
///
|
|
/// Discovery and caching belong to `HostSkillsService`; this provider only maps a
|
|
/// snapshot into the authority-aware catalog/read contract.
|
|
#[derive(Clone, Default)]
|
|
pub struct HostSkillProvider;
|
|
|
|
impl HostSkillProvider {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl SkillProvider for HostSkillProvider {
|
|
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog> {
|
|
Box::pin(async move {
|
|
let Some(host_snapshot) = query.host_snapshot else {
|
|
return Err(SkillProviderError::new(
|
|
"host skill provider requires a host skills snapshot",
|
|
));
|
|
};
|
|
|
|
Ok(catalog_from_outcome(host_snapshot.outcome()))
|
|
})
|
|
}
|
|
|
|
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
|
|
Box::pin(async move {
|
|
let Some(host_snapshot) = request.host_snapshot else {
|
|
return Err(SkillProviderError::new(
|
|
"host skill provider requires a host skills snapshot",
|
|
));
|
|
};
|
|
let Some(skill) = host_snapshot.outcome().skills.iter().find(|skill| {
|
|
let skill_path = skill.path_to_skills_md.to_string_lossy();
|
|
skill_path == request.resource.as_str()
|
|
|| skill_path.replace('\\', "/") == request.resource.as_str()
|
|
}) else {
|
|
return Err(SkillProviderError::new(format!(
|
|
"host skill resource is not loaded: {}",
|
|
request.resource.as_str()
|
|
)));
|
|
};
|
|
|
|
let contents = host_snapshot.read_skill_text(skill).await.map_err(|err| {
|
|
SkillProviderError::new(format!(
|
|
"failed to read host skill resource {}: {err}",
|
|
request.resource.as_str()
|
|
))
|
|
})?;
|
|
|
|
Ok(SkillReadResult {
|
|
resource: request.resource,
|
|
contents,
|
|
})
|
|
})
|
|
}
|
|
|
|
fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult> {
|
|
Box::pin(async { Ok(SkillSearchResult::default()) })
|
|
}
|
|
}
|
|
|
|
fn catalog_from_outcome(outcome: &SkillLoadOutcome) -> SkillCatalog {
|
|
let root_order_by_path = outcome
|
|
.skill_roots_in_discovery_order()
|
|
.enumerate()
|
|
.map(|(index, root)| (root.as_path(), index))
|
|
.collect::<HashMap<_, _>>();
|
|
let mut catalog = SkillCatalog {
|
|
entries: Vec::new(),
|
|
warnings: outcome
|
|
.errors
|
|
.iter()
|
|
.map(|err| {
|
|
format!(
|
|
"Failed to load skill at {}: {}",
|
|
err.path.display(),
|
|
err.message
|
|
)
|
|
})
|
|
.collect(),
|
|
};
|
|
|
|
for (skill, enabled) in outcome.skills_with_enabled() {
|
|
let mut entry = catalog_entry_from_skill(skill, enabled);
|
|
if let Some(root) = outcome.skill_root_for_path(&skill.path_to_skills_md) {
|
|
entry = entry.with_display_path_root(root.to_string_lossy().replace('\\', "/"));
|
|
if let Some(root_order) = root_order_by_path.get(root.as_path()) {
|
|
entry = entry.with_display_path_root_order(*root_order);
|
|
}
|
|
}
|
|
catalog.push_entry(entry);
|
|
}
|
|
|
|
catalog
|
|
}
|
|
|
|
fn catalog_entry_from_skill(skill: &SkillMetadata, enabled: bool) -> SkillCatalogEntry {
|
|
let skill_path = skill.path_to_skills_md.to_string_lossy().into_owned();
|
|
let display_path = skill_path.replace('\\', "/");
|
|
let mut entry = SkillCatalogEntry::new(
|
|
SkillPackageId(skill_path.clone()),
|
|
SkillAuthority::new(SkillSourceKind::Host, HOST_AUTHORITY_ID),
|
|
skill.name.clone(),
|
|
skill.description.clone(),
|
|
SkillResourceId::new(skill_path),
|
|
)
|
|
.with_short_description(skill.short_description.clone())
|
|
.with_display_path(display_path)
|
|
.with_prompt_scope(skill.scope)
|
|
.with_dependencies(skill.dependencies.clone());
|
|
|
|
if !enabled {
|
|
entry = entry.disabled();
|
|
}
|
|
if !skill.allows_implicit_invocation() {
|
|
entry = entry.hidden_from_prompt();
|
|
}
|
|
|
|
entry
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "host_tests.rs"]
|
|
mod tests;
|