Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion codex-rs/ext/skills/src/shadow_selection_experiment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use codex_otel::MetricsClient;
use codex_protocol::user_input::UserInput;

use crate::catalog::SkillCatalog;
use crate::catalog::SkillSourceKind;
use crate::dynamic_skill_selector::CheapSkillSelection;
use crate::dynamic_skill_selector::CheapSkillSelector;
use crate::dynamic_skill_selector::SkillSelectionDocument;
Expand Down Expand Up @@ -50,7 +51,16 @@ impl ShadowSelectionExperiment {
.entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.enabled && entry.prompt_visible)
.filter(|(_, entry)| {
entry.enabled
&& entry.prompt_visible
// Invocation observation currently exists only for host shell use and
// orchestrator reads. Keep the candidate set aligned with that universe.
&& matches!(
&entry.authority.kind,
SkillSourceKind::Host | SkillSourceKind::Orchestrator
)
})
.map(|(id, entry)| SkillSelectionDocument {
id,
name: entry.name.as_str(),
Expand Down
60 changes: 58 additions & 2 deletions codex-rs/ext/skills/tests/implicit_invocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolCall;
use codex_extension_api::ToolPayload;
use codex_extension_api::TurnInputContext;
use codex_extension_api::WorldStateContributionInput;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_otel::MetricsClient;
use codex_otel::MetricsConfig;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
use codex_protocol::protocol::TruncationPolicy;
Expand All @@ -39,6 +42,7 @@ use codex_skills_extension::provider::SkillProviderFuture;
use codex_skills_extension::provider::SkillReadRequest;
use codex_skills_extension::provider::SkillSearchRequest;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use opentelemetry_sdk::metrics::InMemoryMetricExporter;
use opentelemetry_sdk::metrics::data::AggregatedMetrics;
use opentelemetry_sdk::metrics::data::MetricData;
Expand Down Expand Up @@ -80,6 +84,40 @@ impl SkillProvider for OrchestratorProvider {
}
}

#[derive(Clone)]
struct ExecutorProvider;

impl SkillProvider for ExecutorProvider {
fn list(&self, _query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog> {
Box::pin(std::future::ready(Ok(SkillCatalog {
entries: (0..5)
.map(|index| {
let resource = format!("skill://executor/demo-{index}/SKILL.md");
SkillCatalogEntry::new(
SkillPackageId(format!("executor/demo-{index}")),
SkillAuthority::new(SkillSourceKind::Executor, "executor"),
"演示文稿",
"创建演示文稿。",
SkillResourceId::new(resource),
)
})
.collect(),
warnings: Vec::new(),
})))
}

fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
Box::pin(std::future::ready(Ok(SkillReadResult {
resource: request.resource,
contents: "# 演示文稿".to_string(),
})))
}

fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult> {
Box::pin(std::future::ready(Ok(SkillSearchResult::default())))
}
}

#[tokio::test]
async fn implicit_core_and_native_read_invocations_share_turn_local_recording() -> TestResult {
let metrics = MetricsClient::new(
Expand Down Expand Up @@ -174,7 +212,7 @@ async fn implicit_core_and_native_read_invocations_share_turn_local_recording()
}

#[tokio::test]
async fn host_snapshot_is_available_only_to_shadow_selection() -> TestResult {
async fn shadow_selection_uses_host_snapshot_and_excludes_executor_candidates() -> TestResult {
let metrics = MetricsClient::new(
MetricsConfig::in_memory(
"test",
Expand All @@ -187,7 +225,7 @@ async fn host_snapshot_is_available_only_to_shadow_selection() -> TestResult {
let mut builder = ExtensionRegistryBuilder::<()>::new();
install_with_providers_and_metrics(
&mut builder,
SkillProviders::new(),
SkillProviders::new().with_executor_provider(Arc::new(ExecutorProvider)),
Some(metrics.clone()),
|_| SkillsExtensionConfig {
include_instructions: true,
Expand Down Expand Up @@ -230,6 +268,24 @@ async fn host_snapshot_is_available_only_to_shadow_selection() -> TestResult {
});
let turn_store = ExtensionData::new("turn-host");
turn_store.insert(HostSkillsSnapshot::new(Arc::new(outcome)));
let selected_roots = [SelectedCapabilityRoot {
id: "executor-root".to_string(),
location: CapabilityRootLocation::Environment {
environment_id: "executor".to_string(),
path: PathUri::parse("file:///skills").expect("executor skill root URI"),
},
}];
registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-host",
environments: &[],
ready_selected_capability_roots: &selected_roots,
session_store: &session_store,
thread_store: &thread_store,
turn_store: &turn_store,
})
.await;

let fragments = registry.turn_input_contributors()[0]
.contribute(
Expand Down
Loading