mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why Capability discovery needs the roots captured with each environment snapshot. Threads sharing an executor can select different roots, and changing environment order must preserve executor skill aliases. ## What changed - Attach the receiving thread's capability roots when constructing `TurnEnvironmentSelection` at startup and settings updates, including roots loaded from saved history. - Resolve capability roots from environment snapshots while preserving their original order. Retain startup roots so deselecting and reselecting an environment restores its capabilities. - Match inherited configuration by environment and workspace so differences in capability root metadata do not interrupt configuration propagation. ## Testing Add regression coverage for root ordering across environment states and configuration changes, independent root selections on a shared executor, reselection with startup or saved roots, active turn updates, and pending configuration failures reaching child and grandchild threads. GitOrigin-RevId: 4f2e4bb414d27195a6752406117a6050718a1198
90 lines
3.2 KiB
Rust
90 lines
3.2 KiB
Rust
use codex_utils_path_uri::LegacyAppPathString;
|
|
use codex_utils_path_uri::PathUri;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::sync::Arc;
|
|
use ts_rs::TS;
|
|
|
|
/// A user-selected root that can expose one or more runtime capabilities.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct SelectedCapabilityRoot {
|
|
/// Stable identifier supplied by the capability selection platform.
|
|
pub id: String,
|
|
/// Where the selected root can be resolved.
|
|
pub location: CapabilityRootLocation,
|
|
}
|
|
|
|
/// Roots selected for one environment, retaining their order in the thread's root list.
|
|
///
|
|
/// The index is each root's position in the original thread list. Executor skills assign
|
|
/// aliases (`e0`, `e1`, ...) in root order. Keeping the index lets us split roots across
|
|
/// environments and combine them again without changing those aliases.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct EnvironmentCapabilityRoots(Arc<[(usize, SelectedCapabilityRoot)]>);
|
|
|
|
impl EnvironmentCapabilityRoots {
|
|
/// Selects this environment's roots without losing their original positions.
|
|
pub fn for_environment(environment_id: &str, roots: &[SelectedCapabilityRoot]) -> Self {
|
|
let mut selected = Vec::new();
|
|
for (index, root) in roots.iter().enumerate() {
|
|
let CapabilityRootLocation::Environment {
|
|
environment_id: root_environment_id,
|
|
..
|
|
} = &root.location;
|
|
if root_environment_id == environment_id {
|
|
selected.push((index, root.clone()));
|
|
}
|
|
}
|
|
Self(selected.into())
|
|
}
|
|
|
|
/// Restores root order so changing environment order does not change skill aliases.
|
|
pub fn collect<'a>(
|
|
selections: impl IntoIterator<Item = &'a Self>,
|
|
) -> Vec<SelectedCapabilityRoot> {
|
|
let mut roots = Vec::new();
|
|
for selection in selections {
|
|
roots.extend(selection.0.iter());
|
|
}
|
|
roots.sort_by_key(|(index, _)| *index);
|
|
roots.into_iter().map(|(_, root)| root.clone()).collect()
|
|
}
|
|
}
|
|
|
|
/// Location used to resolve a selected capability root.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
|
#[serde(tag = "type", rename_all = "camelCase")]
|
|
#[ts(tag = "type")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum CapabilityRootLocation {
|
|
/// A path owned by an execution environment.
|
|
Environment {
|
|
#[serde(rename = "environmentId")]
|
|
#[ts(rename = "environmentId")]
|
|
environment_id: String,
|
|
/// Absolute path for the root in the selected environment.
|
|
#[serde(deserialize_with = "deserialize_path_uri_from_api_path")]
|
|
#[schemars(with = "String")]
|
|
#[ts(type = "string")]
|
|
path: PathUri,
|
|
},
|
|
}
|
|
|
|
fn deserialize_path_uri_from_api_path<'de, D>(deserializer: D) -> Result<PathUri, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let path = LegacyAppPathString::deserialize(deserializer)?;
|
|
if let Ok(path_uri) = PathUri::parse(path.as_str()) {
|
|
return Ok(path_uri);
|
|
}
|
|
path.try_into().map_err(serde::de::Error::custom)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "capabilities_tests.rs"]
|
|
mod tests;
|