#![allow(
unused_imports,
reason = "Intentional compatibility, platform, or test-only suppression."
)]
mod background;
mod config;
mod constants;
mod discovery;
mod model;
mod prompt;
mod types;
pub use background::{
background_record_id, build_background_subagent_command, extract_tail_lines, load_archive_preview,
subagent_display_label,
};
pub use config::{
ResolvedAgentRuntimeView, build_child_config, compose_subagent_instructions, filter_child_tools,
normalize_background_child_max_turns, normalize_child_max_turns, prepare_child_runtime_config,
};
pub use discovery::discover_controller_subagents;
pub use model::{
agent_type_for_spec, load_memory_appendix, load_memory_appendix_async, load_primary_memory_appendix,
load_primary_memory_appendix_async,
};
pub use prompt::{
contains_explicit_delegation_request, contains_explicit_model_request, delegated_task_requires_clarification,
extract_explicit_agent_mentions, normalize_requested_model_override, request_prompt, sanitize_subagent_input_items,
};
pub use types::{
BackgroundCompletionEvent, BackgroundRecord, BackgroundSubprocessEntry, BackgroundSubprocessSnapshot,
BackgroundSubprocessStatus, ChildRecord, ChildRunResult, ControllerState, PersistedBackgroundRecord,
PersistedBackgroundState, SendInputRequest, SpawnAgentRequest, SpawnBackgroundSubprocessRequest,
StatusEntryBuilder, SubagentInputItem, SubagentStatus, SubagentStatusEntry, SubagentThreadSnapshot,
TurnDelegationHints,
};
pub fn is_subagent_tool(name: &str) -> bool {
SUBAGENT_TOOL_NAMES.contains(&name)
}
#[derive(Clone, Default)]
pub(super) struct BackgroundLaunchOverrides {
prompt: Option<String>,
max_turns: Option<usize>,
model_override: Option<String>,
reasoning_override: Option<String>,
}
#[derive(Clone, Default)]
pub(super) struct PreparedDelegationContext {
requested_agent: Option<String>,
explicit_mentions: Vec<String>,
explicit_request: bool,
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub approved: bool,
pub issues: Vec<String>,
pub reasoning: String,
}
use anyhow::{Context, Result, anyhow, bail};
use chrono::Utc;
use futures::future::select_all;
use parking_lot::Mutex as ParkingMutex;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::{Mutex, Notify, RwLock, broadcast};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::config::VTCodeConfig;
use crate::config::types::ReasoningEffortLevel;
use crate::core::agent::runner::{AgentRunner, RunnerSettings};
use crate::core::agent::task::Task;
use crate::core::threads::{ThreadBootstrap, ThreadId, ThreadRuntimeHandle, ThreadSnapshot};
use crate::hooks::{LifecycleHookEngine, SessionStartTrigger};
use crate::llm::provider::Message;
use crate::tools::exec_session::ExecSessionManager;
use crate::tools::pty::{PtyManager, PtySize};
use crate::utils::session_archive::{SessionArchive, find_session_by_identifier};
use vtcode_config::SubagentSpec;
use vtcode_config::auth::OpenAIChatGptAuthHandle;
use self::background::*;
use self::config::*;
use self::constants::*;
use self::model::*;
use vtcode_config::subagents::SUBAGENT_HARD_CONCURRENCY_LIMIT;
const BACKGROUND_COMPLETION_CHANNEL_CAPACITY: usize = 64;
struct BackgroundCompletionChannel {
sender: broadcast::Sender<BackgroundCompletionEvent>,
parent_sender: broadcast::Sender<BackgroundCompletionEvent>,
pending_parent_events: VecDeque<BackgroundCompletionEvent>,
parent_subscribed: bool,
}
impl BackgroundCompletionChannel {
fn new() -> Self {
let (sender, _) = broadcast::channel(BACKGROUND_COMPLETION_CHANNEL_CAPACITY);
let (parent_sender, _) = broadcast::channel(BACKGROUND_COMPLETION_CHANNEL_CAPACITY);
Self {
sender,
parent_sender,
pending_parent_events: VecDeque::new(),
parent_subscribed: false,
}
}
fn subscribe(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
self.sender.subscribe()
}
fn subscribe_parent(&mut self) -> (broadcast::Receiver<BackgroundCompletionEvent>, bool) {
let receiver = self.parent_sender.subscribe();
self.parent_subscribed = true;
let replayed = !self.pending_parent_events.is_empty();
while let Some(event) = self.pending_parent_events.pop_front() {
let _ = self.parent_sender.send(event);
}
(receiver, replayed)
}
fn publish(&mut self, event: BackgroundCompletionEvent) {
if !self.parent_subscribed {
if self.pending_parent_events.len() >= BACKGROUND_COMPLETION_CHANNEL_CAPACITY {
self.pending_parent_events.pop_front();
tracing::warn!(
capacity = BACKGROUND_COMPLETION_CHANNEL_CAPACITY,
"Dropping oldest undelivered parent background completion"
);
}
self.pending_parent_events.push_back(event.clone());
} else {
let _ = self.parent_sender.send(event.clone());
}
let _ = self.sender.send(event);
}
}
#[derive(Clone)]
pub struct SubagentControllerConfig {
pub workspace_root: PathBuf,
pub parent_session_id: String,
pub parent_model: String,
pub parent_provider: String,
pub parent_reasoning_effort: ReasoningEffortLevel,
pub api_key: String,
pub vt_cfg: VTCodeConfig,
pub openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
pub depth: usize,
pub workspace_gated: bool,
pub exec_sessions: ExecSessionManager,
pub pty_manager: PtyManager,
pub managed_background_runtime: bool,
}
#[derive(Clone)]
pub struct SubagentController {
config: Arc<SubagentControllerConfig>,
parent_session_id: Arc<RwLock<String>>,
lifecycle_hooks: Option<LifecycleHookEngine>,
state: Arc<RwLock<ControllerState>>,
shutdown_requested: Arc<AtomicBool>,
closing: Arc<AtomicBool>,
background_completion_channel: Arc<ParkingMutex<BackgroundCompletionChannel>>,
background_completion_notify: Arc<Notify>,
background_completion_shutdown: CancellationToken,
background_completion_monitor: Arc<Mutex<Option<JoinHandle<()>>>>,
}
impl SubagentController {
pub async fn new(config: SubagentControllerConfig) -> Result<Self> {
let discovered = discover_controller_subagents(&config.workspace_root).await?;
Box::pin(Self::new_with_discovered(config, discovered)).await
}
pub async fn new_with_discovered(
config: SubagentControllerConfig,
discovered: vtcode_config::DiscoveredSubagents,
) -> Result<Self> {
let workspace_gated = config.workspace_gated;
let lifecycle_hooks = LifecycleHookEngine::new_with_session_gated(
config.workspace_root.clone(),
&config.vt_cfg.hooks,
SessionStartTrigger::Startup,
config.parent_session_id.clone(),
workspace_gated,
)?;
if let Some(engine) = lifecycle_hooks.as_ref() {
crate::hooks::lifecycle::restore_workspace_hook_approval(engine, &config.workspace_root).await;
}
let background_children = load_background_state(&config.workspace_root)
.await?
.records
.into_iter()
.map(|record| (record.id.clone(), BackgroundRecord::from_persisted(record)))
.collect();
let controller = Self {
parent_session_id: Arc::new(RwLock::new(config.parent_session_id.clone())),
lifecycle_hooks,
config: Arc::new(config),
state: Arc::new(RwLock::new(ControllerState {
discovered,
parent_messages: Vec::new(),
turn_hints: TurnDelegationHints::default(),
children: std::collections::BTreeMap::new(),
background_children,
background_completion_identities: VecDeque::new(),
})),
shutdown_requested: Arc::new(AtomicBool::new(false)),
closing: Arc::new(AtomicBool::new(false)),
background_completion_channel: Arc::new(ParkingMutex::new(BackgroundCompletionChannel::new())),
background_completion_notify: Arc::new(Notify::new()),
background_completion_shutdown: CancellationToken::new(),
background_completion_monitor: Arc::new(Mutex::new(None)),
};
controller.start_background_completion_monitor().await;
Ok(controller)
}
pub fn subscribe_background_completions(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
self.background_completion_channel.lock().subscribe()
}
pub fn subscribe_parent_background_completions(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
let (receiver, replayed) = self.background_completion_channel.lock().subscribe_parent();
if replayed {
self.background_completion_notify.notify_one();
}
receiver
}
pub fn background_completion_notify(&self) -> Arc<Notify> {
Arc::clone(&self.background_completion_notify)
}
pub async fn reload(&self) -> Result<()> {
let discovered = discover_controller_subagents(&self.config.workspace_root).await?;
self.state.write().await.discovered = discovered;
Ok(())
}
pub async fn set_parent_messages(&self, messages: &[Message]) {
let cloned = messages.to_vec();
self.state.write().await.parent_messages = cloned;
}
pub async fn set_turn_delegation_hints_from_input(&self, input: &str) -> Vec<String> {
let mut state = self.state.write().await;
let explicit_mentions = extract_explicit_agent_mentions(input, state.discovered.effective.as_slice());
let explicit_request = contains_explicit_delegation_request(input, explicit_mentions.as_slice());
state.turn_hints = TurnDelegationHints {
explicit_mentions: explicit_mentions.clone(),
explicit_request,
current_input: input.to_string(),
};
explicit_mentions
}
pub async fn clear_turn_delegation_hints(&self) {
self.state.write().await.turn_hints = TurnDelegationHints::default();
}
pub async fn set_parent_session_id(&self, session_id: impl Into<String>) {
*self.parent_session_id.write().await = session_id.into();
}
pub async fn effective_specs(&self) -> Vec<SubagentSpec> {
self.state.read().await.discovered.effective.clone()
}
pub async fn shadowed_specs(&self) -> Vec<SubagentSpec> {
self.state.read().await.discovered.shadowed.clone()
}
pub async fn status_entries(&self) -> Vec<SubagentStatusEntry> {
let state = self.state.read().await;
state.children.values().map(ChildRecord::build_status_entry).collect()
}
}
mod controller_background_ops;
mod controller_child_loop;
mod controller_helpers;
mod controller_spawn_run;
mod controller_verify;
#[allow(
unused_imports,
reason = "Intentional compatibility, platform, or test-only suppression."
)]
pub(super) use controller_helpers::*;
#[cfg(test)]
mod tests;