mod archive;
mod execution_policy;
mod metrics;
mod plan_seed;
mod support;
use super::*;
use crate::agent::runloop::git::{compute_session_code_change_delta, normalize_workspace_path};
use crate::agent::runloop::unified::inline_events::harness::{HARNESS_LOG_MAX_AGE_DAYS, prune_old_harness_logs};
use crate::agent::runloop::unified::planning_workflow_state::render_planning_workflow_next_step_hint;
use crate::agent::runloop::unified::postamble::{ExitData, print_exit_summary};
use crate::agent::runloop::unified::turn::turn_loop::TurnLoopOutcome;
use crate::updater::{InlineUpdateOutcome, display_update_notice, run_inline_update_prompt};
use hashbrown::HashSet;
use vtcode_config::loader::SimpleConfigWatcher;
use vtcode_core::core::agent::features::FeatureSet;
use vtcode_core::core::agent::runtime::AgentRuntime;
use vtcode_core::core::agent::session::AgentSessionState;
use vtcode_core::core::interfaces::session::PlanningEntrySource;
const PLAN_APPROVED_EXECUTION_DIRECTIVE: &str = "Plan was approved. Start implementation immediately: execute the plan step by step beginning with the first pending step. Do not ask for another implementation confirmation.";
const PLAN_APPROVED_EXECUTION_INPUT: &str = "Implement the approved plan now.";
use archive::{create_session_archive, refresh_runtime_debug_context_for_next_session, workspace_archive_label};
use execution_policy::effective_max_tool_calls_for_turn;
use metrics::{
TurnExecutionMetrics, capture_code_change_snapshot, emit_turn_execution_metrics, estimate_history_bytes,
};
use plan_seed::load_active_plan_seed;
use support::{
append_transient_turn_notes, checkpoint_session_archive_start, force_reload_workspace_config_for_execution,
latest_assistant_result_text, live_reload_preserves_session_config, prepare_resume_bootstrap_without_archive,
prompt_startup_planning_workflow, remove_transient_system_notes, take_pending_resumed_user_prompt,
};
use tokio::sync::{Notify, mpsc};
use vtcode_core::llm::provider::MessageRole;
use vtcode_core::utils::session_archive;
use vtcode_ui::tui::app::ArchivedPromptEntry;
pub(super) async fn run_single_agent_loop_unified_impl(
config: &CoreAgentConfig,
initial_vt_cfg: Option<VTCodeConfig>,
skip_confirmations: bool,
full_auto: bool,
primary_agent_explicitly_configured: bool,
planning_entry_source: PlanningEntrySource,
resume: Option<ResumeSession>,
steering_receiver: &mut Option<mpsc::UnboundedReceiver<SteeringMessage>>,
) -> Result<()> {
let _terminal_cleanup_guard = TerminalCleanupGuard::new();
let mut config = config.clone();
let mut resume_state = resume;
let mut _consecutive_idle_cycles = 0;
let mut last_activity_time: Option<Instant> = None;
let mut config_watcher = SimpleConfigWatcher::new(config.workspace.clone());
config_watcher.set_check_interval(15);
config_watcher.set_debounce_duration(500);
let live_reload_enabled = live_reload_preserves_session_config(initial_vt_cfg.as_ref(), &config);
if !live_reload_enabled {
tracing::debug!(
"Configuration live reload disabled because startup overrides cannot be reproduced from workspace config"
);
}
let mut vt_cfg = initial_vt_cfg.or_else(|| config_watcher.load_config());
let mut idle_config = extract_idle_config(vt_cfg.as_ref());
let mut pending_session_start_trigger = None;
loop {
let session_started_at = Instant::now();
let start_code_changes = capture_code_change_snapshot(&config.workspace, "start").await;
let resume_request = resume_state.take();
let resume_ref = resume_request.as_ref();
let session_trigger = pending_session_start_trigger.take().unwrap_or_else(|| {
if resume_ref.is_some() {
SessionStartTrigger::Resume
} else {
SessionStartTrigger::Startup
}
});
let active_thread_label = resume_ref.map_or("main", ResumeSession::thread_label);
let thread_manager = vtcode_core::core::threads::ThreadManager::new();
let archive_metadata = vtcode_core::core::threads::build_thread_archive_metadata(
&config.workspace,
&config.model,
&config.provider,
&config.theme,
config.reasoning_effort.as_str(),
)
.with_debug_log_path(
crate::main_helpers::runtime_debug_log_path().map(|path| path.to_string_lossy().to_string()),
);
let reserved_archive_id = crate::main_helpers::runtime_archive_session_id();
let history_enabled = session_archive::history_persistence_enabled();
let summarized_fork_provider = if resume_ref.is_some_and(|resume| resume.summarize_fork()) {
Some(crate::agent::runloop::unified::session_setup::create_provider_client(&config, vt_cfg.as_ref())?)
} else {
None
};
let (thread_handle, mut session_archive) = if let Some(resume) = resume_ref {
if history_enabled {
let mut prepared = vtcode_core::core::threads::prepare_archived_session(
resume.listing().clone(),
config.workspace.clone(),
archive_metadata.clone(),
resume.intent().clone(),
if resume.is_fork() {
reserved_archive_id.clone()
} else {
None
},
)
.await?;
if let Some(provider) = summarized_fork_provider.as_deref() {
prepared.bootstrap.messages =
crate::agent::runloop::unified::turn::compaction::build_summarized_fork_history(
provider,
&config.model,
&resume.identifier(),
&prepared.thread_id,
&config.workspace,
vt_cfg.as_ref(),
resume.history(),
resume.budget_limit_continuation().is_some(),
)
.await?;
}
(
thread_manager.start_thread_with_identifier(prepared.thread_id.clone(), prepared.bootstrap),
Some(prepared.archive),
)
} else {
let (mut bootstrap, thread_id) = prepare_resume_bootstrap_without_archive(
resume,
archive_metadata.clone(),
reserved_archive_id.clone(),
);
if let Some(provider) = summarized_fork_provider.as_deref() {
bootstrap.messages =
crate::agent::runloop::unified::turn::compaction::build_summarized_fork_history(
provider,
&config.model,
&resume.identifier(),
&thread_id,
&config.workspace,
vt_cfg.as_ref(),
resume.history(),
resume.budget_limit_continuation().is_some(),
)
.await?;
}
(thread_manager.start_thread_with_identifier(thread_id, bootstrap), None)
}
} else {
let thread_id = if let Some(identifier) = reserved_archive_id.clone() {
identifier
} else if history_enabled {
session_archive::reserve_session_archive_identifier(&workspace_archive_label(&config.workspace), None)
.await?
} else {
session_archive::generate_session_archive_identifier(&workspace_archive_label(&config.workspace), None)
};
let bootstrap = vtcode_core::core::threads::ThreadBootstrap::new(Some(archive_metadata.clone()));
let archive = if history_enabled {
Some(create_session_archive(archive_metadata.clone(), Some(thread_id.clone())).await?)
} else {
None
};
(thread_manager.start_thread_with_identifier(thread_id, bootstrap), archive)
};
crate::main_helpers::set_runtime_archive_session_id(Some(thread_handle.thread_id().to_string()));
if let Some(archive) = session_archive.as_ref()
&& let Err(err) = checkpoint_session_archive_start(archive, &thread_handle).await
{
tracing::warn!("Failed to checkpoint session archive at startup: {}", err);
}
let mut session_state = initialize_session(
&config,
vt_cfg.as_ref(),
full_auto,
primary_agent_explicitly_configured,
resume_ref,
thread_handle.thread_id().as_str(),
)
.await?;
if let Some(archive) = session_archive.as_mut() {
archive.set_primary_agent(session_state.active_primary_agent.active().name());
}
let harness_config = vt_cfg.as_ref().map(|cfg| cfg.agent.harness.clone()).unwrap_or_default();
let turn_run_id = TurnRunId(thread_handle.thread_id().to_string());
let effective_log_path: Option<String> = harness_config
.event_log_path
.as_ref()
.filter(|path| !path.trim().is_empty())
.cloned()
.or_else(|| default_harness_log_dir().map(|dir| dir.to_string_lossy().into_owned()));
if let Some(ref log_path) = effective_log_path {
let log_dir = std::path::Path::new(log_path);
let dir = if log_dir.extension().is_some() {
log_dir.parent().unwrap_or(log_dir)
} else {
log_dir
};
prune_old_harness_logs(dir, HARNESS_LOG_MAX_AGE_DAYS);
}
let harness_emitter: Option<HarnessEventEmitter> = effective_log_path.as_deref().and_then(|path| {
let resolved = resolve_event_log_path(path, &turn_run_id);
HarnessEventEmitter::new(resolved).ok()
});
if let Some(emitter) = harness_emitter.as_ref() {
let open_responses_config = vt_cfg.as_ref().map(|cfg| cfg.agent.open_responses.clone()).unwrap_or_default();
let features = FeatureSet::from_config(vt_cfg.as_ref());
if features.open_responses.emit_events {
let or_path = effective_log_path.as_ref().map(|base| {
let parent = std::path::Path::new(base.as_str())
.parent()
.unwrap_or(std::path::Path::new("."));
let timestamp = Utc::now().format("%Y%m%dT%H%M%SZ");
parent.join(format!("open-responses-{}-{}.jsonl", turn_run_id.0, timestamp))
});
let _ = emitter.enable_open_responses(open_responses_config, &config.model, or_path);
}
let atif_enabled = vt_cfg.as_ref().map(|cfg| cfg.telemetry.atif_enabled).unwrap_or(false);
if atif_enabled {
let dir = effective_log_path
.as_ref()
.map(|base| std::path::PathBuf::from(base.as_str()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
let timestamp = Utc::now().format("%Y%m%dT%H%M%SZ");
let atif_path = dir.join(format!("atif-trajectory-{}-{}.json", turn_run_id.0, timestamp));
let _ = emitter.enable_atif(&config.model, atif_path);
}
let _ = emitter.emit(ThreadEvent::ThreadStarted(ThreadStartedEvent { thread_id: turn_run_id.0.clone() }));
}
let steering_sender = if steering_receiver.is_none() {
let (sender, receiver) = mpsc::unbounded_channel();
*steering_receiver = Some(receiver);
Some(sender)
} else {
None
};
let ui_setup = initialize_session_ui(
&config,
vt_cfg.as_ref(),
thread_handle.thread_id().as_str(),
&mut session_state,
session_trigger,
resume_ref,
crate::agent::runloop::unified::session_setup::SessionUiLaunchOptions {
session_archive,
full_auto,
skip_confirmations,
steering_sender,
},
)
.await?;
let mut renderer = ui_setup.renderer;
let mut session = ui_setup.session;
let handle = ui_setup.handle;
{
let handle = handle.clone();
tokio::spawn(async move {
load_archived_prompts_for_history(&handle).await;
});
}
let mut header_context = ui_setup.header_context;
let mut ide_context_bridge = ui_setup.ide_context_bridge;
let ctrl_c_state = ui_setup.ctrl_c_state;
let ctrl_c_notify = ui_setup.ctrl_c_notify;
let input_activity_counter = ui_setup.input_activity_counter;
let checkpoint_manager = ui_setup.checkpoint_manager;
let mut session_archive = ui_setup.session_archive;
let mut lifecycle_hooks = ui_setup.lifecycle_hooks;
let mut context_manager = ui_setup.context_manager;
let mut default_placeholder = ui_setup.default_placeholder;
let mut follow_up_placeholder = ui_setup.follow_up_placeholder;
let mut next_checkpoint_turn = ui_setup.next_checkpoint_turn;
let mut session_end_reason = ui_setup.session_end_reason;
let mut turn_id = turn_run_id.0.clone();
let _file_palette_task_guard = ui_setup.file_palette_task_guard;
let _background_subprocess_task_guard = ui_setup.background_subprocess_task_guard;
let _startup_update_task_guard = ui_setup.startup_update_task_guard;
let startup_update_cached_notice = ui_setup.startup_update_cached_notice;
let mut startup_update_notice_rx = ui_setup.startup_update_notice_rx;
let SessionState {
session_bootstrap,
mut provider_client,
mut tool_registry,
tools,
tool_catalog,
conversation_history,
execution,
metadata,
async_mcp_manager,
mut mcp_panel_state,
loaded_skills,
mut active_primary_agent,
..
} = session_state;
let decision_ledger = metadata.decision_ledger;
let traj = metadata.trajectory;
let telemetry = metadata.telemetry;
let error_recovery = metadata.error_recovery;
let max_tool_loops = vt_cfg
.as_ref()
.map(|cfg| cfg.tools.max_tool_loops)
.filter(|limit| *limit > 0)
.unwrap_or(vtcode_core::config::constants::defaults::DEFAULT_MAX_TOOL_LOOPS);
let max_context_tokens = vt_cfg
.as_ref()
.map(|cfg| cfg.context.max_context_tokens)
.unwrap_or_else(vtcode_config::context::default_max_context_tokens);
let mut runtime = AgentRuntime::new(
AgentSessionState::new(
SessionId::generate().into_inner(),
config.max_conversation_turns,
max_tool_loops,
max_context_tokens,
),
None,
steering_receiver.take(),
);
runtime.state.messages = conversation_history;
if resume_ref.is_some()
&& let Some(pending_prompt) = take_pending_resumed_user_prompt(runtime.state.messages_mut())
{
let (_, runtime_steering) = runtime.split_mut();
runtime_steering.queue_follow_up_input(pending_prompt);
}
let tool_result_cache = execution.tool_result_cache;
let tool_permission_cache = execution.tool_permission_cache;
let permissions_state = execution.permissions_state;
let approval_recorder = execution.approval_recorder;
let safety_validator = execution.safety_validator;
let circuit_breaker = execution.circuit_breaker;
let tool_health_tracker = execution.tool_health_tracker;
let rate_limiter = execution.rate_limiter;
let validation_cache = execution.validation_cache;
let autonomous_executor = execution.autonomous_executor;
let cancel_token = CancellationToken::new();
let _cancel_guard = CancelGuard(cancel_token.clone());
let _signal_handler = spawn_signal_handler(
ctrl_c_state.clone(),
ctrl_c_notify.clone(),
async_mcp_manager.clone(),
cancel_token.clone(),
);
let mut session_stats = SessionStats::default();
session_stats.circuit_breaker = circuit_breaker.clone();
session_stats.tool_health_tracker = tool_health_tracker.clone();
session_stats.rate_limiter = rate_limiter.clone();
session_stats.validation_cache = validation_cache.clone();
session_stats.set_prompt_cache_lineage_id(
thread_handle
.snapshot()
.metadata
.as_ref()
.and_then(|metadata| metadata.prompt_cache_lineage_id.clone()),
);
session_stats.set_prompt_cache_profile(
thread_handle
.snapshot()
.metadata
.as_ref()
.and_then(|metadata| metadata.budget_limit_continuation())
.map(|_| vtcode_core::llm::provider::PromptCacheProfile::BudgetContinuation),
);
session_stats.vim_mode_enabled = vt_cfg.as_ref().is_some_and(|cfg| cfg.ui.vim_mode);
let mut plan_session =
crate::agent::runloop::unified::planning_workflow_state::PlanningWorkflowSessionState::default();
if planning_entry_source.should_auto_enter() {
transition_to_planning_workflow(
&tool_registry,
&mut session_stats,
&mut plan_session,
&handle,
planning_entry_source,
true,
true,
)
.await;
render_planning_workflow_next_step_hint(&mut renderer)?;
} else if planning_entry_source.requires_startup_prompt() && resume_ref.is_none() {
let should_enter =
prompt_startup_planning_workflow(&handle, &mut session, &ctrl_c_state, &ctrl_c_notify).await?;
if should_enter {
transition_to_planning_workflow(
&tool_registry,
&mut session_stats,
&mut plan_session,
&handle,
planning_entry_source,
true,
true,
)
.await;
render_planning_workflow_next_step_hint(&mut renderer)?;
}
}
let mut linked_directories: Vec<LinkedDirectory> = Vec::with_capacity(4);
let mut model_picker_state: Option<ModelPickerState> = None;
let mut palette_state: Option<ActivePalette> = None;
let mut last_forced_redraw = Instant::now();
let mut input_status_state = InputStatusState::default();
let mut dismissed_memory_cleanup_fingerprint: Option<(usize, usize)> = None;
let mut prefer_latest_queued_input_once = false;
crate::agent::runloop::unified::status_line::update_ide_context_source(
&mut input_status_state,
crate::agent::runloop::unified::session_setup::ide_context_status_label_from_bridge(
&context_manager,
config.workspace.as_path(),
vt_cfg.as_ref(),
ide_context_bridge.as_ref(),
),
);
let mut queued_inputs: VecDeque<crate::agent::runloop::unified::inline_events::QueuedInput> =
VecDeque::with_capacity(8);
let mut agent_touched_paths = std::collections::BTreeSet::new();
let mut ctrl_c_notice_displayed = false;
let mut inline_prompt_cost_notice_shown = false;
let mut mcp_catalog_initialized = tool_registry.mcp_client().is_some();
let mut last_known_mcp_tools: Vec<String> = Vec::with_capacity(16);
let mut pending_mcp_refresh = false;
let mut last_mcp_refresh = Instant::now();
let startup_update_requested_restart = if let Some(notice) = startup_update_cached_notice.as_ref() {
display_update_notice(&handle, &mut header_context, renderer.should_use_unicode_formatting(), notice);
matches!(
run_inline_update_prompt(
&mut renderer,
&handle,
&mut session,
&ctrl_c_state,
&ctrl_c_notify,
config.workspace.as_path(),
notice,
)
.await?,
InlineUpdateOutcome::RestartRequested
)
} else {
false
};
if startup_update_requested_restart {
session_end_reason = SessionEndReason::Completed;
}
if !startup_update_requested_restart
&& let Some((ref version, ref highlights)) = session_bootstrap.release_highlights
{
crate::updater::display_release_notes(&handle, version, highlights);
crate::updater::record_current_version_seen();
}
let mut cross_turn_tracker = crate::agent::runloop::unified::run_loop_context::CrossTurnTracker::new();
if !startup_update_requested_restart {
loop {
use crate::agent::runloop::unified::turn::session::interaction_loop::InteractionOutcome;
if let Some(controller) = tool_registry.subagent_controller() {
controller.set_parent_messages(&runtime.state.messages).await;
}
let interaction_outcome = if let Some(input) = runtime.run_until_idle() {
let turn_id = SessionId::generate().into_inner();
InteractionOutcome::Continue { input, prompt_message_index: None, turn_id }
} else {
let mut interaction_turn_metadata_cache = None;
let (session_state, runtime_steering) = runtime.split_mut();
let mut interaction_ctx =
crate::agent::runloop::unified::turn::session::interaction_loop::InteractionLoopContext {
thread_id: &turn_run_id.0,
active_thread_label,
thread_handle: &thread_handle,
renderer: &mut renderer,
session: &mut session,
handle: &handle,
header_context: &mut header_context,
ide_context_bridge: &mut ide_context_bridge,
ctrl_c_state: &ctrl_c_state,
ctrl_c_notify: &ctrl_c_notify,
input_activity_counter: &input_activity_counter,
config: &mut config,
vt_cfg: &mut vt_cfg,
provider_client: &mut provider_client,
session_bootstrap: &session_bootstrap,
async_mcp_manager: &async_mcp_manager,
tool_registry: &mut tool_registry,
tools: &tools,
tool_catalog: &tool_catalog,
conversation_history: &mut session_state.messages,
agent_touched_paths: &mut agent_touched_paths,
decision_ledger: &decision_ledger,
context_manager: &mut context_manager,
active_primary_agent: &mut active_primary_agent,
session_stats: &mut session_stats,
plan_session: &mut plan_session,
mcp_panel_state: &mut mcp_panel_state,
linked_directories: &mut linked_directories,
lifecycle_hooks: &mut lifecycle_hooks,
full_auto,
skip_confirmations,
approval_recorder: &approval_recorder,
tool_permission_cache: &tool_permission_cache,
permissions_state: &permissions_state,
loaded_skills: &loaded_skills,
default_placeholder: &mut default_placeholder,
follow_up_placeholder: &mut follow_up_placeholder,
checkpoint_manager: checkpoint_manager.as_ref(),
tool_result_cache: &tool_result_cache,
traj: &traj,
harness_emitter: harness_emitter.as_ref(),
safety_validator: &safety_validator,
circuit_breaker: &circuit_breaker,
tool_health_tracker: &tool_health_tracker,
rate_limiter: &rate_limiter,
telemetry: &telemetry,
autonomous_executor: &autonomous_executor,
error_recovery: &error_recovery,
last_forced_redraw: &mut last_forced_redraw,
turn_metadata_cache: &mut interaction_turn_metadata_cache,
harness_config: harness_config.clone(),
runtime_steering,
startup_update_notice_rx: &mut startup_update_notice_rx,
};
let mut interaction_state =
crate::agent::runloop::unified::turn::session::interaction_loop::InteractionState {
input_status_state: &mut input_status_state,
dismissed_memory_cleanup_fingerprint: &mut dismissed_memory_cleanup_fingerprint,
queued_inputs: &mut queued_inputs,
prefer_latest_queued_input_once: &mut prefer_latest_queued_input_once,
model_picker_state: &mut model_picker_state,
palette_state: &mut palette_state,
last_known_mcp_tools: &mut last_known_mcp_tools,
pending_mcp_refresh: &mut pending_mcp_refresh,
mcp_catalog_initialized: &mut mcp_catalog_initialized,
last_mcp_refresh: &mut last_mcp_refresh,
ctrl_c_notice_displayed: &mut ctrl_c_notice_displayed,
inline_prompt_cost_notice_shown: &mut inline_prompt_cost_notice_shown,
};
crate::agent::runloop::unified::turn::session::interaction_loop::run_interaction_loop(
&mut interaction_ctx,
&mut interaction_state,
)
.await?
};
let (next_turn_input, completed_turn_prompt_message_index) = match interaction_outcome {
InteractionOutcome::Exit { reason } => {
session_end_reason = reason;
break;
}
InteractionOutcome::Resume { resume_session } => {
resume_state = Some(*resume_session);
session_end_reason = SessionEndReason::Completed;
break;
}
InteractionOutcome::DirectToolHandled => {
continue;
}
InteractionOutcome::Continue { input, prompt_message_index, turn_id: next_turn_id } => {
turn_id = next_turn_id;
(input, prompt_message_index)
}
InteractionOutcome::PlanApproved { auto_accept } => {
let plan_seed = load_active_plan_seed(&tool_registry).await;
crate::agent::runloop::unified::planning_workflow_state::finish_planning_workflow(
&tool_registry,
&mut plan_session,
&handle,
true,
)
.await;
active_primary_agent.reset_to_default_from_specs(&[]);
let build_display = active_primary_agent.active().display_name.clone();
let build_color = active_primary_agent.active().color.clone().filter(|c| !c.trim().is_empty());
handle.set_primary_agent(Some(build_display), build_color);
handle.set_skip_confirmations(auto_accept);
renderer.line(MessageStyle::Info, "Executing approved plan...")?;
if let Err(err) = force_reload_workspace_config_for_execution(
config.workspace.as_path(),
&config,
&mut vt_cfg,
&mut tool_registry,
async_mcp_manager.as_deref(),
)
.await
{
tracing::warn!("Failed to reload workspace configuration at plan approval: {}", err);
renderer.line(MessageStyle::Error, &format!("Failed to reload configuration: {err}"))?;
}
let mut execution_directive = PLAN_APPROVED_EXECUTION_DIRECTIVE.to_string();
if let Some(seed) = plan_seed {
execution_directive.push_str("\n\nApproved plan context:\n");
execution_directive.push_str(&seed);
}
runtime
.state
.messages_mut()
.push(vtcode_core::llm::provider::Message::system(execution_directive));
(PLAN_APPROVED_EXECUTION_INPUT.to_string(), None)
}
};
if next_turn_input.trim().is_empty() {
continue;
}
let (session_state, runtime_steering) = runtime.split_mut();
let mut working_history = std::mem::take(&mut session_state.messages);
let transient_system_notes = append_transient_turn_notes(
&mut working_history,
config.workspace.as_path(),
&tool_registry,
&agent_touched_paths,
);
let turn_started_at = Instant::now();
let history_snapshot_bytes = estimate_history_bytes(&working_history);
let mut turn_metadata_cache = None;
let mut cross_turn_read_sigs: Vec<String> = Vec::new();
let mut cross_turn_written: HashSet<String> = HashSet::new();
let mut cross_turn_shell_cmd: Option<String> = None;
let outcome = match {
let mut auto_finish_planning_attempted = false;
let planning_active = tool_registry.is_planning_active();
let max_tool_calls_per_turn =
effective_max_tool_calls_for_turn(harness_config.max_tool_calls_per_turn, planning_active);
let mut harness_state = HarnessTurnState::new(
TurnRunId(turn_run_id.0.clone()),
TurnId(turn_id.clone()),
max_tool_calls_per_turn,
harness_config.max_tool_wall_clock_secs,
harness_config.max_tool_retries,
);
let turn_loop_ctx = crate::agent::runloop::unified::turn::TurnLoopContext::new(
&mut renderer,
&handle,
&mut session,
&mut session_stats,
&mut plan_session,
&mut auto_finish_planning_attempted,
&mut mcp_panel_state,
&tool_result_cache,
&approval_recorder,
&decision_ledger,
&mut tool_registry,
&tools,
&tool_catalog,
&ctrl_c_state,
&ctrl_c_notify,
&mut context_manager,
&mut last_forced_redraw,
&mut input_status_state,
lifecycle_hooks.as_ref(),
&default_placeholder,
&tool_permission_cache,
&permissions_state,
&safety_validator,
&circuit_breaker,
&tool_health_tracker,
&rate_limiter,
&telemetry,
&autonomous_executor,
&error_recovery,
&mut harness_state,
harness_emitter.as_ref(),
&mut config,
vt_cfg.as_ref(),
&mut turn_metadata_cache,
&mut provider_client,
&traj,
&active_primary_agent,
skip_confirmations,
full_auto,
runtime_steering,
);
let result =
crate::agent::runloop::unified::turn::run_turn_loop(&mut working_history, turn_loop_ctx).await;
match result {
Ok(inner) => {
cross_turn_read_sigs =
harness_state.seen_successful_readonly_signatures.iter().cloned().collect();
cross_turn_written = harness_state.recently_written_files.clone();
cross_turn_shell_cmd = harness_state.last_shell_command_signature.clone();
Ok(inner)
}
Err(err) => Err(err),
}
} {
Ok(outcome) => outcome,
Err(err) => {
handle.set_input_status(None, None);
let _ = renderer.line_if_not_empty(MessageStyle::Output);
tracing::error!("Turn execution error: {}", err);
let _ = renderer.line(MessageStyle::Error, &format!("Error: {err}"));
TurnLoopOutcome {
result: RunLoopTurnLoopResult::Aborted,
turn_modified_files: std::collections::BTreeSet::new(),
pending_primary_agent: None,
}
}
};
remove_transient_system_notes(&mut working_history, &transient_system_notes);
if let Some(cross_turn_warning) = cross_turn_tracker.seal_turn(
&cross_turn_read_sigs,
&cross_turn_written,
cross_turn_shell_cmd.as_deref(),
) {
tracing::warn!(warning = %cross_turn_warning, "Cross-turn loop detector triggered");
working_history.push(vtcode_core::llm::provider::Message::system(cross_turn_warning));
}
agent_touched_paths.extend(
outcome
.turn_modified_files
.iter()
.map(|path| normalize_workspace_path(config.workspace.as_path(), path)),
);
agent_touched_paths.extend(context_manager.tracked_instruction_activity_paths());
runtime.state.messages = working_history;
let outcome_result = outcome.result.clone();
let switch_primary_agent = outcome.pending_primary_agent.clone();
let turn_elapsed = turn_started_at.elapsed();
let show_turn_timer = vt_cfg.as_ref().map(|cfg| cfg.ui.show_turn_timer).unwrap_or(true);
let harness_snapshot = tool_registry.harness_context_snapshot();
if let Err(err) = crate::agent::runloop::unified::turn::apply_turn_outcome(
outcome,
crate::agent::runloop::unified::turn::TurnOutcomeContext {
conversation_history: &mut runtime.state.messages,
completed_turn_prompt: Some(next_turn_input.as_str()),
completed_turn_prompt_message_index,
renderer: &mut renderer,
handle: &handle,
ctrl_c_state: &ctrl_c_state,
default_placeholder: &default_placeholder,
checkpoint_manager: checkpoint_manager.as_ref(),
next_checkpoint_turn: &mut next_checkpoint_turn,
session_end_reason: &mut session_end_reason,
turn_elapsed,
show_turn_timer,
workspace: &config.workspace,
session_id: &harness_snapshot.session_id,
harness_emitter: harness_emitter.as_ref(),
},
)
.await
{
tracing::error!("Failed to apply turn outcome: {}", err);
renderer
.line(MessageStyle::Error, &format!("Failed to finalize turn: {err}"))
.ok();
}
if let Some(agent) = switch_primary_agent {
let specs = if let Some(controller) = tool_registry.subagent_controller() {
controller
.effective_specs()
.await
.into_iter()
.filter(|s| s.is_primary())
.collect::<Vec<_>>()
} else {
Vec::new()
};
match active_primary_agent.select_from_specs(&specs, &agent) {
Ok(_) => {
let display = active_primary_agent.active().display_name.clone();
let color = active_primary_agent.active().color.clone().filter(|c| !c.trim().is_empty());
handle.set_primary_agent(Some(display), color);
tracing::info!(
target: "vtcode.planning_workflow",
agent = %agent,
"Switched primary agent after plan approval"
);
}
Err(err) => {
tracing::warn!(
agent = %agent,
error = %err,
"Primary agent handoff failed; keeping current agent"
);
}
}
}
if let Err(err) = crate::agent::runloop::unified::turn::compaction::refresh_session_memory_envelope(
config.workspace.as_path(),
&harness_snapshot.session_id,
vt_cfg.as_ref(),
&runtime.state.messages,
&session_stats,
None,
) {
tracing::warn!(
error = %err,
session_id = %harness_snapshot.session_id,
"Failed to refresh session memory envelope after turn"
);
}
emit_turn_execution_metrics(TurnExecutionMetrics {
attempts_made: 1,
retry_count: 0,
history_snapshot_bytes,
timeout_secs: harness_config.max_tool_wall_clock_secs,
elapsed_ms: turn_elapsed.as_millis(),
outcome: match &outcome_result {
RunLoopTurnLoopResult::Completed => "completed",
RunLoopTurnLoopResult::Aborted => "aborted",
RunLoopTurnLoopResult::Cancelled => "cancelled",
RunLoopTurnLoopResult::Exit => "exit",
RunLoopTurnLoopResult::Blocked { .. } => "blocked",
},
});
last_activity_time = Some(Instant::now());
vtcode_core::tools::cache::FILE_CACHE.check_pressure_and_evict().await;
tool_result_cache.write().await.check_pressure_and_evict();
if let Some(archive) = session_archive.as_ref() {
let messages: Vec<SessionMessage> =
runtime.state.messages.iter().map(SessionMessage::from).collect();
let mut recent_messages: Vec<SessionMessage> = runtime
.state
.messages
.iter()
.rev()
.take(RECENT_MESSAGE_LIMIT)
.map(SessionMessage::from)
.collect();
recent_messages.reverse();
let progress_turn = next_checkpoint_turn.saturating_sub(1).max(1);
let distinct_tools = session_stats.sorted_tools();
let skill_names: Vec<String> = loaded_skills.read().await.keys().cloned().collect();
if let Err(err) = archive
.persist_progress_async(SessionProgressArgs {
total_messages: runtime.state.messages.len(),
distinct_tools: distinct_tools.clone(),
messages,
recent_messages,
turn_number: progress_turn,
token_usage: None,
max_context_tokens: None,
loaded_skills: Some(skill_names),
})
.await
{
tracing::warn!("Failed to persist session progress: {}", err);
}
}
match &outcome_result {
RunLoopTurnLoopResult::Aborted => {
session_stats
.mark_turn_stalled(true, Some("Turn aborted due to an execution error.".to_string()));
}
RunLoopTurnLoopResult::Blocked { reason } => {
session_stats.mark_turn_stalled(
true,
reason
.clone()
.or_else(|| Some("Turn blocked due to repeated failing tool behavior.".to_string())),
);
if !renderer.supports_inline_ui()
&& session_stats.auto_permission_prompt_fallback_active()
&& session_stats.last_auto_permission_denial().is_some()
{
session_end_reason = SessionEndReason::Error;
break;
}
}
_ => {
session_stats.mark_turn_stalled(false, None);
}
}
if matches!(session_end_reason, SessionEndReason::Exit) {
break;
}
continue;
}
}
if let Some(archive) = session_archive.as_mut() {
let skill_names: Vec<String> = loaded_skills.read().await.keys().cloned().collect();
archive.set_loaded_skills(skill_names);
archive.set_continuation_metadata(session_stats.budget_limit().map(|(max_budget_usd, actual_cost_usd)| {
session_archive::SessionContinuationMetadata::budget_limit(
max_budget_usd,
actual_cost_usd,
crate::agent::runloop::unified::turn::compaction::has_latest_memory_envelope(
&config.workspace,
thread_handle.thread_id().as_str(),
),
)
}));
}
if let Some(emitter) = harness_emitter.as_ref() {
let harness_snapshot = tool_registry.harness_context_snapshot();
let (outcome_code, subtype) =
session_end_reason.thread_completion_status(session_stats.budget_limit().is_some());
let result = subtype
.is_success()
.then(|| latest_assistant_result_text(&runtime.state.messages))
.flatten();
let total_cost_usd = session_stats.total_cost_usd().and_then(serde_json::Number::from_f64);
let event = crate::agent::runloop::unified::inline_events::harness::thread_completed_event(
turn_run_id.0.clone(),
harness_snapshot.session_id,
subtype,
outcome_code,
result,
session_stats.stop_reason().map(str::to_string),
session_stats.total_usage(),
total_cost_usd,
session_stats.total_turns(),
);
if let Err(err) = emitter.emit(event) {
tracing::debug!(error = %err, "harness thread.completed event emission failed");
}
}
if let Some(emitter) = harness_emitter.as_ref() {
emitter.finish_open_responses();
emitter.finish_atif();
}
agent_touched_paths.extend(context_manager.tracked_instruction_activity_paths());
if !matches!(session_end_reason, SessionEndReason::Exit) {
match timeout(
Duration::from_secs(5),
vtcode_core::persistent_memory::finalize_persistent_memory(
&config,
vt_cfg.as_ref(),
&runtime.state.messages,
),
)
.await
{
Ok(Err(err)) => {
tracing::warn!("Failed to update persistent memory at session finalization: {}", err);
}
Err(_elapsed) => {
tracing::warn!("Persistent memory finalization timed out, skipping");
}
Ok(Ok(_)) => {}
}
}
let finalization_output = match finalize_session(
&mut renderer,
lifecycle_hooks.as_ref(),
&turn_id,
session_end_reason,
&mut session_archive,
&session_stats,
&runtime.state.messages,
linked_directories,
async_mcp_manager.as_deref(),
&handle,
)
.await
{
Ok(output) => Some(output),
Err(err) => {
tracing::error!("Failed to finalize session: {}", err);
renderer
.line(MessageStyle::Error, &format!("Failed to finalize session: {err}"))
.ok();
None
}
};
if let Some(next_resume) = resume_state.as_ref() {
refresh_runtime_debug_context_for_next_session(config.workspace.as_path(), Some(next_resume)).await?;
continue;
}
if matches!(session_end_reason, SessionEndReason::NewSession) {
if live_reload_enabled && config_watcher.should_reload() {
vt_cfg = config_watcher.load_config();
crate::agent::agents::apply_runtime_overrides(vt_cfg.as_mut(), &config);
idle_config = extract_idle_config(vt_cfg.as_ref());
tracing::debug!("Configuration reloaded due to file changes");
}
refresh_runtime_debug_context_for_next_session(config.workspace.as_path(), None).await?;
resume_state = None;
pending_session_start_trigger = Some(SessionStartTrigger::NewSession);
_consecutive_idle_cycles = 0;
continue;
}
if live_reload_enabled && config_watcher.should_reload() {
vt_cfg = config_watcher.load_config();
crate::agent::agents::apply_runtime_overrides(vt_cfg.as_mut(), &config);
idle_config = extract_idle_config(vt_cfg.as_ref());
tracing::debug!("Configuration reloaded during idle period");
}
if idle_config.enabled
&& let Some(last_activity) = last_activity_time
{
let idle_duration = last_activity.elapsed().as_millis() as u64;
if idle_duration >= idle_config.timeout_ms {
_consecutive_idle_cycles += 1;
if idle_config.backoff_ms > 0 {
if _consecutive_idle_cycles >= idle_config.max_cycles {
sleep(Duration::from_millis(idle_config.backoff_ms * 2)).await;
_consecutive_idle_cycles = 0;
} else {
sleep(Duration::from_millis(idle_config.backoff_ms)).await;
}
}
} else {
_consecutive_idle_cycles = 0;
}
}
let end_code_changes = capture_code_change_snapshot(&config.workspace, "end").await;
let code_change_delta =
compute_session_code_change_delta(start_code_changes.as_ref(), end_code_changes.as_ref());
let finalization_succeeded = finalization_output.is_some();
let resume_identifier = finalization_output
.as_ref()
.and_then(|output| output.archive_path.as_ref())
.and_then(|path| path.file_stem())
.and_then(|stem| stem.to_str());
let trust_label = match session_bootstrap.acp_workspace_trust {
Some(vtcode_core::config::AgentClientProtocolZedWorkspaceTrustMode::FullAuto) => "full auto",
Some(vtcode_core::config::AgentClientProtocolZedWorkspaceTrustMode::ToolsPolicy) => "tools policy",
None if full_auto => "full auto",
None => "tools policy",
};
let provider_label = {
let label = crate::agent::runloop::unified::session_setup::resolve_provider_label(&config, vt_cfg.as_ref());
if label.is_empty() {
provider_client.name().to_string()
} else {
label
}
};
let reasoning_label = vt_cfg
.as_ref()
.map(|cfg| cfg.agent.reasoning_effort.as_str().to_string())
.unwrap_or_else(|| config.reasoning_effort.as_str().to_string());
let (code_additions, code_deletions) = code_change_delta.map(|d| (d.additions, d.deletions)).unwrap_or((0, 0));
if !finalization_succeeded {
let _ = vtcode_ui::tui::panic_hook::restore_tui();
}
let session_total_usage = session_stats.total_usage();
print_exit_summary(ExitData {
app_name: "VT Code",
version: env!("CARGO_PKG_VERSION"),
model: &config.model,
provider: &provider_label,
trust_label,
reasoning: &reasoning_label,
session_duration: session_started_at.elapsed(),
prompt_tokens: session_total_usage.input_tokens,
completion_tokens: session_total_usage.output_tokens,
cached_tokens: session_total_usage.cached_input_tokens,
cache_creation_tokens: session_total_usage.cache_creation_tokens,
cache_hit_rate_percent: session_total_usage.cache_hit_rate().map(|rate| rate * 100.0),
code_additions,
code_deletions,
resume_identifier,
budget_limit: session_stats.budget_limit(),
});
if let Some(controller) = tool_registry.subagent_controller() {
controller.signal_shutdown().await;
}
if matches!(session_end_reason, SessionEndReason::Error) {
return Err(anyhow::anyhow!(
"{}",
session_stats
.turn_stall_reason()
.unwrap_or("Session ended with an execution error.")
));
}
break;
}
Ok(())
}
async fn load_archived_prompts_for_history(handle: &vtcode_ui::tui::app::InlineHandle) {
let listings = match session_archive::list_recent_sessions(50).await {
Ok(listings) => listings,
Err(_) => return,
};
let mut entries = Vec::new();
for listing in &listings {
let session_label = listing.identifier();
for msg in &listing.snapshot.messages {
if msg.role != MessageRole::User {
continue;
}
let content = msg.content.as_text();
let trimmed = content.trim();
if trimmed.is_empty() {
continue;
}
let preview = trimmed.lines().next().unwrap_or(trimmed).chars().take(200).collect::<String>();
entries.push(ArchivedPromptEntry {
content: preview,
created_at: listing.snapshot.started_at,
session_label: session_label.clone(),
});
}
}
entries.truncate(20);
if !entries.is_empty() {
handle.set_archived_history(entries);
}
}
#[cfg(test)]
mod tests;