feat: add extension turn-input contributors (#25959)

## Disclaimer
Do not use for now

## Why

Extensions can already contribute prompt fragments and request same-turn
item injection, but there was no host-owned hook for contributing
structured `ResponseItem`s while Codex is assembling a new turn's
initial model input. This change adds that seam so extensions can attach
turn-local input that depends on the submitted user input and resolved
turn environments without routing through prompt text or late injection.

## What changed

- add `TurnInputContributor` to `codex_extension_api` and export the new
`TurnInputContext` / `TurnInputEnvironment` types it receives
- teach `ExtensionRegistry` to register and expose turn-input
contributors alongside the existing extension hooks
- call registered turn-input contributors from
`core/src/session/turn.rs` while building the initial injected input for
a turn, then append their returned `ResponseItem`s after the skill and
plugin injections
This commit is contained in:
jif
2026-06-03 01:33:31 +02:00
committed by GitHub
Unverified
parent a28b32a835
commit 271d5cecf2
5 changed files with 120 additions and 0 deletions
+53
View File
@@ -69,6 +69,8 @@ use codex_analytics::InvocationType;
use codex_analytics::TurnResolvedConfigFact;
use codex_analytics::build_track_events_context;
use codex_async_utils::OrCancelExt;
use codex_extension_api::TurnInputContext;
use codex_extension_api::TurnInputEnvironment;
use codex_features::Feature;
use codex_git_utils::get_git_repo_root;
use codex_git_utils::get_git_repo_root_with_fs;
@@ -513,6 +515,9 @@ async fn build_skills_and_plugins(
};
let skills_outcome = turn_context.turn_skills.outcome.as_ref();
let connector_slug_counts = build_connector_slug_counts(&available_connectors);
let extension_injection_items =
build_extension_turn_input_items(sess, turn_context, &user_input, cancellation_token)
.await?;
let skill_name_counts_lower =
build_skill_name_counts(&skills_outcome.skills, &skills_outcome.disabled_paths).1;
let mentioned_skills = collect_explicit_skill_mentions(
@@ -588,9 +593,57 @@ async fn build_skills_and_plugins(
let mut injection_items = skill_items;
injection_items.extend(plugin_items);
injection_items.extend(extension_injection_items);
Some((injection_items, explicitly_enabled_connectors))
}
async fn build_extension_turn_input_items(
sess: &Arc<Session>,
turn_context: &TurnContext,
user_input: &[UserInput],
cancellation_token: &CancellationToken,
) -> Option<Vec<ResponseItem>> {
let contributors = sess.services.extensions.turn_input_contributors().to_vec();
if contributors.is_empty() {
return Some(Vec::new());
}
let environments = turn_context
.environments
.turn_environments
.iter()
.enumerate()
.map(|(index, environment)| TurnInputEnvironment {
environment_id: environment.environment_id.clone(),
cwd: environment.cwd.as_path().to_path_buf(),
is_primary: index == 0,
})
.collect::<Vec<_>>();
let input = TurnInputContext {
turn_id: turn_context.sub_id.to_string(),
user_input: user_input.to_vec(),
environments,
};
let mut items = Vec::new();
for contributor in contributors {
let contributed_items = contributor
.contribute(
input.clone(),
&sess.services.session_extension_data,
&sess.services.thread_extension_data,
turn_context.extension_data.as_ref(),
)
.or_cancel(cancellation_token)
.await
.ok()?;
items.extend(contributed_items);
}
Some(items)
}
async fn track_turn_resolved_config_analytics(
sess: &Session,
turn_context: &TurnContext,
@@ -2,6 +2,7 @@ use std::future::Future;
use std::sync::Arc;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::TokenUsageInfo;
use codex_tools::ToolCall;
@@ -12,6 +13,7 @@ use crate::ExtensionData;
mod prompt;
mod thread_lifecycle;
mod tool_lifecycle;
mod turn_input;
mod turn_lifecycle;
pub use prompt::PromptFragment;
@@ -25,6 +27,8 @@ pub use tool_lifecycle::ToolCallSource;
pub use tool_lifecycle::ToolFinishInput;
pub use tool_lifecycle::ToolLifecycleFuture;
pub use tool_lifecycle::ToolStartInput;
pub use turn_input::TurnInputContext;
pub use turn_input::TurnInputEnvironment;
pub use turn_lifecycle::TurnAbortInput;
pub use turn_lifecycle::TurnErrorInput;
pub use turn_lifecycle::TurnStartInput;
@@ -85,6 +89,25 @@ pub trait TurnLifecycleContributor: Send + Sync {
async fn on_turn_error(&self, _input: TurnErrorInput<'_>) {}
}
/// WARNING: DO NOT USE YET
/// Extension contribution that can add turn-local model input.
///
/// Implementations should resolve only the model-visible input they own and
/// must preserve authority boundaries for external resources. Expensive or
/// host-specific dependencies belong on the extension value installed by the
/// host, not in this input.
#[async_trait::async_trait]
pub trait TurnInputContributor: Send + Sync {
/// Returns additional model input items for one submitted turn.
async fn contribute(
&self,
input: TurnInputContext,
session_store: &ExtensionData,
thread_store: &ExtensionData,
turn_store: &ExtensionData,
) -> Vec<ResponseItem>;
}
/// Contributor for host-owned configuration changes.
///
/// Implementations should treat the supplied values as immutable before/after
@@ -0,0 +1,25 @@
use std::path::PathBuf;
use codex_protocol::user_input::UserInput;
/// Host-owned turn environment summary visible to turn-input contributors.
#[derive(Debug, Clone)]
pub struct TurnInputEnvironment {
/// Stable host environment id used to route executor-scoped capabilities.
pub environment_id: String,
/// Effective working directory for this turn in the environment.
pub cwd: PathBuf,
/// Whether this is the primary environment for the turn.
pub is_primary: bool,
}
/// Turn facts supplied before the host records turn-local model input items.
#[derive(Debug, Clone)]
pub struct TurnInputContext {
/// Stable host-owned turn identifier.
pub turn_id: String,
/// User input submitted for this turn.
pub user_input: Vec<UserInput>,
/// Resolved turn environments, in host priority order.
pub environments: Vec<TurnInputEnvironment>,
}
+4
View File
@@ -10,6 +10,7 @@ pub use capabilities::NoopExtensionEventSink;
pub use capabilities::NoopResponseItemInjector;
pub use capabilities::ResponseItemInjectionFuture;
pub use capabilities::ResponseItemInjector;
pub use codex_protocol::models::ResponseItem;
pub use codex_tools::ConversationHistory;
pub use codex_tools::ExtensionTurnItem;
pub use codex_tools::FunctionCallError;
@@ -46,6 +47,9 @@ pub use contributors::ToolLifecycleFuture;
pub use contributors::ToolStartInput;
pub use contributors::TurnAbortInput;
pub use contributors::TurnErrorInput;
pub use contributors::TurnInputContext;
pub use contributors::TurnInputContributor;
pub use contributors::TurnInputEnvironment;
pub use contributors::TurnItemContributor;
pub use contributors::TurnLifecycleContributor;
pub use contributors::TurnStartInput;
@@ -12,6 +12,7 @@ use crate::ThreadLifecycleContributor;
use crate::TokenUsageContributor;
use crate::ToolContributor;
use crate::ToolLifecycleContributor;
use crate::TurnInputContributor;
use crate::TurnItemContributor;
use crate::TurnLifecycleContributor;
@@ -23,6 +24,7 @@ pub struct ExtensionRegistryBuilder<C: Sync> {
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
context_contributors: Vec<Arc<dyn ContextContributor>>,
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
tool_contributors: Vec<Arc<dyn ToolContributor>>,
tool_lifecycle_contributors: Vec<Arc<dyn ToolLifecycleContributor>>,
turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
@@ -39,6 +41,7 @@ impl<C: Sync> Default for ExtensionRegistryBuilder<C> {
token_usage_contributors: Vec::new(),
approval_review_contributors: Vec::new(),
context_contributors: Vec::new(),
turn_input_contributors: Vec::new(),
tool_contributors: Vec::new(),
tool_lifecycle_contributors: Vec::new(),
turn_item_contributors: Vec::new(),
@@ -98,6 +101,11 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
self.context_contributors.push(contributor);
}
/// Registers one turn-input contributor.
pub fn turn_input_contributor(&mut self, contributor: Arc<dyn TurnInputContributor>) {
self.turn_input_contributors.push(contributor);
}
/// Registers one native tool contributor.
pub fn tool_contributor(&mut self, contributor: Arc<dyn ToolContributor>) {
self.tool_contributors.push(contributor);
@@ -123,6 +131,7 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
token_usage_contributors: self.token_usage_contributors,
approval_review_contributors: self.approval_review_contributors,
context_contributors: self.context_contributors,
turn_input_contributors: self.turn_input_contributors,
tool_contributors: self.tool_contributors,
tool_lifecycle_contributors: self.tool_lifecycle_contributors,
turn_item_contributors: self.turn_item_contributors,
@@ -138,6 +147,7 @@ pub struct ExtensionRegistry<C: Sync> {
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
context_contributors: Vec<Arc<dyn ContextContributor>>,
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
tool_contributors: Vec<Arc<dyn ToolContributor>>,
tool_lifecycle_contributors: Vec<Arc<dyn ToolLifecycleContributor>>,
turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
@@ -195,6 +205,11 @@ impl<C: Sync> ExtensionRegistry<C> {
&self.context_contributors
}
/// Returns the registered turn-input contributors.
pub fn turn_input_contributors(&self) -> &[Arc<dyn TurnInputContributor>] {
&self.turn_input_contributors
}
/// Returns the registered native tool contributors.
pub fn tool_contributors(&self) -> &[Arc<dyn ToolContributor>] {
&self.tool_contributors