maybe at handler

This commit is contained in:
alexsong-oai
2026-02-21 23:30:59 -08:00
parent 4dc29789b8
commit 018df690be
9 changed files with 108 additions and 115 deletions

View File

@@ -34,7 +34,7 @@ pub(crate) fn build_track_events_context(
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub(crate) struct SkillInvocation {
pub(crate) skill_name: String,
pub(crate) skill_scope: SkillScope,
@@ -42,7 +42,7 @@ pub(crate) struct SkillInvocation {
pub(crate) invocation_type: InvocationType,
}
#[derive(Clone, Copy, Serialize)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum InvocationType {
Explicit,

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;
@@ -16,7 +17,6 @@ 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::TrackEventsContext;
use crate::analytics_client::build_track_events_context;
use crate::apps::render_apps_section;
use crate::commit_attribution::commit_message_trailer_instruction;
@@ -35,7 +35,6 @@ use crate::parse_command::parse_command;
use crate::parse_turn_item;
use crate::rollout::session_index;
use crate::skills::ImplicitInvocationContext;
use crate::skills::build_implicit_invocation_context;
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;
@@ -568,6 +567,7 @@ pub(crate) struct TurnContext {
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
pub(crate) implicit_invocation_seen_skill_ids: 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> {
@@ -650,6 +650,7 @@ impl TurnContext {
dynamic_tools: self.dynamic_tools.clone(),
turn_metadata_state: self.turn_metadata_state.clone(),
implicit_invocation_seen_skill_ids: self.implicit_invocation_seen_skill_ids.clone(),
implicit_invocation_context: self.implicit_invocation_context.clone(),
}
}
@@ -993,6 +994,7 @@ impl Session {
dynamic_tools: session_configuration.dynamic_tools.clone(),
turn_metadata_state,
implicit_invocation_seen_skill_ids: Arc::new(Mutex::new(HashSet::new())),
implicit_invocation_context: Arc::new(OnceLock::new()),
}
}
@@ -4150,6 +4152,7 @@ async fn spawn_review_thread(
truncation_policy: model_info.truncation_policy.into(),
turn_metadata_state,
implicit_invocation_seen_skill_ids: 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.
@@ -4273,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
@@ -4332,11 +4340,6 @@ pub(crate) async fn run_turn(
thread_id,
turn_context.sub_id.clone(),
);
let implicit_invocation_context = build_implicit_invocation_context(
skills_outcome.as_ref().map_or_else(Vec::new, |outcome| {
outcome.allowed_skills_for_implicit_invocation()
}),
);
let SkillInjections {
items: skill_items,
warnings: skill_warnings,
@@ -4459,8 +4462,6 @@ pub(crate) async fn run_turn(
&explicitly_enabled_connectors,
skills_outcome.as_ref(),
&mut server_model_warning_emitted_for_turn,
implicit_invocation_context.as_ref(),
&tracking,
cancellation_token.child_token(),
)
.await
@@ -4835,8 +4836,6 @@ async fn run_sampling_request(
explicitly_enabled_connectors: &HashSet<String>,
skills_outcome: Option<&SkillLoadOutcome>,
server_model_warning_emitted_for_turn: &mut bool,
implicit_invocation_context: Option<&ImplicitInvocationContext>,
tracking: &TrackEventsContext,
cancellation_token: CancellationToken,
) -> CodexResult<SamplingRequestResult> {
let router = built_tools(
@@ -4863,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(
@@ -4875,8 +4873,6 @@ async fn run_sampling_request(
Arc::clone(&turn_diff_tracker),
server_model_warning_emitted_for_turn,
&prompt,
implicit_invocation_context,
tracking,
cancellation_token.child_token(),
)
.await
@@ -5446,8 +5442,6 @@ async fn try_run_sampling_request(
turn_diff_tracker: SharedTurnDiffTracker,
server_model_warning_emitted_for_turn: &mut bool,
prompt: &Prompt,
implicit_invocation_context: Option<&ImplicitInvocationContext>,
tracking: &TrackEventsContext,
cancellation_token: CancellationToken,
) -> CodexResult<SamplingRequestResult> {
let collaboration_mode = sess.current_collaboration_mode().await;
@@ -5561,8 +5555,6 @@ async fn try_run_sampling_request(
turn_context: turn_context.clone(),
tool_runtime: tool_runtime.clone(),
cancellation_token: cancellation_token.child_token(),
implicit_invocation_context,
tracking,
};
let output_result = handle_output_item_done(&mut ctx, item, previously_active_item)

View File

@@ -2,41 +2,29 @@ use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use codex_protocol::models::ResponseItem;
use serde::Deserialize;
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)]
#[derive(Clone, Debug)]
pub(crate) struct ImplicitSkillCandidate {
pub(crate) invocation: SkillInvocation,
}
#[derive(Default)]
#[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,
}
#[derive(Deserialize)]
struct ShellCommandDetectionArgs {
command: String,
workdir: Option<String>,
}
#[derive(Deserialize)]
struct ExecCommandDetectionArgs {
cmd: String,
workdir: Option<String>,
}
pub(crate) fn build_implicit_invocation_context(
skills: Vec<SkillMetadata>,
) -> Option<ImplicitInvocationContext> {
@@ -68,21 +56,15 @@ pub(crate) fn build_implicit_invocation_context(
Some(ImplicitInvocationContext { detector })
}
pub(crate) fn detect_implicit_skill_invocation(
fn detect_implicit_skill_invocation_for_command(
detector: &ImplicitSkillDetector,
turn_context: &TurnContext,
item: &ResponseItem,
command: &str,
workdir: Option<&str>,
) -> Option<ImplicitSkillCandidate> {
let ResponseItem::FunctionCall {
name, arguments, ..
} = item
else {
return None;
};
let (command, workdir) = parse_implicit_detection_command(name, arguments)?;
let workdir = turn_context.resolve_path(workdir);
let workdir = turn_context.resolve_path(workdir.map(str::to_owned));
let workdir = normalize_path(workdir.as_path());
let tokens = tokenize_command(command.as_str());
let tokens = tokenize_command(command);
if let Some(candidate) = detect_skill_script_run(detector, tokens.as_slice(), workdir.as_path())
{
@@ -96,19 +78,63 @@ pub(crate) fn detect_implicit_skill_invocation(
None
}
fn parse_implicit_detection_command(
tool_name: &str,
arguments: &str,
) -> Option<(String, Option<String>)> {
match tool_name {
"shell_command" => serde_json::from_str::<ShellCommandDetectionArgs>(arguments)
.ok()
.map(|args| (args.command, args.workdir)),
"exec_command" => serde_json::from_str::<ExecCommandDetectionArgs>(arguments)
.ok()
.map(|args| (args.cmd, args.workdir)),
_ => 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.as_str();
let seen_key = format!("{skill_scope}:{skill_path}:{skill_name}");
let inserted = {
let mut seen_skills = turn_context.implicit_invocation_seen_skill_ids.lock().await;
seen_skills.insert(seen_key)
};
if !inserted {
return;
}
turn_context.otel_manager.counter(
"codex.skill.injected",
1,
&[
("status", "ok"),
("skill", skill_name),
("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> {

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

@@ -16,7 +16,7 @@ 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::detect_implicit_skill_invocation;
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

@@ -5,7 +5,6 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::items::TurnItem;
use tokio_util::sync::CancellationToken;
use crate::analytics_client::TrackEventsContext;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::error::CodexErr;
@@ -13,8 +12,6 @@ use crate::error::Result;
use crate::function_tool::FunctionCallError;
use crate::parse_turn_item;
use crate::proposed_plan_parser::strip_proposed_plan_blocks;
use crate::skills::ImplicitInvocationContext;
use crate::skills::detect_implicit_skill_invocation;
use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolRouter;
use codex_protocol::models::FunctionCallOutputBody;
@@ -38,18 +35,16 @@ pub(crate) struct OutputItemResult {
pub tool_future: Option<InFlightFuture<'static>>,
}
pub(crate) struct HandleOutputCtx<'a> {
pub(crate) struct HandleOutputCtx {
pub sess: Arc<Session>,
pub turn_context: Arc<TurnContext>,
pub tool_runtime: ToolCallRuntime,
pub cancellation_token: CancellationToken,
pub implicit_invocation_context: Option<&'a ImplicitInvocationContext>,
pub tracking: &'a TrackEventsContext,
}
#[instrument(level = "trace", skip_all)]
pub(crate) async fn handle_output_item_done(
ctx: &mut HandleOutputCtx<'_>,
ctx: &mut HandleOutputCtx,
item: ResponseItem,
previously_active_item: Option<TurnItem>,
) -> Result<OutputItemResult> {
@@ -67,8 +62,6 @@ pub(crate) async fn handle_output_item_done(
payload_preview
);
maybe_emit_implicit_skill_invocation(ctx, &item).await;
ctx.sess
.record_conversation_items(&ctx.turn_context, std::slice::from_ref(&item))
.await;
@@ -165,51 +158,6 @@ pub(crate) async fn handle_output_item_done(
Ok(output)
}
async fn maybe_emit_implicit_skill_invocation(ctx: &mut HandleOutputCtx<'_>, item: &ResponseItem) {
let Some(implicit) = ctx.implicit_invocation_context else {
return;
};
let Some(candidate) =
detect_implicit_skill_invocation(&implicit.detector, ctx.turn_context.as_ref(), item)
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.as_str();
let seen_key = format!("{skill_scope}:{skill_path}:{skill_name}");
let inserted = {
let mut seen_skills = ctx
.turn_context
.implicit_invocation_seen_skill_ids
.lock()
.await;
seen_skills.insert(seen_key)
};
if !inserted {
return;
}
ctx.turn_context.otel_manager.counter(
"codex.skill.injected",
1,
&[
("status", "ok"),
("skill", skill_name),
("invoke_type", "implicit"),
],
);
ctx.sess
.services
.analytics_events_client
.track_skill_invocations(ctx.tracking.clone(), vec![candidate.invocation]);
}
pub(crate) async fn handle_non_tool_response_item(
item: &ResponseItem,
plan_mode: bool,

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