1#![allow(
2 unused_imports,
3 reason = "Intentional compatibility, platform, or test-only suppression."
4)]
5mod background;
8mod config;
9mod constants;
10mod discovery;
11pub mod matrix;
12mod model;
13mod prompt;
14mod types;
15
16pub use background::{
19 background_record_id, build_background_subagent_command, extract_tail_lines, load_archive_preview,
20 subagent_display_label,
21};
22pub(crate) use config::inline_mcp_provider;
23pub use config::{
24 ResolvedAgentRuntimeView, build_child_config, compose_subagent_instructions, filter_child_tools,
25 normalize_background_child_max_turns, normalize_child_max_turns, prepare_child_runtime_config,
26};
27pub use discovery::discover_controller_subagents;
28pub use model::{
29 agent_type_for_spec, load_memory_appendix, load_memory_appendix_async, load_primary_memory_appendix,
30 load_primary_memory_appendix_async,
31};
32pub use prompt::{
33 contains_explicit_delegation_request, contains_explicit_model_request, delegated_task_requires_clarification,
34 extract_explicit_agent_mentions, normalize_requested_model_override, request_prompt, sanitize_subagent_input_items,
35};
36pub use types::{
37 BackgroundCompletionEvent, BackgroundRecord, BackgroundSubprocessEntry, BackgroundSubprocessSnapshot,
38 BackgroundSubprocessStatus, ChildRecord, ChildRunResult, ControllerState, PersistedBackgroundRecord,
39 PersistedBackgroundState, SendInputRequest, SpawnAgentRequest, SpawnBackgroundSubprocessRequest,
40 StatusEntryBuilder, SubagentInputItem, SubagentStatus, SubagentStatusEntry, SubagentThreadSnapshot,
41 TurnDelegationHints,
42};
43
44pub fn is_subagent_tool(name: &str) -> bool {
51 SUBAGENT_TOOL_NAMES.contains(&name)
52}
53
54#[derive(Clone, Default)]
55pub(super) struct BackgroundLaunchOverrides {
56 prompt: Option<String>,
57 max_turns: Option<usize>,
58 model_override: Option<String>,
59 reasoning_override: Option<String>,
60}
61
62#[derive(Clone, Default)]
63pub(super) struct PreparedDelegationContext {
64 requested_agent: Option<String>,
65 explicit_mentions: Vec<String>,
66 explicit_request: bool,
67}
68
69#[derive(Debug, Clone)]
74pub struct VerificationResult {
75 pub approved: bool,
77 pub issues: Vec<String>,
79 pub reasoning: String,
81}
82
83use anyhow::{Context, Result, anyhow, bail};
86use chrono::Utc;
87use futures::future::select_all;
88use parking_lot::Mutex as ParkingMutex;
89use std::collections::VecDeque;
90use std::path::PathBuf;
91use std::sync::Arc;
92use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
93use tokio::sync::{Mutex, Notify, RwLock, broadcast};
94use tokio::task::JoinHandle;
95use tokio_util::sync::CancellationToken;
96
97use crate::config::VTCodeConfig;
98use crate::config::types::ReasoningEffortLevel;
99use crate::core::agent::runner::{AgentRunner, RunnerSettings};
100use crate::core::agent::task::Task;
101use crate::core::threads::{ThreadBootstrap, ThreadId, ThreadRuntimeHandle, ThreadSnapshot};
102use crate::hooks::{LifecycleHookEngine, SessionStartTrigger};
103use crate::llm::provider::Message;
104use crate::tools::exec_session::ExecSessionManager;
105use crate::tools::pty::{PtyManager, PtySize};
106use crate::utils::session_archive::{SessionArchive, find_session_by_identifier};
107use vtcode_config::SubagentSpec;
108use vtcode_config::auth::OpenAIChatGptAuthHandle;
109
110use self::background::*;
111use self::config::*;
112use self::constants::*;
113use self::model::*;
114use vtcode_config::subagents::SUBAGENT_HARD_CONCURRENCY_LIMIT;
115
116const BACKGROUND_COMPLETION_CHANNEL_CAPACITY: usize = 64;
117
118struct BackgroundCompletionChannel {
119 sender: broadcast::Sender<BackgroundCompletionEvent>,
120 parent_sender: broadcast::Sender<BackgroundCompletionEvent>,
121 pending_parent_events: VecDeque<BackgroundCompletionEvent>,
122 parent_subscribed: bool,
123}
124
125impl BackgroundCompletionChannel {
126 fn new() -> Self {
127 let (sender, _) = broadcast::channel(BACKGROUND_COMPLETION_CHANNEL_CAPACITY);
128 let (parent_sender, _) = broadcast::channel(BACKGROUND_COMPLETION_CHANNEL_CAPACITY);
129 Self {
130 sender,
131 parent_sender,
132 pending_parent_events: VecDeque::new(),
133 parent_subscribed: false,
134 }
135 }
136
137 fn subscribe(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
138 self.sender.subscribe()
139 }
140
141 fn subscribe_parent(&mut self) -> (broadcast::Receiver<BackgroundCompletionEvent>, bool) {
142 let receiver = self.parent_sender.subscribe();
143 self.parent_subscribed = true;
144 let replayed = !self.pending_parent_events.is_empty();
145 while let Some(event) = self.pending_parent_events.pop_front() {
146 let _ = self.parent_sender.send(event);
147 }
148 (receiver, replayed)
149 }
150
151 fn publish(&mut self, event: BackgroundCompletionEvent) {
152 if !self.parent_subscribed {
153 if self.pending_parent_events.len() >= BACKGROUND_COMPLETION_CHANNEL_CAPACITY {
154 self.pending_parent_events.pop_front();
155 tracing::warn!(
156 capacity = BACKGROUND_COMPLETION_CHANNEL_CAPACITY,
157 "Dropping oldest undelivered parent background completion"
158 );
159 }
160 self.pending_parent_events.push_back(event.clone());
161 } else {
162 let _ = self.parent_sender.send(event.clone());
163 }
164 let _ = self.sender.send(event);
165 }
166}
167
168#[derive(Clone)]
172pub struct SubagentControllerConfig {
173 pub workspace_root: PathBuf,
175 pub parent_session_id: String,
177 pub parent_model: String,
179 pub parent_provider: String,
181 pub parent_reasoning_effort: ReasoningEffortLevel,
183 pub api_key: String,
185 pub vt_cfg: VTCodeConfig,
187 pub openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
189 pub depth: usize,
191 pub workspace_gated: bool,
198 pub exec_sessions: ExecSessionManager,
200 pub pty_manager: PtyManager,
202 pub managed_background_runtime: bool,
204}
205
206pub struct SubagentController {
210 admission: Arc<tokio::sync::Semaphore>,
211 matrix: Arc<matrix::MatrixRuntime>,
212 config: Arc<SubagentControllerConfig>,
213 parent_session_id: Arc<RwLock<String>>,
214 lifecycle_hooks: Option<LifecycleHookEngine>,
215 state: Arc<RwLock<ControllerState>>,
216 shutdown_requested: Arc<AtomicBool>,
217 closing: Arc<AtomicBool>,
223 background_completion_channel: Arc<ParkingMutex<BackgroundCompletionChannel>>,
224 background_completion_notify: Arc<Notify>,
225 background_completion_shutdown: CancellationToken,
226 background_completion_monitor: Arc<Mutex<Option<JoinHandle<()>>>>,
227 background_completion_owners: Arc<AtomicUsize>,
229 background_completion_monitor_owner: bool,
232}
233
234impl Clone for SubagentController {
235 fn clone(&self) -> Self {
236 self.background_completion_owners.fetch_add(1, Ordering::Relaxed);
237 Self {
238 admission: Arc::clone(&self.admission),
239 matrix: Arc::clone(&self.matrix),
240 config: Arc::clone(&self.config),
241 parent_session_id: Arc::clone(&self.parent_session_id),
242 lifecycle_hooks: self.lifecycle_hooks.clone(),
243 state: Arc::clone(&self.state),
244 shutdown_requested: Arc::clone(&self.shutdown_requested),
245 closing: Arc::clone(&self.closing),
246 background_completion_channel: Arc::clone(&self.background_completion_channel),
247 background_completion_notify: Arc::clone(&self.background_completion_notify),
248 background_completion_shutdown: self.background_completion_shutdown.clone(),
249 background_completion_monitor: Arc::clone(&self.background_completion_monitor),
250 background_completion_owners: Arc::clone(&self.background_completion_owners),
251 background_completion_monitor_owner: true,
252 }
253 }
254}
255
256impl Drop for SubagentController {
257 fn drop(&mut self) {
258 if !self.background_completion_monitor_owner
259 || self.background_completion_owners.fetch_sub(1, Ordering::AcqRel) != 1
260 {
261 return;
262 }
263
264 self.background_completion_shutdown.cancel();
265 if let Ok(mut monitor_slot) = self.background_completion_monitor.try_lock()
266 && let Some(monitor) = monitor_slot.take()
267 {
268 monitor.abort();
269 }
270 }
271}
272
273impl SubagentController {
274 pub async fn new(config: SubagentControllerConfig) -> Result<Self> {
276 let discovered = discover_controller_subagents(&config.workspace_root).await?;
277 Box::pin(Self::new_with_discovered(config, discovered)).await
281 }
282
283 pub async fn new_with_discovered(
289 config: SubagentControllerConfig,
290 discovered: vtcode_config::DiscoveredSubagents,
291 ) -> Result<Self> {
292 let workspace_gated = config.workspace_gated;
293 let lifecycle_hooks = LifecycleHookEngine::new_with_session_gated(
294 config.workspace_root.clone(),
295 &config.vt_cfg.hooks,
296 SessionStartTrigger::Startup,
297 config.parent_session_id.clone(),
298 workspace_gated,
299 )?;
300 if let Some(engine) = lifecycle_hooks.as_ref() {
301 crate::hooks::lifecycle::restore_workspace_hook_approval(engine, &config.workspace_root).await;
302 }
303 let background_children = load_background_state(&config.workspace_root)
304 .await?
305 .records
306 .into_iter()
307 .map(|record| (record.id.clone(), BackgroundRecord::from_persisted(record)))
308 .collect();
309 let controller = Self {
310 admission: Arc::new(tokio::sync::Semaphore::new(
311 config.vt_cfg.subagents.max_concurrent.min(SUBAGENT_HARD_CONCURRENCY_LIMIT),
312 )),
313 matrix: Arc::new(matrix::MatrixRuntime::default()),
314 parent_session_id: Arc::new(RwLock::new(config.parent_session_id.clone())),
315 lifecycle_hooks,
316 config: Arc::new(config),
317 state: Arc::new(RwLock::new(ControllerState {
318 discovered,
319 parent_messages: Vec::new(),
320 turn_hints: TurnDelegationHints::default(),
321 children: std::collections::BTreeMap::new(),
322 background_children,
323 background_completion_identities: VecDeque::new(),
324 })),
325 shutdown_requested: Arc::new(AtomicBool::new(false)),
326 closing: Arc::new(AtomicBool::new(false)),
327 background_completion_channel: Arc::new(ParkingMutex::new(BackgroundCompletionChannel::new())),
328 background_completion_notify: Arc::new(Notify::new()),
329 background_completion_shutdown: CancellationToken::new(),
330 background_completion_monitor: Arc::new(Mutex::new(None)),
331 background_completion_owners: Arc::new(AtomicUsize::new(1)),
332 background_completion_monitor_owner: true,
333 };
334 controller.start_background_completion_monitor().await;
335 Ok(controller)
336 }
337
338 pub fn subscribe_background_completions(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
340 self.background_completion_channel.lock().subscribe()
341 }
342
343 pub fn subscribe_parent_background_completions(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
346 let (receiver, replayed) = self.background_completion_channel.lock().subscribe_parent();
347 if replayed {
348 self.background_completion_notify.notify_one();
349 }
350 receiver
351 }
352
353 pub fn background_completion_notify(&self) -> Arc<Notify> {
355 Arc::clone(&self.background_completion_notify)
356 }
357
358 pub async fn reload(&self) -> Result<()> {
360 let discovered = discover_controller_subagents(&self.config.workspace_root).await?;
361 self.state.write().await.discovered = discovered;
362 Ok(())
363 }
364
365 pub async fn set_parent_messages(&self, messages: &[Message]) {
367 let cloned = messages.to_vec();
368 self.state.write().await.parent_messages = cloned;
369 }
370
371 pub async fn set_turn_delegation_hints_from_input(&self, input: &str) -> Vec<String> {
373 let mut state = self.state.write().await;
374 let explicit_mentions = extract_explicit_agent_mentions(input, state.discovered.effective.as_slice());
375 let explicit_request = contains_explicit_delegation_request(input, explicit_mentions.as_slice());
376 state.turn_hints = TurnDelegationHints {
377 explicit_mentions: explicit_mentions.clone(),
378 explicit_request,
379 current_input: input.to_string(),
380 };
381 explicit_mentions
382 }
383
384 pub async fn clear_turn_delegation_hints(&self) {
386 self.state.write().await.turn_hints = TurnDelegationHints::default();
387 }
388
389 pub async fn set_parent_session_id(&self, session_id: impl Into<String>) {
391 *self.parent_session_id.write().await = session_id.into();
392 }
393
394 pub async fn effective_specs(&self) -> Vec<SubagentSpec> {
396 self.state.read().await.discovered.effective.clone()
397 }
398
399 pub async fn shadowed_specs(&self) -> Vec<SubagentSpec> {
401 self.state.read().await.discovered.shadowed.clone()
402 }
403
404 pub async fn status_entries(&self) -> Vec<SubagentStatusEntry> {
406 let state = self.state.read().await;
407 let mut entries = state.children.values().map(ChildRecord::build_status_entry).collect::<Vec<_>>();
408 drop(state);
409 entries.extend(self.matrix_projection_entries().await);
410 entries
411 }
412}
413
414mod controller_background_ops;
417mod controller_child_loop;
418mod controller_helpers;
419mod controller_spawn_run;
420mod controller_verify;
421
422#[allow(
423 unused_imports,
424 reason = "Intentional compatibility, platform, or test-only suppression."
425)]
426pub(super) use controller_helpers::*;
427
428#[cfg(test)]
431mod tests;