Support implicit skill invocation analytics events (#12049)

- use `skills_for_cwd` lookup to scope allowed skills and build
invocation context for downstream processing
- add detection in `stream_events_utils` to classify tool calls as
implicit skill invocations per the proposal (script runners, extensions,
`scripts` dirs, and SKILL.md reads)
- deduplicate invocations per turn and emit analytics/OTEL events on the
same background queue as explicit invokes
This commit is contained in:
alexsong-oai
2026-02-23 21:55:49 -08:00
committed by GitHub
Unverified
parent fbeda61cc3
commit 09a82f364f
10 changed files with 477 additions and 24 deletions
+19 -9
View File
@@ -34,16 +34,25 @@ pub(crate) fn build_track_events_context(
}
}
#[derive(Clone, Debug)]
pub(crate) struct SkillInvocation {
pub(crate) skill_name: String,
pub(crate) skill_scope: SkillScope,
pub(crate) skill_path: PathBuf,
pub(crate) invocation_type: InvocationType,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum InvocationType {
Explicit,
Implicit,
}
pub(crate) struct AppInvocation {
pub(crate) connector_id: Option<String>,
pub(crate) app_name: Option<String>,
pub(crate) invoke_type: Option<String>,
pub(crate) invocation_type: Option<InvocationType>,
}
#[derive(Clone)]
@@ -197,7 +206,7 @@ struct SkillInvocationEventParams {
skill_scope: Option<String>,
repo_url: Option<String>,
thread_id: Option<String>,
invoke_type: Option<String>,
invoke_type: Option<InvocationType>,
model_slug: Option<String>,
}
@@ -208,7 +217,7 @@ struct CodexAppMetadata {
turn_id: Option<String>,
app_name: Option<String>,
product_client_id: Option<String>,
invoke_type: Option<String>,
invoke_type: Option<InvocationType>,
model_slug: Option<String>,
}
@@ -328,7 +337,7 @@ async fn send_track_skill_invocations(auth_manager: &AuthManager, job: TrackSkil
skill_name: invocation.skill_name.clone(),
event_params: SkillInvocationEventParams {
thread_id: Some(tracking.thread_id.clone()),
invoke_type: Some("explicit".to_string()),
invoke_type: Some(invocation.invocation_type),
model_slug: Some(tracking.model_slug.clone()),
product_client_id: Some(crate::default_client::originator().value),
repo_url,
@@ -383,7 +392,7 @@ fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> Code
turn_id: Some(tracking.turn_id.clone()),
app_name: app.app_name,
product_client_id: Some(crate::default_client::originator().value),
invoke_type: app.invoke_type,
invoke_type: app.invocation_type,
model_slug: Some(tracking.model_slug.clone()),
}
}
@@ -437,7 +446,7 @@ async fn send_track_events(
}
}
fn skill_id_for_local_skill(
pub(crate) fn skill_id_for_local_skill(
repo_url: Option<&str>,
repo_root: Option<&Path>,
skill_path: &Path,
@@ -485,6 +494,7 @@ mod tests {
use super::AppInvocation;
use super::CodexAppMentionedEventRequest;
use super::CodexAppUsedEventRequest;
use super::InvocationType;
use super::TrackEventRequest;
use super::TrackEventsContext;
use super::codex_app_metadata;
@@ -567,7 +577,7 @@ mod tests {
AppInvocation {
connector_id: Some("calendar".to_string()),
app_name: Some("Calendar".to_string()),
invoke_type: Some("explicit".to_string()),
invocation_type: Some(InvocationType::Explicit),
},
),
});
@@ -605,7 +615,7 @@ mod tests {
AppInvocation {
connector_id: Some("drive".to_string()),
app_name: Some("Google Drive".to_string()),
invoke_type: Some("implicit".to_string()),
invocation_type: Some(InvocationType::Implicit),
},
),
});
@@ -639,7 +649,7 @@ mod tests {
let app = AppInvocation {
connector_id: Some("calendar".to_string()),
app_name: Some("Calendar".to_string()),
invoke_type: Some("implicit".to_string()),
invocation_type: Some(InvocationType::Implicit),
};
let turn_1 = TrackEventsContext {
+35 -10
View File
@@ -14,6 +14,7 @@ use crate::agent::AgentStatus;
use crate::agent::agent_status_from_event;
use crate::analytics_client::AnalyticsEventsClient;
use crate::analytics_client::AppInvocation;
use crate::analytics_client::InvocationType;
use crate::analytics_client::build_track_events_context;
use crate::apps::render_apps_section;
use crate::commit_attribution::commit_message_trailer_instruction;
@@ -542,6 +543,20 @@ pub(crate) struct Session {
next_internal_sub_id: AtomicU64,
}
#[derive(Clone, Debug)]
pub(crate) struct TurnSkillsContext {
pub(crate) outcome: Arc<SkillLoadOutcome>,
pub(crate) implicit_invocation_seen_skills: Arc<Mutex<HashSet<String>>>,
}
impl TurnSkillsContext {
pub(crate) fn new(outcome: Arc<SkillLoadOutcome>) -> Self {
Self {
outcome,
implicit_invocation_seen_skills: Arc::new(Mutex::new(HashSet::new())),
}
}
}
/// The context needed for a single turn of the thread.
#[derive(Debug)]
pub(crate) struct TurnContext {
@@ -578,6 +593,7 @@ pub(crate) struct TurnContext {
pub(crate) js_repl: Arc<JsReplHandle>,
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
pub(crate) turn_skills: TurnSkillsContext,
}
impl TurnContext {
pub(crate) fn model_context_window(&self) -> Option<i64> {
@@ -660,6 +676,7 @@ impl TurnContext {
js_repl: Arc::clone(&self.js_repl),
dynamic_tools: self.dynamic_tools.clone(),
turn_metadata_state: self.turn_metadata_state.clone(),
turn_skills: self.turn_skills.clone(),
}
}
@@ -938,6 +955,7 @@ impl Session {
network: Option<NetworkProxy>,
sub_id: String,
js_repl: Arc<JsReplHandle>,
skills_outcome: Arc<SkillLoadOutcome>,
) -> TurnContext {
let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort();
let reasoning_summary = session_configuration.model_reasoning_summary;
@@ -1000,6 +1018,7 @@ impl Session {
js_repl,
dynamic_tools: session_configuration.dynamic_tools.clone(),
turn_metadata_state,
turn_skills: TurnSkillsContext::new(skills_outcome),
}
}
@@ -2084,6 +2103,12 @@ impl Session {
&per_turn_config,
)
.await;
let skills_outcome = Arc::new(
self.services
.skills_manager
.skills_for_cwd(&session_configuration.cwd, false)
.await,
);
let mut turn_context: TurnContext = Self::make_turn_context(
Some(Arc::clone(&self.services.auth_manager)),
&self.services.otel_manager,
@@ -2097,6 +2122,7 @@ impl Session {
.map(StartedNetworkProxy::proxy),
sub_id,
Arc::clone(&self.js_repl),
skills_outcome,
);
if let Some(final_schema) = final_output_json_schema {
@@ -4522,6 +4548,7 @@ async fn spawn_review_thread(
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
truncation_policy: model_info.truncation_policy.into(),
turn_metadata_state,
turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.outcome.clone()),
};
// Seed the child task with the review prompt as the initial user message.
@@ -4646,6 +4673,8 @@ pub(crate) async fn run_turn(
return None;
}
let skills_outcome = Some(turn_context.turn_skills.outcome.as_ref());
let previous_model = sess.previous_model().await;
sess.record_context_updates_and_set_reference_context_item(
turn_context.as_ref(),
@@ -4653,13 +4682,6 @@ pub(crate) async fn run_turn(
)
.await;
let skills_outcome = Some(
sess.services
.skills_manager
.skills_for_cwd(&turn_context.cwd, false)
.await,
);
let available_connectors = if turn_context.config.features.enabled(Feature::Apps) {
let mcp_tools = match sess
.services
@@ -4751,7 +4773,7 @@ pub(crate) async fn run_turn(
app_name: connector_names_by_id
.get(connector_id.as_str())
.map(|name| (*name).to_string()),
invoke_type: Some("explicit".to_string()),
invocation_type: Some(InvocationType::Explicit),
})
.collect::<Vec<_>>();
sess.services
@@ -4843,7 +4865,7 @@ pub(crate) async fn run_turn(
turn_metadata_header.as_deref(),
sampling_request_input,
&explicitly_enabled_connectors,
skills_outcome.as_ref(),
skills_outcome,
&mut server_model_warning_emitted_for_turn,
cancellation_token.child_token(),
)
@@ -5272,7 +5294,6 @@ async fn run_sampling_request(
personality: turn_context.personality,
output_schema: turn_context.final_output_json_schema.clone(),
};
let mut retries = 0;
loop {
let err = match try_run_sampling_request(
@@ -8085,6 +8106,7 @@ mod tests {
config.js_repl_node_module_dirs.clone(),
));
let skills_outcome = Arc::new(services.skills_manager.skills_for_config(&per_turn_config));
let turn_context = Session::make_turn_context(
Some(Arc::clone(&auth_manager)),
&otel_manager,
@@ -8095,6 +8117,7 @@ mod tests {
None,
"turn_id".to_string(),
Arc::clone(&js_repl),
skills_outcome,
);
let session = Session {
@@ -8238,6 +8261,7 @@ mod tests {
config.js_repl_node_module_dirs.clone(),
));
let skills_outcome = Arc::new(services.skills_manager.skills_for_config(&per_turn_config));
let turn_context = Arc::new(Session::make_turn_context(
Some(Arc::clone(&auth_manager)),
&otel_manager,
@@ -8248,6 +8272,7 @@ mod tests {
None,
"turn_id".to_string(),
Arc::clone(&js_repl),
skills_outcome,
));
let session = Arc::new(Session {
+6 -5
View File
@@ -4,6 +4,7 @@ use std::time::Instant;
use tracing::error;
use crate::analytics_client::AppInvocation;
use crate::analytics_client::InvocationType;
use crate::analytics_client::build_track_events_context;
use crate::codex::Session;
use crate::codex::TurnContext;
@@ -276,15 +277,15 @@ async fn maybe_track_codex_app_used(
let (connector_id, app_name) = metadata
.map(|metadata| (metadata.connector_id, metadata.app_name))
.unwrap_or((None, None));
let invoke_type = if let Some(connector_id) = connector_id.as_deref() {
let invocation_type = if let Some(connector_id) = connector_id.as_deref() {
let mentioned_connector_ids = sess.get_connector_selection().await;
if mentioned_connector_ids.contains(connector_id) {
"explicit"
InvocationType::Explicit
} else {
"implicit"
InvocationType::Implicit
}
} else {
"implicit"
InvocationType::Implicit
};
let tracking = build_track_events_context(
@@ -297,7 +298,7 @@ async fn maybe_track_codex_app_used(
AppInvocation {
connector_id,
app_name,
invoke_type: Some(invoke_type.to_string()),
invocation_type: Some(invocation_type),
},
);
}
+2
View File
@@ -3,6 +3,7 @@ use std::collections::HashSet;
use std::path::PathBuf;
use crate::analytics_client::AnalyticsEventsClient;
use crate::analytics_client::InvocationType;
use crate::analytics_client::SkillInvocation;
use crate::analytics_client::TrackEventsContext;
use crate::instructions::SkillInstructions;
@@ -43,6 +44,7 @@ pub(crate) async fn build_skill_injections(
skill_name: skill.name.clone(),
skill_scope: skill.scope,
skill_path: skill.path.clone(),
invocation_type: InvocationType::Explicit,
});
result.items.push(ResponseItem::from(SkillInstructions {
name: skill.name.clone(),
@@ -0,0 +1,385 @@
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use crate::analytics_client::InvocationType;
use crate::analytics_client::SkillInvocation;
use crate::analytics_client::build_track_events_context;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::skills::SkillMetadata;
#[derive(Clone, Debug)]
pub(crate) struct ImplicitSkillCandidate {
pub(crate) invocation: SkillInvocation,
}
#[derive(Default, Debug)]
pub(crate) struct ImplicitSkillDetector {
pub(crate) by_scripts_dir: HashMap<PathBuf, ImplicitSkillCandidate>,
pub(crate) by_skill_doc_path: HashMap<PathBuf, ImplicitSkillCandidate>,
}
#[derive(Debug)]
pub(crate) struct ImplicitInvocationContext {
pub(crate) detector: ImplicitSkillDetector,
}
pub(crate) fn build_implicit_invocation_context(
skills: Vec<SkillMetadata>,
) -> Option<ImplicitInvocationContext> {
if skills.is_empty() {
return None;
}
let mut detector = ImplicitSkillDetector::default();
for skill in skills {
let invocation = SkillInvocation {
skill_name: skill.name,
skill_scope: skill.scope,
skill_path: skill.path,
invocation_type: InvocationType::Implicit,
};
let candidate = ImplicitSkillCandidate { invocation };
let skill_doc_path = normalize_path(candidate.invocation.skill_path.as_path());
detector
.by_skill_doc_path
.insert(skill_doc_path, candidate.clone());
if let Some(skill_dir) = candidate.invocation.skill_path.parent() {
let scripts_dir = normalize_path(&skill_dir.join("scripts"));
detector.by_scripts_dir.insert(scripts_dir, candidate);
}
}
Some(ImplicitInvocationContext { detector })
}
fn detect_implicit_skill_invocation_for_command(
detector: &ImplicitSkillDetector,
turn_context: &TurnContext,
command: &str,
workdir: Option<&str>,
) -> Option<ImplicitSkillCandidate> {
let workdir = turn_context.resolve_path(workdir.map(str::to_owned));
let workdir = normalize_path(workdir.as_path());
let tokens = tokenize_command(command);
if let Some(candidate) = detect_skill_script_run(detector, tokens.as_slice(), workdir.as_path())
{
return Some(candidate);
}
if let Some(candidate) = detect_skill_doc_read(detector, tokens.as_slice(), workdir.as_path()) {
return Some(candidate);
}
None
}
pub(crate) async fn maybe_emit_implicit_skill_invocation(
sess: &Session,
turn_context: &TurnContext,
command: &str,
workdir: Option<&str>,
) {
let Some(implicit) = turn_context
.turn_skills
.outcome
.implicit_invocation_context
.as_deref()
else {
return;
};
let Some(candidate) = detect_implicit_skill_invocation_for_command(
&implicit.detector,
turn_context,
command,
workdir,
) else {
return;
};
let skill_scope = match candidate.invocation.skill_scope {
codex_protocol::protocol::SkillScope::User => "user",
codex_protocol::protocol::SkillScope::Repo => "repo",
codex_protocol::protocol::SkillScope::System => "system",
codex_protocol::protocol::SkillScope::Admin => "admin",
};
let skill_path = candidate.invocation.skill_path.to_string_lossy();
let skill_name = candidate.invocation.skill_name.clone();
let seen_key = format!("{skill_scope}:{skill_path}:{skill_name}");
let inserted = {
let mut seen_skills = turn_context
.turn_skills
.implicit_invocation_seen_skills
.lock()
.await;
seen_skills.insert(seen_key)
};
if !inserted {
return;
}
turn_context.otel_manager.counter(
"codex.skill.injected",
1,
&[
("status", "ok"),
("skill", skill_name.as_str()),
("invoke_type", "implicit"),
],
);
sess.services
.analytics_events_client
.track_skill_invocations(
build_track_events_context(
turn_context.model_info.slug.clone(),
sess.conversation_id.to_string(),
turn_context.sub_id.clone(),
),
vec![candidate.invocation],
);
}
fn tokenize_command(command: &str) -> Vec<String> {
shlex::split(command).unwrap_or_else(|| {
command
.split_whitespace()
.map(std::string::ToString::to_string)
.collect()
})
}
fn script_run_token(tokens: &[String]) -> Option<&str> {
const RUNNERS: [&str; 10] = [
"python", "python3", "bash", "zsh", "sh", "node", "deno", "ruby", "perl", "pwsh",
];
const SCRIPT_EXTENSIONS: [&str; 7] = [".py", ".sh", ".js", ".ts", ".rb", ".pl", ".ps1"];
let runner_token = tokens.first()?;
let runner = command_basename(runner_token).to_ascii_lowercase();
let runner = runner.strip_suffix(".exe").unwrap_or(&runner);
if !RUNNERS.contains(&runner) {
return None;
}
let mut script_token: Option<&str> = None;
for token in tokens.iter().skip(1) {
if token == "--" {
continue;
}
if token.starts_with('-') {
continue;
}
script_token = Some(token.as_str());
break;
}
let script_token = script_token?;
if SCRIPT_EXTENSIONS
.iter()
.any(|extension| script_token.to_ascii_lowercase().ends_with(extension))
{
return Some(script_token);
}
None
}
fn detect_skill_script_run(
detector: &ImplicitSkillDetector,
tokens: &[String],
workdir: &Path,
) -> Option<ImplicitSkillCandidate> {
let script_token = script_run_token(tokens)?;
let script_path = Path::new(script_token);
let script_path = if script_path.is_absolute() {
script_path.to_path_buf()
} else {
workdir.join(script_path)
};
let script_path = normalize_path(script_path.as_path());
for ancestor in script_path.ancestors() {
if let Some(candidate) = detector.by_scripts_dir.get(ancestor) {
return Some(candidate.clone());
}
}
None
}
fn detect_skill_doc_read(
detector: &ImplicitSkillDetector,
tokens: &[String],
workdir: &Path,
) -> Option<ImplicitSkillCandidate> {
if !command_reads_file(tokens) {
return None;
}
for token in tokens.iter().skip(1) {
if token.starts_with('-') {
continue;
}
let path = Path::new(token);
let candidate_path = if path.is_absolute() {
normalize_path(path)
} else {
normalize_path(&workdir.join(path))
};
if let Some(candidate) = detector.by_skill_doc_path.get(&candidate_path) {
return Some(candidate.clone());
}
}
None
}
fn command_reads_file(tokens: &[String]) -> bool {
const READERS: [&str; 8] = ["cat", "sed", "head", "tail", "less", "more", "bat", "awk"];
let Some(program) = tokens.first() else {
return false;
};
let program = command_basename(program).to_ascii_lowercase();
READERS.contains(&program.as_str())
}
fn command_basename(command: &str) -> String {
Path::new(command)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(command)
.to_string()
}
fn normalize_path(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::ImplicitSkillCandidate;
use super::ImplicitSkillDetector;
use super::InvocationType;
use super::SkillInvocation;
use super::detect_skill_doc_read;
use super::detect_skill_script_run;
use super::normalize_path;
use super::script_run_token;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn script_run_detection_matches_runner_plus_extension() {
let tokens = vec![
"python3".to_string(),
"-u".to_string(),
"scripts/fetch_comments.py".to_string(),
];
assert_eq!(script_run_token(&tokens).is_some(), true);
}
#[test]
fn script_run_detection_excludes_python_c() {
let tokens = vec![
"python3".to_string(),
"-c".to_string(),
"print(1)".to_string(),
];
assert_eq!(script_run_token(&tokens).is_some(), false);
}
#[test]
fn skill_doc_read_detection_matches_absolute_path() {
let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md");
let normalized_skill_doc_path = normalize_path(skill_doc_path.as_path());
let invocation = SkillInvocation {
skill_name: "test-skill".to_string(),
skill_scope: codex_protocol::protocol::SkillScope::User,
skill_path: skill_doc_path,
invocation_type: InvocationType::Implicit,
};
let candidate = ImplicitSkillCandidate { invocation };
let detector = ImplicitSkillDetector {
by_scripts_dir: HashMap::new(),
by_skill_doc_path: HashMap::from([(normalized_skill_doc_path, candidate)]),
};
let tokens = vec![
"cat".to_string(),
"/tmp/skill-test/SKILL.md".to_string(),
"|".to_string(),
"head".to_string(),
];
let found = detect_skill_doc_read(&detector, &tokens, Path::new("/tmp"));
assert_eq!(
found.map(|value| value.invocation.skill_name),
Some("test-skill".to_string())
);
}
#[test]
fn skill_script_run_detection_matches_relative_path_from_skill_root() {
let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md");
let scripts_dir = normalize_path(Path::new("/tmp/skill-test/scripts"));
let invocation = SkillInvocation {
skill_name: "test-skill".to_string(),
skill_scope: codex_protocol::protocol::SkillScope::User,
skill_path: skill_doc_path,
invocation_type: InvocationType::Implicit,
};
let candidate = ImplicitSkillCandidate { invocation };
let detector = ImplicitSkillDetector {
by_scripts_dir: HashMap::from([(scripts_dir, candidate)]),
by_skill_doc_path: HashMap::new(),
};
let tokens = vec![
"python3".to_string(),
"scripts/fetch_comments.py".to_string(),
];
let found = detect_skill_script_run(&detector, &tokens, Path::new("/tmp/skill-test"));
assert_eq!(
found.map(|value| value.invocation.skill_name),
Some("test-skill".to_string())
);
}
#[test]
fn skill_script_run_detection_matches_absolute_path_from_any_workdir() {
let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md");
let scripts_dir = normalize_path(Path::new("/tmp/skill-test/scripts"));
let invocation = SkillInvocation {
skill_name: "test-skill".to_string(),
skill_scope: codex_protocol::protocol::SkillScope::User,
skill_path: skill_doc_path,
invocation_type: InvocationType::Implicit,
};
let candidate = ImplicitSkillCandidate { invocation };
let detector = ImplicitSkillDetector {
by_scripts_dir: HashMap::from([(scripts_dir, candidate)]),
by_skill_doc_path: HashMap::new(),
};
let tokens = vec![
"python3".to_string(),
"/tmp/skill-test/scripts/fetch_comments.py".to_string(),
];
let found = detect_skill_script_run(&detector, &tokens, Path::new("/tmp/other"));
assert_eq!(
found.map(|value| value.invocation.skill_name),
Some("test-skill".to_string())
);
}
}
+8
View File
@@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use codex_protocol::protocol::SkillScope;
@@ -16,6 +17,7 @@ use crate::config_loader::CloudRequirementsLoader;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::load_config_layers_state;
use crate::skills::SkillLoadOutcome;
use crate::skills::build_implicit_invocation_context;
use crate::skills::loader::SkillRoot;
use crate::skills::loader::load_skills_from_roots;
use crate::skills::loader::skill_roots_from_layer_stack_with_agents;
@@ -50,6 +52,9 @@ impl SkillsManager {
skill_roots_from_layer_stack_with_agents(&config.config_layer_stack, &config.cwd);
let mut outcome = load_skills_from_roots(roots);
outcome.disabled_paths = disabled_paths_from_stack(&config.config_layer_stack);
outcome.implicit_invocation_context =
build_implicit_invocation_context(outcome.allowed_skills_for_implicit_invocation())
.map(Arc::new);
let mut cache = match self.cache_by_cwd.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
@@ -125,6 +130,9 @@ impl SkillsManager {
);
let mut outcome = load_skills_from_roots(roots);
outcome.disabled_paths = disabled_paths_from_stack(&config_layer_stack);
outcome.implicit_invocation_context =
build_implicit_invocation_context(outcome.allowed_skills_for_implicit_invocation())
.map(Arc::new);
let mut cache = match self.cache_by_cwd.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
+3
View File
@@ -1,5 +1,6 @@
mod env_var_dependencies;
pub mod injection;
pub(crate) mod invocation_utils;
pub mod loader;
pub mod manager;
pub mod model;
@@ -13,6 +14,8 @@ pub(crate) use env_var_dependencies::resolve_skill_dependencies_for_turn;
pub(crate) use injection::SkillInjections;
pub(crate) use injection::build_skill_injections;
pub(crate) use injection::collect_explicit_skill_mentions;
pub(crate) use invocation_utils::build_implicit_invocation_context;
pub(crate) use invocation_utils::maybe_emit_implicit_skill_invocation;
pub use loader::load_skills;
pub use manager::SkillsManager;
pub use model::SkillError;
+3
View File
@@ -1,7 +1,9 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use crate::config::Permissions;
use crate::skills::invocation_utils::ImplicitInvocationContext;
use codex_protocol::protocol::SkillScope;
#[derive(Debug, Clone, PartialEq)]
@@ -68,6 +70,7 @@ pub struct SkillLoadOutcome {
pub skills: Vec<SkillMetadata>,
pub errors: Vec<SkillError>,
pub disabled_paths: HashSet<PathBuf>,
pub(crate) implicit_invocation_context: Option<Arc<ImplicitInvocationContext>>,
}
impl SkillLoadOutcome {
@@ -13,6 +13,7 @@ use crate::function_tool::FunctionCallError;
use crate::is_safe_command::is_known_safe_command;
use crate::protocol::ExecCommandSource;
use crate::shell::Shell;
use crate::skills::maybe_emit_implicit_skill_invocation;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
@@ -228,6 +229,13 @@ impl ToolHandler for ShellCommandHandler {
};
let params: ShellCommandToolCallParams = parse_arguments(&arguments)?;
maybe_emit_implicit_skill_invocation(
session.as_ref(),
turn.as_ref(),
&params.command,
params.workdir.as_deref(),
)
.await;
let prefix_rule = params.prefix_rule.clone();
let exec_params = Self::to_exec_params(
&params,
@@ -5,6 +5,7 @@ use crate::protocol::TerminalInteractionEvent;
use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::shell::get_shell_by_model_provided_path;
use crate::skills::maybe_emit_implicit_skill_invocation;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
@@ -131,6 +132,13 @@ impl ToolHandler for UnifiedExecHandler {
let response = match tool_name.as_str() {
"exec_command" => {
let args: ExecCommandArgs = parse_arguments(&arguments)?;
maybe_emit_implicit_skill_invocation(
session.as_ref(),
turn.as_ref(),
&args.cmd,
args.workdir.as_deref(),
)
.await;
let process_id = manager.allocate_process_id().await;
let command = get_command(
&args,