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
11pub 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 "claude-opus-4.7" => models::anthropic::CLAUDE_OPUS_4_7,
234 other => other,
235 }
236}
237
238fn alias_model_for_provider(parent_provider: &str, alias: &str, parent_model: &str) -> String {
239 match infer_provider(Some(parent_provider), parent_model) {
240 Some(Provider::Anthropic) => match alias.to_ascii_lowercase().as_str() {
241 "haiku" => models::anthropic::CLAUDE_HAIKU_4_5.to_string(),
242 "opus" => models::anthropic::CLAUDE_OPUS_4_8.to_string(),
243 _ => models::anthropic::CLAUDE_SONNET_4_6.to_string(),
244 },
245 Some(Provider::OpenAI) => match alias.to_ascii_lowercase().as_str() {
246 "haiku" => models::openai::GPT_5_4_MINI.to_string(),
247 "opus" => models::openai::GPT_5_4.to_string(),
248 _ => models::openai::GPT_5_4.to_string(),
249 },
250 Some(Provider::Gemini) => match alias.to_ascii_lowercase().as_str() {
251 "haiku" => models::google::GEMINI_3_FLASH_PREVIEW.to_string(),
252 _ => models::google::GEMINI_3_1_PRO_PREVIEW.to_string(),
253 },
254 _ => parent_model.to_string(),
255 }
256}
257
258pub fn agent_type_for_spec(spec: &SubagentSpec) -> AgentType {
259 match spec.name.as_str() {
260 "explorer" | "explore" => AgentType::Explore,
261 "plan" => AgentType::Plan,
262 "worker" | "general" | "general-purpose" | "default" => AgentType::General,
263 _ => AgentType::Custom(spec.name.clone()),
264 }
265}
266
267use super::constants::{
270 SUBAGENT_MEMORY_BYTES_LIMIT, SUBAGENT_MEMORY_HIGHLIGHT_LIMIT, SUBAGENT_MEMORY_LINE_LIMIT,
271};
272use crate::persistent_memory::extract_memory_highlights;
273use vtcode_config::SubagentMemoryScope;
274
275pub fn load_memory_appendix(
276 workspace_root: &std::path::Path,
277 agent_name: &str,
278 scope: Option<SubagentMemoryScope>,
279) -> Result<Option<String>> {
280 let Some(scope) = scope else {
281 return Ok(None);
282 };
283
284 let memory_dir = match scope {
285 SubagentMemoryScope::Project => {
286 workspace_root.join(".vtcode/agent-memory").join(agent_name)
287 }
288 SubagentMemoryScope::Local => workspace_root
289 .join(".vtcode/agent-memory-local")
290 .join(agent_name),
291 SubagentMemoryScope::User => dirs::home_dir()
292 .unwrap_or_default()
293 .join(".vtcode/agent-memory")
294 .join(agent_name),
295 };
296 std::fs::create_dir_all(&memory_dir).with_context(|| {
297 format!(
298 "Failed to create subagent memory directory {}",
299 memory_dir.display()
300 )
301 })?;
302 let memory_file = memory_dir.join("MEMORY.md");
303 if !memory_file.exists() {
304 return Ok(Some(format!(
305 "Persistent memory file: {}. Create or update `MEMORY.md` with concise reusable notes when you discover stable repository conventions.",
306 memory_file.display()
307 )));
308 }
309
310 let content = std::fs::read_to_string(&memory_file)
311 .with_context(|| format!("Failed to read {}", memory_file.display()))?;
312 let total_lines = content.lines().count();
313 let mut bytes = 0usize;
314 let mut excerpt_lines = Vec::new();
315 for line in content.lines().take(SUBAGENT_MEMORY_LINE_LIMIT) {
316 let next_bytes = bytes.saturating_add(line.len() + 1);
317 if next_bytes > SUBAGENT_MEMORY_BYTES_LIMIT {
318 break;
319 }
320 bytes = next_bytes;
321 excerpt_lines.push(line);
322 }
323
324 let excerpt = excerpt_lines.join("\n");
325 let truncated = excerpt_lines.len() < total_lines;
326 let highlights = extract_memory_highlights(&excerpt, SUBAGENT_MEMORY_HIGHLIGHT_LIMIT);
327 let mut appendix = String::new();
328 appendix.push_str(&format!(
329 "Persistent memory file: {}.\nRead and maintain `MEMORY.md` for durable learnings.",
330 memory_file.display()
331 ));
332
333 if !highlights.is_empty() {
334 appendix.push_str("\n\nKey points:\n");
335 for highlight in highlights {
336 appendix.push_str("- ");
337 appendix.push_str(&highlight);
338 appendix.push('\n');
339 }
340 }
341
342 appendix.push_str("\nOpen `MEMORY.md` when exact wording or more detail matters.");
343 if truncated {
344 appendix.push_str("\nMemory indexing stopped after the configured startup budget.");
345 }
346
347 Ok(Some(appendix))
348}