Skip to main content

vtcode_core/subagents/
model.rs

1use anyhow::{Context, Result};
2use vtcode_config::SubagentSpec;
3
4use crate::config::VTCodeConfig;
5use crate::config::constants::models;
6use crate::config::models::{ModelId, Provider};
7use crate::core::agent::types::AgentType;
8use crate::llm::auto_lightweight_model;
9use crate::llm::factory::{infer_provider, infer_provider_from_model};
10
11// ─── Model Resolution ───────────────────────────────────────────────────────
12
13pub fn resolve_subagent_model(
14    vt_cfg: &VTCodeConfig,
15    parent_model: &str,
16    parent_provider: &str,
17    requested: Option<&str>,
18    agent_name: &str,
19) -> Result<ModelId> {
20    let requested = requested.unwrap_or("inherit").trim();
21    if requested.eq_ignore_ascii_case("inherit") || requested.is_empty() {
22        return resolve_inherit_model(parent_model, parent_provider, agent_name);
23    }
24    resolve_explicit_model(vt_cfg, parent_provider, parent_model, requested, agent_name)
25}
26
27fn resolve_inherit_model(
28    parent_model: &str,
29    parent_provider: &str,
30    agent_name: &str,
31) -> Result<ModelId> {
32    if let Ok(model) = parent_model.parse::<ModelId>() {
33        return Ok(model);
34    }
35    if parent_provider.eq_ignore_ascii_case("copilot") {
36        let fallback = ModelId::default_orchestrator_for_provider(Provider::Copilot);
37        tracing::warn!(
38            agent_name,
39            parent_model = parent_model.trim(),
40            parent_provider = parent_provider.trim(),
41            fallback_model = fallback.as_str(),
42            "Falling back to the default Copilot subagent model because the inherited parent model identifier is not supported internally"
43        );
44        return Ok(fallback);
45    }
46
47    let normalized = normalize_subagent_model_alias(parent_model);
48    normalized.parse::<ModelId>().with_context(|| {
49        format!(
50            "Failed to resolve model '{}' for subagent {}",
51            normalized, agent_name
52        )
53    })
54}
55
56fn resolve_explicit_model(
57    vt_cfg: &VTCodeConfig,
58    parent_provider: &str,
59    parent_model: &str,
60    requested: &str,
61    agent_name: &str,
62) -> Result<ModelId> {
63    let resolved = if requested.eq_ignore_ascii_case("small") {
64        resolve_lightweight_model(vt_cfg, parent_provider, parent_model, agent_name)
65    } else if matches!(
66        requested.to_ascii_lowercase().as_str(),
67        "haiku" | "sonnet" | "opus"
68    ) {
69        alias_model_for_provider(parent_provider, requested, parent_model)
70    } else {
71        requested.to_string()
72    };
73
74    let normalized = normalize_subagent_model_alias(resolved.as_str());
75    normalized.parse::<ModelId>().with_context(|| {
76        format!(
77            "Failed to resolve model '{}' for subagent {}",
78            normalized, agent_name
79        )
80    })
81}
82
83fn resolve_lightweight_model(
84    vt_cfg: &VTCodeConfig,
85    parent_provider: &str,
86    parent_model: &str,
87    agent_name: &str,
88) -> String {
89    if vt_cfg.agent.small_model.model.trim().is_empty() {
90        return auto_lightweight_model(parent_provider, parent_model);
91    }
92
93    let configured = vt_cfg.agent.small_model.model.trim();
94    let active_provider = infer_provider(Some(parent_provider), parent_model);
95    let configured_provider =
96        infer_provider_from_model(configured).or_else(|| infer_provider(None, configured));
97
98    if configured_provider.is_some() && configured_provider != active_provider {
99        tracing::warn!(
100            agent_name,
101            configured_model = configured,
102            active_provider = active_provider
103                .map(|provider| provider.to_string())
104                .unwrap_or_else(|| parent_provider.to_string()),
105            "Ignoring cross-provider lightweight subagent model; using same-provider automatic route"
106        );
107        auto_lightweight_model(parent_provider, parent_model)
108    } else {
109        configured.to_string()
110    }
111}
112
113pub fn resolve_effective_subagent_model(
114    vt_cfg: &VTCodeConfig,
115    parent_model: &str,
116    parent_provider: &str,
117    model_override: Option<&str>,
118    spec_model: Option<&str>,
119    agent_name: &str,
120) -> Result<ModelId> {
121    if let Some(requested) = model_override {
122        match resolve_subagent_model(
123            vt_cfg,
124            parent_model,
125            parent_provider,
126            Some(requested),
127            agent_name,
128        ) {
129            Ok(model) => return Ok(model),
130            Err(err) => {
131                return handle_model_override_failure(
132                    vt_cfg,
133                    parent_model,
134                    parent_provider,
135                    requested,
136                    spec_model,
137                    agent_name,
138                    err,
139                );
140            }
141        }
142    }
143
144    match resolve_subagent_model(
145        vt_cfg,
146        parent_model,
147        parent_provider,
148        spec_model,
149        agent_name,
150    ) {
151        Ok(model) => Ok(model),
152        Err(err)
153            if spec_model
154                .map(str::trim)
155                .is_some_and(|v| v.eq_ignore_ascii_case("small")) =>
156        {
157            tracing::warn!(
158                agent_name,
159                error = %err,
160                "Failed to resolve lightweight subagent model from spec; falling back to parent model"
161            );
162            resolve_subagent_model(
163                vt_cfg,
164                parent_model,
165                parent_provider,
166                Some("inherit"),
167                agent_name,
168            )
169        }
170        Err(err) => Err(err),
171    }
172}
173
174fn handle_model_override_failure(
175    vt_cfg: &VTCodeConfig,
176    parent_model: &str,
177    parent_provider: &str,
178    requested: &str,
179    spec_model: Option<&str>,
180    agent_name: &str,
181    err: anyhow::Error,
182) -> Result<ModelId> {
183    if requested.trim().eq_ignore_ascii_case("small") {
184        tracing::warn!(
185            agent_name,
186            requested_model = requested.trim(),
187            error = %err,
188            "Failed to bootstrap lightweight subagent model; falling back to parent model"
189        );
190        return resolve_subagent_model(
191            vt_cfg,
192            parent_model,
193            parent_provider,
194            Some("inherit"),
195            agent_name,
196        );
197    }
198    let fallback = spec_model
199        .map(str::trim)
200        .filter(|v| !v.is_empty())
201        .unwrap_or("inherit");
202    tracing::warn!(
203        agent_name,
204        requested_model = requested.trim(),
205        fallback_model = fallback,
206        error = %err,
207        "Failed to resolve subagent model override; falling back"
208    );
209    Ok(resolve_subagent_model(
210        vt_cfg,
211        parent_model,
212        parent_provider,
213        spec_model,
214        agent_name,
215    )
216    .unwrap_or_else(|_| {
217        resolve_subagent_model(
218            vt_cfg,
219            parent_model,
220            parent_provider,
221            Some("inherit"),
222            agent_name,
223        )
224        .expect("inherit fallback should succeed")
225    }))
226}
227
228fn normalize_subagent_model_alias(model: &str) -> &str {
229    match model.trim() {
230        "claude-haiku-4.5" => models::anthropic::CLAUDE_HAIKU_4_5,
231        "claude-sonnet-4.6" => models::anthropic::CLAUDE_SONNET_4_6,
232        "claude-opus-4.8" => models::anthropic::CLAUDE_OPUS_4_8,
233        other => other,
234    }
235}
236
237fn alias_model_for_provider(parent_provider: &str, alias: &str, parent_model: &str) -> String {
238    match infer_provider(Some(parent_provider), parent_model) {
239        Some(Provider::Anthropic) => match alias.to_ascii_lowercase().as_str() {
240            "haiku" => models::anthropic::CLAUDE_HAIKU_4_5.to_string(),
241            "opus" => models::anthropic::CLAUDE_OPUS_4_8.to_string(),
242            _ => models::anthropic::CLAUDE_SONNET_4_6.to_string(),
243        },
244        Some(Provider::OpenAI) => match alias.to_ascii_lowercase().as_str() {
245            "haiku" => models::openai::GPT_5_4_MINI.to_string(),
246            "opus" => models::openai::GPT_5_4.to_string(),
247            _ => models::openai::GPT_5_4.to_string(),
248        },
249        Some(Provider::Gemini) => match alias.to_ascii_lowercase().as_str() {
250            "haiku" => models::google::GEMINI_3_FLASH_PREVIEW.to_string(),
251            _ => models::google::GEMINI_3_1_PRO_PREVIEW.to_string(),
252        },
253        _ => parent_model.to_string(),
254    }
255}
256
257pub fn agent_type_for_spec(spec: &SubagentSpec) -> AgentType {
258    match spec.name.as_str() {
259        "explorer" | "explore" => AgentType::Explore,
260        "plan" => AgentType::Plan,
261        "worker" | "general" | "general-purpose" | "default" => AgentType::General,
262        _ => AgentType::Custom(spec.name.clone()),
263    }
264}
265
266// ─── Memory Appendix ────────────────────────────────────────────────────────
267
268use super::constants::{
269    SUBAGENT_MEMORY_BYTES_LIMIT, SUBAGENT_MEMORY_HIGHLIGHT_LIMIT, SUBAGENT_MEMORY_LINE_LIMIT,
270};
271use crate::persistent_memory::extract_memory_highlights;
272use vtcode_config::SubagentMemoryScope;
273
274pub fn load_memory_appendix(
275    workspace_root: &std::path::Path,
276    agent_name: &str,
277    scope: Option<SubagentMemoryScope>,
278) -> Result<Option<String>> {
279    let Some(scope) = scope else {
280        return Ok(None);
281    };
282
283    let memory_dir = match scope {
284        SubagentMemoryScope::Project => {
285            workspace_root.join(".vtcode/agent-memory").join(agent_name)
286        }
287        SubagentMemoryScope::Local => workspace_root
288            .join(".vtcode/agent-memory-local")
289            .join(agent_name),
290        SubagentMemoryScope::User => dirs::home_dir()
291            .unwrap_or_default()
292            .join(".vtcode/agent-memory")
293            .join(agent_name),
294    };
295    std::fs::create_dir_all(&memory_dir).with_context(|| {
296        format!(
297            "Failed to create subagent memory directory {}",
298            memory_dir.display()
299        )
300    })?;
301    let memory_file = memory_dir.join("MEMORY.md");
302    if !memory_file.exists() {
303        return Ok(Some(format!(
304            "Persistent memory file: {}. Create or update `MEMORY.md` with concise reusable notes when you discover stable repository conventions.",
305            memory_file.display()
306        )));
307    }
308
309    let content = std::fs::read_to_string(&memory_file)
310        .with_context(|| format!("Failed to read {}", memory_file.display()))?;
311    let total_lines = content.lines().count();
312    let mut bytes = 0usize;
313    let mut excerpt_lines = Vec::new();
314    for line in content.lines().take(SUBAGENT_MEMORY_LINE_LIMIT) {
315        let next_bytes = bytes.saturating_add(line.len() + 1);
316        if next_bytes > SUBAGENT_MEMORY_BYTES_LIMIT {
317            break;
318        }
319        bytes = next_bytes;
320        excerpt_lines.push(line);
321    }
322
323    let excerpt = excerpt_lines.join("\n");
324    let truncated = excerpt_lines.len() < total_lines;
325    let highlights = extract_memory_highlights(&excerpt, SUBAGENT_MEMORY_HIGHLIGHT_LIMIT);
326    let mut appendix = String::new();
327    appendix.push_str(&format!(
328        "Persistent memory file: {}.\nRead and maintain `MEMORY.md` for durable learnings.",
329        memory_file.display()
330    ));
331
332    if !highlights.is_empty() {
333        appendix.push_str("\n\nKey points:\n");
334        for highlight in highlights {
335            appendix.push_str("- ");
336            appendix.push_str(&highlight);
337            appendix.push('\n');
338        }
339    }
340
341    appendix.push_str("\nOpen `MEMORY.md` when exact wording or more detail matters.");
342    if truncated {
343        appendix.push_str("\nMemory indexing stopped after the configured startup budget.");
344    }
345
346    Ok(Some(appendix))
347}