Compare commits

...

8 Commits

Author SHA1 Message Date
alexsong-oai
7af3aa4129 minor 2026-02-21 23:54:22 -08:00
alexsong-oai
4db4c3cc1b rename 2026-02-21 23:45:05 -08:00
alexsong-oai
018df690be maybe at handler 2026-02-21 23:30:59 -08:00
alexsong-oai
4dc29789b8 move to invocation_utils 2026-02-21 22:52:20 -08:00
alexsong-oai
7bf1b1585c fmt 2026-02-17 20:09:56 -08:00
alexsong-oai
8d1c545abd rename 2026-02-17 17:56:03 -08:00
alexsong-oai
f380cb3972 update 2026-02-17 17:49:52 -08:00
alexsong-oai
05b95b8030 Support implicit skill invocation analytics events 2026-02-17 14:40:28 -08:00
10 changed files with 455 additions and 16 deletions

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 {

View File

@@ -4,6 +4,7 @@ use std::fmt::Debug;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicU64;
use crate::AuthManager;
@@ -15,6 +16,7 @@ use crate::agent::MAX_THREAD_SPAWN_DEPTH;
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;
@@ -32,6 +34,7 @@ use crate::models_manager::manager::ModelsManager;
use crate::parse_command::parse_command;
use crate::parse_turn_item;
use crate::rollout::session_index;
use crate::skills::ImplicitInvocationContext;
use crate::stream_events_utils::HandleOutputCtx;
use crate::stream_events_utils::handle_non_tool_response_item;
use crate::stream_events_utils::handle_output_item_done;
@@ -563,6 +566,8 @@ 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) implicit_invocation_seen_skills: Arc<Mutex<HashSet<String>>>,
pub(crate) implicit_invocation_context: Arc<OnceLock<Option<Arc<ImplicitInvocationContext>>>>,
}
impl TurnContext {
pub(crate) fn model_context_window(&self) -> Option<i64> {
@@ -644,6 +649,8 @@ impl TurnContext {
js_repl: Arc::clone(&self.js_repl),
dynamic_tools: self.dynamic_tools.clone(),
turn_metadata_state: self.turn_metadata_state.clone(),
implicit_invocation_seen_skills: self.implicit_invocation_seen_skills.clone(),
implicit_invocation_context: self.implicit_invocation_context.clone(),
}
}
@@ -986,6 +993,8 @@ impl Session {
js_repl,
dynamic_tools: session_configuration.dynamic_tools.clone(),
turn_metadata_state,
implicit_invocation_seen_skills: Arc::new(Mutex::new(HashSet::new())),
implicit_invocation_context: Arc::new(OnceLock::new()),
}
}
@@ -4142,6 +4151,8 @@ async fn spawn_review_thread(
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
truncation_policy: model_info.truncation_policy.into(),
turn_metadata_state,
implicit_invocation_seen_skills: Arc::new(Mutex::new(HashSet::new())),
implicit_invocation_context: Arc::new(OnceLock::new()),
};
// Seed the child task with the review prompt as the initial user message.
@@ -4265,6 +4276,11 @@ pub(crate) async fn run_turn(
.skills_for_cwd(&turn_context.cwd, false)
.await,
);
let _ = turn_context.implicit_invocation_context.set(
skills_outcome
.as_ref()
.and_then(|outcome| outcome.implicit_invocation_context.clone()),
);
let available_connectors = if turn_context.config.features.enabled(Feature::Apps) {
let mcp_tools = match sess
@@ -4357,7 +4373,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
@@ -4846,7 +4862,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(

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;
@@ -232,15 +233,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(
@@ -253,7 +254,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),
},
);
}

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(),

View File

@@ -0,0 +1,380 @@
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
.implicit_invocation_context
.get()
.and_then(|value| value.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.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())
);
}
}

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(),

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,9 @@ 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::ImplicitInvocationContext;
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;

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 {

View File

@@ -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;
@@ -209,6 +210,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,

View File

@@ -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;
@@ -128,6 +129,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, session.user_shell());