mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Move explicit skill invocation into SkillsExtension
This commit is contained in:
@@ -75,23 +75,15 @@ pub(crate) mod prompt_debug;
|
||||
#[doc(hidden)]
|
||||
pub use prompt_debug::build_prompt_input;
|
||||
pub(crate) mod mentions {
|
||||
pub(crate) use crate::plugins::build_connector_slug_counts;
|
||||
pub(crate) use crate::plugins::build_skill_name_counts;
|
||||
pub(crate) use crate::plugins::collect_explicit_app_ids;
|
||||
pub(crate) use crate::plugins::collect_explicit_plugin_mentions;
|
||||
pub(crate) use crate::plugins::collect_tool_mentions_from_messages;
|
||||
}
|
||||
mod sandbox_tags;
|
||||
pub mod sandboxing;
|
||||
mod session_prefix;
|
||||
mod session_startup_prewarm;
|
||||
pub mod skills;
|
||||
pub(crate) use skills::SkillInjections;
|
||||
pub(crate) use skills::SkillMetadata;
|
||||
pub(crate) use skills::SkillsService;
|
||||
pub(crate) use skills::build_skill_injections;
|
||||
pub(crate) use skills::build_skill_name_counts;
|
||||
pub(crate) use skills::collect_explicit_skill_mentions;
|
||||
pub(crate) use skills::injection;
|
||||
pub(crate) use skills::maybe_emit_implicit_skill_invocation;
|
||||
pub(crate) use skills::skills_load_input_from_config;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_config::ConfigEditsBuilder;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::load_global_mcp_servers;
|
||||
use codex_login::default_client::is_first_party_originator;
|
||||
use codex_login::default_client::originator;
|
||||
@@ -15,13 +13,13 @@ use codex_rmcp_client::perform_oauth_login;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::SkillMetadata;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::skills::model::SkillToolDependency;
|
||||
use codex_mcp::ElicitationReviewerHandle;
|
||||
use codex_mcp::McpOAuthLoginSupport;
|
||||
use codex_mcp::McpPermissionPromptAutoApproveContext;
|
||||
use codex_mcp::McpServerDependencies;
|
||||
use codex_mcp::canonical_server_key;
|
||||
use codex_mcp::mcp_permission_prompt_is_auto_approved;
|
||||
use codex_mcp::oauth_login_support;
|
||||
use codex_mcp::resolve_oauth_scopes;
|
||||
@@ -35,7 +33,7 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
cancellation_token: &CancellationToken,
|
||||
mentioned_skills: &[SkillMetadata],
|
||||
dependencies: &McpServerDependencies,
|
||||
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
||||
) {
|
||||
let originator_value = originator().value;
|
||||
@@ -45,7 +43,7 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies(
|
||||
}
|
||||
|
||||
let config = turn_context.config.clone();
|
||||
if mentioned_skills.is_empty()
|
||||
if dependencies.is_empty()
|
||||
|| !config
|
||||
.features
|
||||
.enabled(codex_features::Feature::SkillMcpDependencyInstall)
|
||||
@@ -54,7 +52,7 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies(
|
||||
}
|
||||
|
||||
let installed = sess.runtime_mcp_servers(config.as_ref()).await;
|
||||
let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed);
|
||||
let missing = dependencies.missing_from(&installed);
|
||||
if missing.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -71,7 +69,7 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies(
|
||||
sess,
|
||||
turn_context,
|
||||
config.as_ref(),
|
||||
mentioned_skills,
|
||||
dependencies,
|
||||
elicitation_reviewer,
|
||||
)
|
||||
.await;
|
||||
@@ -82,10 +80,10 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
config: &crate::config::Config,
|
||||
mentioned_skills: &[SkillMetadata],
|
||||
dependencies: &McpServerDependencies,
|
||||
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
||||
) {
|
||||
if mentioned_skills.is_empty()
|
||||
if dependencies.is_empty()
|
||||
|| !config
|
||||
.features
|
||||
.enabled(codex_features::Feature::SkillMcpDependencyInstall)
|
||||
@@ -95,7 +93,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
|
||||
let codex_home = config.codex_home.clone();
|
||||
let installed = sess.runtime_mcp_servers(config).await;
|
||||
let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed);
|
||||
let missing = dependencies.missing_from(&installed);
|
||||
if missing.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +278,7 @@ async fn should_install_mcp_dependencies(
|
||||
|
||||
let prompted_keys = missing
|
||||
.iter()
|
||||
.map(|(name, config)| canonical_mcp_server_key(name, config));
|
||||
.map(|(name, config)| canonical_server_key(name, config));
|
||||
sess.record_mcp_dependency_prompted(prompted_keys).await;
|
||||
|
||||
install
|
||||
@@ -297,7 +295,7 @@ async fn filter_prompted_mcp_dependencies(
|
||||
|
||||
missing
|
||||
.iter()
|
||||
.filter(|(name, config)| !prompted.contains(&canonical_mcp_server_key(name, config)))
|
||||
.filter(|(name, config)| !prompted.contains(&canonical_server_key(name, config)))
|
||||
.map(|(name, config)| (name.clone(), config.clone()))
|
||||
.collect()
|
||||
}
|
||||
@@ -307,165 +305,3 @@ fn format_missing_mcp_dependencies(missing: &HashMap<String, McpServerConfig>) -
|
||||
names.sort();
|
||||
names.join(", ")
|
||||
}
|
||||
|
||||
fn canonical_mcp_key(transport: &str, identifier: &str, fallback: &str) -> String {
|
||||
let identifier = identifier.trim();
|
||||
if identifier.is_empty() {
|
||||
fallback.to_string()
|
||||
} else {
|
||||
format!("mcp__{transport}__{identifier}")
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_mcp_server_key(name: &str, config: &McpServerConfig) -> String {
|
||||
match &config.transport {
|
||||
McpServerTransportConfig::Stdio { command, .. } => {
|
||||
canonical_mcp_key("stdio", command, name)
|
||||
}
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => {
|
||||
canonical_mcp_key("streamable_http", url, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_mcp_dependency_key(dependency: &SkillToolDependency) -> Result<String, String> {
|
||||
let transport = dependency.transport.as_deref().unwrap_or("streamable_http");
|
||||
if transport.eq_ignore_ascii_case("streamable_http") {
|
||||
let url = dependency
|
||||
.url
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing url for streamable_http dependency".to_string())?;
|
||||
return Ok(canonical_mcp_key("streamable_http", url, &dependency.value));
|
||||
}
|
||||
if transport.eq_ignore_ascii_case("stdio") {
|
||||
let command = dependency
|
||||
.command
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing command for stdio dependency".to_string())?;
|
||||
return Ok(canonical_mcp_key("stdio", command, &dependency.value));
|
||||
}
|
||||
Err(format!("unsupported transport {transport}"))
|
||||
}
|
||||
|
||||
fn mcp_dependency_to_server_config(
|
||||
dependency: &SkillToolDependency,
|
||||
) -> Result<McpServerConfig, String> {
|
||||
let transport = dependency.transport.as_deref().unwrap_or("streamable_http");
|
||||
if transport.eq_ignore_ascii_case("streamable_http") {
|
||||
let url = dependency
|
||||
.url
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing url for streamable_http dependency".to_string())?;
|
||||
return Ok(McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.clone(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
if transport.eq_ignore_ascii_case("stdio") {
|
||||
let command = dependency
|
||||
.command
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing command for stdio dependency".to_string())?;
|
||||
return Ok(McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.clone(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
Err(format!("unsupported transport {transport}"))
|
||||
}
|
||||
|
||||
fn collect_missing_mcp_dependencies(
|
||||
mentioned_skills: &[SkillMetadata],
|
||||
installed: &HashMap<String, McpServerConfig>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
let mut missing = HashMap::new();
|
||||
let installed_keys: HashSet<String> = installed
|
||||
.iter()
|
||||
.map(|(name, config)| canonical_mcp_server_key(name, config))
|
||||
.collect();
|
||||
let mut seen_canonical_keys = HashSet::new();
|
||||
|
||||
for skill in mentioned_skills {
|
||||
let Some(dependencies) = skill.dependencies.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for tool in &dependencies.tools {
|
||||
if !tool.r#type.eq_ignore_ascii_case("mcp") {
|
||||
continue;
|
||||
}
|
||||
let dependency_key = match canonical_mcp_dependency_key(tool) {
|
||||
Ok(key) => key,
|
||||
Err(err) => {
|
||||
let dependency = tool.value.as_str();
|
||||
let skill_name = skill.name.as_str();
|
||||
warn!(
|
||||
"unable to auto-install MCP dependency {dependency} for skill {skill_name}: {err}",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if installed_keys.contains(&dependency_key)
|
||||
|| seen_canonical_keys.contains(&dependency_key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let config = match mcp_dependency_to_server_config(tool) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
let dependency = dependency_key.as_str();
|
||||
let skill_name = skill.name.as_str();
|
||||
warn!(
|
||||
"unable to auto-install MCP dependency {dependency} for skill {skill_name}: {err}",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
missing.insert(tool.value.clone(), config);
|
||||
seen_canonical_keys.insert(dependency_key);
|
||||
}
|
||||
}
|
||||
|
||||
missing
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_connectors::metadata::connector_mention_slug;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
use crate::connectors;
|
||||
use crate::injection::ToolMentionKind;
|
||||
use crate::injection::app_id_from_path;
|
||||
use crate::injection::extract_tool_mentions_with_sigil;
|
||||
@@ -15,27 +12,20 @@ use crate::mention_syntax::TOOL_MENTION_SIGIL;
|
||||
|
||||
use super::PluginCapabilitySummary;
|
||||
|
||||
pub(crate) struct CollectedToolMentions {
|
||||
pub(crate) plain_names: HashSet<String>,
|
||||
pub(crate) paths: HashSet<String>,
|
||||
fn collect_tool_paths_from_messages(messages: &[String]) -> HashSet<String> {
|
||||
collect_tool_paths_from_messages_with_sigil(messages, TOOL_MENTION_SIGIL)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_tool_mentions_from_messages(messages: &[String]) -> CollectedToolMentions {
|
||||
collect_tool_mentions_from_messages_with_sigil(messages, TOOL_MENTION_SIGIL)
|
||||
}
|
||||
|
||||
fn collect_tool_mentions_from_messages_with_sigil(
|
||||
fn collect_tool_paths_from_messages_with_sigil(
|
||||
messages: &[String],
|
||||
sigil: char,
|
||||
) -> CollectedToolMentions {
|
||||
let mut plain_names = HashSet::new();
|
||||
) -> HashSet<String> {
|
||||
let mut paths = HashSet::new();
|
||||
for message in messages {
|
||||
let mentions = extract_tool_mentions_with_sigil(message, sigil);
|
||||
plain_names.extend(mentions.plain_names().map(str::to_string));
|
||||
paths.extend(mentions.paths().map(str::to_string));
|
||||
}
|
||||
CollectedToolMentions { plain_names, paths }
|
||||
paths
|
||||
}
|
||||
|
||||
pub(crate) fn collect_explicit_app_ids(input: &[UserInput]) -> HashSet<String> {
|
||||
@@ -53,7 +43,7 @@ pub(crate) fn collect_explicit_app_ids(input: &[UserInput]) -> HashSet<String> {
|
||||
UserInput::Mention { path, .. } => Some(path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.chain(collect_tool_mentions_from_messages(&messages).paths)
|
||||
.chain(collect_tool_paths_from_messages(&messages))
|
||||
.filter(|path| tool_kind_for_path(path.as_str()) == ToolMentionKind::App)
|
||||
.filter_map(|path| app_id_from_path(path.as_str()).map(str::to_string))
|
||||
.collect()
|
||||
@@ -84,8 +74,7 @@ pub(crate) fn collect_explicit_plugin_mentions(
|
||||
})
|
||||
.chain(
|
||||
// Plugin plaintext links use `@`, not the default `$` tool sigil.
|
||||
collect_tool_mentions_from_messages_with_sigil(&messages, PLUGIN_TEXT_MENTION_SIGIL)
|
||||
.paths,
|
||||
collect_tool_paths_from_messages_with_sigil(&messages, PLUGIN_TEXT_MENTION_SIGIL),
|
||||
)
|
||||
.filter(|path| tool_kind_for_path(path.as_str()) == ToolMentionKind::Plugin)
|
||||
.filter_map(|path| plugin_config_name_from_path(path.as_str()).map(str::to_string))
|
||||
@@ -102,19 +91,6 @@ pub(crate) fn collect_explicit_plugin_mentions(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) use crate::build_skill_name_counts;
|
||||
|
||||
pub(crate) fn build_connector_slug_counts(
|
||||
connectors: &[connectors::AppInfo],
|
||||
) -> HashMap<String, usize> {
|
||||
let mut counts: HashMap<String, usize> = HashMap::new();
|
||||
for connector in connectors {
|
||||
let slug = connector_mention_slug(connector);
|
||||
*counts.entry(slug).or_insert(0) += 1;
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "mentions_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -11,8 +11,5 @@ pub(crate) use discoverable::list_tool_suggest_discoverable_plugins;
|
||||
pub(crate) use injection::build_plugin_injections;
|
||||
pub(crate) use render::render_explicit_plugin_instructions;
|
||||
|
||||
pub(crate) use mentions::build_connector_slug_counts;
|
||||
pub(crate) use mentions::build_skill_name_counts;
|
||||
pub(crate) use mentions::collect_explicit_app_ids;
|
||||
pub(crate) use mentions::collect_explicit_plugin_mentions;
|
||||
pub(crate) use mentions::collect_tool_mentions_from_messages;
|
||||
|
||||
@@ -227,8 +227,6 @@ use self::session::SessionConfiguration;
|
||||
pub(crate) use self::session::SessionSettingsUpdate;
|
||||
#[cfg(test)]
|
||||
use self::turn::AssistantMessageStreamParsers;
|
||||
#[cfg(test)]
|
||||
use self::turn::collect_explicit_app_ids_from_skill_items;
|
||||
use self::turn::realtime_text_for_event;
|
||||
use self::turn_context::TurnContext;
|
||||
use self::turn_context::TurnSkillsContext;
|
||||
@@ -288,8 +286,6 @@ pub(crate) struct PreviousTurnSettings {
|
||||
pub(crate) realtime_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::SkillMetadata;
|
||||
use crate::SkillsService;
|
||||
use crate::agents_md::load_project_instructions;
|
||||
use crate::exec_policy::ExecPolicyUpdateError;
|
||||
@@ -301,6 +297,8 @@ use crate::session_startup_prewarm::SessionStartupPrewarmHandle;
|
||||
use crate::shell;
|
||||
#[cfg(test)]
|
||||
use crate::skills::SkillLoadOutcome;
|
||||
#[cfg(test)]
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::state::AutoCompactWindowSnapshot;
|
||||
use crate::state::PendingRequestPermissions;
|
||||
use crate::state::SessionServices;
|
||||
|
||||
@@ -99,6 +99,7 @@ pub(super) async fn spawn_review_thread(
|
||||
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(
|
||||
review_turn_id.clone(),
|
||||
));
|
||||
extension_data.insert(session_telemetry_for_context.clone());
|
||||
extension_data.insert(parent_turn_context.turn_skills.snapshot.clone());
|
||||
|
||||
let review_turn_context = TurnContext {
|
||||
|
||||
@@ -959,6 +959,7 @@ impl Session {
|
||||
));
|
||||
let session_extension_data =
|
||||
codex_extension_api::ExtensionData::new(session_id.to_string());
|
||||
session_extension_data.insert(analytics_events_client.clone());
|
||||
session_extension_data.insert(McpResourceClient::new(Arc::clone(
|
||||
&mcp_connection_manager,
|
||||
)));
|
||||
|
||||
@@ -80,7 +80,6 @@ use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::router::ToolCallSource;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::McpElicitationSchema;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
@@ -257,18 +256,6 @@ fn histogram_sum(resource_metrics: &ResourceMetrics, name: &str) -> u64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn skill_message(text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_turn_emits_turn_started_with_trace_id_without_waiting_for_startup_prewarm() {
|
||||
let _trace_test_context = install_test_tracing("codex-core-tests");
|
||||
@@ -584,24 +571,6 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
|
||||
ToolCallRuntime::new(router, session, turn_context, tracker)
|
||||
}
|
||||
|
||||
fn make_connector(id: &str, name: &str) -> AppInfo {
|
||||
AppInfo {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assistant_message_stream_parsers_can_be_seeded_from_output_item_added_text() {
|
||||
let mut parsers = AssistantMessageStreamParsers::new(/*plan_mode*/ false);
|
||||
@@ -1538,49 +1507,6 @@ disabled_tools = [
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_explicit_app_ids_from_skill_items_includes_linked_mentions() {
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
let skill_items = vec\n</skill>",
|
||||
)];
|
||||
|
||||
let connector_ids =
|
||||
collect_explicit_app_ids_from_skill_items(&skill_items, &connectors, &HashMap::new());
|
||||
|
||||
assert_eq!(connector_ids, HashSet::from(["calendar".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_explicit_app_ids_from_skill_items_resolves_unambiguous_plain_mentions() {
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
let skill_items = vec![skill_message(
|
||||
"<skill>\n<name>demo</name>\n<path>/tmp/skills/demo/SKILL.md</path>\nuse $calendar\n</skill>",
|
||||
)];
|
||||
|
||||
let connector_ids =
|
||||
collect_explicit_app_ids_from_skill_items(&skill_items, &connectors, &HashMap::new());
|
||||
|
||||
assert_eq!(connector_ids, HashSet::from(["calendar".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_conflicts() {
|
||||
let connectors = vec![make_connector("calendar", "Calendar")];
|
||||
let skill_items = vec![skill_message(
|
||||
"<skill>\n<name>demo</name>\n<path>/tmp/skills/demo/SKILL.md</path>\nuse $calendar\n</skill>",
|
||||
)];
|
||||
let skill_name_counts_lower = HashMap::from([("calendar".to_string(), 1)]);
|
||||
|
||||
let connector_ids = collect_explicit_app_ids_from_skill_items(
|
||||
&skill_items,
|
||||
&connectors,
|
||||
&skill_name_counts_lower,
|
||||
);
|
||||
|
||||
assert_eq!(connector_ids, HashSet::<String>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconstruct_history_matches_live_compactions() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
|
||||
@@ -4,12 +4,9 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::SkillInjections;
|
||||
use crate::build_skill_injections;
|
||||
use crate::client::ModelClientSession;
|
||||
use crate::client_common::Prompt;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::collect_explicit_skill_mentions;
|
||||
use crate::compact::InitialContextInjection;
|
||||
use crate::compact::run_inline_auto_compact_task;
|
||||
use crate::compact::should_use_remote_compact_task;
|
||||
@@ -24,16 +21,10 @@ use crate::hook_runtime::record_pending_input;
|
||||
use crate::hook_runtime::run_legacy_after_agent_hook;
|
||||
use crate::hook_runtime::run_pending_session_start_hooks;
|
||||
use crate::hook_runtime::run_turn_stop_hooks;
|
||||
use crate::injection::ToolMentionKind;
|
||||
use crate::injection::app_id_from_path;
|
||||
use crate::injection::tool_kind_for_path;
|
||||
use crate::mcp_skill_dependencies::maybe_prompt_and_install_mcp_dependencies;
|
||||
use crate::mcp_tool_exposure::build_mcp_tool_exposure;
|
||||
use crate::mentions::build_connector_slug_counts;
|
||||
use crate::mentions::build_skill_name_counts;
|
||||
use crate::mentions::collect_explicit_app_ids;
|
||||
use crate::mentions::collect_explicit_plugin_mentions;
|
||||
use crate::mentions::collect_tool_mentions_from_messages;
|
||||
use crate::plugins::build_plugin_injections;
|
||||
use crate::responses_metadata::CodexResponsesMetadata;
|
||||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||||
@@ -74,7 +65,6 @@ use codex_analytics::TurnResolvedConfigFact;
|
||||
use codex_analytics::build_track_events_context;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_core_plugins::RecommendedPluginCandidatesInput;
|
||||
use codex_core_skills::injection::InjectedHostSkillPrompts;
|
||||
use codex_extension_api::TurnInputContext;
|
||||
use codex_extension_api::TurnInputEnvironment;
|
||||
use codex_features::Feature;
|
||||
@@ -528,57 +518,35 @@ async fn build_skills_and_plugins(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let skills_outcome = turn_context.turn_skills.snapshot.outcome();
|
||||
let connector_slug_counts = build_connector_slug_counts(&available_connectors);
|
||||
let extension_injection_items =
|
||||
build_extension_turn_input_items(sess, turn_context, &user_input, cancellation_token)
|
||||
.await?;
|
||||
let skill_name_counts_lower =
|
||||
build_skill_name_counts(&skills_outcome.skills, &skills_outcome.disabled_paths).1;
|
||||
let mentioned_skills = collect_explicit_skill_mentions(
|
||||
let reserved_plain_tool_names = available_connectors
|
||||
.iter()
|
||||
.map(codex_connectors::metadata::connector_mention_slug)
|
||||
.collect();
|
||||
let extension_injection_items = build_extension_turn_input_items(
|
||||
sess,
|
||||
turn_context,
|
||||
&user_input,
|
||||
&skills_outcome.skills,
|
||||
&skills_outcome.disabled_paths,
|
||||
&connector_slug_counts,
|
||||
);
|
||||
reserved_plain_tool_names,
|
||||
cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
let mcp_dependencies = turn_context
|
||||
.extension_data
|
||||
.get::<codex_mcp::McpServerDependencies>()
|
||||
.unwrap_or_default();
|
||||
maybe_prompt_and_install_mcp_dependencies(
|
||||
sess,
|
||||
turn_context,
|
||||
cancellation_token,
|
||||
&mentioned_skills,
|
||||
&mcp_dependencies,
|
||||
Some(sess.mcp_elicitation_reviewer()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let injected_host_skill_prompts = turn_context
|
||||
let skill_connector_ids = turn_context
|
||||
.extension_data
|
||||
.get::<InjectedHostSkillPrompts>();
|
||||
let SkillInjections {
|
||||
items: skill_injections,
|
||||
warnings: skill_warnings,
|
||||
} = build_skill_injections(
|
||||
&mentioned_skills,
|
||||
Some(skills_outcome),
|
||||
Some(&turn_context.session_telemetry),
|
||||
&sess.services.analytics_events_client,
|
||||
tracking.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
for message in skill_warnings {
|
||||
sess.send_event(turn_context, EventMsg::Warning(WarningEvent { message }))
|
||||
.await;
|
||||
}
|
||||
|
||||
let skill_items: Vec<ResponseItem> = skill_injections
|
||||
.iter()
|
||||
.map(|skill| ContextualUserFragment::into(crate::context::SkillInstructions::from(skill)))
|
||||
.collect();
|
||||
let skill_connector_ids = collect_explicit_app_ids_from_skill_items(
|
||||
&skill_items,
|
||||
&available_connectors,
|
||||
&skill_name_counts_lower,
|
||||
);
|
||||
.get::<codex_connectors::ExplicitConnectorMentions>()
|
||||
.map(|mentions| mentions.resolve(&available_connectors))
|
||||
.unwrap_or_default();
|
||||
let plugin_items =
|
||||
build_plugin_injections(&mentioned_plugins, &mcp_tools, &available_connectors);
|
||||
let mut explicitly_enabled_connectors = collect_explicit_app_ids(&user_input);
|
||||
@@ -609,17 +577,7 @@ async fn build_skills_and_plugins(
|
||||
.track_plugin_used(tracking.clone(), plugin);
|
||||
}
|
||||
|
||||
let mut injection_items: Vec<ResponseItem> = match injected_host_skill_prompts {
|
||||
Some(injected_host_skill_prompts) => skill_injections
|
||||
.iter()
|
||||
.filter(|skill| !injected_host_skill_prompts.contains_path(&skill.path))
|
||||
.map(|skill| {
|
||||
ContextualUserFragment::into(crate::context::SkillInstructions::from(skill))
|
||||
})
|
||||
.collect(),
|
||||
None => skill_items,
|
||||
};
|
||||
injection_items.extend(plugin_items);
|
||||
let mut injection_items = plugin_items;
|
||||
injection_items.extend(extension_injection_items);
|
||||
Some((injection_items, explicitly_enabled_connectors))
|
||||
}
|
||||
@@ -628,6 +586,7 @@ async fn build_extension_turn_input_items(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &TurnContext,
|
||||
user_input: &[UserInput],
|
||||
reserved_plain_tool_names: HashSet<String>,
|
||||
cancellation_token: &CancellationToken,
|
||||
) -> Option<Vec<ResponseItem>> {
|
||||
let contributors = sess.services.extensions.turn_input_contributors().to_vec();
|
||||
@@ -654,7 +613,9 @@ async fn build_extension_turn_input_items(
|
||||
let input = TurnInputContext {
|
||||
thread_id: sess.thread_id(),
|
||||
turn_id: turn_context.sub_id.to_string(),
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
user_input: user_input.to_vec(),
|
||||
reserved_plain_tool_names,
|
||||
environments,
|
||||
};
|
||||
|
||||
@@ -967,57 +928,6 @@ async fn run_auto_compact(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn collect_explicit_app_ids_from_skill_items(
|
||||
skill_items: &[ResponseItem],
|
||||
connectors: &[connectors::AppInfo],
|
||||
skill_name_counts_lower: &HashMap<String, usize>,
|
||||
) -> HashSet<String> {
|
||||
if skill_items.is_empty() || connectors.is_empty() {
|
||||
return HashSet::new();
|
||||
}
|
||||
|
||||
let skill_messages = skill_items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ResponseItem::Message { content, .. } => {
|
||||
content.iter().find_map(|content_item| match content_item {
|
||||
ContentItem::InputText { text } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
if skill_messages.is_empty() {
|
||||
return HashSet::new();
|
||||
}
|
||||
|
||||
let mentions = collect_tool_mentions_from_messages(&skill_messages);
|
||||
let mention_names_lower = mentions
|
||||
.plain_names
|
||||
.iter()
|
||||
.map(|name| name.to_ascii_lowercase())
|
||||
.collect::<HashSet<String>>();
|
||||
let mut connector_ids = mentions
|
||||
.paths
|
||||
.iter()
|
||||
.filter(|path| tool_kind_for_path(path) == ToolMentionKind::App)
|
||||
.filter_map(|path| app_id_from_path(path).map(str::to_string))
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
let connector_slug_counts = build_connector_slug_counts(connectors);
|
||||
for connector in connectors {
|
||||
let slug = codex_connectors::metadata::connector_mention_slug(connector);
|
||||
let connector_count = connector_slug_counts.get(&slug).copied().unwrap_or(0);
|
||||
let skill_count = skill_name_counts_lower.get(&slug).copied().unwrap_or(0);
|
||||
if connector_count == 1 && skill_count == 0 && mention_names_lower.contains(&slug) {
|
||||
connector_ids.insert(connector.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
connector_ids
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) fn build_prompt(
|
||||
input: Vec<ResponseItem>,
|
||||
|
||||
@@ -22,8 +22,6 @@ pub use codex_core_skills::default_skill_metadata_budget;
|
||||
pub use codex_core_skills::detect_implicit_skill_invocation_for_command;
|
||||
pub use codex_core_skills::filter_skill_load_outcome_for_product;
|
||||
pub use codex_core_skills::injection;
|
||||
pub use codex_core_skills::injection::SkillInjections;
|
||||
pub use codex_core_skills::injection::build_skill_injections;
|
||||
pub use codex_core_skills::injection::collect_explicit_skill_mentions;
|
||||
pub use codex_core_skills::loader;
|
||||
pub use codex_core_skills::model;
|
||||
|
||||
Reference in New Issue
Block a user