use std::sync::Arc; use std::sync::Weak; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadGoalUpdatedNotification; use codex_core::NewThread; use codex_core::StartThreadOptions; use codex_core::ThreadManager; use codex_core::config::Config; use codex_extension_api::AgentSpawnFuture; use codex_extension_api::AgentSpawner; use codex_extension_api::ExtensionEventSink; use codex_extension_api::ExtensionRegistry; use codex_extension_api::ExtensionRegistryBuilder; use codex_login::AuthManager; use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use crate::outgoing_message::OutgoingMessageSender; pub(crate) fn thread_extensions( guardian_agent_spawner: S, event_sink: Arc, auth_manager: Arc, ) -> Arc> where S: AgentSpawner + 'static, { let mut builder = ExtensionRegistryBuilder::::with_event_sink(event_sink); codex_guardian::install(&mut builder, guardian_agent_spawner); codex_memories_extension::install(&mut builder, codex_otel::global()); codex_web_search_extension::install(&mut builder, auth_manager.clone()); codex_image_generation_extension::install(&mut builder, auth_manager); Arc::new(builder.build()) } pub(crate) fn app_server_extension_event_sink( outgoing: Arc, ) -> Arc { Arc::new(AppServerExtensionEventSink { outgoing }) } struct AppServerExtensionEventSink { outgoing: Arc, } impl ExtensionEventSink for AppServerExtensionEventSink { fn emit(&self, event: Event) { match event.msg { EventMsg::ThreadGoalUpdated(thread_goal_event) => { self.outgoing .try_send_server_notification(ServerNotification::ThreadGoalUpdated( ThreadGoalUpdatedNotification { thread_id: thread_goal_event.thread_id.to_string(), turn_id: thread_goal_event.turn_id, goal: thread_goal_event.goal.into(), }, )); } msg => { tracing::debug!(event_id = %event.id, ?msg, "dropping unsupported extension event"); } } } } pub(crate) fn guardian_agent_spawner( thread_manager: Weak, ) -> impl AgentSpawner { move |forked_from_thread_id: ThreadId, options: StartThreadOptions| -> AgentSpawnFuture<'static, NewThread, CodexErr> { let thread_manager = thread_manager.clone(); Box::pin(async move { let thread_manager = thread_manager.upgrade().ok_or_else(|| { CodexErr::UnsupportedOperation("thread manager dropped".to_string()) })?; thread_manager .spawn_subagent(forked_from_thread_id, options) .await }) } } #[cfg(test)] mod tests { use std::time::Duration; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadGoal as AppServerThreadGoal; use codex_app_server_protocol::ThreadGoalStatus as AppServerThreadGoalStatus; use codex_protocol::protocol::ThreadGoal; use codex_protocol::protocol::ThreadGoalStatus; use codex_protocol::protocol::ThreadGoalUpdatedEvent; use pretty_assertions::assert_eq; use tokio::sync::mpsc; use tokio::time::timeout; use super::*; use crate::outgoing_message::OutgoingEnvelope; use crate::outgoing_message::OutgoingMessage; #[tokio::test] async fn app_server_event_sink_forwards_thread_goal_updates() { let (outgoing_tx, mut outgoing_rx) = mpsc::channel(4); let outgoing = Arc::new(OutgoingMessageSender::new( outgoing_tx, AnalyticsEventsClient::disabled(), )); let sink = app_server_extension_event_sink(outgoing); let thread_id = ThreadId::default(); sink.emit(Event { id: "call-1".to_string(), msg: EventMsg::ThreadGoalUpdated(ThreadGoalUpdatedEvent { thread_id, turn_id: Some("turn-1".to_string()), goal: ThreadGoal { thread_id, objective: "wire extension events".to_string(), status: ThreadGoalStatus::Active, token_budget: Some(123), tokens_used: 45, time_used_seconds: 6, created_at: 7, updated_at: 8, }, }), }); let envelope = timeout(Duration::from_secs(1), outgoing_rx.recv()) .await .expect("timed out waiting for forwarded extension event") .expect("outgoing channel closed unexpectedly"); let OutgoingEnvelope::Broadcast { message } = envelope else { panic!("expected broadcast notification"); }; let OutgoingMessage::AppServerNotification(ServerNotification::ThreadGoalUpdated( notification, )) = message else { panic!("expected thread goal updated notification"); }; assert_eq!( ThreadGoalUpdatedNotification { thread_id: thread_id.to_string(), turn_id: Some("turn-1".to_string()), goal: AppServerThreadGoal { thread_id: thread_id.to_string(), objective: "wire extension events".to_string(), status: AppServerThreadGoalStatus::Active, token_budget: Some(123), tokens_used: 45, time_used_seconds: 6, created_at: 7, updated_at: 8, }, }, notification ); } }