Skip to content

Conversation

@xl-openai
Copy link
Collaborator

@xl-openai xl-openai commented Dec 12, 2025

refactor the way we load and manage skills:

  1. Move skill discovery/caching into SkillsManager and reuse it across sessions.
  2. Add the skills/list API (Op::ListSkills/SkillsListResponse) to fetch skills for one or more cwds. Also update app-server for VSCE/App;
  3. Trigger skills/list during session startup so UIs preload skills and handle errors immediately.

@xl-openai xl-openai force-pushed the xl/skills branch 2 times, most recently from 44f6068 to ba5a045 Compare December 12, 2025 05:53
Comment on lines +24 to +41
pub fn skills_for_cwd(&self, cwd: &Path) -> SkillLoadOutcome {
let cached = match self.cache_by_cwd.read() {
Ok(cache) => cache.get(cwd).cloned(),
Err(err) => err.into_inner().get(cwd).cloned(),
};
if let Some(outcome) = cached {
return outcome;
}

let mut roots = vec![user_skills_root(&self.codex_home)];
if let Some(repo_root) = repo_skills_root(cwd) {
roots.push(repo_root);
}
let outcome = load_skills_from_roots(roots);
match self.cache_by_cwd.write() {
Ok(mut cache) => {
cache.insert(cwd.to_path_buf(), outcome.clone());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary for SkillsManager to cache SkillLoadOutcome for the lifetime of the host process (keyed by cwd) rather than caching per session/conversation (e.g. load once during Codex::spawn)?

As it is, edits/additions to a skill won’t be reflected by skills/list or by starting a “new chat” inside the same long-lived backend, e.g.:

  1. VS Code “New chat” creates a new conversation but reuses the same backend process + SkillsManager cache for that cwd.
  2. TUI /new starts a new session in the same process, so it also reuses the cached skills for that cwd.

If skill hot-refreshing is planned as a direct follow-up, this may be fine, but otherwise I feel like users may expect “new chat” to reflect the latest skills.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. This change is just the first step toward the dynamic loading you’re envisioning. skills/list can be called more frequently, with optional forced reloads.

Copy link

@ambrosino-oai ambrosino-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't speak to the rust code, but from an interface perspective for App/VSCE this LGTM

pub name: String,
pub description: String,
pub path: PathBuf,
pub scope: SkillScope,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll want the icon fields too (cc @gverma-openai @edward-bayes)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could extend the API later. This is just a minimum set to start with.

#[ts(export_to = "v2/")]
pub struct SkillsListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub cwds: Option<Vec<PathBuf>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does UI need to associate individual skill with CWD it came from? Are we better served by asking for a single CWD at a time?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, we return combinations of skills and errors. Hmm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this Optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about a case where you might want to query the default skills without tying them to a specific CWD, but yeah, maybe that’s not a valid use case.

#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsListResponse {
pub data: Vec<SkillsListEntry>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding pagination like other endpoints (models/list)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider having many directories open, do we want to wait for all of them to load skills before this method returns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! We can treat pagination/streaming as a follow-up optimization once we see more skills enabled in practice. For now, loading everything in one go keeps it simple, and clients that need finer-grained control can already split requests by cwd.

Some(cwds) if !cwds.is_empty() => cwds,
_ => vec![self.config.cwd.clone()],
};
let data = if self.config.features.enabled(Feature::Skills) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no strong feelings but consider encapsulating this check in skills_manager

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m inclined to keep the Feature::Skills guard at the call site. SkillsManager doesn’t have access to config/session state, so pushing the check down would either require passing Config in (extra coupling) or just moving the same if behind another method without changing behavior.

- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
- `model/list` — list available models (with reasoning effort options).
- `skills/list` — list skills for one or more `cwd` values; each skill includes a `scope` of `user` or `repo` (not thread-scoped).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not thread-scoped). ? Prompt leftovers?

#[cfg(any(test, feature = "test-support"))]
/// Construct with a dummy AuthManager containing the provided CodexAuth and codex home.
/// Used for integration tests: should not be used by ordinary business logic.
pub fn with_models_provider_and_home(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's have with_models_provider a call to this new method to have fewer copies of logic

});
sess.send_event(&turn_context, event).await;

let skills_outcome = if sess.enabled(Feature::Skills) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider encapsulating in skills_manager


#[cfg(any(test, feature = "test-support"))]
/// Create an AuthManager with a specific CodexAuth and codex home, for testing only.
pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc<Self> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you care which home is set on auth?

ListSkills {
/// Optional working directories to scope repo skills discovery.
#[serde(skip_serializing_if = "Option::is_none")]
cwds: Option<Vec<PathBuf>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider required

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants