diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index b74338243a..5f0e322dae 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -77,6 +77,7 @@ use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::compact::collect_user_messages; use crate::config::Config; +use crate::config::GhostSnapshotConfig; use crate::config::types::ShellEnvironmentPolicy; use crate::context_manager::ContextManager; use crate::environment_context::EnvironmentContext; @@ -106,8 +107,7 @@ use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; use crate::protocol::SessionConfiguredEvent; use crate::protocol::SkillErrorInfo; -use crate::protocol::SkillInfo; -use crate::protocol::SkillLoadOutcomeInfo; +use crate::protocol::SkillMetadata as ProtocolSkillMetadata; use crate::protocol::StreamErrorEvent; use crate::protocol::Submission; use crate::protocol::TokenCountEvent; @@ -120,10 +120,11 @@ use crate::rollout::RolloutRecorderParams; use crate::rollout::map_session_init_error; use crate::shell; use crate::shell_snapshot::ShellSnapshot; +use crate::skills::SkillError; use crate::skills::SkillInjections; -use crate::skills::SkillLoadOutcome; +use crate::skills::SkillMetadata; +use crate::skills::SkillsManager; use crate::skills::build_skill_injections; -use crate::skills::load_skills; use crate::state::ActiveTurn; use crate::state::SessionServices; use crate::state::SessionState; @@ -207,6 +208,7 @@ impl Codex { config: Config, auth_manager: Arc, models_manager: Arc, + skills_manager: Arc, conversation_history: InitialHistory, session_source: SessionSource, ) -> CodexResult { @@ -214,7 +216,7 @@ impl Codex { let (tx_event, rx_event) = async_channel::unbounded(); let loaded_skills = if config.features.enabled(Feature::Skills) { - Some(load_skills(&config)) + Some(skills_manager.skills_for_cwd(&config.cwd)) } else { None }; @@ -229,11 +231,9 @@ impl Codex { } } - let skills_outcome = loaded_skills.clone(); - let user_instructions = get_user_instructions( &config, - skills_outcome + loaded_skills .as_ref() .map(|outcome| outcome.skills.as_slice()), ) @@ -279,7 +279,7 @@ impl Codex { tx_event.clone(), conversation_history, session_source_clone, - skills_outcome.clone(), + skills_manager, ) .await .map_err(|e| { @@ -365,6 +365,7 @@ pub(crate) struct TurnContext { pub(crate) sandbox_policy: SandboxPolicy, pub(crate) shell_environment_policy: ShellEnvironmentPolicy, pub(crate) tools_config: ToolsConfig, + pub(crate) ghost_snapshot: GhostSnapshotConfig, pub(crate) final_output_json_schema: Option, pub(crate) codex_linux_sandbox_exe: Option, pub(crate) tool_call_gate: Arc, @@ -526,6 +527,7 @@ impl Session { sandbox_policy: session_configuration.sandbox_policy.clone(), shell_environment_policy: per_turn_config.shell_environment_policy.clone(), tools_config, + ghost_snapshot: per_turn_config.ghost_snapshot.clone(), final_output_json_schema: None, codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), @@ -546,7 +548,7 @@ impl Session { tx_event: Sender, initial_history: InitialHistory, session_source: SessionSource, - skills: Option, + skills_manager: Arc, ) -> anyhow::Result> { debug!( "Configuring session: model={}; provider={:?}", @@ -666,7 +668,7 @@ impl Session { otel_manager, models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), - skills: skills.clone(), + skills_manager, }; let sess = Arc::new(Session { @@ -682,8 +684,6 @@ impl Session { // Dispatch the SessionConfiguredEvent first and then report any errors. // If resuming, include converted initial messages in the payload so UIs can render them immediately. let initial_messages = initial_history.get_event_msgs(); - let skill_load_outcome = skill_load_outcome_for_client(skills.as_ref()); - let events = std::iter::once(Event { id: INITIAL_SUBMIT_ID.to_owned(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { @@ -697,7 +697,6 @@ impl Session { history_log_id, history_entry_count, initial_messages, - skill_load_outcome, rollout_path, }), }) @@ -1585,6 +1584,9 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::ListCustomPrompts => { handlers::list_custom_prompts(&sess, sub.id.clone()).await; } + Op::ListSkills { cwds } => { + handlers::list_skills(&sess, sub.id.clone(), cwds).await; + } Op::Undo => { handlers::undo(&sess, sub.id.clone()).await; } @@ -1629,6 +1631,7 @@ mod handlers { use crate::codex::spawn_review_thread; use crate::config::Config; + use crate::features::Feature; use crate::mcp::auth::compute_auth_statuses; use crate::mcp::collect_mcp_snapshot_from_manager; use crate::review_prompts::resolve_review_request; @@ -1642,9 +1645,11 @@ mod handlers { use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ListCustomPromptsResponseEvent; + use codex_protocol::protocol::ListSkillsResponseEvent; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; + use codex_protocol::protocol::SkillsListEntry; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::WarningEvent; @@ -1652,6 +1657,7 @@ mod handlers { use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; use mcp_types::RequestId; + use std::path::PathBuf; use std::sync::Arc; use tracing::info; use tracing::warn; @@ -1879,6 +1885,43 @@ mod handlers { sess.send_event_raw(event).await; } + pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec) { + let cwds = if cwds.is_empty() { + let state = sess.state.lock().await; + vec![state.session_configuration.cwd.clone()] + } else { + cwds + }; + let skills = if sess.enabled(Feature::Skills) { + let skills_manager = &sess.services.skills_manager; + cwds.into_iter() + .map(|cwd| { + let outcome = skills_manager.skills_for_cwd(&cwd); + let errors = super::errors_to_info(&outcome.errors); + let skills = super::skills_to_info(&outcome.skills); + SkillsListEntry { + cwd, + skills, + errors, + } + }) + .collect() + } else { + cwds.into_iter() + .map(|cwd| SkillsListEntry { + cwd, + skills: Vec::new(), + errors: Vec::new(), + }) + .collect() + }; + let event = Event { + id: sub_id, + msg: EventMsg::ListSkillsResponse(ListSkillsResponseEvent { skills }), + }; + sess.send_event_raw(event).await; + } + pub async fn undo(sess: &Arc, sub_id: String) { let turn_context = sess .new_turn_with_sub_id(sub_id, SessionSettingsUpdate::default()) @@ -2030,6 +2073,7 @@ async fn spawn_review_thread( sub_id: sub_id.to_string(), client, tools_config, + ghost_snapshot: parent_turn_context.ghost_snapshot.clone(), developer_instructions: None, user_instructions: None, base_instructions: Some(base_instructions.clone()), @@ -2061,28 +2105,26 @@ async fn spawn_review_thread( .await; } -fn skill_load_outcome_for_client( - outcome: Option<&SkillLoadOutcome>, -) -> Option { - outcome.map(|outcome| SkillLoadOutcomeInfo { - skills: outcome - .skills - .iter() - .map(|skill| SkillInfo { - name: skill.name.clone(), - description: skill.description.clone(), - path: skill.path.clone(), - }) - .collect(), - errors: outcome - .errors - .iter() - .map(|err| SkillErrorInfo { - path: err.path.clone(), - message: err.message.clone(), - }) - .collect(), - }) +fn skills_to_info(skills: &[SkillMetadata]) -> Vec { + skills + .iter() + .map(|skill| ProtocolSkillMetadata { + name: skill.name.clone(), + description: skill.description.clone(), + path: skill.path.clone(), + scope: skill.scope, + }) + .collect() +} + +fn errors_to_info(errors: &[SkillError]) -> Vec { + errors + .iter() + .map(|err| SkillErrorInfo { + path: err.path.clone(), + message: err.message.clone(), + }) + .collect() } /// Takes a user message as input and runs a loop where, at each turn, the model @@ -2113,10 +2155,20 @@ pub(crate) async fn run_task( }); sess.send_event(&turn_context, event).await; + let skills_outcome = if sess.enabled(Feature::Skills) { + Some( + sess.services + .skills_manager + .skills_for_cwd(&turn_context.cwd), + ) + } else { + None + }; + let SkillInjections { items: skill_items, warnings: skill_warnings, - } = build_skill_injections(&input, sess.services.skills.as_ref()).await; + } = build_skill_injections(&input, skills_outcome.as_ref()).await; for message in skill_warnings { sess.send_event(&turn_context, EventMsg::Warning(WarningEvent { message })) @@ -2424,6 +2476,7 @@ async fn try_run_turn( let mut needs_follow_up = false; let mut last_agent_message: Option = None; let mut active_item: Option = None; + let mut should_emit_turn_diff = false; let receiving_span = info_span!("receiving_stream"); let outcome: CodexResult = loop { let handle_responses = info_span!( @@ -2499,14 +2552,7 @@ async fn try_run_turn( } => { sess.update_token_usage_info(&turn_context, token_usage.as_ref()) .await; - let unified_diff = { - let mut tracker = turn_diff_tracker.lock().await; - tracker.get_unified_diff() - }; - if let Ok(Some(unified_diff)) = unified_diff { - let msg = EventMsg::TurnDiff(TurnDiffEvent { unified_diff }); - sess.send_event(&turn_context, msg).await; - } + should_emit_turn_diff = true; break Ok(TurnRunResult { needs_follow_up, @@ -2580,7 +2626,18 @@ async fn try_run_turn( } }; - drain_in_flight(&mut in_flight, sess, turn_context).await?; + drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?; + + if should_emit_turn_diff { + let unified_diff = { + let mut tracker = turn_diff_tracker.lock().await; + tracker.get_unified_diff() + }; + if let Ok(Some(unified_diff)) = unified_diff { + let msg = EventMsg::TurnDiff(TurnDiffEvent { unified_diff }); + sess.clone().send_event(&turn_context, msg).await; + } + } outcome } @@ -3013,6 +3070,7 @@ mod tests { ); let state = SessionState::new(session_configuration.clone()); + let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())), @@ -3026,7 +3084,7 @@ mod tests { otel_manager: otel_manager.clone(), models_manager, tool_approvals: Mutex::new(ApprovalStore::default()), - skills: None, + skills_manager, }; let turn_context = Session::make_turn_context( @@ -3103,6 +3161,7 @@ mod tests { ); let state = SessionState::new(session_configuration.clone()); + let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())), @@ -3116,7 +3175,7 @@ mod tests { otel_manager: otel_manager.clone(), models_manager, tool_approvals: Mutex::new(ApprovalStore::default()), - skills: None, + skills_manager, }; let turn_context = Arc::new(Session::make_turn_context(