Codex Handbook
ext/goal/src/extension.rs 495 lines
use std::sync::Arc;use std::sync::Weak;use codex_analytics::AnalyticsEventsClient;use codex_core::ThreadManager;use codex_extension_api::ConfigContributor;use codex_extension_api::ExtensionData;use codex_extension_api::ExtensionEventSink;use codex_extension_api::ExtensionFuture;use codex_extension_api::ExtensionRegistryBuilder;use codex_extension_api::ThreadIdleInput;use codex_extension_api::ThreadLifecycleContributor;use codex_extension_api::ThreadResumeInput;use codex_extension_api::ThreadStartInput;use codex_extension_api::ThreadStopInput;use codex_extension_api::TokenUsageContributor;use codex_extension_api::ToolCallOutcome;use codex_extension_api::ToolContributor;use codex_extension_api::ToolFinishInput;use codex_extension_api::ToolLifecycleContributor;use codex_extension_api::ToolLifecycleFuture;use codex_extension_api::TurnAbortInput;use codex_extension_api::TurnErrorInput;use codex_extension_api::TurnLifecycleContributor;use codex_extension_api::TurnStartInput;use codex_extension_api::TurnStopInput;use codex_otel::MetricsClient;use codex_protocol::ThreadId;use codex_protocol::protocol::CodexErrorInfo;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use codex_protocol::protocol::ThreadGoalStatus;use codex_protocol::protocol::TokenUsageInfo;use crate::accounting::BudgetLimitedGoalDisposition;use crate::accounting::GoalAccountingState;use crate::analytics::GoalAnalytics;use crate::api::GoalService;use crate::events::GoalEventEmitter;use crate::metrics::GoalMetrics;use crate::runtime::ActiveGoalStopReason;use crate::runtime::GoalRuntimeConfig;use crate::runtime::GoalRuntimeHandle;use crate::spec::UPDATE_GOAL_TOOL_NAME;use crate::steering::budget_limit_steering_item;use crate::tool::GoalToolExecutor;#[derive(Clone, Debug)]pub struct GoalExtensionConfig {    pub enabled: bool,}impl GoalExtensionConfig {    fn from_enabled(enabled: bool) -> Self {        Self { enabled }    }}#[derive(Clone)]pub struct GoalExtension<C> {    state_dbs: Arc<codex_state::StateRuntime>,    analytics: GoalAnalytics,    event_emitter: GoalEventEmitter,    metrics: GoalMetrics,    thread_manager: Weak<ThreadManager>,    goal_service: Arc<GoalService>,    goals_enabled: Arc<dyn Fn(&C) -> bool + Send + Sync>,}impl<C> std::fmt::Debug for GoalExtension<C> {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.debug_struct("GoalExtension").finish_non_exhaustive()    }}impl<C> GoalExtension<C> {    pub(crate) fn new_with_host_capabilities(        state_dbs: Arc<codex_state::StateRuntime>,        analytics_events_client: AnalyticsEventsClient,        event_sink: Arc<dyn ExtensionEventSink>,        metrics_client: Option<MetricsClient>,        thread_manager: Weak<ThreadManager>,        goal_service: Arc<GoalService>,        goals_enabled: impl Fn(&C) -> bool + Send + Sync + 'static,    ) -> Self {        Self {            state_dbs,            analytics: GoalAnalytics::new(analytics_events_client),            event_emitter: GoalEventEmitter::new(event_sink),            metrics: GoalMetrics::new(metrics_client),            thread_manager,            goal_service,            goals_enabled: Arc::new(goals_enabled),        }    }}impl<C> ThreadLifecycleContributor<C> for GoalExtension<C>where    C: Send + Sync + 'static,{    fn on_thread_start<'a>(&'a self, input: ThreadStartInput<'a, C>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let enabled = (self.goals_enabled)(input.config);            let tools_available_for_thread = input.persistent_thread_state_available                && !matches!(                    input.session_source,                    SessionSource::SubAgent(SubAgentSource::Review)                );            input                .thread_store                .insert(GoalExtensionConfig::from_enabled(enabled));            let accounting_state = input                .thread_store                .get_or_init::<GoalAccountingState>(GoalAccountingState::default);            let Ok(thread_id) = ThreadId::from_string(input.thread_store.level_id()) else {                return;            };            let runtime = input.thread_store.get_or_init::<GoalRuntimeHandle>(|| {                GoalRuntimeHandle::new(                    thread_id,                    Arc::clone(&self.state_dbs),                    self.event_emitter.clone(),                    self.metrics.clone(),                    self.thread_manager.clone(),                    accounting_state,                    GoalRuntimeConfig {                        analytics: self.analytics.clone(),                        enabled,                        tools_available_for_thread,                    },                )            });            runtime.set_enabled(enabled);            self.goal_service.register_runtime(&runtime);        })    }    fn on_thread_resume<'a>(&'a self, input: ThreadResumeInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            if let Err(err) = runtime.restore_after_resume().await {                tracing::warn!(                    "failed to restore goal runtime after thread resume for {}: {err}",                    runtime.thread_id()                );            }        })    }    fn on_thread_idle<'a>(&'a self, input: ThreadIdleInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            if let Err(err) = runtime.continue_if_idle().await {                tracing::warn!(                    "failed to continue active goal for idle thread {}: {err}",                    runtime.thread_id()                );            }        })    }    fn on_thread_stop<'a>(&'a self, input: ThreadStopInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            if let Some(runtime) = goal_runtime_handle(input.thread_store) {                self.goal_service.unregister_runtime(&runtime);            }        })    }}impl<C> ConfigContributor<C> for GoalExtension<C>where    C: Send + Sync + 'static,{    fn on_config_changed(        &self,        _session_store: &ExtensionData,        thread_store: &ExtensionData,        _previous_config: &C,        new_config: &C,    ) {        let enabled = (self.goals_enabled)(new_config);        thread_store.insert(GoalExtensionConfig::from_enabled(enabled));        if let Some(runtime) = goal_runtime_handle(thread_store) {            runtime.set_enabled(enabled);        }    }}impl<C> TurnLifecycleContributor for GoalExtension<C>where    C: Send + Sync + 'static,{    fn on_turn_start<'a>(&'a self, input: TurnStartInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            if !runtime.is_enabled() {                return;            }            let accounting = runtime.accounting_state();            accounting.start_turn(                input.turn_id,                input.collaboration_mode.mode,                input.token_usage_at_turn_start,            );            if matches!(                input.collaboration_mode.mode,                codex_protocol::config_types::ModeKind::Plan            ) {                accounting.clear_current_turn_goal();                return;            }            let Ok(goal) = self                .state_dbs                .thread_goals()                .get_thread_goal(runtime.thread_id())                .await            else {                return;            };            if let Some(goal) = goal                && matches!(                    goal.status,                    codex_state::ThreadGoalStatus::Active                        | codex_state::ThreadGoalStatus::BudgetLimited                )            {                accounting.mark_turn_goal_active(input.turn_id, goal.goal_id);            }        })    }    fn on_turn_stop<'a>(&'a self, input: TurnStopInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            if !runtime.is_enabled() {                return;            }            let turn_id = input.turn_store.level_id();            if let Err(err) = runtime                .account_active_goal_progress(                    turn_id,                    &format!("{turn_id}:turn-stop"),                    codex_state::GoalAccountingMode::ActiveOnly,                    BudgetLimitedGoalDisposition::ClearActive,                )                .await            {                tracing::warn!(                    "failed to account active goal progress at turn stop for {turn_id}: {err}"                );                return;            }            runtime.accounting_state().finish_turn(turn_id);        })    }    fn on_turn_abort<'a>(&'a self, input: TurnAbortInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            if !runtime.is_enabled() {                return;            }            let turn_id = input.turn_store.level_id();            if let Err(err) = runtime                .account_active_goal_progress(                    turn_id,                    &format!("{turn_id}:turn-abort"),                    codex_state::GoalAccountingMode::ActiveOnly,                    BudgetLimitedGoalDisposition::ClearActive,                )                .await            {                tracing::warn!(                    "failed to account active goal progress after turn abort for {turn_id}: {err}"                );                return;            }            runtime.accounting_state().finish_turn(turn_id);        })    }    fn on_turn_error<'a>(&'a self, input: TurnErrorInput<'a>) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            let reason = match input.error {                CodexErrorInfo::UsageLimitExceeded => ActiveGoalStopReason::UsageLimit,                // The turn has ended because the error was non-retryable or its                // retries were exhausted. Block the goal to prevent automatic                // continuation from looping and consuming tokens, as can happen                // with compaction errors.                _ => ActiveGoalStopReason::TurnError,            };            if let Err(err) = runtime                .stop_active_goal_for_turn(input.turn_id, reason)                .await            {                tracing::warn!(                    error = ?input.error,                    "failed to stop active goal after turn error: {err}"                );            }        })    }}impl<C> TokenUsageContributor for GoalExtension<C>where    C: Send + Sync + 'static,{    fn on_token_usage<'a>(        &'a self,        _session_store: &'a ExtensionData,        thread_store: &'a ExtensionData,        turn_store: &'a ExtensionData,        token_usage: &'a TokenUsageInfo,    ) -> ExtensionFuture<'a, ()> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(thread_store) else {                return;            };            if !runtime.is_enabled() {                return;            }            let Some(_recorded) = runtime                .accounting_state()                .record_token_usage(turn_store.level_id(), &token_usage.total_token_usage)            else {                return;            };        })    }}impl<C> ToolLifecycleContributor for GoalExtension<C>where    C: Send + Sync + 'static,{    fn on_tool_finish<'a>(&'a self, input: ToolFinishInput<'a>) -> ToolLifecycleFuture<'a> {        Box::pin(async move {            let Some(runtime) = goal_runtime_handle(input.thread_store) else {                return;            };            let should_count_for_goal_progress = runtime.is_enabled()                && tool_attempt_counts_for_goal_progress(input.outcome)                && !(input.tool_name.namespace.is_none()                    && input.tool_name.name == UPDATE_GOAL_TOOL_NAME);            if !should_count_for_goal_progress {                return;            }            let turn_id = input.turn_id;            let progress = match runtime                .account_active_goal_progress(                    turn_id,                    input.call_id,                    codex_state::GoalAccountingMode::ActiveOnly,                    BudgetLimitedGoalDisposition::KeepActive,                )                .await            {                Ok(Some(progress)) => progress,                Ok(None) => return,                Err(err) => {                    tracing::warn!(                        "failed to account active goal progress after tool finish for {turn_id}: {err}"                    );                    return;                }            };            let goal = progress.goal;            if goal.status != ThreadGoalStatus::BudgetLimited {                return;            }            if !runtime                .accounting_state()                .mark_budget_limit_reported_if_new(progress.goal_id.as_str())            {                return;            }            let item = budget_limit_steering_item(&goal);            runtime.inject_active_turn_steering(item).await;        })    }}impl<C> ToolContributor for GoalExtension<C>where    C: Send + Sync + 'static,{    fn tools(        &self,        _session_store: &ExtensionData,        thread_store: &ExtensionData,    ) -> Vec<Arc<dyn codex_extension_api::ToolExecutor<codex_extension_api::ToolCall>>> {        let Some(runtime) = goal_runtime_handle(thread_store) else {            return Vec::new();        };        if !runtime.tools_visible() {            return Vec::new();        }        vec![            Arc::new(GoalToolExecutor::get(                runtime.thread_id(),                Arc::clone(&self.state_dbs),                runtime.accounting_state(),                self.analytics.clone(),                self.event_emitter.clone(),                self.metrics.clone(),            )),            Arc::new(GoalToolExecutor::create(                runtime.thread_id(),                Arc::clone(&self.state_dbs),                runtime.accounting_state(),                self.analytics.clone(),                self.event_emitter.clone(),                self.metrics.clone(),            )),            Arc::new(GoalToolExecutor::update(                runtime.thread_id(),                Arc::clone(&self.state_dbs),                runtime.accounting_state(),                self.analytics.clone(),                self.event_emitter.clone(),                self.metrics.clone(),            )),        ]    }}pub fn install_with_backend<C>(    registry: &mut ExtensionRegistryBuilder<C>,    state_dbs: Arc<codex_state::StateRuntime>,    analytics_events_client: AnalyticsEventsClient,    metrics_client: Option<MetricsClient>,    thread_manager: Weak<ThreadManager>,    goal_service: Arc<GoalService>,    goals_enabled: impl Fn(&C) -> bool + Send + Sync + 'static,) where    C: Send + Sync + 'static,{    let extension = Arc::new(GoalExtension::new_with_host_capabilities(        state_dbs,        analytics_events_client,        registry.event_sink(),        metrics_client,        thread_manager,        Arc::clone(&goal_service),        goals_enabled,    ));    registry.thread_lifecycle_contributor(extension.clone());    registry.config_contributor(extension.clone());    registry.turn_lifecycle_contributor(extension.clone());    registry.token_usage_contributor(extension.clone());    registry.tool_lifecycle_contributor(extension.clone());    registry.tool_contributor(extension);}fn goal_runtime_handle(thread_store: &ExtensionData) -> Option<Arc<GoalRuntimeHandle>> {    thread_store.get::<GoalRuntimeHandle>()}fn tool_attempt_counts_for_goal_progress(outcome: ToolCallOutcome) -> bool {    match outcome {        ToolCallOutcome::Completed { .. } => true,        ToolCallOutcome::Failed {            handler_executed: true,        } => true,        ToolCallOutcome::Blocked        | ToolCallOutcome::Failed {            handler_executed: false,        }        | ToolCallOutcome::Aborted => false,    }}