Codex Handbook
ext/goal/src/api.rs 312 lines
use std::collections::HashMap;use std::fmt;use std::sync::Arc;use std::sync::Mutex;use std::sync::PoisonError;use std::sync::Weak;use codex_protocol::ThreadId;use codex_protocol::protocol::ThreadGoal;use codex_protocol::protocol::ThreadGoalStatus;use codex_protocol::protocol::validate_thread_goal_objective;use crate::runtime::GoalRuntimeHandle;use crate::runtime::PreviousGoalSnapshot;use crate::tool::fill_empty_thread_preview_if_possible;use crate::tool::protocol_goal_from_state;use crate::tool::state_status_from_protocol;use crate::tool::validate_goal_budget;#[derive(Debug, Clone, PartialEq, Eq)]pub enum GoalServiceError {    InvalidRequest(String),    Internal(String),}impl fmt::Display for GoalServiceError {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        match self {            Self::InvalidRequest(message) | Self::Internal(message) => f.write_str(message),        }    }}impl std::error::Error for GoalServiceError {}#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum GoalObjectiveUpdate<'a> {    Keep,    Set(&'a str),}#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum GoalTokenBudgetUpdate {    Keep,    Set(Option<i64>),}#[derive(Clone, Copy, Debug)]pub struct GoalSetRequest<'a> {    pub thread_id: ThreadId,    pub objective: GoalObjectiveUpdate<'a>,    pub status: Option<ThreadGoalStatus>,    pub token_budget: GoalTokenBudgetUpdate,}#[derive(Clone, Debug)]pub struct GoalSetOutcome {    pub goal: ThreadGoal,    state_goal: codex_state::ThreadGoal,    previous_goal: Option<PreviousGoalSnapshot>,}impl GoalSetOutcome {    pub async fn apply_runtime_effects(&self, goal_service: &GoalService) {        if let Some(runtime) = goal_service.runtime_for_thread(self.goal.thread_id)            && let Err(err) = runtime                .apply_external_goal_set(self.state_goal.clone(), self.previous_goal.clone())                .await        {            tracing::warn!("failed to apply external goal status runtime effects: {err}");        }    }}#[derive(Debug, Default)]pub struct GoalService {    runtimes: Mutex<HashMap<String, Weak<GoalRuntimeHandle>>>,}impl GoalService {    pub fn new() -> Self {        Self::default()    }    pub async fn get_thread_goal(        &self,        state_db: &codex_state::StateRuntime,        thread_id: ThreadId,    ) -> Result<Option<ThreadGoal>, GoalServiceError> {        state_db            .thread_goals()            .get_thread_goal(thread_id)            .await            .map(|goal| goal.map(protocol_goal_from_state))            .map_err(|err| GoalServiceError::Internal(format!("failed to read thread goal: {err}")))    }    pub async fn set_thread_goal(        &self,        state_db: &codex_state::StateRuntime,        request: GoalSetRequest<'_>,    ) -> Result<GoalSetOutcome, GoalServiceError> {        let GoalSetRequest {            thread_id,            objective,            status,            token_budget,        } = request;        let status = status.map(state_status_from_protocol);        let objective = match objective {            GoalObjectiveUpdate::Keep => None,            GoalObjectiveUpdate::Set(objective) => Some(objective.trim()),        };        let token_budget = match token_budget {            GoalTokenBudgetUpdate::Keep => None,            GoalTokenBudgetUpdate::Set(token_budget) => Some(token_budget),        };        if let Some(objective) = objective {            validate_thread_goal_objective(objective).map_err(GoalServiceError::InvalidRequest)?;        }        if objective.is_some() || token_budget.is_some() {            validate_goal_budget(token_budget.flatten())                .map_err(GoalServiceError::InvalidRequest)?;        }        let runtime = self.runtime_for_thread(thread_id);        // Hold this through the prepare/write window so idle continuation cannot        // launch from goal state that this external mutation is about to change.        let _goal_state_permit = match runtime.as_ref() {            Some(runtime) => Some(                runtime                    .goal_state_permit()                    .await                    .map_err(GoalServiceError::Internal)?,            ),            None => None,        };        if let Some(runtime) = runtime.as_ref()            && let Err(err) = runtime.prepare_external_goal_mutation().await        {            tracing::warn!("failed to prepare external goal mutation: {err}");        }        let (goal, previous_goal) = if let Some(objective) = objective {            let existing_goal = state_db                .thread_goals()                .get_thread_goal(thread_id)                .await                .map_err(|err| {                    GoalServiceError::Internal(format!("failed to read thread goal: {err}"))                })?;            if let Some(existing_goal) = existing_goal.as_ref() {                let previous_goal = PreviousGoalSnapshot::from(existing_goal);                state_db                    .thread_goals()                    .update_thread_goal(                        thread_id,                        codex_state::GoalUpdate {                            objective: Some(objective.to_string()),                            status,                            token_budget,                            expected_goal_id: Some(existing_goal.goal_id.clone()),                        },                    )                    .await                    .map_err(|err| {                        GoalServiceError::Internal(format!("failed to update thread goal: {err}"))                    })?                    .ok_or_else(|| {                        GoalServiceError::InvalidRequest(format!(                            "cannot update goal for thread {thread_id}: no goal exists"                        ))                    })                    .map(|goal| (goal, Some(previous_goal)))?            } else {                state_db                    .thread_goals()                    .replace_thread_goal(                        thread_id,                        objective,                        status.unwrap_or(codex_state::ThreadGoalStatus::Active),                        token_budget.flatten(),                    )                    .await                    .map_err(|err| {                        GoalServiceError::Internal(format!("failed to replace thread goal: {err}"))                    })                    .map(|goal| (goal, None))?            }        } else {            let existing_goal = state_db                .thread_goals()                .get_thread_goal(thread_id)                .await                .map_err(|err| {                    GoalServiceError::Internal(format!("failed to read thread goal: {err}"))                })?                .ok_or_else(|| {                    GoalServiceError::InvalidRequest(format!(                        "cannot update goal for thread {thread_id}: no goal exists"                    ))                })?;            let previous_goal = PreviousGoalSnapshot::from(&existing_goal);            let expected_goal_id = existing_goal.goal_id.clone();            state_db                .thread_goals()                .update_thread_goal(                    thread_id,                    codex_state::GoalUpdate {                        objective: None,                        status,                        token_budget,                        expected_goal_id: Some(expected_goal_id),                    },                )                .await                .map_err(|err| {                    GoalServiceError::Internal(format!("failed to update thread goal: {err}"))                })?                .ok_or_else(|| {                    GoalServiceError::InvalidRequest(format!(                        "cannot update goal for thread {thread_id}: no goal exists"                    ))                })                .map(|goal| (goal, Some(previous_goal)))?        };        if objective.is_some() {            fill_empty_thread_preview_if_possible(state_db, thread_id, &goal).await;        }        Ok(GoalSetOutcome {            goal: protocol_goal_from_state(goal.clone()),            state_goal: goal,            previous_goal,        })    }    pub async fn clear_thread_goal(        &self,        state_db: &codex_state::StateRuntime,        thread_id: ThreadId,    ) -> Result<bool, GoalServiceError> {        let runtime = self.runtime_for_thread(thread_id);        // Hold this through the prepare/write window so idle continuation cannot        // launch from goal state that this external mutation is about to change.        let goal_state_permit = match runtime.as_ref() {            Some(runtime) => Some(                runtime                    .goal_state_permit()                    .await                    .map_err(GoalServiceError::Internal)?,            ),            None => None,        };        if let Some(runtime) = runtime.as_ref()            && let Err(err) = runtime.prepare_external_goal_mutation().await        {            tracing::warn!("failed to prepare external goal mutation: {err}");        }        let cleared_goal = state_db            .thread_goals()            .delete_thread_goal(thread_id)            .await            .map_err(|err| {                GoalServiceError::Internal(format!("failed to clear thread goal: {err}"))            })?;        let cleared = cleared_goal.is_some();        drop(goal_state_permit);        drop(runtime);        if let (Some(runtime), Some(goal)) = (self.runtime_for_thread(thread_id), cleared_goal)            && let Err(err) = runtime.apply_external_goal_clear(goal).await        {            tracing::warn!("failed to apply external goal clear runtime effects: {err}");        }        Ok(cleared)    }    pub(crate) fn register_runtime(&self, runtime: &Arc<GoalRuntimeHandle>) {        self.runtimes()            .insert(runtime.thread_id().to_string(), Arc::downgrade(runtime));    }    pub(crate) fn unregister_runtime(&self, runtime: &Arc<GoalRuntimeHandle>) {        let key = runtime.thread_id().to_string();        let runtime = Arc::downgrade(runtime);        let mut runtimes = self.runtimes();        if runtimes            .get(&key)            .is_some_and(|registered| registered.ptr_eq(&runtime))        {            runtimes.remove(&key);        }    }    fn runtime_for_thread(&self, thread_id: ThreadId) -> Option<Arc<GoalRuntimeHandle>> {        let key = thread_id.to_string();        let mut runtimes = self.runtimes();        let runtime = runtimes.get(&key).and_then(Weak::upgrade);        if runtime.is_none() {            runtimes.remove(&key);        }        runtime    }    fn runtimes(&self) -> std::sync::MutexGuard<'_, HashMap<String, Weak<GoalRuntimeHandle>>> {        self.runtimes.lock().unwrap_or_else(PoisonError::into_inner)    }}