nanocodex-agent 0.3.0

Owned OpenAI agent lifecycle for Nanocodex
Documentation
use super::*;

pub(super) struct ModelSessionState {
    pub(super) workspace: String,
    pub(super) tools: ToolRuntime,
    pub(super) factory: ResponsesAttemptFactory,
    pub(super) conversation: ConversationState,
    pub(super) context: ContextState,
    pub(super) preserve_inherited_delta: bool,
}

impl ModelSessionState {
    pub(super) fn validate_workspace(&self, requested: Option<&str>) -> Result<()> {
        let Some(requested) = requested else {
            return Ok(());
        };
        if requested != self.workspace {
            return Err(NanocodexError::WorkspaceChanged {
                current: self.workspace.clone(),
                requested: requested.to_owned(),
            });
        }
        Ok(())
    }
}

#[derive(Clone, Copy, Eq, PartialEq)]
pub(super) struct ContinuationPolicy {
    pub(super) thinking: Thinking,
    pub(super) fast_mode: bool,
}

#[derive(Clone)]
pub(super) struct ConversationState {
    pub(super) canonical_context: Arc<ResponseItem>,
    pub(super) managed: ManagedSessionState,
    pub(super) continuation_policy: Option<ContinuationPolicy>,
}

impl ConversationState {
    pub(super) fn empty(canonical_context: ResponseItem) -> Self {
        Self {
            canonical_context: Arc::new(canonical_context),
            managed: ManagedSessionState::new(Vec::new()),
            continuation_policy: None,
        }
    }

    pub(super) fn new(history: Vec<ResponseItem>) -> Result<Self> {
        let canonical_context = history
            .iter()
            .find(|item| item.is_user_message())
            .cloned()
            .ok_or(NanocodexError::MalformedResponse {
                detail: "task input did not include initial context",
            })?;
        Ok(Self {
            canonical_context: Arc::new(canonical_context),
            managed: ManagedSessionState::new(history),
            continuation_policy: None,
        })
    }

    pub(super) fn resume(
        mut canonical_context: ResponseItem,
        history: Vec<ResponseItem>,
    ) -> Result<Self> {
        if !canonical_context.is_user_message() {
            return Err(NanocodexError::InvalidSessionSnapshot(
                "canonical context must be a user message".to_owned(),
            ));
        }
        assign_missing_response_item_id(&mut canonical_context);
        let managed = ManagedSessionState::resume(history)
            .map_err(|error| NanocodexError::InvalidSessionSnapshot(error.to_string()))?;
        Ok(Self {
            canonical_context: Arc::new(canonical_context),
            managed,
            continuation_policy: None,
        })
    }

    pub(super) fn flattened_history(&self) -> Vec<ResponseItem> {
        self.managed.flattened_history()
    }

    pub(super) fn clear_delta(&mut self) {
        self.managed.clear_delta();
    }

    pub(super) fn append(&mut self, items: impl IntoIterator<Item = ResponseItem>) {
        self.managed.append(items);
    }

    pub(super) fn update_token_info(&mut self, usage: Option<&Usage>) {
        self.managed.update_token_info(usage);
    }

    pub(super) const fn observe_server_reasoning(&mut self, included: bool) {
        self.managed.observe_server_reasoning(included);
    }

    pub(super) fn active_context_tokens(&self) -> u64 {
        self.managed.active_context_tokens()
    }

    pub(super) fn prompt_history(&self) -> nanocodex_oai_api::responses::ResponseHistory {
        self.managed.prompt_history()
    }

    pub(super) fn prompt_history_with_repair(
        &self,
    ) -> (nanocodex_oai_api::responses::ResponseHistory, bool) {
        self.managed.prompt_history_with_repair()
    }

    pub(super) fn adopt_prompt_history(
        &mut self,
        history: nanocodex_oai_api::responses::ResponseHistory,
    ) {
        self.managed.adopt_prompt_history(history);
    }

    pub(super) fn shared_history(&self) -> nanocodex_oai_api::responses::ResponseHistory {
        self.managed.shared_history()
    }

    pub(super) const fn delta_start(&self) -> usize {
        self.managed.delta_start()
    }

    pub(super) fn previous_response_id(&self) -> Option<&str> {
        self.managed.previous_response_id()
    }

    pub(super) fn set_previous_response_id(&mut self, response_id: impl Into<String>) {
        self.managed.set_previous_response_id(response_id);
    }

    #[allow(dead_code, reason = "consumed by the native durability boundary only")]
    pub(super) const fn history_revision(&self) -> u64 {
        self.managed.history_revision()
    }

    pub(super) fn install_pre_turn_compaction(
        &mut self,
        item: ResponseItem,
        request_prefix: &[ResponseItem],
    ) {
        self.managed.install_compaction(item, [], request_prefix);
    }

    pub(super) fn install_mid_turn_compaction(
        &mut self,
        item: ResponseItem,
        canonical_developer_context: ResponseItem,
        canonical_context: ResponseItem,
        request_prefix: &[ResponseItem],
    ) {
        self.canonical_context = Arc::new(canonical_context.clone());
        let initial_context = [canonical_developer_context, canonical_context];
        self.managed
            .install_compaction(item, initial_context, request_prefix);
    }

    pub(super) fn append_canonical_context(
        &mut self,
        canonical_developer_context: ResponseItem,
        canonical_context: ResponseItem,
    ) {
        self.canonical_context = Arc::new(canonical_context.clone());
        self.managed
            .append([canonical_developer_context, canonical_context]);
    }

    pub(super) fn set_canonical_context(&mut self, canonical_context: ResponseItem) {
        self.canonical_context = Arc::new(canonical_context);
    }

    pub(super) fn reset_for_full_request(&mut self) {
        self.managed.reset_for_full_request();
    }

    pub(super) fn prepare_request_policy(&mut self, policy: ContinuationPolicy) {
        if self
            .continuation_policy
            .is_some_and(|previous| previous != policy)
        {
            self.reset_for_full_request();
        }
        self.continuation_policy = Some(policy);
    }

    pub(super) fn commit(&mut self) -> Result<()> {
        self.managed
            .commit()
            .map_err(|_| NanocodexError::MalformedResponse {
                detail: "completed turn did not have a response ID",
            })
    }

    pub(super) fn commit_interrupted(&mut self) {
        self.managed.commit_interrupted();
    }

    pub(super) fn commit_tail(&mut self) {
        self.managed.commit_tail();
    }
}