Allow thread instruction providers to share updates with subagents (#46577)

## Why

Subagents inherit applied instruction snapshots, so later provider updates do not reach running descendants. Hosts need an explicit way to share current thread instructions across an agent tree.

## What changed

- Add `ThreadInstructionsProvider::share_with_subagents()`, defaulting to `false`. Opted-in descendants refresh instructions at their next model-request boundary.
- Retain shared providers across root unloads and descendant reloads. Let a resumed root replace the provider for surviving descendants, or preserve their last shared instructions when it supplies a non-sharing provider.
- Keep providers scoped to their root tree and preserve snapshot-only instructions for root forks and isolated sessions, including Guardian reviews.

## Testing

Add coverage for child and grandchild instruction updates and clearing, provider replacement after root restart, lazy descendant reloads, default snapshot inheritance, and isolation between roots and Guardian sessions.

GitOrigin-RevId: 44c9fb7fe9ae1f96156bdf3c47ab6c4a81bd65e9
This commit is contained in:
vinh-gpt
2026-09-19 02:15:20 +00:00
committed by copyberry
parent fd59b42a09
commit e24ac448b3
14 changed files with 864 additions and 45 deletions
+15 -1
View File
@@ -1,4 +1,5 @@
mod managed;
mod shared_instructions;
use crate::CodexAppsToolsCache;
use crate::agent::LocalAgentControl;
@@ -394,6 +395,7 @@ pub(crate) struct ResumeThreadWithHistoryOptions {
/// function to require an `Arc<&Self>`.
pub(crate) struct ThreadManagerState {
threads: Arc<RwLock<HashMap<ThreadId, Arc<CodexThread>>>>,
shared_thread_instructions: shared_instructions::SharedThreadInstructionsProviders,
thread_created_tx: broadcast::Sender<ThreadId>,
thread_id_generator: ThreadIdGenerator,
auth_manager: Arc<AuthManager>,
@@ -542,6 +544,7 @@ impl ThreadManager {
Self {
state: Arc::new(ThreadManagerState {
threads: Arc::new(RwLock::new(HashMap::new())),
shared_thread_instructions: Default::default(),
thread_created_tx,
thread_id_generator: default_thread_id_generator(),
models_manager,
@@ -690,6 +693,7 @@ impl ThreadManager {
Self {
state: Arc::new(ThreadManagerState {
threads: Arc::new(RwLock::new(HashMap::new())),
shared_thread_instructions: Default::default(),
thread_created_tx,
thread_id_generator: default_thread_id_generator(),
models_manager: create_model_provider(provider, Some(auth_manager.clone()))
@@ -1566,6 +1570,15 @@ impl ThreadManager {
}
impl ThreadManagerState {
pub(crate) fn shared_thread_instructions_provider(
&self,
root_thread_id: ThreadId,
provider: Option<Arc<dyn ThreadInstructionsProvider>>,
) -> Option<Arc<dyn ThreadInstructionsProvider>> {
self.shared_thread_instructions
.for_root(root_thread_id, provider)
}
pub(crate) fn agent_graph_store(&self) -> Option<Arc<dyn AgentGraphStore>> {
self.agent_graph_store.clone()
}
@@ -1733,7 +1746,8 @@ impl ThreadManagerState {
///
/// Fresh roots, cold resumes, and root forks retain the global and any supplied
/// thread provider. The session's AgentsMdManager loads them at startup and
/// subsequent context captures. Subagents inherit only applied parent snapshots.
/// subsequent context captures. Subagents inherit applied parent snapshots and
/// any thread provider that explicitly opts into sharing with descendants.
/// A root fork without a provider preserves the live source's thread snapshot,
/// never its task-bound provider. Hosts must supply a provider for offline forks.
/// Warm resumes retain the existing session and do not call this function.