mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: add thread lifecycle contributor hooks (#22476)
## Why Extensions that need thread-scoped state currently only get a start-time callback. That is enough for seeding stores, but it leaves the host without a shared extension seam for later thread rehydrate and flush work as thread ownership evolves. This PR turns that start-only seam into a host-owned thread lifecycle contributor contract so extension-private state can stay behind the extension API instead of leaking extra orchestration through core. ## What changed - Replaced `ThreadStartContributor` with `ThreadLifecycleContributor` and added typed lifecycle inputs for thread start, resume, and stop. The contract lives in [`contributors/thread_lifecycle.rs`](https://github.com/openai/codex/blob/d0e9211f70e58d6b07ef07e84f359d1b9aa25955/codex-rs/ext/extension-api/src/contributors/thread_lifecycle.rs#L1-L64). - Kept the existing start-time behavior intact by routing session construction through `on_thread_start`. - Invoked `on_thread_stop` during session shutdown before thread-scoped extension state is dropped, while isolating contributor failures behind warning logs. - Migrated `git-attribution` and `guardian` onto the lifecycle registration path. - Renamed the extension registry plumbing from start-specific contributors to lifecycle-specific contributors. ## Notes `on_thread_resume` is introduced at the API boundary here so extensions can target the final lifecycle shape; host resume dispatch can be wired where that runtime path is finalized.
This commit is contained in:
committed by
GitHub
Unverified
parent
7fbd342fb3
commit
5ab7e6b4c6
Generated
-1
@@ -2918,7 +2918,6 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
]
|
||||
|
||||
|
||||
@@ -140,6 +140,22 @@ impl CodexThread {
|
||||
self.codex.session_loop_termination.clone().await;
|
||||
}
|
||||
|
||||
pub(crate) fn emit_thread_resume_lifecycle(&self) {
|
||||
for contributor in self
|
||||
.codex
|
||||
.session
|
||||
.services
|
||||
.extensions
|
||||
.thread_lifecycle_contributors()
|
||||
{
|
||||
contributor.on_thread_resume(codex_extension_api::ThreadResumeInput {
|
||||
thread_id: self.codex.session.conversation_id,
|
||||
session_store: &self.codex.session.services.session_extension_data,
|
||||
thread_store: &self.codex.session.services.thread_extension_data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn apply_goal_resume_runtime_effects(&self) -> anyhow::Result<()> {
|
||||
self.codex
|
||||
.session
|
||||
|
||||
@@ -643,6 +643,14 @@ pub async fn shutdown(sess: &Arc<Session>, sub_id: String) -> bool {
|
||||
&[],
|
||||
);
|
||||
|
||||
for contributor in sess.services.extensions.thread_lifecycle_contributors() {
|
||||
contributor.on_thread_stop(codex_extension_api::ThreadStopInput {
|
||||
thread_id: sess.conversation_id,
|
||||
session_store: &sess.services.session_extension_data,
|
||||
thread_store: &sess.services.thread_extension_data,
|
||||
});
|
||||
}
|
||||
|
||||
// Gracefully flush and shutdown thread persistence on session end so tests
|
||||
// that inspect durable state do not race with the background writer.
|
||||
if let Some(live_thread) = sess.live_thread()
|
||||
|
||||
@@ -813,13 +813,13 @@ impl Session {
|
||||
let agent_control = agent_control.with_session_id(session_id);
|
||||
let session_extension_data = codex_extension_api::ExtensionData::new();
|
||||
let thread_extension_data = codex_extension_api::ExtensionData::new();
|
||||
for contributor in extensions.thread_start_contributors() {
|
||||
contributor.contribute(
|
||||
for contributor in extensions.thread_lifecycle_contributors() {
|
||||
contributor.on_thread_start(codex_extension_api::ThreadStartInput {
|
||||
thread_id,
|
||||
config.as_ref(),
|
||||
&session_extension_data,
|
||||
&thread_extension_data,
|
||||
);
|
||||
config: config.as_ref(),
|
||||
session_store: &session_extension_data,
|
||||
thread_store: &thread_extension_data,
|
||||
});
|
||||
}
|
||||
|
||||
let services = SessionServices {
|
||||
|
||||
@@ -1227,10 +1227,11 @@ impl ThreadManagerState {
|
||||
let new_thread = self
|
||||
.finalize_thread_spawn(codex, thread_id, tracked_session_source)
|
||||
.await?;
|
||||
if is_resumed_thread
|
||||
&& let Err(err) = new_thread.thread.apply_goal_resume_runtime_effects().await
|
||||
{
|
||||
warn!("failed to apply goal resume runtime effects: {err}");
|
||||
if is_resumed_thread {
|
||||
new_thread.thread.emit_thread_resume_lifecycle();
|
||||
if let Err(err) = new_thread.thread.apply_goal_resume_runtime_effects().await {
|
||||
warn!("failed to apply goal resume runtime effects: {err}");
|
||||
}
|
||||
}
|
||||
Ok(new_thread)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
|
||||
use crate::ExtensionData;
|
||||
|
||||
mod prompt;
|
||||
mod thread_lifecycle;
|
||||
mod tools;
|
||||
|
||||
pub use prompt::PromptFragment;
|
||||
pub use prompt::PromptSlot;
|
||||
pub use thread_lifecycle::ThreadResumeInput;
|
||||
pub use thread_lifecycle::ThreadStartInput;
|
||||
pub use thread_lifecycle::ThreadStopInput;
|
||||
pub use tools::ExtensionToolExecutor;
|
||||
pub use tools::ExtensionToolFuture;
|
||||
pub use tools::ExtensionToolOutput;
|
||||
|
||||
/// Contributor that receives the live thread id and host-owned thread-start
|
||||
/// input before later contributors read from extension stores.
|
||||
pub trait ThreadStartContributor<C>: Send + Sync {
|
||||
fn contribute(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
input: &C,
|
||||
session_store: &ExtensionData,
|
||||
thread_store: &ExtensionData,
|
||||
);
|
||||
}
|
||||
|
||||
/// Extension contribution that adds prompt fragments during prompt assembly.
|
||||
pub trait ContextContributor: Send + Sync {
|
||||
fn contribute(
|
||||
@@ -37,6 +28,23 @@ pub trait ContextContributor: Send + Sync {
|
||||
) -> Vec<PromptFragment>;
|
||||
}
|
||||
|
||||
/// Contributor for host-owned thread lifecycle gates.
|
||||
///
|
||||
/// Implementations should use these callbacks to seed, rehydrate, or flush
|
||||
/// extension-private thread state. Heavy dependencies belong on the extension
|
||||
/// value created by the host, not in these inputs.
|
||||
pub trait ThreadLifecycleContributor<C>: Send + Sync {
|
||||
/// Called after thread-scoped extension stores are created, before later
|
||||
/// contributors can read from them.
|
||||
fn on_thread_start(&self, _input: ThreadStartInput<'_, C>) {}
|
||||
|
||||
/// Called after the host constructs a runtime from persisted history.
|
||||
fn on_thread_resume(&self, _input: ThreadResumeInput<'_>) {}
|
||||
|
||||
/// Called before the host drops the thread runtime and thread-scoped store.
|
||||
fn on_thread_stop(&self, _input: ThreadStopInput<'_>) {}
|
||||
}
|
||||
|
||||
/// Extension contribution that exposes native tools owned by a feature.
|
||||
pub trait ToolContributor: Send + Sync {
|
||||
/// Returns the native tools visible for the supplied extension stores.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
use crate::ExtensionData;
|
||||
|
||||
/// Input supplied when the host starts a runtime for a thread.
|
||||
pub struct ThreadStartInput<'a, C> {
|
||||
/// Identifier for the thread whose runtime is starting.
|
||||
pub thread_id: ThreadId,
|
||||
/// Host configuration visible at thread start.
|
||||
pub config: &'a C,
|
||||
/// Store scoped to the host session runtime.
|
||||
pub session_store: &'a ExtensionData,
|
||||
/// Store scoped to this thread runtime.
|
||||
pub thread_store: &'a ExtensionData,
|
||||
}
|
||||
|
||||
/// Input supplied when the host resumes an existing thread.
|
||||
pub struct ThreadResumeInput<'a> {
|
||||
/// Identifier for the thread being resumed.
|
||||
pub thread_id: ThreadId,
|
||||
/// Store scoped to the host session runtime.
|
||||
pub session_store: &'a ExtensionData,
|
||||
/// Store scoped to this thread runtime.
|
||||
pub thread_store: &'a ExtensionData,
|
||||
}
|
||||
|
||||
/// Input supplied when the host stops a thread runtime.
|
||||
pub struct ThreadStopInput<'a> {
|
||||
/// Identifier for the thread whose runtime is stopping.
|
||||
pub thread_id: ThreadId,
|
||||
/// Store scoped to the host session runtime.
|
||||
pub session_store: &'a ExtensionData,
|
||||
/// Store scoped to this thread runtime.
|
||||
pub thread_store: &'a ExtensionData,
|
||||
}
|
||||
@@ -21,7 +21,10 @@ pub use contributors::ExtensionToolFuture;
|
||||
pub use contributors::ExtensionToolOutput;
|
||||
pub use contributors::PromptFragment;
|
||||
pub use contributors::PromptSlot;
|
||||
pub use contributors::ThreadStartContributor;
|
||||
pub use contributors::ThreadLifecycleContributor;
|
||||
pub use contributors::ThreadResumeInput;
|
||||
pub use contributors::ThreadStartInput;
|
||||
pub use contributors::ThreadStopInput;
|
||||
pub use contributors::ToolContributor;
|
||||
pub use contributors::TurnItemContributionFuture;
|
||||
pub use contributors::TurnItemContributor;
|
||||
|
||||
@@ -4,13 +4,13 @@ use crate::ApprovalReviewContributor;
|
||||
use crate::ApprovalReviewFuture;
|
||||
use crate::ContextContributor;
|
||||
use crate::ExtensionData;
|
||||
use crate::ThreadStartContributor;
|
||||
use crate::ThreadLifecycleContributor;
|
||||
use crate::ToolContributor;
|
||||
use crate::TurnItemContributor;
|
||||
|
||||
/// Mutable registry used while hosts register typed runtime contributions.
|
||||
pub struct ExtensionRegistryBuilder<C> {
|
||||
thread_start_contributors: Vec<Arc<dyn ThreadStartContributor<C>>>,
|
||||
thread_lifecycle_contributors: Vec<Arc<dyn ThreadLifecycleContributor<C>>>,
|
||||
context_contributors: Vec<Arc<dyn ContextContributor>>,
|
||||
tool_contributors: Vec<Arc<dyn ToolContributor>>,
|
||||
turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
|
||||
@@ -20,7 +20,7 @@ pub struct ExtensionRegistryBuilder<C> {
|
||||
impl<C> Default for ExtensionRegistryBuilder<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
thread_start_contributors: Vec::new(),
|
||||
thread_lifecycle_contributors: Vec::new(),
|
||||
approval_review_contributors: Vec::new(),
|
||||
context_contributors: Vec::new(),
|
||||
tool_contributors: Vec::new(),
|
||||
@@ -40,9 +40,12 @@ impl<C> ExtensionRegistryBuilder<C> {
|
||||
self.approval_review_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Registers one thread-start contributor.
|
||||
pub fn thread_start_contributor(&mut self, contributor: Arc<dyn ThreadStartContributor<C>>) {
|
||||
self.thread_start_contributors.push(contributor);
|
||||
/// Registers one thread-lifecycle contributor.
|
||||
pub fn thread_lifecycle_contributor(
|
||||
&mut self,
|
||||
contributor: Arc<dyn ThreadLifecycleContributor<C>>,
|
||||
) {
|
||||
self.thread_lifecycle_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Registers one prompt contributor.
|
||||
@@ -63,7 +66,7 @@ impl<C> ExtensionRegistryBuilder<C> {
|
||||
/// Finishes construction and returns the immutable registry.
|
||||
pub fn build(self) -> ExtensionRegistry<C> {
|
||||
ExtensionRegistry {
|
||||
thread_start_contributors: self.thread_start_contributors,
|
||||
thread_lifecycle_contributors: self.thread_lifecycle_contributors,
|
||||
approval_review_contributors: self.approval_review_contributors,
|
||||
context_contributors: self.context_contributors,
|
||||
tool_contributors: self.tool_contributors,
|
||||
@@ -74,7 +77,7 @@ impl<C> ExtensionRegistryBuilder<C> {
|
||||
|
||||
/// Immutable typed registry produced after extensions are installed.
|
||||
pub struct ExtensionRegistry<C> {
|
||||
thread_start_contributors: Vec<Arc<dyn ThreadStartContributor<C>>>,
|
||||
thread_lifecycle_contributors: Vec<Arc<dyn ThreadLifecycleContributor<C>>>,
|
||||
context_contributors: Vec<Arc<dyn ContextContributor>>,
|
||||
tool_contributors: Vec<Arc<dyn ToolContributor>>,
|
||||
turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
|
||||
@@ -82,9 +85,9 @@ pub struct ExtensionRegistry<C> {
|
||||
}
|
||||
|
||||
impl<C> ExtensionRegistry<C> {
|
||||
/// Returns the registered thread-start contributors.
|
||||
pub fn thread_start_contributors(&self) -> &[Arc<dyn ThreadStartContributor<C>>] {
|
||||
&self.thread_start_contributors
|
||||
/// Returns the registered thread-lifecycle contributors.
|
||||
pub fn thread_lifecycle_contributors(&self) -> &[Arc<dyn ThreadLifecycleContributor<C>>] {
|
||||
&self.thread_lifecycle_contributors
|
||||
}
|
||||
|
||||
/// Claims the first rendered approval-review prompt accepted by an
|
||||
|
||||
@@ -16,7 +16,6 @@ workspace = true
|
||||
codex-core = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -5,9 +5,9 @@ use codex_extension_api::ContextContributor;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::PromptFragment;
|
||||
use codex_extension_api::ThreadStartContributor;
|
||||
use codex_extension_api::ThreadLifecycleContributor;
|
||||
use codex_extension_api::ThreadStartInput;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex <noreply@openai.com>";
|
||||
|
||||
@@ -40,17 +40,11 @@ struct GitAttributionConfig {
|
||||
prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl ThreadStartContributor<Config> for GitAttributionExtension {
|
||||
fn contribute(
|
||||
&self,
|
||||
_thread_id: ThreadId,
|
||||
config: &Config,
|
||||
_session_store: &ExtensionData,
|
||||
thread_store: &ExtensionData,
|
||||
) {
|
||||
thread_store.insert(GitAttributionConfig {
|
||||
enabled: config.features.enabled(Feature::CodexGitCommit),
|
||||
prompt: config.commit_attribution.clone(),
|
||||
impl ThreadLifecycleContributor<Config> for GitAttributionExtension {
|
||||
fn on_thread_start(&self, input: ThreadStartInput<'_, Config>) {
|
||||
input.thread_store.insert(GitAttributionConfig {
|
||||
enabled: input.config.features.enabled(Feature::CodexGitCommit),
|
||||
prompt: input.config.commit_attribution.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -58,7 +52,7 @@ impl ThreadStartContributor<Config> for GitAttributionExtension {
|
||||
/// Installs the git-attribution contributors into the extension registry.
|
||||
pub fn install(registry: &mut ExtensionRegistryBuilder<Config>) {
|
||||
let extension = Arc::new(GitAttributionExtension);
|
||||
registry.thread_start_contributor(extension.clone());
|
||||
registry.thread_lifecycle_contributor(extension.clone());
|
||||
registry.prompt_contributor(extension);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::sync::Arc;
|
||||
use codex_core::config::Config;
|
||||
use codex_extension_api::AgentSpawnFuture;
|
||||
use codex_extension_api::AgentSpawner;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::ThreadStartContributor;
|
||||
use codex_extension_api::ThreadLifecycleContributor;
|
||||
use codex_extension_api::ThreadStartInput;
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
/// Guardian extension dependencies supplied by the host at construction time.
|
||||
@@ -47,19 +47,13 @@ impl GuardianThreadContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> ThreadStartContributor<Config> for GuardianExtension<S>
|
||||
impl<S> ThreadLifecycleContributor<Config> for GuardianExtension<S>
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
fn contribute(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
_input: &Config,
|
||||
_session_store: &ExtensionData,
|
||||
thread_store: &ExtensionData,
|
||||
) {
|
||||
thread_store.insert(GuardianThreadContext {
|
||||
forked_from_thread_id: thread_id,
|
||||
fn on_thread_start(&self, input: ThreadStartInput<'_, Config>) {
|
||||
input.thread_store.insert(GuardianThreadContext {
|
||||
forked_from_thread_id: input.thread_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -69,5 +63,5 @@ pub fn install<S>(registry: &mut ExtensionRegistryBuilder<Config>, agent_spawner
|
||||
where
|
||||
S: Send + Sync + 'static,
|
||||
{
|
||||
registry.thread_start_contributor(Arc::new(GuardianExtension::new(agent_spawner)));
|
||||
registry.thread_lifecycle_contributor(Arc::new(GuardianExtension::new(agent_spawner)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user