use anyhow::Result;
use std::path::PathBuf;
use vtcode_core::config::ReasoningEffortLevel;
use vtcode_core::config::loader::{ConfigManager, VTCodeConfig};
use vtcode_core::config::types::{AgentConfig as CoreAgentConfig, ModelSelectionSource};
use vtcode_core::core::interfaces::session::PlanModeEntrySource;
use vtcode_core::core::interfaces::{SessionRuntime, SessionRuntimeParams};
use vtcode_core::core::threads::{
ArchivedSessionIntent, ThreadBootstrap, loaded_skills_from_session_listing,
messages_from_session_listing,
};
use vtcode_core::llm::provider::Message as ProviderMessage;
use vtcode_core::utils::session_archive::SessionArchiveMetadata;
use vtcode_core::utils::session_archive::{
SessionContinuationMetadata, SessionListing, SessionSnapshot,
};
use vtcode_core::utils::terminal_color_probe::probe_and_cache_terminal_palette_harmony;
#[derive(Clone, Debug)]
pub(crate) struct SessionContinuation {
listing: SessionListing,
bootstrap: ThreadBootstrap,
history: Vec<ProviderMessage>,
loaded_skills: Vec<String>,
intent: ArchivedSessionIntent,
thread_label: String,
root_thread: bool,
vt_cfg_override: Option<VTCodeConfig>,
}
impl SessionContinuation {
pub(crate) fn listing(&self) -> &SessionListing {
&self.listing
}
pub(crate) fn identifier(&self) -> String {
self.listing.identifier()
}
pub(crate) fn snapshot(&self) -> &SessionSnapshot {
&self.listing.snapshot
}
pub(crate) fn path(&self) -> &PathBuf {
&self.listing.path
}
pub(crate) fn is_fork(&self) -> bool {
matches!(self.intent, ArchivedSessionIntent::ForkNewArchive { .. })
}
pub(crate) fn intent(&self) -> &ArchivedSessionIntent {
&self.intent
}
pub(crate) fn bootstrap(&self) -> &ThreadBootstrap {
&self.bootstrap
}
pub(crate) fn custom_suffix(&self) -> Option<&str> {
match &self.intent {
ArchivedSessionIntent::ForkNewArchive { custom_suffix, .. } => custom_suffix.as_deref(),
ArchivedSessionIntent::ResumeInPlace => None,
}
}
pub(crate) fn summarize_fork(&self) -> bool {
match &self.intent {
ArchivedSessionIntent::ForkNewArchive { summarize, .. } => *summarize,
ArchivedSessionIntent::ResumeInPlace => false,
}
}
pub(crate) fn history(&self) -> &[ProviderMessage] {
&self.history
}
pub(crate) fn loaded_skills(&self) -> &[String] {
&self.loaded_skills
}
pub(crate) fn message_count(&self) -> usize {
self.history.len()
}
pub(crate) fn thread_label(&self) -> &str {
&self.thread_label
}
pub(crate) fn is_root_thread(&self) -> bool {
self.root_thread
}
pub(crate) fn vt_cfg_override(&self) -> Option<&VTCodeConfig> {
self.vt_cfg_override.as_ref()
}
pub(crate) fn budget_limit_continuation(&self) -> Option<&SessionContinuationMetadata> {
self.snapshot().metadata.budget_limit_continuation()
}
pub(crate) fn from_listing(listing: &SessionListing, intent: ArchivedSessionIntent) -> Self {
Self {
listing: listing.clone(),
bootstrap: ThreadBootstrap::from_listing(listing.clone()),
history: messages_from_session_listing(listing),
loaded_skills: loaded_skills_from_session_listing(listing),
intent,
thread_label: "main".to_string(),
root_thread: true,
vt_cfg_override: None,
}
}
}
pub(crate) type ResumeSession = SessionContinuation;
pub(crate) async fn load_resume_session(
identifier: &str,
intent: ArchivedSessionIntent,
) -> Result<Option<SessionContinuation>> {
let Some(listing) =
vtcode_core::utils::session_archive::find_session_by_identifier(identifier).await?
else {
return Ok(None);
};
Ok(Some(SessionContinuation::from_listing(&listing, intent)))
}
pub(crate) async fn run_single_agent_loop(
config: &CoreAgentConfig,
initial_vt_cfg: Option<VTCodeConfig>,
skip_confirmations: bool,
full_auto: bool,
plan_mode_entry_source: PlanModeEntrySource,
resume: Option<SessionContinuation>,
) -> Result<()> {
probe_and_cache_terminal_palette_harmony();
let mut runtime_cfg = config.clone();
if let Some(resume_session) = resume.as_ref() {
apply_resume_runtime_overrides(&mut runtime_cfg, resume_session);
}
let vt_cfg = prepare_session_vt_config(initial_vt_cfg, &runtime_cfg, resume.as_ref());
let runtime = if runtime_cfg
.provider
.eq_ignore_ascii_case(crate::codex_app_server::CODEX_PROVIDER)
{
EitherRuntime::Codex(crate::codex_app_server::CodexSessionRuntime)
} else {
EitherRuntime::Unified(crate::agent::runloop::unified::UnifiedSessionRuntime)
};
let mut steering_receiver = None;
let params = SessionRuntimeParams::new(
&runtime_cfg,
vt_cfg,
skip_confirmations,
full_auto,
plan_mode_entry_source,
resume,
&mut steering_receiver,
);
runtime.run_session(params).await
}
enum EitherRuntime {
Unified(crate::agent::runloop::unified::UnifiedSessionRuntime),
Codex(crate::codex_app_server::CodexSessionRuntime),
}
impl EitherRuntime {
async fn run_session(self, params: SessionRuntimeParams<'_, ResumeSession>) -> Result<()> {
match self {
Self::Unified(runtime) => runtime.run_session(params).await,
Self::Codex(runtime) => runtime.run_session(params).await,
}
}
}
fn prepare_session_vt_config(
initial_vt_cfg: Option<VTCodeConfig>,
runtime_cfg: &CoreAgentConfig,
resume: Option<&SessionContinuation>,
) -> Option<VTCodeConfig> {
let mut vt_cfg = resume
.and_then(|session| session.vt_cfg_override().cloned())
.or(initial_vt_cfg)
.or_else(|| {
ConfigManager::load_from_workspace(&runtime_cfg.workspace)
.ok()
.map(|manager| manager.config().clone())
});
apply_runtime_overrides(vt_cfg.as_mut(), runtime_cfg);
vt_cfg
}
fn apply_resume_runtime_overrides(runtime_cfg: &mut CoreAgentConfig, resume: &SessionContinuation) {
apply_persisted_resume_metadata(runtime_cfg, Some(&resume.snapshot().metadata));
}
pub(crate) fn apply_runtime_overrides(
vt_cfg: Option<&mut VTCodeConfig>,
runtime_cfg: &CoreAgentConfig,
) {
if let Some(cfg) = vt_cfg {
cfg.agent.provider = runtime_cfg.provider.clone();
cfg.agent.reasoning_effort = runtime_cfg.reasoning_effort;
cfg.agent.theme = runtime_cfg.theme.clone();
if matches!(runtime_cfg.model_source, ModelSelectionSource::CliOverride) {
cfg.agent.default_model = runtime_cfg.model.clone();
}
}
}
pub(crate) fn apply_persisted_resume_metadata(
runtime_cfg: &mut CoreAgentConfig,
metadata: Option<&SessionArchiveMetadata>,
) {
let Some(metadata) = metadata else {
return;
};
if matches!(runtime_cfg.model_source, ModelSelectionSource::CliOverride) {
return;
}
let persisted_model = metadata.model.trim();
if persisted_model.is_empty() {
return;
}
runtime_cfg.model = persisted_model.to_owned();
if !metadata.provider.trim().is_empty() {
runtime_cfg.provider = metadata.provider.clone();
}
if let Some(reasoning_effort) = ReasoningEffortLevel::parse(&metadata.reasoning_effort) {
runtime_cfg.reasoning_effort = reasoning_effort;
}
runtime_cfg.model_source = ModelSelectionSource::CliOverride;
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use vtcode_core::config::core::PromptCachingConfig;
use vtcode_core::config::models::Provider;
use vtcode_core::config::types::{ReasoningEffortLevel, UiSurfacePreference};
use vtcode_core::core::agent::snapshots::{
DEFAULT_CHECKPOINTS_ENABLED, DEFAULT_MAX_AGE_DAYS, DEFAULT_MAX_SNAPSHOTS,
};
#[test]
fn cli_model_override_updates_default_model() {
const OVERRIDE_MODEL: &str = "override-model";
let mut vt_cfg = VTCodeConfig::default();
vt_cfg.agent.default_model = "config-model".to_string();
let current_dir = std::env::current_dir().unwrap();
let runtime_cfg = CoreAgentConfig {
model: OVERRIDE_MODEL.to_string(),
api_key: String::new(),
provider: "cli-provider".to_string(),
api_key_env: Provider::Gemini.default_api_key_env().to_string(),
workspace: current_dir.clone(),
verbose: false,
quiet: false,
theme: String::new(),
reasoning_effort: ReasoningEffortLevel::default(),
ui_surface: UiSurfacePreference::default(),
prompt_cache: PromptCachingConfig::default(),
model_source: ModelSelectionSource::CliOverride,
custom_api_keys: BTreeMap::new(),
checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
checkpointing_storage_dir: None,
checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
max_conversation_turns: 1000,
model_behavior: None,
openai_chatgpt_auth: None,
};
apply_runtime_overrides(Some(&mut vt_cfg), &runtime_cfg);
assert_eq!(vt_cfg.agent.default_model, OVERRIDE_MODEL);
assert_eq!(vt_cfg.agent.provider, "cli-provider");
}
#[test]
fn workspace_config_preserves_default_model() {
let mut vt_cfg = VTCodeConfig::default();
vt_cfg.agent.default_model = "config-model".to_string();
let current_dir = std::env::current_dir().unwrap();
let runtime_cfg = CoreAgentConfig {
model: "config-standard".to_string(),
api_key: String::new(),
provider: "config-provider".to_string(),
api_key_env: Provider::Gemini.default_api_key_env().to_string(),
workspace: current_dir.clone(),
verbose: false,
quiet: false,
theme: String::new(),
reasoning_effort: ReasoningEffortLevel::default(),
ui_surface: UiSurfacePreference::default(),
prompt_cache: PromptCachingConfig::default(),
model_source: ModelSelectionSource::WorkspaceConfig,
custom_api_keys: BTreeMap::new(),
checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
checkpointing_storage_dir: None,
checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
max_conversation_turns: 1000,
model_behavior: None,
openai_chatgpt_auth: None,
};
apply_runtime_overrides(Some(&mut vt_cfg), &runtime_cfg);
assert_eq!(vt_cfg.agent.default_model, "config-model");
assert_eq!(vt_cfg.agent.provider, "config-provider");
}
#[test]
fn initial_session_config_is_preserved() {
let mut initial_vt_cfg = VTCodeConfig::default();
initial_vt_cfg.agent.default_model = "startup-model".to_string();
initial_vt_cfg.agent.provider = "startup-provider".to_string();
let current_dir = std::env::current_dir().unwrap();
let runtime_cfg = CoreAgentConfig {
model: "startup-model".to_string(),
api_key: String::new(),
provider: "startup-provider".to_string(),
api_key_env: Provider::Gemini.default_api_key_env().to_string(),
workspace: current_dir,
verbose: false,
quiet: false,
theme: String::new(),
reasoning_effort: ReasoningEffortLevel::default(),
ui_surface: UiSurfacePreference::default(),
prompt_cache: PromptCachingConfig::default(),
model_source: ModelSelectionSource::WorkspaceConfig,
custom_api_keys: BTreeMap::new(),
checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
checkpointing_storage_dir: None,
checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
max_conversation_turns: 1000,
model_behavior: None,
openai_chatgpt_auth: None,
};
let prepared = prepare_session_vt_config(Some(initial_vt_cfg), &runtime_cfg, None)
.expect("startup config should be preserved");
assert_eq!(prepared.agent.default_model, "startup-model");
assert_eq!(prepared.agent.provider, "startup-provider");
}
#[test]
fn persisted_resume_metadata_reuses_model_and_reasoning_effort() {
let mut runtime_cfg = CoreAgentConfig {
model: "current-model".to_string(),
api_key: String::new(),
provider: "current-provider".to_string(),
api_key_env: Provider::Gemini.default_api_key_env().to_string(),
workspace: std::env::current_dir().unwrap(),
verbose: false,
quiet: false,
theme: String::new(),
reasoning_effort: ReasoningEffortLevel::Low,
ui_surface: UiSurfacePreference::default(),
prompt_cache: PromptCachingConfig::default(),
model_source: ModelSelectionSource::WorkspaceConfig,
custom_api_keys: BTreeMap::new(),
checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
checkpointing_storage_dir: None,
checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
max_conversation_turns: 1000,
model_behavior: None,
openai_chatgpt_auth: None,
};
let metadata = SessionArchiveMetadata::new(
"workspace",
"/tmp/workspace",
"persisted-model",
"persisted-provider",
"theme",
"high",
);
apply_persisted_resume_metadata(&mut runtime_cfg, Some(&metadata));
assert_eq!(runtime_cfg.model, "persisted-model");
assert_eq!(runtime_cfg.provider, "persisted-provider");
assert_eq!(runtime_cfg.reasoning_effort, ReasoningEffortLevel::High);
assert_eq!(runtime_cfg.model_source, ModelSelectionSource::CliOverride);
}
#[test]
fn persisted_resume_metadata_does_not_override_explicit_model_selection() {
let mut runtime_cfg = CoreAgentConfig {
model: "explicit-model".to_string(),
api_key: String::new(),
provider: "explicit-provider".to_string(),
api_key_env: Provider::Gemini.default_api_key_env().to_string(),
workspace: std::env::current_dir().unwrap(),
verbose: false,
quiet: false,
theme: String::new(),
reasoning_effort: ReasoningEffortLevel::Low,
ui_surface: UiSurfacePreference::default(),
prompt_cache: PromptCachingConfig::default(),
model_source: ModelSelectionSource::CliOverride,
custom_api_keys: BTreeMap::new(),
checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
checkpointing_storage_dir: None,
checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
max_conversation_turns: 1000,
model_behavior: None,
openai_chatgpt_auth: None,
};
let metadata = SessionArchiveMetadata::new(
"workspace",
"/tmp/workspace",
"persisted-model",
"persisted-provider",
"theme",
"high",
);
apply_persisted_resume_metadata(&mut runtime_cfg, Some(&metadata));
assert_eq!(runtime_cfg.model, "explicit-model");
assert_eq!(runtime_cfg.provider, "explicit-provider");
assert_eq!(runtime_cfg.reasoning_effort, ReasoningEffortLevel::Low);
assert_eq!(runtime_cfg.model_source, ModelSelectionSource::CliOverride);
}
}