use anyhow::{Context, Result};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use vtcode_config::{SubagentSpec, builtin_subagents};
use vtcode_core::config::types::AgentConfig;
use vtcode_core::hooks::{LifecycleHookEngine, SessionStartTrigger};
use vtcode_core::llm::provider as uni;
use vtcode_core::notifications::set_global_notification_hook_engine;
use vtcode_core::primary_agent::{
ActivePrimaryAgent, build_primary_agent_hook_config, build_primary_agent_runtime_config,
};
use vtcode_core::tools::registry::ToolRegistry;
use crate::agent::runloop::unified::async_mcp_manager::AsyncMcpManager;
use crate::agent::runloop::unified::session_setup::{active_deferred_tool_policy, refresh_tool_snapshot};
use crate::agent::runloop::unified::tool_catalog::ToolCatalogState;
use vtcode_core::config::loader::VTCodeConfig;
pub(crate) struct PrimaryAgentRuntimeSyncContext<'a> {
pub(crate) config: &'a AgentConfig,
pub(crate) vt_cfg: Option<&'a VTCodeConfig>,
pub(crate) thread_id: &'a str,
pub(crate) active_primary_agent: &'a ActivePrimaryAgent,
pub(crate) lifecycle_hooks: &'a mut Option<LifecycleHookEngine>,
pub(crate) async_mcp_manager: Option<&'a Arc<AsyncMcpManager>>,
pub(crate) tool_registry: &'a mut ToolRegistry,
pub(crate) tools: &'a Arc<RwLock<Vec<uni::ToolDefinition>>>,
pub(crate) tool_catalog: &'a ToolCatalogState,
pub(crate) mcp_catalog_initialized: &'a mut bool,
pub(crate) pending_mcp_refresh: &'a mut bool,
pub(crate) provider_client: &'a dyn uni::LLMProvider,
}
pub(crate) fn sync_primary_agent_permissions(
vt_cfg: &mut Option<VTCodeConfig>,
active_primary_agent: &ActivePrimaryAgent,
) {
if let Some(cfg) = vt_cfg {
cfg.runtime_agent_permissions = Some(active_primary_agent.permissions.clone());
}
}
pub(crate) async fn sync_primary_agent_runtime(ctx: &mut PrimaryAgentRuntimeSyncContext<'_>) -> Result<()> {
let Some(cfg) = ctx.vt_cfg else {
*ctx.lifecycle_hooks = None;
set_global_notification_hook_engine(None);
return Ok(());
};
let transcript_path = match ctx.lifecycle_hooks.as_ref() {
Some(hooks) => hooks.transcript_path().await,
None => None,
};
let hooks_config = build_primary_agent_hook_config(&cfg.hooks, ctx.active_primary_agent);
let next = LifecycleHookEngine::new_with_session(
ctx.config.workspace.clone(),
&hooks_config,
SessionStartTrigger::Startup,
ctx.thread_id,
)?;
if let (Some(hooks), Some(path)) = (next.as_ref(), transcript_path) {
hooks.update_transcript_path(Some(path)).await;
}
set_global_notification_hook_engine(next.clone());
*ctx.lifecycle_hooks = next;
if let Some(manager) = ctx.async_mcp_manager.filter(|_| cfg.mcp.enabled) {
let merged_mcp = build_primary_agent_runtime_config(cfg, ctx.active_primary_agent).mcp;
let restarted_mcp_runtime = manager.reconfigure_active_runtime(merged_mcp).await?;
ctx.tool_registry.clear_mcp_client().await;
*ctx.mcp_catalog_initialized = false;
*ctx.pending_mcp_refresh = true;
let deferred_tool_policy = active_deferred_tool_policy(ctx.config, Some(cfg), ctx.provider_client);
refresh_tool_snapshot(
ctx.tool_registry,
ctx.tools,
ctx.tool_catalog,
ctx.config,
Some(cfg),
cfg.agent.tool_documentation_mode,
&deferred_tool_policy,
)
.await;
ctx.tool_catalog.mark_pending_refresh("primary_agent_mcp_reconfigure");
if restarted_mcp_runtime {
tracing::debug!("Restarted active MCP runtime after primary agent switch");
}
}
Ok(())
}
pub(crate) async fn load_primary_agent_specs(
tool_registry: &ToolRegistry,
workspace: &Path,
) -> Result<Vec<SubagentSpec>> {
if let Some(controller) = tool_registry.subagent_controller() {
let specs = controller
.effective_specs()
.await
.into_iter()
.filter(SubagentSpec::is_primary)
.collect::<Vec<_>>();
if !specs.is_empty() {
return Ok(specs);
}
}
let discovered =
vtcode_config::discover_subagents(&vtcode_config::SubagentDiscoveryInput::new(workspace.to_path_buf()))
.with_context(|| format!("Failed to discover primary agents in {}", workspace.display()))?;
Ok(discovered.effective.into_iter().filter(SubagentSpec::is_primary).collect())
}
pub(crate) fn builtin_primary_agent_specs() -> Vec<SubagentSpec> {
builtin_subagents().into_iter().filter(SubagentSpec::is_primary).collect()
}
pub(crate) fn resolve_approved_plan_execution_agent(
requested_agent: Option<&str>,
configured_default_agent: Option<&str>,
specs: &[SubagentSpec],
) -> Option<String> {
for candidate in [requested_agent, configured_default_agent, Some("build"), Some("auto")]
.into_iter()
.flatten()
{
if let Some(spec) = specs.iter().find(|spec| {
spec.is_primary()
&& !spec.is_read_only()
&& (spec.name.eq_ignore_ascii_case(candidate)
|| spec.aliases.iter().any(|alias| alias.eq_ignore_ascii_case(candidate)))
}) {
return Some(spec.name.clone());
}
}
specs
.iter()
.find(|spec| spec.is_primary() && !spec.is_read_only())
.map(|spec| spec.name.clone())
}
#[cfg(test)]
mod tests {
use super::{resolve_approved_plan_execution_agent, sync_primary_agent_permissions};
use vtcode_config::core::permissions::{AgentPermissionsConfig, PermissionDefault};
use vtcode_config::{builtin_primary_auto_agent, builtin_primary_build_agent, builtin_primary_duck_agent};
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::primary_agent::ActivePrimaryAgent;
#[test]
fn read_only_requested_agent_falls_back_to_build() {
let specs = vec![builtin_primary_duck_agent(), builtin_primary_build_agent()];
assert_eq!(
resolve_approved_plan_execution_agent(Some("duck"), Some("duck"), &specs),
Some("build".to_string())
);
}
#[test]
fn write_capable_requested_agent_is_preserved() {
let specs = vec![builtin_primary_auto_agent(), builtin_primary_build_agent()];
assert_eq!(
resolve_approved_plan_execution_agent(Some("auto"), Some("build"), &specs),
Some("auto".to_string())
);
}
#[test]
fn all_read_only_agents_produce_no_execution_handoff() {
let specs = vec![builtin_primary_duck_agent()];
assert_eq!(resolve_approved_plan_execution_agent(Some("duck"), None, &specs), None);
}
#[test]
fn plan_handoff_replaces_stale_runtime_permissions() {
let mut vt_cfg = Some(VTCodeConfig {
runtime_agent_permissions: Some(AgentPermissionsConfig::new(PermissionDefault::Deny)),
..VTCodeConfig::default()
});
let build = ActivePrimaryAgent::from_spec(&builtin_primary_build_agent());
sync_primary_agent_permissions(&mut vt_cfg, &build);
assert_eq!(
vt_cfg.expect("runtime config should remain present").runtime_agent_permissions,
Some(build.permissions)
);
}
}