codex: adapt skill search to extension API changes

This commit is contained in:
Dylan Hurd
2026-06-11 22:40:16 -07:00
parent e9c2f7c6e5
commit 167210ff1d
10 changed files with 71 additions and 44 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -3815,9 +3815,9 @@ dependencies = [
name = "codex-skill-search-extension"
version = "0.0.0"
dependencies = [
"async-trait",
"bm25",
"codex-core",
"codex-core-skills",
"codex-extension-api",
"codex-features",
"codex-protocol",

View File

@@ -200,24 +200,29 @@ async fn skill_search_tool_is_visible_and_returns_matching_repo_skill() -> Resul
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex
.submit(Op::UserTurn {
environments: None,
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "Find the right repo skill.".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy),
permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: session_model,
reasoning_effort: None,
developer_instructions: None,
},
}),
..Default::default()
},
})
.await?;

View File

@@ -70,6 +70,7 @@ impl ToolContributor for AllContributors {
&self,
_session_store: &ExtensionData,
_thread_store: &ExtensionData,
_turn_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
Vec::new()
}

View File

@@ -411,6 +411,7 @@ where
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
_turn_store: &ExtensionData,
) -> Vec<Arc<dyn codex_extension_api::ToolExecutor<codex_extension_api::ToolCall>>> {
let Some(runtime) = goal_runtime_handle(thread_store) else {
return Vec::new();

View File

@@ -75,6 +75,7 @@ impl ToolContributor for ImageGenerationExtension {
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
_turn_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
let Some(config) = thread_store.get::<ImageGenerationExtensionConfig>() else {
return Vec::new();

View File

@@ -13,9 +13,9 @@ doctest = false
workspace = true
[dependencies]
async-trait = { workspace = true }
bm25 = { workspace = true }
codex-core = { workspace = true }
codex-core-skills = { workspace = true }
codex-extension-api = { workspace = true }
codex-features = { workspace = true }
codex-protocol = { workspace = true }

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use codex_core::config::Config;
use codex_core::skills::SkillLoadOutcome;
use codex_core_skills::HostLoadedSkills;
use codex_extension_api::ConfigContributor;
use codex_extension_api::ContextContributor;
use codex_extension_api::ExtensionData;
@@ -54,10 +54,15 @@ impl ContextContributor for SkillSearchExtension {
}
impl ThreadLifecycleContributor<Config> for SkillSearchExtension {
fn on_thread_start(&self, input: ThreadStartInput<'_, Config>) {
input
.thread_store
.insert(SkillSearchExtensionConfig::from_config(input.config));
fn on_thread_start<'a>(
&'a self,
input: ThreadStartInput<'a, Config>,
) -> codex_extension_api::ExtensionFuture<'a, ()> {
Box::pin(async move {
input
.thread_store
.insert(SkillSearchExtensionConfig::from_config(input.config));
})
}
}
@@ -88,9 +93,9 @@ impl ToolContributor for SkillSearchExtension {
}
let skills = turn_store
.get::<SkillLoadOutcome>()
.map_or_else(Vec::new, |outcome| {
outcome.allowed_skills_for_implicit_invocation()
.get::<HostLoadedSkills>()
.map_or_else(Vec::new, |skills| {
skills.outcome().allowed_skills_for_implicit_invocation()
});
let tool = turn_store.get_or_init(|| SkillSearchTool::new(skills));
vec![tool]

View File

@@ -99,14 +99,13 @@ struct SkillSearchArgs {
limit: Option<usize>,
}
#[async_trait::async_trait]
impl ToolExecutor<ToolCall> for SkillSearchTool {
fn tool_name(&self) -> ToolName {
ToolName::plain(SKILL_SEARCH_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(ToolSpec::Function(ResponsesApiTool {
fn spec(&self) -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: SKILL_SEARCH_TOOL_NAME.to_string(),
description: "Search available Codex skills by relevance and return plain-text matches with their descriptions and SKILL.md paths.".to_string(),
strict: false,
@@ -128,31 +127,33 @@ impl ToolExecutor<ToolCall> for SkillSearchTool {
Some(false.into()),
),
output_schema: None,
}))
})
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
async fn handle(&self, call: ToolCall) -> Result<Box<dyn ToolOutput>, FunctionCallError> {
let args = parse_args(&call)?;
let query = args.query.trim();
if query.is_empty() {
return Err(FunctionCallError::RespondToModel(
"query must not be empty".to_string(),
));
}
let limit = args.limit.unwrap_or(DEFAULT_SKILL_SEARCH_LIMIT);
if limit == 0 {
return Err(FunctionCallError::RespondToModel(
"limit must be greater than zero".to_string(),
));
}
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_> {
Box::pin(async move {
let args = parse_args(&call)?;
let query = args.query.trim();
if query.is_empty() {
return Err(FunctionCallError::RespondToModel(
"query must not be empty".to_string(),
));
}
let limit = args.limit.unwrap_or(DEFAULT_SKILL_SEARCH_LIMIT);
if limit == 0 {
return Err(FunctionCallError::RespondToModel(
"limit must be greater than zero".to_string(),
));
}
Ok(Box::new(PlainTextToolOutput {
text: self.search(query, limit),
}))
Ok(Box::new(PlainTextToolOutput {
text: self.search(query, limit),
}) as Box<dyn ToolOutput>)
})
}
}
@@ -194,9 +195,14 @@ impl ToolOutput for PlainTextToolOutput {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use codex_core::skills::SkillPolicy;
use codex_extension_api::ConversationHistory;
use codex_extension_api::NoopTurnItemEmitter;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::protocol::SkillScope;
use codex_protocol::protocol::TruncationPolicy;
use codex_tools::ToolPayload;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
@@ -224,8 +230,14 @@ mod tests {
fn call(arguments: serde_json::Value) -> ToolCall {
ToolCall {
turn_id: "turn-skill-search".to_string(),
call_id: "call-skill-search".to_string(),
tool_name: ToolName::plain(SKILL_SEARCH_TOOL_NAME),
model: "gpt-test".to_string(),
truncation_policy: TruncationPolicy::Bytes(1024),
conversation_history: ConversationHistory::default(),
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
environments: Vec::new(),
payload: ToolPayload::Function {
arguments: arguments.to_string(),
},

View File

@@ -138,6 +138,7 @@ where
&self,
session_store: &ExtensionData,
_thread_store: &ExtensionData,
_turn_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
if !self.providers.has_orchestrator_provider() {
return Vec::new();

View File

@@ -111,6 +111,7 @@ impl ToolContributor for WebSearchExtension {
&self,
session_store: &ExtensionData,
thread_store: &ExtensionData,
_turn_store: &ExtensionData,
) -> Vec<Arc<dyn codex_extension_api::ToolExecutor<codex_extension_api::ToolCall>>> {
let Some(config) = thread_store.get::<WebSearchExtensionConfig>() else {
return Vec::new();