Support asynchronous host service-tier reads and acceptance (#60075)

## What changed

Make `AgentServiceTier` reads asynchronous and have updates return a receipt for local acceptance. Await acceptance during root startup and settings updates outside state and registry locks, sharing the receipt with explicit same-tier retries.

Await shared-tier reads during child preparation, startup prewarm, and step capture. Allow cancellation while capturing a step, and keep host propagation independent of the acceptance wait.

## Testing

Extend the host-child WebSocket test to cover delayed acceptance, same-tier retries without duplicate updates, accessible settings while acceptance is pending, and tier inheritance during prewarm and subsequent steps.

GitOrigin-RevId: 0e2e1899834c37684e904c4a36e66344a0c6c7f3
This commit is contained in:
rhan-oai
2026-11-07 06:51:10 +00:00
committed by copyberry
parent f4275a2a2e
commit f5ac7a757e
8 changed files with 176 additions and 49 deletions
+2 -1
View File
@@ -282,7 +282,8 @@ pub(crate) async fn apply_spawn_agent_service_tier(
session: &Session,
config: &mut Config,
) -> Result<(), String> {
let Some(service_tier) = crate::agent::host::service_tier(session, config.service_tier.clone())
let Some(service_tier) =
crate::agent::host::service_tier(session, config.service_tier.clone()).await
else {
config.service_tier = None;
return Ok(());
+1
View File
@@ -22,6 +22,7 @@ pub use mcp_metadata::AgentMcpMetadataRecipient;
pub use ownership::AgentRuntimeOwnership;
pub(crate) use ownership::AgentRuntimeServices;
pub use service_tier::AgentServiceTier;
pub(crate) use service_tier::ServiceTierAcceptance;
pub(crate) use service_tier::host_child_uses_shared_service_tier;
pub(crate) use service_tier::propagate_config_update;
pub(crate) use service_tier::service_tier;
+38 -22
View File
@@ -4,21 +4,32 @@ use crate::agent::api::AgentConfigUpdate;
use crate::agent::host::AgentRuntimeServices;
use crate::session::session::Session;
use codex_protocol::SessionId;
use futures::FutureExt;
use futures::future::BoxFuture;
use futures::future::Shared;
/// The host's local view of the service tier shared within an agent session.
pub(crate) type ServiceTierAcceptance = Shared<BoxFuture<'static, ()>>;
/// The host's view of the service tier shared within an agent session.
///
/// Install an `Arc<dyn AgentServiceTier>` in thread extension data before startup. Host-owned
/// children with a provider reject explicit per-turn tier overrides. Host-owned runtimes without
/// a provider retain their configured tier and do not propagate shared tier updates.
/// Controller-owned runtimes use their controller.
/// Both methods must be cheap, synchronous and non-reentrant: Core may call them while
/// holding configuration or registration locks. They must perform no network I/O. The host publishes
/// committed root settings into its cache and arranges propagation to other runtimes.
/// Children read the latest cached value at the existing preparation and step boundaries;
/// Core awaits reads during preparation and step capture. A host may answer from a cache.
/// Hosts own cross-runtime propagation and recovery. Until an update reaches a runtime,
/// its `current` may return the prior tier.
/// Children read the latest value at the existing preparation and step boundaries;
/// updates do not change requests already in flight. `None` restores the default tier.
pub trait AgentServiceTier: Send + Sync {
fn current(&self) -> Option<String>;
fn update(&self, service_tier: Option<String>);
fn current(&self) -> BoxFuture<'_, Option<String>>;
/// Hand off the committed tier in call order and return a receipt for local acceptance.
/// Core calls this method under its commit locks: the handoff must be fast and nonblocking.
/// Drive the update independently of the receipt: dropping the receipt only cancels the wait.
/// Core awaits it outside the state and registry locks. Explicit settings updates that
/// request the same committed tier share this receipt until the tier changes.
fn update(&self, service_tier: Option<String>) -> BoxFuture<'static, ()>;
}
pub(crate) fn host_child_uses_shared_service_tier(session: &Session) -> bool {
@@ -26,34 +37,39 @@ pub(crate) fn host_child_uses_shared_service_tier(session: &Session) -> bool {
&& session.session_id() != SessionId::from(session.thread_id())
}
pub(crate) fn service_tier(
pub(crate) async fn service_tier(
session: &Session,
configured_service_tier: Option<String>,
) -> Option<String> {
match &session.agent_runtime {
AgentRuntimeServices::Controller => session.services.agent_control.service_tier(),
AgentRuntimeServices::Host(services) => services
.service_tier
.as_ref()
.map_or(configured_service_tier, |provider| provider.current()),
AgentRuntimeServices::Host(services) => match &services.service_tier {
Some(provider) => provider.current().await,
None => configured_service_tier,
},
}
}
pub(crate) fn propagate_config_update(session: &Session, update: AgentConfigUpdate) {
pub(crate) fn propagate_config_update(
session: &Session,
update: AgentConfigUpdate,
) -> Option<ServiceTierAcceptance> {
match &session.agent_runtime {
AgentRuntimeServices::Controller => session
.services
.agent_control
.propagate_config_update(update),
AgentRuntimeServices::Controller => {
session
.services
.agent_control
.propagate_config_update(update);
None
}
AgentRuntimeServices::Host(services) => {
// Only the root can publish shared settings. Compare session and thread IDs
// because a detached child may have no structural parent ID.
if session.session_id() == SessionId::from(session.thread_id())
&& let Some(provider) = &services.service_tier
{
let AgentConfigUpdate::ServiceTier(tier) = update;
provider.update(tier);
if session.session_id() != SessionId::from(session.thread_id()) {
return None;
}
let AgentConfigUpdate::ServiceTier(tier) = update;
Some(services.service_tier.as_ref()?.update(tier).shared())
}
}
}