Skip to main content

vtcode_core/subagents/
mod.rs

1#![allow(
2    unused_imports,
3    reason = "Intentional compatibility, platform, or test-only suppression."
4)]
5// ─── Module Structure ───────────────────────────────────────────────────────
6
7mod background;
8mod config;
9mod constants;
10mod discovery;
11pub mod matrix;
12mod model;
13mod prompt;
14mod types;
15
16// ─── Re-exports ─────────────────────────────────────────────────────────────
17
18pub 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
44// VerificationResult is defined in this module (below) and re-exported at the
45// crate root via `pub use subagents::VerificationResult`.
46
47// ─── Public Utilities ───────────────────────────────────────────────────────
48
49/// Returns `true` if `name` is one of the reserved subagent-internal tool names.
50pub 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/// Result of a propose/verify cycle.
70///
71/// Returned by [`SubagentController::verify_proposed_change`]. The caller
72/// inspects `approved` to decide whether to commit or retry the mutation.
73#[derive(Debug, Clone)]
74pub struct VerificationResult {
75    /// Whether the verifier approved the change.
76    pub approved: bool,
77    /// Concrete issues identified by the verifier (empty if approved).
78    pub issues: Vec<String>,
79    /// Free-text reasoning from the verifier.
80    pub reasoning: String,
81}
82
83// ─── Controller ─────────────────────────────────────────────────────────────
84
85use 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// ─── Controller Config ─────────────────────────────────────────────────────
169
170/// Configuration required to construct a [`SubagentController`].
171#[derive(Clone)]
172pub struct SubagentControllerConfig {
173    /// Workspace root directory for the session.
174    pub workspace_root: PathBuf,
175    /// Session identifier of the parent agent.
176    pub parent_session_id: String,
177    /// Model identifier used by the parent agent.
178    pub parent_model: String,
179    /// Provider name used by the parent agent.
180    pub parent_provider: String,
181    /// Reasoning effort level of the parent agent.
182    pub parent_reasoning_effort: ReasoningEffortLevel,
183    /// API key for LLM provider access.
184    pub api_key: String,
185    /// Full VT Code configuration.
186    pub vt_cfg: VTCodeConfig,
187    /// Optional OpenAI ChatGPT authentication handle.
188    pub openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
189    /// Current nesting depth of the subagent hierarchy.
190    pub depth: usize,
191    /// Whether the subagent lifecycle engine must be workspace-gated. Mirrors
192    /// the main-session rule: pass `true` whenever workspace-controlled hook
193    /// content is present (workspace vtcode.toml/.vtcode layers OR a primary
194    /// agent spec contributing workspace-controlled hooks). Failing to gate
195    /// here would let workspace hooks run without user approval in a
196    /// subagent context.
197    pub workspace_gated: bool,
198    /// Manager for exec sessions (PTY and pipe).
199    pub exec_sessions: ExecSessionManager,
200    /// PTY session manager.
201    pub pty_manager: PtyManager,
202    /// Whether this controller manages a background runtime subprocess.
203    pub managed_background_runtime: bool,
204}
205
206/// Central controller that manages spawning, lifecycle, and state of all subagents.
207/// The background completion monitor is cancelled when the final controller
208/// owner is dropped; its task-held clone is intentionally non-owning.
209pub 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    /// Transient close-in-progress flag. Unlike `shutdown_requested` this is
218    /// cleared when a subtree is reopened via `reopen_single`, so a resumed
219    /// child can delegate again and its controller keeps saving background
220    /// state. Set only while `close_tree`/`signal_shutdown` are tearing a
221    /// subtree down.
222    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    /// Counts controller clones that participate in monitor ownership.
228    background_completion_owners: Arc<AtomicUsize>,
229    /// The monitor task retains a clone for processing but must not keep the
230    /// monitor alive after all external controller owners are gone.
231    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    /// Creates a new controller, discovering subagent specs and loading persisted background state.
275    pub async fn new(config: SubagentControllerConfig) -> Result<Self> {
276        let discovered = discover_controller_subagents(&config.workspace_root).await?;
277        // Box the inner constructor: it carries `VTCodeConfig` + spec state
278        // across awaits, which would otherwise bloat every `new` caller's
279        // async frame past the `large_futures` budget (denied in test builds).
280        Box::pin(Self::new_with_discovered(config, discovered)).await
281    }
282
283    /// Creates a new controller reusing an already-discovered spec set.
284    ///
285    /// Interactive startup discovers specs once on the first-paint path;
286    /// hydration passes that result here to skip a second workspace/plugin
287    /// filesystem scan. Behavior matches [`Self::new`] otherwise.
288    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    /// Subscribes to terminal notifications for managed background subprocesses.
339    pub fn subscribe_background_completions(&self) -> broadcast::Receiver<BackgroundCompletionEvent> {
340        self.background_completion_channel.lock().subscribe()
341    }
342
343    /// Subscribes the parent run loop and replays completions that arrived
344    /// before its receiver was installed.
345    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    /// Returns the wake signal used by the interactive loop while it is idle.
354    pub fn background_completion_notify(&self) -> Arc<Notify> {
355        Arc::clone(&self.background_completion_notify)
356    }
357
358    /// Re-discovers subagent specs from the workspace.
359    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    /// Stores the parent conversation messages for context forking into children.
366    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    /// Parses the current user input to extract explicit agent mentions and delegation signals.
372    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    /// Resets delegation hints at the end of a turn.
385    pub async fn clear_turn_delegation_hints(&self) {
386        self.state.write().await.turn_hints = TurnDelegationHints::default();
387    }
388
389    /// Updates the parent session identifier at runtime.
390    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    /// Returns the currently effective subagent specifications (merged builtin + workspace).
395    pub async fn effective_specs(&self) -> Vec<SubagentSpec> {
396        self.state.read().await.discovered.effective.clone()
397    }
398
399    /// Returns specs that are shadowed by workspace-level overrides.
400    pub async fn shadowed_specs(&self) -> Vec<SubagentSpec> {
401        self.state.read().await.discovered.shadowed.clone()
402    }
403
404    /// Returns status entries for all tracked child subagents.
405    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
414// ─── Controller submodule split ────────────────────────────────────────────
415
416mod 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// ─── Tests ──────────────────────────────────────────────────────────────────
429
430#[cfg(test)]
431mod tests;