mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Inject agent graph store into ThreadManager (#29736)
Pick up the AgentGraphStore migration. - Inject an explicit optional agent graph store into `ThreadManager` - Move all calls to spawn, close, recursive resume, and subtree/archive/delete/feedback traversal through it - Keep using `LocalAgentGraphStore` when SQLite is available This required some changes to the interface to deal with futures: - The interface now matches `ThreadStore`'s object-safe pattern by returning a boxed `AgentGraphStoreFuture` directly, allowing `ThreadManager` to hold `Arc<dyn AgentGraphStore>` *Slight behavior change!* Unfiltered subtree enumeration now performs a single all-status breadth-first traversal, so a closed grandchild beneath an open edge is included; the previous Open-then-Closed traversals could not cross mixed-status paths and silently omitted it.
This commit is contained in:
@@ -36,7 +36,6 @@ use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -639,7 +638,7 @@ impl AgentControl {
|
||||
|
||||
async fn persist_thread_spawn_edge_for_source(
|
||||
&self,
|
||||
thread: &crate::CodexThread,
|
||||
child_thread: &crate::CodexThread,
|
||||
child_thread_id: ThreadId,
|
||||
session_source: Option<&SessionSource>,
|
||||
) {
|
||||
@@ -647,14 +646,20 @@ impl AgentControl {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(state_db_ctx) = thread.state_db() else {
|
||||
if child_thread.config_snapshot().await.ephemeral {
|
||||
return;
|
||||
}
|
||||
let Ok(state) = self.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = state_db_ctx
|
||||
let Some(agent_graph_store) = state.agent_graph_store() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = agent_graph_store
|
||||
.upsert_thread_spawn_edge(
|
||||
parent_thread_id,
|
||||
child_thread_id,
|
||||
DirectionalThreadSpawnEdgeStatus::Open,
|
||||
codex_agent_graph_store::ThreadSpawnEdgeStatus::Open,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -31,11 +31,12 @@ impl AgentControl {
|
||||
let known_agent = self.state.agent_metadata_for_thread(agent_id).is_some();
|
||||
match state.get_thread(agent_id).await {
|
||||
Ok(thread) => {
|
||||
if let Some(state_db_ctx) = thread.state_db()
|
||||
&& let Err(err) = state_db_ctx
|
||||
if !thread.config_snapshot().await.ephemeral
|
||||
&& let Some(agent_graph_store) = state.agent_graph_store()
|
||||
&& let Err(err) = agent_graph_store
|
||||
.set_thread_spawn_edge_status(
|
||||
agent_id,
|
||||
DirectionalThreadSpawnEdgeStatus::Closed,
|
||||
codex_agent_graph_store::ThreadSpawnEdgeStatus::Closed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -43,11 +44,11 @@ impl AgentControl {
|
||||
}
|
||||
}
|
||||
Err(CodexErr::ThreadNotFound(_)) if known_agent => {
|
||||
if let Some(state_db_ctx) = state.state_db()
|
||||
&& let Err(err) = state_db_ctx
|
||||
if let Some(agent_graph_store) = state.agent_graph_store()
|
||||
&& let Err(err) = agent_graph_store
|
||||
.set_thread_spawn_edge_status(
|
||||
agent_id,
|
||||
DirectionalThreadSpawnEdgeStatus::Closed,
|
||||
codex_agent_graph_store::ThreadSpawnEdgeStatus::Closed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -545,19 +545,16 @@ impl AgentControl {
|
||||
{
|
||||
return Ok(resumed_thread_id);
|
||||
}
|
||||
let Ok(resumed_thread) = state.get_thread(resumed_thread_id).await else {
|
||||
return Ok(resumed_thread_id);
|
||||
};
|
||||
let Some(state_db_ctx) = resumed_thread.state_db() else {
|
||||
let Some(agent_graph_store) = state.agent_graph_store() else {
|
||||
return Ok(resumed_thread_id);
|
||||
};
|
||||
|
||||
let mut resume_queue = VecDeque::from([(thread_id, root_depth)]);
|
||||
while let Some((parent_thread_id, parent_depth)) = resume_queue.pop_front() {
|
||||
let child_ids = match state_db_ctx
|
||||
.list_thread_spawn_children_with_status(
|
||||
let child_ids = match agent_graph_store
|
||||
.list_thread_spawn_children(
|
||||
parent_thread_id,
|
||||
DirectionalThreadSpawnEdgeStatus::Open,
|
||||
Some(codex_agent_graph_store::ThreadSpawnEdgeStatus::Open),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -613,7 +610,6 @@ impl AgentControl {
|
||||
session_source: SessionSource,
|
||||
) -> CodexResult<(ThreadId, MultiAgentVersion)> {
|
||||
let state = self.upgrade()?;
|
||||
let state_db_ctx = state.state_db();
|
||||
let stored_thread = state
|
||||
.read_stored_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
@@ -621,6 +617,14 @@ impl AgentControl {
|
||||
include_history: true,
|
||||
})
|
||||
.await?;
|
||||
let resumed_agent_path = stored_thread
|
||||
.agent_path
|
||||
.as_deref()
|
||||
.map(AgentPath::try_from)
|
||||
.transpose()
|
||||
.map_err(|err| CodexErr::InvalidRequest(format!("invalid stored agent path: {err}")))?;
|
||||
let resumed_agent_nickname = stored_thread.agent_nickname.clone();
|
||||
let resumed_agent_role = stored_thread.agent_role.clone();
|
||||
let history = stored_thread
|
||||
.history
|
||||
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?
|
||||
@@ -649,26 +653,15 @@ impl AgentControl {
|
||||
agent_path,
|
||||
agent_role: _,
|
||||
agent_nickname: _,
|
||||
}) => {
|
||||
let (resumed_agent_nickname, resumed_agent_role) =
|
||||
if let Some(state_db_ctx) = state_db_ctx.as_ref() {
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role),
|
||||
Ok(None) | Err(_) => (None, None),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
self.prepare_thread_spawn(
|
||||
&mut reservation,
|
||||
&config,
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path,
|
||||
resumed_agent_role,
|
||||
resumed_agent_nickname,
|
||||
)?
|
||||
}
|
||||
}) => self.prepare_thread_spawn(
|
||||
&mut reservation,
|
||||
&config,
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path.or(resumed_agent_path),
|
||||
resumed_agent_role,
|
||||
resumed_agent_nickname,
|
||||
)?,
|
||||
other => (other, AgentMetadata::default()),
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
|
||||
@@ -12,7 +12,9 @@ use crate::init_state_db;
|
||||
use crate::thread_manager::StartThreadOptions;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -33,6 +35,7 @@ use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_thread_store::ArchiveThreadParams;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::LocalThreadStoreConfig;
|
||||
use codex_thread_store::ThreadStore;
|
||||
@@ -849,6 +852,42 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||
assert_eq!(captured, Some(expected));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ephemeral_spawn_does_not_persist_agent_graph_edge() {
|
||||
let (home, mut config) = test_config().await;
|
||||
config.ephemeral = true;
|
||||
let harness = AgentControlHarness::new_with_config(home, config).await;
|
||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent(
|
||||
harness.config.clone(),
|
||||
text_input("spawned"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect("ephemeral agent spawn should succeed");
|
||||
|
||||
let persisted_children = harness
|
||||
.state_db
|
||||
.as_ref()
|
||||
.expect("manager should retain state db")
|
||||
.list_thread_spawn_children(parent_thread_id)
|
||||
.await
|
||||
.expect("persisted child list should load");
|
||||
assert_eq!(persisted_children, Vec::<ThreadId>::new());
|
||||
assert!(
|
||||
harness.manager.get_thread(child_thread_id).await.is_ok(),
|
||||
"ephemeral child should remain live"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_thread_subagent_uses_supplied_initial_multi_agent_mode_without_history() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
@@ -2354,24 +2393,27 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_agent_mode() {
|
||||
let (home, mut config) = test_config().await;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
.expect("test config should allow sqlite");
|
||||
let state_db = init_state_db(&config).await;
|
||||
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
state_db.clone(),
|
||||
let (home, config) = test_config().await;
|
||||
let thread_store = Arc::new(InMemoryThreadStore::default());
|
||||
let manager = ThreadManager::new(
|
||||
&config,
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
empty_extension_registry(),
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store.clone(),
|
||||
/*agent_graph_store*/ None,
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
let harness = AgentControlHarness {
|
||||
_home: home,
|
||||
config,
|
||||
state_db,
|
||||
state_db: None,
|
||||
manager,
|
||||
control,
|
||||
};
|
||||
@@ -2457,14 +2499,18 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age
|
||||
.session_source
|
||||
.get_nickname()
|
||||
.expect("spawned sub-agent should have a nickname");
|
||||
let state_db = child_thread
|
||||
.state_db()
|
||||
.expect("sqlite state db should be available for nickname resume test");
|
||||
timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
if let Ok(Some(metadata)) = state_db.get_thread(child_thread_id).await
|
||||
&& metadata.agent_nickname.is_some()
|
||||
&& metadata.agent_role.as_deref() == Some("explorer")
|
||||
if let Ok(stored_thread) = thread_store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id: child_thread_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
&& stored_thread.agent_nickname.is_some()
|
||||
&& stored_thread.agent_role.as_deref() == Some("explorer")
|
||||
&& stored_thread.agent_path.as_deref() == Some(agent_path.as_str())
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -2488,7 +2534,7 @@ async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_age
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(agent_path.clone()),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
|
||||
@@ -118,6 +118,7 @@ pub use thread_manager::StartThreadOptions;
|
||||
pub use thread_manager::ThreadManager;
|
||||
pub use thread_manager::ThreadShutdownReport;
|
||||
pub use thread_manager::build_models_manager;
|
||||
pub use thread_manager::local_agent_graph_store_from_state_db;
|
||||
pub use thread_manager::thread_store_from_config;
|
||||
pub use web_search::web_search_action_detail;
|
||||
pub use web_search::web_search_detail;
|
||||
|
||||
@@ -58,7 +58,7 @@ pub async fn build_prompt_input(
|
||||
user_instructions_provider,
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store,
|
||||
state_db.clone(),
|
||||
crate::local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
installation_id,
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
|
||||
@@ -16,6 +16,8 @@ use crate::session::INITIAL_SUBMIT_ID;
|
||||
use crate::session::resolve_multi_agent_version;
|
||||
use crate::tasks::InterruptedTurnHistoryMarker;
|
||||
use crate::tasks::interrupted_turn_history_marker;
|
||||
use codex_agent_graph_store::AgentGraphStore;
|
||||
use codex_agent_graph_store::LocalAgentGraphStore;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -58,7 +60,6 @@ use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::LocalThreadStoreConfig;
|
||||
@@ -241,12 +242,12 @@ pub(crate) struct ThreadManagerState {
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
user_instructions_provider: Arc<dyn UserInstructionsProvider>,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
agent_graph_store: Option<Arc<dyn AgentGraphStore>>,
|
||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||
session_source: SessionSource,
|
||||
installation_id: String,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
// Captures submitted ops for testing purpose when test mode is enabled.
|
||||
ops_log: Option<SharedCapturedOps>,
|
||||
}
|
||||
@@ -283,6 +284,15 @@ pub fn thread_store_from_config(
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct the default SQLite-backed agent graph store when local state is available.
|
||||
pub fn local_agent_graph_store_from_state_db(
|
||||
state_db: Option<&StateDbHandle>,
|
||||
) -> Option<Arc<dyn AgentGraphStore>> {
|
||||
state_db.map(|state_db| {
|
||||
Arc::new(LocalAgentGraphStore::new(Arc::clone(state_db))) as Arc<dyn AgentGraphStore>
|
||||
})
|
||||
}
|
||||
|
||||
impl ThreadManager {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
@@ -294,7 +304,7 @@ impl ThreadManager {
|
||||
user_instructions_provider: Arc<dyn UserInstructionsProvider>,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
agent_graph_store: Option<Arc<dyn AgentGraphStore>>,
|
||||
installation_id: String,
|
||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||
@@ -328,13 +338,13 @@ impl ThreadManager {
|
||||
extensions,
|
||||
user_instructions_provider,
|
||||
thread_store,
|
||||
agent_graph_store,
|
||||
attestation_provider,
|
||||
external_time_provider,
|
||||
auth_manager,
|
||||
session_source,
|
||||
installation_id,
|
||||
analytics_events_client,
|
||||
state_db,
|
||||
ops_log: should_use_test_thread_manager_behavior()
|
||||
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
|
||||
}),
|
||||
@@ -419,6 +429,7 @@ impl ThreadManager {
|
||||
},
|
||||
state_db.clone(),
|
||||
));
|
||||
let agent_graph_store = local_agent_graph_store_from_state_db(state_db.as_ref());
|
||||
Self {
|
||||
state: Arc::new(ThreadManagerState {
|
||||
threads: Arc::new(RwLock::new(HashMap::new())),
|
||||
@@ -434,13 +445,13 @@ impl ThreadManager {
|
||||
crate::test_support::EmptyUserInstructionsProvider,
|
||||
),
|
||||
thread_store,
|
||||
agent_graph_store,
|
||||
attestation_provider: None,
|
||||
external_time_provider: None,
|
||||
auth_manager,
|
||||
session_source: SessionSource::Exec,
|
||||
installation_id,
|
||||
analytics_events_client: None,
|
||||
state_db,
|
||||
ops_log: should_use_test_thread_manager_behavior()
|
||||
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
|
||||
}),
|
||||
@@ -579,21 +590,16 @@ impl ThreadManager {
|
||||
subtree_thread_ids.push(thread_id);
|
||||
seen_thread_ids.insert(thread_id);
|
||||
|
||||
if let Some(state_db_ctx) = self.state.state_db() {
|
||||
for status in [
|
||||
DirectionalThreadSpawnEdgeStatus::Open,
|
||||
DirectionalThreadSpawnEdgeStatus::Closed,
|
||||
] {
|
||||
for descendant_id in state_db_ctx
|
||||
.list_thread_spawn_descendants_with_status(thread_id, status)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
CodexErr::Fatal(format!("failed to load thread-spawn descendants: {err}"))
|
||||
})?
|
||||
{
|
||||
if seen_thread_ids.insert(descendant_id) {
|
||||
subtree_thread_ids.push(descendant_id);
|
||||
}
|
||||
if let Some(agent_graph_store) = self.state.agent_graph_store() {
|
||||
for descendant_id in agent_graph_store
|
||||
.list_thread_spawn_descendants(thread_id, /*status_filter*/ None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
CodexErr::Fatal(format!("failed to load thread-spawn descendants: {err}"))
|
||||
})?
|
||||
{
|
||||
if seen_thread_ids.insert(descendant_id) {
|
||||
subtree_thread_ids.push(descendant_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1062,8 +1068,8 @@ impl ThreadManager {
|
||||
}
|
||||
|
||||
impl ThreadManagerState {
|
||||
pub(crate) fn state_db(&self) -> Option<StateDbHandle> {
|
||||
self.state_db.clone()
|
||||
pub(crate) fn agent_graph_store(&self) -> Option<Arc<dyn AgentGraphStore>> {
|
||||
self.agent_graph_store.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_thread_ids(&self) -> Vec<ThreadId> {
|
||||
|
||||
@@ -35,6 +35,49 @@ use wiremock::MockServer;
|
||||
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
struct FakeAgentGraphStore {
|
||||
root_thread_id: ThreadId,
|
||||
descendant_thread_ids: Vec<ThreadId>,
|
||||
}
|
||||
|
||||
impl codex_agent_graph_store::AgentGraphStore for FakeAgentGraphStore {
|
||||
fn upsert_thread_spawn_edge(
|
||||
&self,
|
||||
_parent_thread_id: ThreadId,
|
||||
_child_thread_id: ThreadId,
|
||||
_status: codex_agent_graph_store::ThreadSpawnEdgeStatus,
|
||||
) -> codex_agent_graph_store::AgentGraphStoreFuture<'_, ()> {
|
||||
Box::pin(async { panic!("unexpected graph upsert") })
|
||||
}
|
||||
|
||||
fn set_thread_spawn_edge_status(
|
||||
&self,
|
||||
_child_thread_id: ThreadId,
|
||||
_status: codex_agent_graph_store::ThreadSpawnEdgeStatus,
|
||||
) -> codex_agent_graph_store::AgentGraphStoreFuture<'_, ()> {
|
||||
Box::pin(async { panic!("unexpected graph status update") })
|
||||
}
|
||||
|
||||
fn list_thread_spawn_children(
|
||||
&self,
|
||||
_parent_thread_id: ThreadId,
|
||||
_status_filter: Option<codex_agent_graph_store::ThreadSpawnEdgeStatus>,
|
||||
) -> codex_agent_graph_store::AgentGraphStoreFuture<'_, Vec<ThreadId>> {
|
||||
Box::pin(async { panic!("unexpected direct-child listing") })
|
||||
}
|
||||
|
||||
fn list_thread_spawn_descendants(
|
||||
&self,
|
||||
root_thread_id: ThreadId,
|
||||
status_filter: Option<codex_agent_graph_store::ThreadSpawnEdgeStatus>,
|
||||
) -> codex_agent_graph_store::AgentGraphStoreFuture<'_, Vec<ThreadId>> {
|
||||
assert_eq!(root_thread_id, self.root_thread_id);
|
||||
assert_eq!(status_filter, None);
|
||||
let descendant_thread_ids = self.descendant_thread_ids.clone();
|
||||
Box::pin(async move { Ok(descendant_thread_ids) })
|
||||
}
|
||||
}
|
||||
|
||||
fn user_msg(text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
@@ -477,7 +520,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
/*agent_graph_store*/ None,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -588,7 +631,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
/*agent_graph_store*/ None,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -713,7 +756,7 @@ async fn explicit_installation_id_skips_codex_home_file() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store,
|
||||
state_db.clone(),
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
installation_id.clone(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -754,7 +797,7 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
/*agent_graph_store*/ None,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -814,7 +857,7 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
/*agent_graph_store*/ None,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -881,7 +924,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store,
|
||||
state_db.clone(),
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -948,6 +991,47 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
||||
.expect("shutdown resumed thread");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subtree_listing_uses_injected_graph_store_without_state_db() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
|
||||
let root_thread_id = ThreadId::new();
|
||||
let descendant_thread_ids = vec![ThreadId::new(), ThreadId::new()];
|
||||
let agent_graph_store = Arc::new(FakeAgentGraphStore {
|
||||
root_thread_id,
|
||||
descendant_thread_ids: descendant_thread_ids.clone(),
|
||||
});
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
|
||||
let manager = ThreadManager::new(
|
||||
&config,
|
||||
auth_manager,
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
empty_extension_registry(),
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
Some(agent_graph_store),
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
);
|
||||
|
||||
let mut expected_thread_ids = vec![root_thread_id];
|
||||
expected_thread_ids.extend(descendant_thread_ids);
|
||||
assert_eq!(
|
||||
manager
|
||||
.list_agent_subtree_thread_ids(root_thread_id)
|
||||
.await
|
||||
.expect("subtree should load from injected graph store"),
|
||||
expected_thread_ids
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
@@ -976,7 +1060,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store.clone(),
|
||||
state_db,
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -1081,7 +1165,7 @@ async fn new_uses_active_provider_for_model_refresh() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
/*agent_graph_store*/ None,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -1303,7 +1387,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -1412,7 +1496,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
@@ -1511,7 +1595,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::config::AgentRoleConfig;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::init_state_db;
|
||||
use crate::local_agent_graph_store_from_state_db;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
@@ -4267,7 +4268,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
/*external_time_provider*/ None,
|
||||
|
||||
Reference in New Issue
Block a user