From c026e7a622ecae45f0af1939729e7baab9d1637b Mon Sep 17 00:00:00 2001 From: jif Date: Fri, 18 Sep 2026 21:17:10 +0000 Subject: [PATCH] Expose a backend-independent agent control contract (#46547) ## What changed Add a public, object-safe `AgentControl` trait and request/response types for coordinating an agent tree independently of where its threads run. Define contracts for lifecycle operations, message delivery, status observation, execution admission, usage accounting, shared configuration, and guardian evidence. Move shared agent types out of the local controller and export them from `codex-core`. Let `AgentExecutionGuard` own a backend-provided reservation while preserving the local limiter's release-on-drop behavior. Include `root_thread_id` in `GuardianRootSnapshot` to identify the source of authorization evidence. ## Testing Add tests for reservation release when a guard is dropped or a turn is cancelled, and extend the guardian authorization test to assert the root thread identity. Check trait object safety at compile time. GitOrigin-RevId: 9385c9fbfd189f08429539fd89061ef8c9efe0d9 --- codex-rs/core/src/agent/api.rs | 247 ++++++++++++++++++ codex-rs/core/src/agent/control.rs | 35 +-- codex-rs/core/src/agent/control/delivery.rs | 14 +- codex-rs/core/src/agent/control/execution.rs | 10 +- codex-rs/core/src/agent/control/spawn.rs | 4 + .../src/agent/control/user_authorization.rs | 1 + codex-rs/core/src/agent/control_tests.rs | 4 + codex-rs/core/src/agent/mod.rs | 2 + codex-rs/core/src/agent/registry.rs | 9 +- codex-rs/core/src/agent/types.rs | 88 +++++++ codex-rs/core/src/agent/types_tests.rs | 34 +++ codex-rs/core/src/codex_thread.rs | 2 + codex-rs/core/src/lib.rs | 24 ++ codex-rs/core/src/rollout_budget.rs | 8 +- codex-rs/core/src/session/multi_agents.rs | 7 +- codex-rs/core/src/state/turn.rs | 2 +- codex-rs/core/src/thread_manager_tests.rs | 2 +- .../src/tools/handlers/multi_agents/spawn.rs | 4 +- .../src/tools/handlers/multi_agents_tests.rs | 12 +- .../src/tools/handlers/multi_agents_v2.rs | 2 +- .../handlers/multi_agents_v2/followup_task.rs | 2 +- .../handlers/multi_agents_v2/message_tool.rs | 2 +- .../handlers/multi_agents_v2/send_message.rs | 2 +- .../tools/handlers/multi_agents_v2/spawn.rs | 6 +- .../suite/guardian_subagent_authorization.rs | 3 +- 25 files changed, 442 insertions(+), 84 deletions(-) create mode 100644 codex-rs/core/src/agent/api.rs create mode 100644 codex-rs/core/src/agent/types.rs create mode 100644 codex-rs/core/src/agent/types_tests.rs diff --git a/codex-rs/core/src/agent/api.rs b/codex-rs/core/src/agent/api.rs new file mode 100644 index 0000000000..4868fee6ab --- /dev/null +++ b/codex-rs/core/src/agent/api.rs @@ -0,0 +1,247 @@ +//! Coordination of one agent tree, independent of where its threads run. +//! +//! The trait and its requests use shared agent types and captured settings. Live threads +//! and turn contexts stay in the runtime. Implementations own membership, loading, +//! delivery and shared resources. These Rust contracts do not define a wire protocol. + +use crate::agent::types::AgentExecutionGuard; +use crate::agent::types::AgentMessage; +use crate::agent::types::AgentMetadata; +use crate::agent::types::LiveAgent; +use crate::agent::types::MessageDeliveryMode; +use crate::agent::types::SpawnAgentOptions; +use crate::codex_thread::GuardianRootSnapshot; +use crate::codex_thread::ThreadConfigSnapshot; +use crate::config::Config; +use crate::rollout_budget::RolloutBudgetReminder; +use codex_protocol::AgentPath; +use codex_protocol::SessionId; +use codex_protocol::ThreadId; +use codex_protocol::error::Result; +use codex_protocol::protocol::AgentStatus; +use codex_protocol::protocol::MultiAgentVersion; +use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::TokenUsage; +use codex_protocol::turn_input::TurnStartOptions; +use codex_protocol::user_input::UserInput; +use futures::future::BoxFuture; +use futures::stream::BoxStream; +use std::num::NonZeroU32; + +/// Initial observation followed by changes, with no gap between the two. Observation failures +/// are errors, not terminal agent states. Backends own reconnection and reconciliation. +pub type StatusSubscription = BoxStream<'static, Result>; + +// Keep dynamic dispatch a compile-time property of the contract. +const _: Option<&dyn AgentControl> = None; + +/// Coordinates one agent tree through a local or host backend. +/// +/// Every operation validates tree membership and caller authority, including calls made +/// through stale controller handles. Unknown IDs return `ThreadNotFound`; invalid or +/// unresolved references return `UnsupportedOperation`, preserving existing resolution +/// errors. Backend failures must not be reported as missing targets or successful operations. +/// Mutation success acknowledges acceptance, not completion of the requested agent work. +/// +/// Implementations preserve MAv2 wake modes and keep loading, capacity checks and delivery +/// behind complete operations. Hosts own transport, durable acceptance, retry identities +/// and recovery. Those mechanisms and channel operations are separate implementation work. +/// Boxed Send futures allow callers to use `Arc`. +pub trait AgentControl: Send + Sync { + fn identity(&self) -> ControlIdentity; + + /// Allocate, register and start a child, then accept its initial input. Return effective + /// settings so callers do not need a second configuration lookup. + fn spawn(&self, request: SpawnRequest) -> BoxFuture<'_, Result>; + + /// Explicitly reopen an agent under the caller's authority and captured settings. + /// Reloading required for message delivery belongs inside `send`. + fn resume( + &self, + caller: ThreadId, + target: AgentTarget, + config: Config, + ) -> BoxFuture<'_, Result>; + + /// Resolve, authorize, reload if needed and accept input with its original attribution + /// and wake mode. Queue-only messages cannot start an idle agent; follow-ups cannot + /// target the root. Acceptance does not mean the model has read the input. + fn send(&self, request: SendRequest) -> BoxFuture<'_, Result>; + + /// Stop current work without closing the agent. Return its pre-interrupt snapshot. + fn interrupt(&self, caller: ThreadId, target: AgentTarget) -> BoxFuture<'_, Result>; + + /// Close the agent and its descendants. Return its pre-close snapshot. + fn close(&self, caller: ThreadId, target: AgentTarget) -> BoxFuture<'_, Result>; + + /// Read identity, runtime state and effective settings without loading a dormant agent. + fn inspect(&self, caller: ThreadId, target: AgentTarget) -> BoxFuture<'_, Result>; + + /// Return a bounded membership page. Callers own model and UI formatting. + fn list(&self, caller: ThreadId, query: AgentQuery) -> BoxFuture<'_, Result>; + + /// Subscribe without loading a dormant agent. Unavailability must remain visible. + fn watch( + &self, + caller: ThreadId, + target: AgentTarget, + ) -> BoxFuture<'_, Result>; + + /// Atomically reserve tree-wide execution capacity. Unlimited turns return no guard. + /// Cancellation during acquisition must release any partial reservation. This contract + /// does not require changing the local policy from rejecting at capacity to waiting. + fn admit_turn( + &self, + thread_id: ThreadId, + turn_id: String, + version: MultiAgentVersion, + source: SessionSource, + ) -> BoxFuture<'_, Result>>; + + /// Account for an inference response, including compaction. Retried reports for the + /// same thread, turn and response must not charge the budget twice. As in the existing + /// rollout-budget path, `SessionBudgetExceeded` means the usage was recorded and the + /// budget is exhausted; retries must preserve that result without charging again. + fn record_usage( + &self, + thread_id: ThreadId, + turn_id: String, + response_id: String, + usage: TokenUsage, + ) -> BoxFuture<'_, Result<()>>; + + /// Accept the terminal result and own completion delivery to the parent and task + /// initiator. Retrying the same thread/turn outcome must not duplicate delivery. + fn turn_finished(&self, outcome: AgentTurnOutcome) -> BoxFuture<'_, Result<()>>; + + /// Publish a root-owned update for live agents and later starts/resumes. Only the root + /// may change shared settings; runtimes read their own effective configuration. + fn propagate_config_update( + &self, + caller: ThreadId, + update: AgentConfigUpdate, + ) -> BoxFuture<'_, Result<()>>; + + /// Read authoritative root evidence, including source, completeness and revision. + /// Fetch failures are errors. Approval consumers must revalidate the evidence revision + /// before accepting an approval; this read alone does not grant authorization. + fn get_guardian_package(&self, agent: ThreadId) -> BoxFuture<'_, Result>; + + fn pending_budget_reminder<'a>( + &'a self, + agent: ThreadId, + window: &'a str, + ) -> BoxFuture<'a, Result>>; + + /// Acknowledge only after inserting the reminder into the agent's history. + fn mark_budget_reminder_delivered<'a>( + &'a self, + agent: ThreadId, + window: &'a str, + reminder: RolloutBudgetReminder, + ) -> BoxFuture<'a, Result<()>>; +} + +/// Persistent tree identity and its current owner generation. Clones and reconnects keep +/// the generation; a replacement owner changes it and fences the previous owner. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ControlIdentity { + pub session_id: SessionId, + pub generation: uuid::Uuid, +} + +/// References resolve relative to the caller. IDs still require membership checks. +#[derive(Clone, Debug)] +pub enum AgentTarget { + Id(ThreadId), + Reference(String), +} + +/// Observes existing registry metadata and runtime snapshots without loading an agent. +/// A known identity survives unloading; unloaded does not mean completed. Missing agents +/// are operation errors, not loaded snapshots with `AgentStatus::NotFound`. +#[derive(Clone, Debug)] +pub enum AgentInfo { + Loaded { + agent: LiveAgent, + config: Box, + }, + /// Membership is known, but no runtime is loaded. Metadata identifies the known agent. + Unloaded(AgentMetadata), +} + +/// User input starts or steers a turn; agent messages retain their sender and wake mode. +pub enum AgentInput { + UserInput(Vec), + Message { + message: AgentMessage, + mode: MessageDeliveryMode, + }, +} + +pub struct SpawnRequest { + pub caller: ThreadId, + pub config: Config, + /// Spawning starts work; message input must use `TriggerTurn`. + pub input: AgentInput, + pub source: SessionSource, + pub options: SpawnAgentOptions, +} + +pub struct SendRequest { + pub caller: ThreadId, + pub target: AgentTarget, + /// Captured caller settings used if the recipient must be restored. + pub resume_config: Config, + pub input: AgentInput, + pub start_options: TurnStartOptions, +} + +pub struct DeliveryReceipt { + pub thread_id: ThreadId, + /// Acceptance identifier, not evidence that the recipient processed the input. + pub submission_id: String, +} + +#[derive(Clone, Debug)] +pub enum AgentScope { + Tree, + Children(AgentTarget), + /// Includes the target itself. + Subtree(AgentTarget), +} + +#[derive(Clone, Copy, Debug)] +pub enum AgentVisibility { + Known, + Loaded, +} + +pub struct AgentQuery { + pub scope: AgentScope, + pub visibility: AgentVisibility, + /// Opaque backend cursor. Pages are observations, not a locked tree snapshot. + pub cursor: Option, + /// Requested maximum; backends must also enforce their own hard page-size cap. + pub limit: NonZeroU32, +} + +pub struct AgentPage { + pub agents: Vec, + pub next_cursor: Option, +} + +pub struct AgentTurnOutcome { + pub thread_id: ThreadId, + pub turn_id: String, + pub source: SessionSource, + pub parent_turn_id: Option, + pub initiating_agent_path: Option, + pub status: AgentStatus, +} + +/// Settings shared by the tree. A service tier of `None` restores the default tier. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AgentConfigUpdate { + ServiceTier(Option), +} diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 9d7e57aae3..fe2d660dc9 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -2,11 +2,11 @@ use crate::TurnInputRequest; use crate::TurnInputSubmission; use crate::TurnStartOptions; use crate::agent::AgentStatus; -use crate::agent::registry::AgentMetadata; use crate::agent::registry::AgentRegistry; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::resolve_role_config; use crate::agent::status::is_final; +use crate::agent::types::AgentMetadata; use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationKind; use crate::codex_thread::ThreadConfigSnapshot; @@ -16,7 +16,6 @@ use crate::context::SubagentNotification; use crate::environment_selection::TurnEnvironmentSnapshot; use crate::rollout_budget::RolloutBudget; use crate::session::emit_subagent_session_started; -use crate::session::multi_agents::ResolvedMultiAgentV2UsageHints; use crate::session_prefix::format_inter_agent_completion_message; use crate::session_prefix::format_subagent_context_line; use crate::thread_manager::ResumeThreadWithHistoryOptions; @@ -52,8 +51,6 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadSource; -use codex_protocol::protocol::TurnEnvironmentSelection; -use codex_protocol::turn_input::CyberAccessProgram; use codex_protocol::user_input::UserInput; use codex_thread_store::LoadThreadHistoryParams; use codex_thread_store::ReadThreadParams; @@ -67,10 +64,7 @@ use tokio::sync::watch; use tracing::warn; use uuid::Uuid; -pub(crate) use self::delivery::AgentMessage; pub(crate) use self::delivery::MessageDeliveryError; -pub(crate) use self::delivery::MessageDeliveryMode; -pub(crate) use self::execution::AgentExecutionGuard; use self::execution::AgentExecutionLimiter; pub(crate) use self::interrupt::AgentInterruptError; pub(crate) use self::interrupt::AgentInterruptOutcome; @@ -89,33 +83,6 @@ mod user_authorization; const MAX_ENVIRONMENT_SUBAGENTS: usize = 8; const MAX_ENVIRONMENT_SUBAGENT_BYTES: usize = 1_024; -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) enum SpawnAgentForkMode { - FullHistory, - LastNTurns(usize), -} - -#[derive(Clone, Debug, Default)] -pub(crate) struct SpawnAgentOptions { - pub(crate) fork_parent_spawn_call_id: Option, - pub(crate) fork_mode: Option, - pub(crate) parent_thread_id: Option, - pub(crate) parent_turn_id: Option, - /// Attribute delegated usage to the turn that initiated it. - pub(crate) turn_trigger: Option, - pub(crate) root_turn_id: Option, - pub(crate) environments: Option>, - pub(crate) multi_agent_v2_usage_hints: Option, - pub(crate) cyber_access_program: Option, -} - -#[derive(Clone, Debug)] -pub(crate) struct LiveAgent { - pub(crate) thread_id: ThreadId, - pub(crate) metadata: AgentMetadata, - pub(crate) status: AgentStatus, -} - #[derive(Clone, Debug, Serialize, PartialEq, Eq)] pub(crate) struct ListedAgent { pub(crate) agent_name: String, diff --git a/codex-rs/core/src/agent/control/delivery.rs b/codex-rs/core/src/agent/control/delivery.rs index faddcc908b..fa6a8ac2b3 100644 --- a/codex-rs/core/src/agent/control/delivery.rs +++ b/codex-rs/core/src/agent/control/delivery.rs @@ -5,6 +5,8 @@ use super::LocalAgentControl; use crate::TurnStartOptions; use crate::agent::child_config::build_agent_resume_config; +use crate::agent::types::AgentMessage; +use crate::agent::types::MessageDeliveryMode; use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationKind; use crate::context::ContextualUserFragment; @@ -16,18 +18,6 @@ use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; use codex_protocol::protocol::InterAgentCommunication; -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum MessageDeliveryMode { - QueueOnly, - TriggerTurn, -} - -/// Keeps model-provided encrypted content distinct from text that needs a context wrapper. -pub(crate) enum AgentMessage { - Plaintext(String), - Encrypted(String), -} - impl AgentMessage { pub(crate) fn into_communication( self, diff --git a/codex-rs/core/src/agent/control/execution.rs b/codex-rs/core/src/agent/control/execution.rs index dc9a10aaa2..500905c3b8 100644 --- a/codex-rs/core/src/agent/control/execution.rs +++ b/codex-rs/core/src/agent/control/execution.rs @@ -1,4 +1,8 @@ +//! Tracks local running capacity and releases reservations through the shared guard. +//! The local permit owns the running count; root and MAv1 turns remain unrestricted. + use super::LocalAgentControl; +use crate::agent::types::AgentExecutionGuard; use crate::codex_thread::CodexThread; use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErrorDetails; @@ -16,11 +20,11 @@ pub(super) struct AgentExecutionLimiter { max_threads: OnceLock, } -pub(crate) struct AgentExecutionGuard { +struct LocalExecutionPermit { limiter: Arc, } -impl Drop for AgentExecutionGuard { +impl Drop for LocalExecutionPermit { fn drop(&mut self) { self.limiter.active.fetch_sub(1, Ordering::AcqRel); } @@ -84,7 +88,7 @@ impl AgentExecutionLimiter { fn guard(self: Arc) -> AgentExecutionGuard { self.active.fetch_add(1, Ordering::AcqRel); - AgentExecutionGuard { limiter: self } + AgentExecutionGuard::new(LocalExecutionPermit { limiter: self }) } } diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index a08267aa29..11793c7ceb 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -2,6 +2,10 @@ use super::residency::is_v2_resident_session_source; use super::*; use crate::agent::child_config::build_agent_resume_config; use crate::agent::role::apply_role_to_config; +use crate::agent::types::AgentMetadata; +use crate::agent::types::LiveAgent; +use crate::agent::types::SpawnAgentForkMode; +use crate::agent::types::SpawnAgentOptions; use crate::codex_thread::CodexThread; use crate::config::PermissionProfileSnapshot; use crate::context::ContextualUserFragment; diff --git a/codex-rs/core/src/agent/control/user_authorization.rs b/codex-rs/core/src/agent/control/user_authorization.rs index 3dd0ed2064..0b3e2fc7c1 100644 --- a/codex-rs/core/src/agent/control/user_authorization.rs +++ b/codex-rs/core/src/agent/control/user_authorization.rs @@ -223,6 +223,7 @@ impl LocalAgentControl { .map(|turn_id| root_evidence.trusted_skill_paths(turn_id)) .unwrap_or_default(); Some(GuardianRootSnapshot { + root_thread_id, authorization_version, messages, trusted_skill_paths, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 36c3e89405..c36c58c5b4 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -4,6 +4,10 @@ use crate::StateDbHandle; use crate::ThreadManager; use crate::agent::agent_status_from_event; use crate::agent::next_thread_spawn_depth; +use crate::agent::types::LiveAgent; +use crate::agent::types::ResolvedMultiAgentV2UsageHints; +use crate::agent::types::SpawnAgentForkMode; +use crate::agent::types::SpawnAgentOptions; use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationKind; use crate::config::AgentRoleConfig; diff --git a/codex-rs/core/src/agent/mod.rs b/codex-rs/core/src/agent/mod.rs index 009d2f69f5..c9a1547990 100644 --- a/codex-rs/core/src/agent/mod.rs +++ b/codex-rs/core/src/agent/mod.rs @@ -1,9 +1,11 @@ pub(crate) mod agent_resolver; +pub(crate) mod api; pub(crate) mod child_config; pub(crate) mod control; mod registry; pub(crate) mod role; pub(crate) mod status; +pub(crate) mod types; pub(crate) use codex_protocol::protocol::AgentStatus; pub(crate) use control::LocalAgentControl; diff --git a/codex-rs/core/src/agent/registry.rs b/codex-rs/core/src/agent/registry.rs index 242542c5c3..d7fae371d5 100644 --- a/codex-rs/core/src/agent/registry.rs +++ b/codex-rs/core/src/agent/registry.rs @@ -1,3 +1,4 @@ +use crate::agent::types::AgentMetadata; use codex_protocol::AgentPath; use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; @@ -49,14 +50,6 @@ impl RegisteredAgent { } } -#[derive(Clone, Debug, Default)] -pub(crate) struct AgentMetadata { - pub(crate) agent_id: Option, - pub(crate) agent_path: Option, - pub(crate) agent_nickname: Option, - pub(crate) agent_role: Option, -} - fn format_agent_nickname(name: &str, nickname_reset_count: usize) -> String { match nickname_reset_count { 0 => name.to_string(), diff --git a/codex-rs/core/src/agent/types.rs b/codex-rs/core/src/agent/types.rs new file mode 100644 index 0000000000..81dcd5354e --- /dev/null +++ b/codex-rs/core/src/agent/types.rs @@ -0,0 +1,88 @@ +//! Agent data and execution reservations shared by controllers and their callers. +//! Backend operations and local admission policy live in the implementations. + +use crate::context::MultiAgentRoleInstructions; +use codex_protocol::AgentPath; +use codex_protocol::ThreadId; +use codex_protocol::protocol::AgentStatus; +use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_protocol::turn_input::CyberAccessProgram; + +/// Registry identity shared by loaded and unloaded agents. +/// Registered agents have an `agent_id`; a reserved spawn can still be awaiting its ID. +#[derive(Clone, Debug, Default)] +pub struct AgentMetadata { + pub agent_id: Option, + pub agent_path: Option, + pub agent_nickname: Option, + pub agent_role: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SpawnAgentForkMode { + FullHistory, + LastNTurns(usize), +} + +#[derive(Clone, Debug, Default)] +pub struct SpawnAgentOptions { + pub fork_parent_spawn_call_id: Option, + pub fork_mode: Option, + pub parent_thread_id: Option, + pub parent_turn_id: Option, + /// Attribute delegated usage to the turn that initiated it. + pub turn_trigger: Option, + pub root_turn_id: Option, + pub environments: Option>, + pub multi_agent_v2_usage_hints: Option, + pub cyber_access_program: Option, +} + +/// Identity and status observed from a loaded agent, without a handle to its runtime. +#[derive(Clone, Debug)] +pub struct LiveAgent { + pub thread_id: ThreadId, + pub metadata: AgentMetadata, + pub status: AgentStatus, +} + +#[derive(Clone, Debug, Default)] +pub struct ResolvedMultiAgentV2UsageHints { + pub root: Option, + pub subagent: Option, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum MessageDeliveryMode { + /// Deliver to the mailbox without starting an idle agent. + QueueOnly, + /// Deliver to the active turn or start work if the agent is idle. + TriggerTurn, +} + +/// Keeps model-provided encrypted content distinct from text that needs a context wrapper. +pub enum AgentMessage { + Plaintext(String), + Encrypted(String), +} + +/// Holds a backend-owned reservation until the turn ends or is cancelled. +/// +/// The permit's destructor releases capacity or arranges backend cleanup. Remote backends +/// must also recover reservations after worker loss, when no Rust destructor can run. +#[must_use = "hold the execution guard for the lifetime of the admitted turn"] +pub struct AgentExecutionGuard { + _permit: Box, +} + +impl AgentExecutionGuard { + pub fn new(permit: impl Send + Sync + 'static) -> Self { + Self { + _permit: Box::new(permit), + } + } +} + +#[cfg(test)] +#[path = "types_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/agent/types_tests.rs b/codex-rs/core/src/agent/types_tests.rs new file mode 100644 index 0000000000..6385ce06c2 --- /dev/null +++ b/codex-rs/core/src/agent/types_tests.rs @@ -0,0 +1,34 @@ +//! Checks shared execution-guard ownership through completion and cancellation. + +use super::AgentExecutionGuard; +use pretty_assertions::assert_eq; +use std::sync::Arc; +use tokio::sync::Semaphore; +use tokio::sync::oneshot; + +#[tokio::test] +async fn dropping_guard_releases_backend_reservation() { + let capacity = Arc::new(Semaphore::new(/*permits*/ 1)); + let permit = Arc::clone(&capacity).acquire_owned().await.unwrap(); + let guard = AgentExecutionGuard::new(permit); + assert_eq!(capacity.available_permits(), 0); + drop(guard); + assert_eq!(capacity.available_permits(), 1); +} + +#[tokio::test] +async fn cancelling_turn_releases_backend_reservation() { + let capacity = Arc::new(Semaphore::new(/*permits*/ 1)); + let permit = Arc::clone(&capacity).acquire_owned().await.unwrap(); + let (started, running) = oneshot::channel(); + let turn = tokio::spawn(async move { + let _guard = AgentExecutionGuard::new(permit); + started.send(()).unwrap(); + std::future::pending::<()>().await; + }); + running.await.unwrap(); + assert_eq!(capacity.available_permits(), 0); + turn.abort(); + assert!(turn.await.unwrap_err().is_cancelled()); + assert_eq!(capacity.available_permits(), 1); +} diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 0c54ced5cb..696822f845 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -173,6 +173,8 @@ pub struct GuardianAuthorizationVersion { /// Bounded root conversation and authorization state from one history snapshot. #[derive(Debug, Eq, PartialEq)] pub struct GuardianRootSnapshot { + /// Authoritative root from which this evidence was captured. + pub root_thread_id: ThreadId, pub authorization_version: GuardianAuthorizationVersion, pub messages: Vec, pub trusted_skill_paths: Vec, diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index c30987fef3..38b87f4dcc 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -53,6 +53,30 @@ pub use codex_thread::ThreadConfigSnapshot; pub use session::turn_context::TurnContext; pub use thread_startup_metadata::ThreadStartupMetadata; mod agent; +pub use agent::api::AgentConfigUpdate; +pub use agent::api::AgentControl; +pub use agent::api::AgentInfo; +pub use agent::api::AgentInput; +pub use agent::api::AgentPage; +pub use agent::api::AgentQuery; +pub use agent::api::AgentScope; +pub use agent::api::AgentTarget; +pub use agent::api::AgentTurnOutcome; +pub use agent::api::AgentVisibility; +pub use agent::api::ControlIdentity; +pub use agent::api::DeliveryReceipt; +pub use agent::api::SendRequest; +pub use agent::api::SpawnRequest; +pub use agent::api::StatusSubscription; +pub use agent::types::AgentExecutionGuard; +pub use agent::types::AgentMessage; +pub use agent::types::AgentMetadata; +pub use agent::types::LiveAgent; +pub use agent::types::MessageDeliveryMode; +pub use agent::types::ResolvedMultiAgentV2UsageHints; +pub use agent::types::SpawnAgentForkMode; +pub use agent::types::SpawnAgentOptions; +pub use rollout_budget::RolloutBudgetReminder; mod agent_communication; mod attestation; mod codex_delegate; diff --git a/codex-rs/core/src/rollout_budget.rs b/codex-rs/core/src/rollout_budget.rs index d66b778fa8..1b010110a8 100644 --- a/codex-rs/core/src/rollout_budget.rs +++ b/codex-rs/core/src/rollout_budget.rs @@ -8,9 +8,11 @@ use std::sync::Mutex; use std::sync::MutexGuard; use std::sync::OnceLock; -pub(crate) struct RolloutBudgetReminder { - pub(crate) remaining_tokens: i64, - reminder_index: i64, +/// Budget reminder returned by a controller and acknowledged after history insertion. +pub struct RolloutBudgetReminder { + pub remaining_tokens: i64, + /// Backend-defined reminder position, returned unchanged on acknowledgement. + pub reminder_index: i64, } /// Shared accounting and reminder state for one root-thread session tree. diff --git a/codex-rs/core/src/session/multi_agents.rs b/codex-rs/core/src/session/multi_agents.rs index 51e43a4dc6..9cec8ae35a 100644 --- a/codex-rs/core/src/session/multi_agents.rs +++ b/codex-rs/core/src/session/multi_agents.rs @@ -1,3 +1,4 @@ +use crate::agent::types::ResolvedMultiAgentV2UsageHints; use crate::config::MultiAgentV2Config; use crate::context::MultiAgentRoleInstructions; use crate::session::step_context::StepContext; @@ -10,12 +11,6 @@ use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; -#[derive(Clone, Debug, Default)] -pub(crate) struct ResolvedMultiAgentV2UsageHints { - pub(crate) root: Option, - pub(crate) subagent: Option, -} - pub(super) fn usage_hint_text(step_context: &StepContext) -> Option { let turn_context = step_context.turn.as_ref(); if turn_context.multi_agent_version != MultiAgentVersion::V2 { diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index 01b8756e1f..8f45fc5026 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -18,7 +18,7 @@ use rmcp::model::RequestId; use tokio::sync::oneshot; use super::TurnTokenUsage; -use crate::agent::control::AgentExecutionGuard; +use crate::agent::types::AgentExecutionGuard; use crate::session::TurnInputQueue; use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 4a14de1024..e97854b85f 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::agent::control::SpawnAgentOptions; +use crate::agent::types::SpawnAgentOptions; use crate::config::test_config; use crate::init_state_db; use crate::installation_id::INSTALLATION_ID_FILENAME; diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index d5fccaa57a..b8cded3455 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -2,12 +2,12 @@ use super::*; use crate::agent::child_config::SpawnConfigOptions; use crate::agent::child_config::SpawnConfigVersion; use crate::agent::child_config::prepare_agent_spawn_config; -use crate::agent::control::SpawnAgentForkMode; -use crate::agent::control::SpawnAgentOptions; use crate::agent::control::render_input_preview; use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; use crate::agent::role::DEFAULT_ROLE_NAME; +use crate::agent::types::SpawnAgentForkMode; +use crate::agent::types::SpawnAgentOptions; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1; use codex_tools::ToolSpec; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 86d80ee720..8e8ad7d76d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1232,7 +1232,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() { agent_nickname: None, agent_role: None, })), - crate::agent::control::SpawnAgentOptions::default(), + crate::agent::types::SpawnAgentOptions::default(), ) .await .expect("worker spawn should succeed") @@ -1308,7 +1308,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() { agent_nickname: None, agent_role: None, })), - crate::agent::control::SpawnAgentOptions::default(), + crate::agent::types::SpawnAgentOptions::default(), ) .await .expect("worker spawn should succeed") @@ -1473,7 +1473,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() { agent_nickname: None, agent_role: None, })), - crate::agent::control::SpawnAgentOptions::default(), + crate::agent::types::SpawnAgentOptions::default(), ) .await .expect("researcher agent should spawn"); @@ -1493,7 +1493,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() { agent_nickname: None, agent_role: None, })), - crate::agent::control::SpawnAgentOptions::default(), + crate::agent::types::SpawnAgentOptions::default(), ) .await .expect("worker agent should spawn"); @@ -4023,7 +4023,7 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_id() { agent_nickname: None, agent_role: None, })), - crate::agent::control::SpawnAgentOptions::default(), + crate::agent::types::SpawnAgentOptions::default(), ) .await .expect("worker spawn should succeed") @@ -4090,7 +4090,7 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_task_name() { agent_nickname: None, agent_role: None, })), - crate::agent::control::SpawnAgentOptions::default(), + crate::agent::types::SpawnAgentOptions::default(), ) .await .expect("worker spawn should succeed") diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs index 40aecee0a9..ea44c0a427 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs @@ -2,7 +2,7 @@ use crate::agent::AgentStatus; use crate::agent::agent_resolver::resolve_agent_target; -use crate::agent::control::AgentMessage; +use crate::agent::types::AgentMessage; use crate::function_tool::FunctionCallError; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs index 161e644d7e..fa129704a0 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs @@ -2,7 +2,7 @@ use super::analytics::ToolCallAnalytics; use super::message_tool::FollowupTaskArgs; use super::message_tool::handle_message_string_tool; use super::*; -use crate::agent::control::MessageDeliveryMode; +use crate::agent::types::MessageDeliveryMode; use crate::tools::handlers::multi_agents_spec::create_followup_task_tool; use codex_tools::ToolSpec; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index e319a20183..826b3f1835 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -6,7 +6,7 @@ use super::analytics::ToolCallAnalytics; use super::*; use crate::agent::control::MessageDeliveryError; -use crate::agent::control::MessageDeliveryMode; +use crate::agent::types::MessageDeliveryMode; use crate::tools::context::FunctionToolOutput; #[derive(Debug, Deserialize)] diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs index f1987170eb..7713f01103 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs @@ -2,7 +2,7 @@ use super::analytics::ToolCallAnalytics; use super::message_tool::SendMessageArgs; use super::message_tool::handle_message_string_tool; use super::*; -use crate::agent::control::MessageDeliveryMode; +use crate::agent::types::MessageDeliveryMode; use crate::tools::handlers::multi_agents_spec::create_send_message_tool; use codex_tools::ToolSpec; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 22589681d1..3de9822882 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -2,11 +2,11 @@ use super::*; use crate::agent::child_config::SpawnConfigOptions; use crate::agent::child_config::SpawnConfigVersion; use crate::agent::child_config::prepare_agent_spawn_config; -use crate::agent::control::MessageDeliveryMode; -use crate::agent::control::SpawnAgentForkMode; -use crate::agent::control::SpawnAgentOptions; use crate::agent::next_thread_spawn_depth; use crate::agent::role::DEFAULT_ROLE_NAME; +use crate::agent::types::MessageDeliveryMode; +use crate::agent::types::SpawnAgentForkMode; +use crate::agent::types::SpawnAgentOptions; use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationKind; use crate::codex_thread::ThreadConfigSnapshot; diff --git a/codex-rs/core/tests/suite/guardian_subagent_authorization.rs b/codex-rs/core/tests/suite/guardian_subagent_authorization.rs index eb0a8d040b..7a2cfd8790 100644 --- a/codex-rs/core/tests/suite/guardian_subagent_authorization.rs +++ b/codex-rs/core/tests/suite/guardian_subagent_authorization.rs @@ -548,10 +548,11 @@ async fn guardian_subagent_review_preserves_late_root_user_authorization( .expect("worker root snapshot"); assert_eq!( ( + snapshot.root_thread_id, snapshot.messages, snapshot.authorization_version.retained_context_complete ), - (expected_messages.clone(), evidence_complete), + (root_thread_id, expected_messages.clone(), evidence_complete), ); let worker_request = worker_review_request.single_request();