mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
## What changed - Add callback lifetimes to extension `ToolCall`, `ToolEnvironment`, turn-input context, and skill-read request types. - Require extension tool executors to handle calls for any invocation lifetime and tie their returned futures to that lifetime. - Update built-in extensions, adapters, and tests to use the lifetime-scoped APIs without retaining host-owned environment capabilities. GitOrigin-RevId: aca9ce3a1b6870df550bc84ec9308cc65533f161
422 lines
14 KiB
Rust
422 lines
14 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::sync::Weak;
|
|
|
|
use codex_exec_server::Environment;
|
|
use codex_exec_server::FileSystemSandboxContext;
|
|
use codex_extension_api::ExtensionMetrics;
|
|
use codex_mcp::McpResourceClient;
|
|
use codex_mcp::McpResourceClientCacheKey;
|
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
use tokio::sync::OnceCell;
|
|
|
|
use crate::SkillsExtensionConfig;
|
|
use crate::catalog::SkillAuthority;
|
|
use crate::catalog::SkillCatalog;
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::catalog::SkillPackageId;
|
|
use crate::catalog::SkillProviderResult;
|
|
use crate::catalog::SkillReadResult;
|
|
use crate::catalog::SkillResourceId;
|
|
use crate::catalog::SkillSourceKind;
|
|
use crate::provider::SkillListQuery;
|
|
use crate::provider::SkillReadRequest;
|
|
use crate::shadow_selection_experiment::RecentSkillInvocations;
|
|
use crate::shadow_selection_experiment::ShadowSelectionTurnState;
|
|
use crate::shadow_selection_experiment::ShadowTaskContext;
|
|
use crate::sources::SkillProviders;
|
|
|
|
const MAX_CACHED_ORCHESTRATOR_RESOURCES: usize = 100;
|
|
const MAX_CACHED_ORCHESTRATOR_CONTENT_BYTES: usize = 8 * 1024 * 1024;
|
|
|
|
pub(crate) struct SkillsSessionState {
|
|
pub(crate) mcp_resources: Option<Arc<McpResourceClient>>,
|
|
pub(crate) extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
|
|
}
|
|
|
|
pub(crate) struct SkillsThreadState {
|
|
config: Mutex<SkillsExtensionConfig>,
|
|
orchestrator_skills_available: bool,
|
|
executor_cache: Mutex<Vec<CachedExecutorCatalog>>,
|
|
executor_discovery_cache: Mutex<Option<CachedExecutorDiscoveryCatalog>>,
|
|
orchestrator_cache: Mutex<Option<Arc<OrchestratorGenerationCache>>>,
|
|
shadow_selection_turn: Mutex<Option<ShadowSelectionTurn>>,
|
|
pub(crate) executor_read_snapshot: Mutex<Option<ExecutorReadSnapshot>>,
|
|
pub(crate) recent_skill_invocations: Arc<RecentSkillInvocations>,
|
|
pub(crate) shadow_task_context: Arc<ShadowTaskContext>,
|
|
}
|
|
|
|
impl SkillsThreadState {
|
|
pub(crate) fn new(config: SkillsExtensionConfig, orchestrator_skills_available: bool) -> Self {
|
|
Self {
|
|
config: Mutex::new(config),
|
|
orchestrator_skills_available,
|
|
executor_cache: Mutex::new(Vec::new()),
|
|
executor_discovery_cache: Mutex::new(None),
|
|
orchestrator_cache: Mutex::new(None),
|
|
shadow_selection_turn: Mutex::new(None),
|
|
executor_read_snapshot: Mutex::new(None),
|
|
recent_skill_invocations: Arc::new(RecentSkillInvocations::default()),
|
|
shadow_task_context: Arc::new(ShadowTaskContext::default()),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn config(&self) -> SkillsExtensionConfig {
|
|
self.config
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.clone()
|
|
}
|
|
|
|
pub(crate) fn set_config(&self, config: SkillsExtensionConfig) {
|
|
*self
|
|
.config
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner) = config;
|
|
}
|
|
|
|
pub(crate) fn orchestrator_skills_enabled(&self) -> bool {
|
|
self.orchestrator_skills_available && self.config().orchestrator_skills_enabled
|
|
}
|
|
|
|
pub(crate) fn replace_shadow_selection_turn(
|
|
&self,
|
|
turn_id: String,
|
|
state: Option<ShadowSelectionTurnState>,
|
|
) {
|
|
*self
|
|
.shadow_selection_turn
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner) =
|
|
state.map(|state| ShadowSelectionTurn {
|
|
turn_id,
|
|
state: Arc::new(state),
|
|
});
|
|
}
|
|
|
|
pub(crate) fn shadow_selection_turn(
|
|
&self,
|
|
turn_id: &str,
|
|
) -> Option<Arc<ShadowSelectionTurnState>> {
|
|
self.shadow_selection_turn
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.as_ref()
|
|
.filter(|turn| turn.turn_id == turn_id)
|
|
.map(|turn| Arc::clone(&turn.state))
|
|
}
|
|
|
|
/// Returns catalogs for stable selected roots.
|
|
///
|
|
/// Successful catalogs, including empty or warning-bearing catalogs, remain cached until
|
|
/// this thread state is dropped. Catalogs backed by failed discovery are not cached, so
|
|
/// later steps can recover. There is no filesystem watcher because selected roots are stable.
|
|
#[tracing::instrument(
|
|
name = "skills.executor.catalog_snapshot",
|
|
level = "info",
|
|
skip_all,
|
|
fields(root_count = query.executor_roots.len())
|
|
)]
|
|
pub(crate) async fn executor_catalog_snapshot(
|
|
&self,
|
|
providers: &SkillProviders,
|
|
mut query: SkillListQuery,
|
|
) -> SkillCatalog {
|
|
if query.executor_capability_discovery.is_some() {
|
|
return self
|
|
.executor_discovery_catalog_snapshot(providers, query)
|
|
.await;
|
|
}
|
|
let roots = std::mem::take(&mut query.executor_roots);
|
|
let mut catalog = SkillCatalog::default();
|
|
for root in roots {
|
|
query.executor_roots = vec![root.clone()];
|
|
catalog.extend(
|
|
self.executor_root_catalog(providers, root, query.clone())
|
|
.await,
|
|
);
|
|
}
|
|
catalog
|
|
}
|
|
|
|
async fn executor_discovery_catalog_snapshot(
|
|
&self,
|
|
providers: &SkillProviders,
|
|
query: SkillListQuery,
|
|
) -> SkillCatalog {
|
|
let discovery_failed = query
|
|
.executor_capability_discovery
|
|
.as_ref()
|
|
.is_some_and(|discovery| discovery.roots().iter().any(|root| root.result.is_err()));
|
|
let sandbox_contexts = query
|
|
.executor_capability_discovery
|
|
.as_ref()
|
|
.map(|discovery| discovery.sandbox_contexts().clone())
|
|
.unwrap_or_default();
|
|
if let Some(cached) = self
|
|
.executor_discovery_cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.as_ref()
|
|
.filter(|cached| {
|
|
cached.roots == query.executor_roots && cached.sandbox_contexts == sandbox_contexts
|
|
})
|
|
{
|
|
return cached.catalog.clone();
|
|
}
|
|
let roots = query.executor_roots.clone();
|
|
let discovered = providers.list_executor_for_turn(query).await;
|
|
if discovery_failed {
|
|
return discovered;
|
|
}
|
|
let mut cache = self
|
|
.executor_discovery_cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if let Some(cached) = cache
|
|
.as_ref()
|
|
.filter(|cached| cached.roots == roots && cached.sandbox_contexts == sandbox_contexts)
|
|
{
|
|
return cached.catalog.clone();
|
|
}
|
|
*cache = Some(CachedExecutorDiscoveryCatalog {
|
|
roots,
|
|
sandbox_contexts,
|
|
catalog: discovered.clone(),
|
|
});
|
|
discovered
|
|
}
|
|
|
|
#[tracing::instrument(
|
|
name = "skills.orchestrator.catalog_snapshot",
|
|
level = "info",
|
|
skip_all
|
|
)]
|
|
pub(crate) async fn orchestrator_catalog_snapshot(
|
|
&self,
|
|
providers: &SkillProviders,
|
|
query: SkillListQuery,
|
|
) -> SkillCatalog {
|
|
if !query.include_orchestrator_skills {
|
|
return SkillCatalog::default();
|
|
}
|
|
|
|
let cache = self.orchestrator_cache(query.mcp_resources.as_deref());
|
|
cache
|
|
.catalog
|
|
.get_or_init(|| async {
|
|
providers
|
|
.list_orchestrator_for_turn(query)
|
|
.await
|
|
.unwrap_or_else(|err| SkillCatalog {
|
|
warnings: vec![err.message],
|
|
..Default::default()
|
|
})
|
|
})
|
|
.await
|
|
.clone()
|
|
}
|
|
|
|
pub(crate) async fn read_skill(
|
|
&self,
|
|
providers: &SkillProviders,
|
|
request: SkillReadRequest<'_>,
|
|
) -> SkillProviderResult<SkillReadResult> {
|
|
if request.authority.kind != SkillSourceKind::Orchestrator {
|
|
return providers.read(request).await;
|
|
}
|
|
|
|
let cache = self.orchestrator_cache(request.mcp_resources.as_deref());
|
|
let cache_key = SkillReadCacheKey::from(&request);
|
|
if let Some(result) = cache
|
|
.resources
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.get(&cache_key)
|
|
{
|
|
return Ok(result);
|
|
}
|
|
|
|
let result = providers.read(request).await?;
|
|
if result.resource != cache_key.resource {
|
|
return Ok(result);
|
|
}
|
|
|
|
Ok(cache
|
|
.resources
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.insert(cache_key, result))
|
|
}
|
|
|
|
fn orchestrator_cache(
|
|
&self,
|
|
mcp_resources: Option<&McpResourceClient>,
|
|
) -> Arc<OrchestratorGenerationCache> {
|
|
let mut cache = self
|
|
.orchestrator_cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let cache_key = mcp_resources.map(McpResourceClient::cache_key);
|
|
if let Some(cache) = cache
|
|
.as_ref()
|
|
.filter(|cache| cache.mcp_cache_key == cache_key)
|
|
{
|
|
return Arc::clone(cache);
|
|
}
|
|
|
|
let next_cache = Arc::new(OrchestratorGenerationCache {
|
|
mcp_cache_key: cache_key,
|
|
catalog: OnceCell::new(),
|
|
resources: Mutex::new(OrchestratorResourceCache::default()),
|
|
});
|
|
*cache = Some(Arc::clone(&next_cache));
|
|
next_cache
|
|
}
|
|
|
|
#[tracing::instrument(name = "skills.executor.catalog_root", level = "info", skip_all)]
|
|
async fn executor_root_catalog(
|
|
&self,
|
|
providers: &SkillProviders,
|
|
root: SelectedCapabilityRoot,
|
|
query: SkillListQuery,
|
|
) -> SkillCatalog {
|
|
if let Some(cached) = self
|
|
.executor_cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.iter()
|
|
.find(|cached| cached.root == root)
|
|
{
|
|
return cached.catalog.clone();
|
|
}
|
|
|
|
let discovered = providers.list_executor_for_turn(query).await;
|
|
let mut cache = self
|
|
.executor_cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if let Some(cached) = cache.iter().find(|cached| cached.root == root) {
|
|
return cached.catalog.clone();
|
|
}
|
|
cache.push(CachedExecutorCatalog {
|
|
root,
|
|
catalog: discovered.clone(),
|
|
});
|
|
discovered
|
|
}
|
|
}
|
|
|
|
/// One bounded executor resource, retained for continuations until replacement or thread drop.
|
|
/// Interleaved resources may evict it; misses reread and validate the content-bound cursor.
|
|
pub(crate) struct ExecutorReadSnapshot {
|
|
pub(crate) authority: SkillAuthority,
|
|
pub(crate) package: SkillPackageId,
|
|
// Named environments can be replaced; do not reuse their old resource or keep them alive.
|
|
pub(crate) environment: Weak<Environment>,
|
|
pub(crate) sandbox: Option<FileSystemSandboxContext>,
|
|
pub(crate) result: Arc<SkillReadResult>,
|
|
}
|
|
|
|
struct ShadowSelectionTurn {
|
|
turn_id: String,
|
|
state: Arc<ShadowSelectionTurnState>,
|
|
}
|
|
|
|
struct CachedExecutorCatalog {
|
|
root: SelectedCapabilityRoot,
|
|
catalog: SkillCatalog,
|
|
}
|
|
|
|
struct CachedExecutorDiscoveryCatalog {
|
|
roots: Vec<SelectedCapabilityRoot>,
|
|
sandbox_contexts: HashMap<String, FileSystemSandboxContext>,
|
|
catalog: SkillCatalog,
|
|
}
|
|
|
|
struct OrchestratorGenerationCache {
|
|
mcp_cache_key: Option<McpResourceClientCacheKey>,
|
|
catalog: OnceCell<SkillCatalog>,
|
|
resources: Mutex<OrchestratorResourceCache>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
struct SkillReadCacheKey {
|
|
authority: SkillAuthority,
|
|
package: SkillPackageId,
|
|
resource: SkillResourceId,
|
|
}
|
|
|
|
impl From<&SkillReadRequest<'_>> for SkillReadCacheKey {
|
|
fn from(request: &SkillReadRequest<'_>) -> Self {
|
|
Self {
|
|
authority: request.authority.clone(),
|
|
package: request.package.clone(),
|
|
resource: request.resource.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct OrchestratorResourceCache {
|
|
entries: HashMap<SkillReadCacheKey, SkillReadResult>,
|
|
contents_bytes: usize,
|
|
}
|
|
|
|
impl OrchestratorResourceCache {
|
|
fn get(&self, key: &SkillReadCacheKey) -> Option<SkillReadResult> {
|
|
self.entries.get(key).cloned()
|
|
}
|
|
|
|
fn insert(&mut self, key: SkillReadCacheKey, result: SkillReadResult) -> SkillReadResult {
|
|
if let Some(cached) = self.entries.get(&key) {
|
|
return cached.clone();
|
|
}
|
|
|
|
let contents_bytes = result.contents.len();
|
|
let Some(next_contents_bytes) = self.contents_bytes.checked_add(contents_bytes) else {
|
|
return result;
|
|
};
|
|
if self.entries.len() >= MAX_CACHED_ORCHESTRATOR_RESOURCES
|
|
|| next_contents_bytes > MAX_CACHED_ORCHESTRATOR_CONTENT_BYTES
|
|
{
|
|
return result;
|
|
}
|
|
|
|
self.contents_bytes = next_contents_bytes;
|
|
self.entries.insert(key, result.clone());
|
|
result
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct EmittedCatalogBudgetWarnings(Mutex<HashSet<String>>);
|
|
|
|
impl EmittedCatalogBudgetWarnings {
|
|
pub(crate) fn insert(&self, warning: &str) -> bool {
|
|
self.0
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.insert(warning.to_string())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub(crate) struct SkillsTurnState {
|
|
pub(crate) catalog: SkillCatalog,
|
|
pub(crate) selected_entries: Vec<SkillCatalogEntry>,
|
|
pub(crate) warnings: Vec<String>,
|
|
pub(crate) main_prompts_injected: bool,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub(crate) struct HostSkillsCatalogInWorldState;
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct ExecutorSkillsStepState(pub(crate) SkillCatalog);
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct HostSkillsStepState(pub(crate) SkillCatalog);
|