use anyhow::{Result, bail};
use vtcode_config::{SubagentSpec, VTCodeConfig};
use vtcode_core::{ActivePrimaryAgentState, constants::defaults::DEFAULT_PRIMARY_AGENT_NAME};
pub(crate) struct SessionModeInput<'a> {
specs: &'a [SubagentSpec],
vt_cfg: Option<&'a VTCodeConfig>,
full_auto: bool,
primary_agent_explicitly_configured: bool,
resumed_primary_agent: Option<String>,
}
impl<'a> SessionModeInput<'a> {
#[must_use]
pub fn new(specs: &'a [SubagentSpec]) -> Self {
Self {
specs,
vt_cfg: None,
full_auto: false,
primary_agent_explicitly_configured: false,
resumed_primary_agent: None,
}
}
#[must_use]
pub fn with_config(mut self, vt_cfg: Option<&'a VTCodeConfig>) -> Self {
self.vt_cfg = vt_cfg;
self
}
#[must_use]
pub fn with_full_auto(mut self, full_auto: bool) -> Self {
self.full_auto = full_auto;
self
}
#[must_use]
pub fn with_explicit_config(mut self, explicit: bool) -> Self {
self.primary_agent_explicitly_configured = explicit;
self
}
#[must_use]
pub fn with_resumed(mut self, resumed: Option<String>) -> Self {
self.resumed_primary_agent = resumed;
self
}
}
pub(crate) fn resolve_session_primary_agent(input: SessionModeInput<'_>) -> Result<ActivePrimaryAgentState> {
let SessionModeInput {
specs,
vt_cfg,
full_auto,
primary_agent_explicitly_configured,
resumed_primary_agent,
} = input;
if full_auto && !primary_agent_explicitly_configured {
let mut active = ActivePrimaryAgentState::default();
if active.select_from_specs(specs, "auto").is_err() {
bail!(
"Full-auto needs the defaulted 'auto' primary agent, but no effective primary agent named 'auto' was discovered. Configure default_primary_agent explicitly or add an 'auto' primary agent."
);
}
return Ok(active);
}
if !primary_agent_explicitly_configured {
if let Some(resumed) = resumed_primary_agent.filter(|name| !name.trim().is_empty()) {
return Ok(ActivePrimaryAgentState::from_specs_with_default(specs, resumed.trim()));
}
}
let default_primary_agent = vt_cfg
.map(|cfg| cfg.default_primary_agent.as_str())
.unwrap_or(DEFAULT_PRIMARY_AGENT_NAME);
Ok(ActivePrimaryAgentState::from_specs_with_default(specs, default_primary_agent))
}
pub(crate) fn active_primary_agent_from_specs_for_mode(
specs: &[SubagentSpec],
vt_cfg: Option<&VTCodeConfig>,
full_auto: bool,
primary_agent_explicitly_configured: bool,
resumed_primary_agent: Option<String>,
) -> Result<ActivePrimaryAgentState> {
resolve_session_primary_agent(
SessionModeInput::new(specs)
.with_config(vt_cfg)
.with_full_auto(full_auto)
.with_explicit_config(primary_agent_explicitly_configured)
.with_resumed(resumed_primary_agent),
)
}
#[cfg(test)]
pub(crate) fn active_primary_agent_from_specs(
specs: &[SubagentSpec],
vt_cfg: Option<&VTCodeConfig>,
) -> Result<ActivePrimaryAgentState> {
active_primary_agent_from_specs_for_mode(specs, vt_cfg, false, false, None)
}
#[cfg(test)]
mod tests {
use super::*;
use vtcode_config::{
AgentMode, SubagentSource, SubagentSpec,
core::permissions::{AgentPermissionsConfig, PermissionDefault},
};
fn spec(name: &str) -> SubagentSpec {
SubagentSpec {
name: name.to_string(),
description: format!("{name} description"),
prompt: format!("{name} instructions"),
tools: None,
disallowed_tools: Vec::new(),
model: None,
color: None,
reasoning_effort: None,
permissions: AgentPermissionsConfig::new(PermissionDefault::Deny),
skills: Vec::new(),
mcp_servers: Vec::new(),
hooks: None,
background: false,
mode: AgentMode::Primary,
max_turns: None,
nickname_candidates: Vec::new(),
initial_prompt: None,
memory: None,
isolation: None,
aliases: Vec::new(),
source: SubagentSource::ProjectVtcode,
file_path: None,
warnings: Vec::new(),
tool_policy_overrides: std::collections::BTreeMap::new(),
}
}
#[test]
fn resumed_mode_wins_over_config_default() {
let cfg = VTCodeConfig {
default_primary_agent: "builder".to_string(),
..VTCodeConfig::default()
};
let active = resolve_session_primary_agent(
SessionModeInput::new(&[spec("plan"), spec("builder")])
.with_config(Some(&cfg))
.with_resumed(Some("plan".to_string())),
)
.expect("resumed primary agent");
assert_eq!(active.active().name(), "plan");
}
#[test]
fn explicit_config_overrides_resumed_mode() {
let cfg = VTCodeConfig {
default_primary_agent: "builder".to_string(),
..VTCodeConfig::default()
};
let active = resolve_session_primary_agent(
SessionModeInput::new(&[spec("plan"), spec("builder")])
.with_config(Some(&cfg))
.with_explicit_config(true)
.with_resumed(Some("plan".to_string())),
)
.expect("configured primary agent");
assert_eq!(active.active().name(), "builder");
}
#[test]
fn missing_resumed_mode_falls_back_to_config_default() {
let cfg = VTCodeConfig {
default_primary_agent: "builder".to_string(),
..VTCodeConfig::default()
};
let active = resolve_session_primary_agent(
SessionModeInput::new(&[spec("builder")])
.with_config(Some(&cfg))
.with_resumed(Some(" ".to_string())),
)
.expect("fallback primary agent");
assert_eq!(active.active().name(), "builder");
}
#[test]
fn full_auto_without_explicit_selects_effective_auto() {
let active =
resolve_session_primary_agent(SessionModeInput::new(&[spec("auto")]).with_full_auto(true)).expect("auto");
assert_eq!(active.active().name(), "auto");
}
}