ktxd 0.1.1

A local Responses API compatibility proxy for Codex CLI and Chat Completions backends.
Documentation
use crate::domain::{Session, TurnOutcome, TurnRecord};
use crate::error::{ProxyError, Result};
use crate::ids::{ResponseId, TurnId};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use tokio::sync::RwLock;

#[async_trait]
pub trait SessionStore: Send + Sync {
    async fn get(&self, response_id: &ResponseId) -> Result<Option<Session>>;
    async fn put(&self, session: Session) -> Result<()>;
}

#[async_trait]
pub trait TurnRecordStore: Send + Sync {
    async fn put(&self, record: TurnRecord) -> Result<()>;
    async fn get(&self, turn_id: &TurnId) -> Result<Option<TurnRecord>>;
    async fn count(&self) -> usize;
}

#[derive(Debug, Default)]
pub struct MemoryStore {
    sessions: RwLock<BTreeMap<ResponseId, Session>>,
    responses: RwLock<BTreeMap<ResponseId, Value>>,
    turns: RwLock<BTreeMap<TurnId, TurnRecord>>,
}

impl MemoryStore {
    pub fn shared() -> Arc<Self> {
        Arc::new(Self::default())
    }

    pub async fn commit_completed(&self, session: Session, record: TurnRecord) -> Result<()> {
        if session.response_id != record.response_id {
            return Err(ProxyError::Internal(
                "session and turn response IDs do not match".to_string(),
            ));
        }
        if session.parent_response_id != record.parent_response_id {
            return Err(ProxyError::Internal(
                "session and turn parent response IDs do not match".to_string(),
            ));
        }
        if record.outcome != TurnOutcome::Completed {
            return Err(ProxyError::Internal(
                "completed commit requires a completed turn record".to_string(),
            ));
        }

        let mut sessions = self.sessions.write().await;
        let mut responses = self.responses.write().await;
        let mut turns = self.turns.write().await;
        if turns.contains_key(&record.turn_id) {
            return Err(ProxyError::Internal("duplicate turn record".to_string()));
        }
        if sessions.contains_key(&session.response_id)
            || responses.contains_key(&session.response_id)
        {
            return Err(ProxyError::Internal(
                "duplicate response record".to_string(),
            ));
        }
        responses.insert(
            session.response_id.clone(),
            session.final_response_json.clone(),
        );
        sessions.insert(session.response_id.clone(), session);
        turns.insert(record.turn_id.clone(), record);
        Ok(())
    }

    pub async fn commit_terminal(
        &self,
        response_id: ResponseId,
        response: Value,
        record: TurnRecord,
    ) -> Result<()> {
        if response_id != record.response_id {
            return Err(ProxyError::Internal(
                "response JSON and turn response IDs do not match".to_string(),
            ));
        }
        if record.outcome == TurnOutcome::Completed {
            return Err(ProxyError::Internal(
                "terminal commit cannot store a completed turn record".to_string(),
            ));
        }

        let sessions = self.sessions.read().await;
        let mut responses = self.responses.write().await;
        let mut turns = self.turns.write().await;
        if turns.contains_key(&record.turn_id) {
            return Err(ProxyError::Internal("duplicate turn record".to_string()));
        }
        if sessions.contains_key(&response_id) || responses.contains_key(&response_id) {
            return Err(ProxyError::Internal(
                "duplicate response record".to_string(),
            ));
        }
        responses.insert(response_id, response);
        turns.insert(record.turn_id.clone(), record);
        Ok(())
    }

    pub async fn get_response_json(&self, response_id: &ResponseId) -> Result<Option<Value>> {
        Ok(self.responses.read().await.get(response_id).cloned())
    }

    pub async fn put_response_json(&self, response_id: ResponseId, response: Value) -> Result<()> {
        self.responses.write().await.insert(response_id, response);
        Ok(())
    }
}

#[async_trait]
impl SessionStore for MemoryStore {
    async fn get(&self, response_id: &ResponseId) -> Result<Option<Session>> {
        Ok(self.sessions.read().await.get(response_id).cloned())
    }

    async fn put(&self, session: Session) -> Result<()> {
        self.sessions
            .write()
            .await
            .insert(session.response_id.clone(), session);
        Ok(())
    }
}

#[async_trait]
impl TurnRecordStore for MemoryStore {
    async fn put(&self, record: TurnRecord) -> Result<()> {
        let mut turns = self.turns.write().await;
        if turns.contains_key(&record.turn_id) {
            return Err(ProxyError::Internal("duplicate turn record".to_string()));
        }
        turns.insert(record.turn_id.clone(), record);
        Ok(())
    }

    async fn get(&self, turn_id: &TurnId) -> Result<Option<TurnRecord>> {
        Ok(self.turns.read().await.get(turn_id).cloned())
    }

    async fn count(&self) -> usize {
        self.turns.read().await.len()
    }
}