mirror of
https://github.com/openai/codex.git
synced 2026-02-23 09:13:47 +00:00
Compare commits
8 Commits
main
...
alexs/impl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af3aa4129 | ||
|
|
4db4c3cc1b | ||
|
|
018df690be | ||
|
|
4dc29789b8 | ||
|
|
7bf1b1585c | ||
|
|
8d1c545abd | ||
|
|
f380cb3972 | ||
|
|
05b95b8030 |
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
380
codex-rs/core/src/skills/invocation_utils.rs
Normal file
380
codex-rs/core/src/skills/invocation_utils.rs
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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(),
|
||||
¶ms.command,
|
||||
params.workdir.as_deref(),
|
||||
)
|
||||
.await;
|
||||
let prefix_rule = params.prefix_rule.clone();
|
||||
let exec_params = Self::to_exec_params(
|
||||
¶ms,
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user