Codex Handbook
agent-graph-store/src/local.rs 323 lines
use codex_protocol::ThreadId;use codex_state::StateRuntime;use std::sync::Arc;use crate::AgentGraphStore;use crate::AgentGraphStoreError;use crate::AgentGraphStoreResult;use crate::ThreadSpawnEdgeStatus;/// SQLite-backed implementation of [`AgentGraphStore`] using an existing state runtime.#[derive(Clone)]pub struct LocalAgentGraphStore {    state_db: Arc<StateRuntime>,}impl std::fmt::Debug for LocalAgentGraphStore {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.debug_struct("LocalAgentGraphStore")            .field("codex_home", &self.state_db.codex_home())            .finish_non_exhaustive()    }}impl LocalAgentGraphStore {    /// Create a local graph store from an already-initialized state runtime.    pub fn new(state_db: Arc<StateRuntime>) -> Self {        Self { state_db }    }}impl AgentGraphStore for LocalAgentGraphStore {    async fn upsert_thread_spawn_edge(        &self,        parent_thread_id: ThreadId,        child_thread_id: ThreadId,        status: ThreadSpawnEdgeStatus,    ) -> AgentGraphStoreResult<()> {        self.state_db            .upsert_thread_spawn_edge(parent_thread_id, child_thread_id, to_state_status(status))            .await            .map_err(internal_error)    }    async fn set_thread_spawn_edge_status(        &self,        child_thread_id: ThreadId,        status: ThreadSpawnEdgeStatus,    ) -> AgentGraphStoreResult<()> {        self.state_db            .set_thread_spawn_edge_status(child_thread_id, to_state_status(status))            .await            .map_err(internal_error)    }    async fn list_thread_spawn_children(        &self,        parent_thread_id: ThreadId,        status_filter: Option<ThreadSpawnEdgeStatus>,    ) -> AgentGraphStoreResult<Vec<ThreadId>> {        if let Some(status) = status_filter {            return self                .state_db                .list_thread_spawn_children_with_status(parent_thread_id, to_state_status(status))                .await                .map_err(internal_error);        }        self.state_db            .list_thread_spawn_children(parent_thread_id)            .await            .map_err(internal_error)    }    async fn list_thread_spawn_descendants(        &self,        root_thread_id: ThreadId,        status_filter: Option<ThreadSpawnEdgeStatus>,    ) -> AgentGraphStoreResult<Vec<ThreadId>> {        match status_filter {            Some(status) => self                .state_db                .list_thread_spawn_descendants_with_status(root_thread_id, to_state_status(status))                .await                .map_err(internal_error),            None => self                .state_db                .list_thread_spawn_descendants(root_thread_id)                .await                .map_err(internal_error),        }    }}fn to_state_status(status: ThreadSpawnEdgeStatus) -> codex_state::DirectionalThreadSpawnEdgeStatus {    match status {        ThreadSpawnEdgeStatus::Open => codex_state::DirectionalThreadSpawnEdgeStatus::Open,        ThreadSpawnEdgeStatus::Closed => codex_state::DirectionalThreadSpawnEdgeStatus::Closed,    }}fn internal_error(err: impl std::fmt::Display) -> AgentGraphStoreError {    AgentGraphStoreError::Internal {        message: err.to_string(),    }}#[cfg(test)]mod tests {    use super::*;    use codex_state::DirectionalThreadSpawnEdgeStatus;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    struct TestRuntime {        state_db: Arc<StateRuntime>,        _codex_home: TempDir,    }    fn thread_id(suffix: u128) -> ThreadId {        ThreadId::from_string(&format!("00000000-0000-0000-0000-{suffix:012}"))            .expect("valid thread id")    }    async fn state_runtime() -> TestRuntime {        let codex_home = TempDir::new().expect("tempdir should be created");        let state_db =            StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string())                .await                .expect("state db should initialize");        TestRuntime {            state_db,            _codex_home: codex_home,        }    }    #[tokio::test]    async fn local_store_upserts_and_lists_direct_children_with_status_filters() {        let fixture = state_runtime().await;        let state_db = fixture.state_db;        let store = LocalAgentGraphStore::new(state_db.clone());        let parent_thread_id = thread_id(/*suffix*/ 1);        let first_child_thread_id = thread_id(/*suffix*/ 2);        let second_child_thread_id = thread_id(/*suffix*/ 3);        store            .upsert_thread_spawn_edge(                parent_thread_id,                second_child_thread_id,                ThreadSpawnEdgeStatus::Closed,            )            .await            .expect("closed child edge should insert");        store            .upsert_thread_spawn_edge(                parent_thread_id,                first_child_thread_id,                ThreadSpawnEdgeStatus::Open,            )            .await            .expect("open child edge should insert");        let all_children = store            .list_thread_spawn_children(parent_thread_id, /*status_filter*/ None)            .await            .expect("all children should load");        assert_eq!(            all_children,            vec![first_child_thread_id, second_child_thread_id]        );        let open_children = store            .list_thread_spawn_children(parent_thread_id, Some(ThreadSpawnEdgeStatus::Open))            .await            .expect("open children should load");        let state_open_children = state_db            .list_thread_spawn_children_with_status(                parent_thread_id,                DirectionalThreadSpawnEdgeStatus::Open,            )            .await            .expect("state open children should load");        assert_eq!(open_children, state_open_children);        assert_eq!(open_children, vec![first_child_thread_id]);        let closed_children = store            .list_thread_spawn_children(parent_thread_id, Some(ThreadSpawnEdgeStatus::Closed))            .await            .expect("closed children should load");        assert_eq!(closed_children, vec![second_child_thread_id]);    }    #[tokio::test]    async fn local_store_updates_edge_status() {        let fixture = state_runtime().await;        let state_db = fixture.state_db;        let store = LocalAgentGraphStore::new(state_db);        let parent_thread_id = thread_id(/*suffix*/ 10);        let child_thread_id = thread_id(/*suffix*/ 11);        store            .upsert_thread_spawn_edge(                parent_thread_id,                child_thread_id,                ThreadSpawnEdgeStatus::Open,            )            .await            .expect("child edge should insert");        store            .set_thread_spawn_edge_status(child_thread_id, ThreadSpawnEdgeStatus::Closed)            .await            .expect("child edge should close");        let open_children = store            .list_thread_spawn_children(parent_thread_id, Some(ThreadSpawnEdgeStatus::Open))            .await            .expect("open children should load");        assert_eq!(open_children, Vec::<ThreadId>::new());        let closed_children = store            .list_thread_spawn_children(parent_thread_id, Some(ThreadSpawnEdgeStatus::Closed))            .await            .expect("closed children should load");        assert_eq!(closed_children, vec![child_thread_id]);    }    #[tokio::test]    async fn local_store_lists_descendants_breadth_first_with_status_filters() {        let fixture = state_runtime().await;        let state_db = fixture.state_db;        let store = LocalAgentGraphStore::new(state_db.clone());        let root_thread_id = thread_id(/*suffix*/ 20);        let later_child_thread_id = thread_id(/*suffix*/ 22);        let earlier_child_thread_id = thread_id(/*suffix*/ 21);        let closed_grandchild_thread_id = thread_id(/*suffix*/ 23);        let open_grandchild_thread_id = thread_id(/*suffix*/ 24);        let closed_child_thread_id = thread_id(/*suffix*/ 25);        let closed_great_grandchild_thread_id = thread_id(/*suffix*/ 26);        for (parent_thread_id, child_thread_id, status) in [            (                root_thread_id,                later_child_thread_id,                ThreadSpawnEdgeStatus::Open,            ),            (                root_thread_id,                earlier_child_thread_id,                ThreadSpawnEdgeStatus::Open,            ),            (                earlier_child_thread_id,                open_grandchild_thread_id,                ThreadSpawnEdgeStatus::Open,            ),            (                later_child_thread_id,                closed_grandchild_thread_id,                ThreadSpawnEdgeStatus::Closed,            ),            (                root_thread_id,                closed_child_thread_id,                ThreadSpawnEdgeStatus::Closed,            ),            (                closed_child_thread_id,                closed_great_grandchild_thread_id,                ThreadSpawnEdgeStatus::Closed,            ),        ] {            store                .upsert_thread_spawn_edge(parent_thread_id, child_thread_id, status)                .await                .expect("edge should insert");        }        let all_descendants = store            .list_thread_spawn_descendants(root_thread_id, /*status_filter*/ None)            .await            .expect("all descendants should load");        assert_eq!(            all_descendants,            vec![                earlier_child_thread_id,                later_child_thread_id,                closed_child_thread_id,                closed_grandchild_thread_id,                open_grandchild_thread_id,                closed_great_grandchild_thread_id,            ]        );        let open_descendants = store            .list_thread_spawn_descendants(root_thread_id, Some(ThreadSpawnEdgeStatus::Open))            .await            .expect("open descendants should load");        let state_open_descendants = state_db            .list_thread_spawn_descendants_with_status(                root_thread_id,                DirectionalThreadSpawnEdgeStatus::Open,            )            .await            .expect("state open descendants should load");        assert_eq!(open_descendants, state_open_descendants);        assert_eq!(            open_descendants,            vec![                earlier_child_thread_id,                later_child_thread_id,                open_grandchild_thread_id,            ]        );        let closed_descendants = store            .list_thread_spawn_descendants(root_thread_id, Some(ThreadSpawnEdgeStatus::Closed))            .await            .expect("closed descendants should load");        assert_eq!(            closed_descendants,            vec![closed_child_thread_id, closed_great_grandchild_thread_id]        );    }}