Skip to main content

ag_agent/model/
agent.rs

1use std::fmt;
2use std::str::FromStr;
3
4/// Supported agent provider families.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AgentKind {
7    /// Google Antigravity CLI/backend.
8    Antigravity,
9    /// Google Gemini CLI/backend.
10    Gemini,
11    /// Anthropic Claude Code CLI/backend.
12    Claude,
13    /// `OpenAI` Codex CLI/backend.
14    Codex,
15}
16
17/// One locally runnable agent CLI and the installed version refreshed at
18/// startup.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct AgentCliInfo {
21    /// Executable name used to launch the provider CLI.
22    pub executable_name: &'static str,
23    /// Agent provider family backed by the executable.
24    pub kind: AgentKind,
25    /// Current automatic update and version probe state for this executable.
26    pub version: AgentCliVersion,
27}
28
29/// Automatic update and version probe state for one locally runnable agent
30/// CLI.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AgentCliVersion {
33    /// Startup update plus version detection is still running in the
34    /// background.
35    Loading,
36    /// Version detection finished, but the executable did not report a usable
37    /// version.
38    Unknown,
39    /// Version detection finished with a parsed display value.
40    Value(String),
41}
42
43impl AgentCliInfo {
44    /// Creates one CLI availability row for a provider and optional version.
45    pub fn new(kind: AgentKind, version: Option<String>) -> Self {
46        Self {
47            executable_name: kind.executable_name(),
48            kind,
49            version: version.map_or(AgentCliVersion::Unknown, AgentCliVersion::Value),
50        }
51    }
52
53    /// Creates one CLI availability row whose update/version refresh is still
54    /// loading.
55    pub fn loading(kind: AgentKind) -> Self {
56        Self {
57            executable_name: kind.executable_name(),
58            kind,
59            version: AgentCliVersion::Loading,
60        }
61    }
62
63    /// Builds unknown-version CLI rows for an existing provider availability
64    /// list.
65    pub fn from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
66        agent_kinds
67            .iter()
68            .copied()
69            .map(|agent_kind| Self::new(agent_kind, None))
70            .collect()
71    }
72
73    /// Builds loading CLI rows for an existing provider availability list
74    /// while the background update/version refresh is running.
75    pub fn loading_from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
76        agent_kinds.iter().copied().map(Self::loading).collect()
77    }
78}
79
80/// Supported agent model names across all providers.
81///
82/// Gemini model ids are shared by the direct Gemini and Antigravity providers,
83/// so provider ownership lives on [`AgentSelection`] rather than on these
84/// variants.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum AgentModel {
87    /// Codex Sol model backed by `gpt-5.6-sol`.
88    Gpt56Sol,
89    /// Codex Terra model backed by `gpt-5.6-terra`.
90    Gpt56Terra,
91    /// Codex Luna model backed by `gpt-5.6-luna`.
92    Gpt56Luna,
93    /// Codex model backed by `gpt-5.5`.
94    Gpt55,
95    /// Fast Gemini preview model backed by `gemini-3-flash-preview`.
96    Gemini3FlashPreview,
97    /// Fast Gemini model backed by `gemini-3.6-flash`.
98    Gemini36Flash,
99    /// Lightweight Gemini model backed by `gemini-3.5-flash-lite`.
100    Gemini35FlashLite,
101    /// Higher-quality Gemini preview model backed by `gemini-3.1-pro-preview`.
102    Gemini31ProPreview,
103    /// Codex spark model backed by `gpt-5.3-codex-spark`.
104    Gpt53CodexSpark,
105    /// Claude Opus model backed by `claude-opus-5`.
106    ClaudeOpus5,
107    /// Claude Opus model backed by `claude-opus-4-8`.
108    ClaudeOpus48,
109    /// Claude Sonnet model backed by `claude-sonnet-5`.
110    ClaudeSonnet5,
111    /// Claude Fable model backed by `claude-fable-5`.
112    ClaudeFable5,
113    /// Claude Haiku model backed by `claude-haiku-4-5-20251001`.
114    ClaudeHaiku4520251001,
115}
116
117/// Session-level agent selection that keeps provider kind and model together.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct AgentSelection {
120    kind: AgentKind,
121    model: AgentModel,
122}
123
124/// Supported reasoning-effort levels for task execution.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126pub enum ReasoningLevel {
127    /// Low reasoning effort for faster responses.
128    Low,
129    /// Medium reasoning effort.
130    Medium,
131    /// High reasoning effort for deeper reasoning.
132    #[default]
133    High,
134    /// Extra-high reasoning effort for deeper analysis.
135    XHigh,
136    /// Maximum reasoning effort for the hardest tasks.
137    Max,
138}
139
140impl AgentSelection {
141    /// Creates a coherent session agent selection.
142    ///
143    /// If `model` is not supported by `kind`, the provider's default model is
144    /// used so the selection never carries unrelated provider/model values.
145    #[must_use]
146    pub fn new(kind: AgentKind, model: AgentModel) -> Self {
147        let model = if kind.supports_model(model) {
148            model
149        } else {
150            kind.default_model()
151        };
152
153        Self { kind, model }
154    }
155
156    /// Returns the selected agent provider kind.
157    #[must_use]
158    pub fn kind(self) -> AgentKind {
159        self.kind
160    }
161
162    /// Returns the selected agent model.
163    #[must_use]
164    pub fn model(self) -> AgentModel {
165        self.model
166    }
167}
168
169/// Human-readable metadata for slash-menu selectable items.
170pub trait AgentSelectionMetadata {
171    /// Returns a stable item name shown in menus.
172    fn name(&self) -> &'static str;
173
174    /// Returns a short descriptive subtitle shown in menus.
175    fn description(&self) -> &'static str;
176}
177
178impl AgentModel {
179    /// Returns the stable wire/model identifier used in persistence and CLI
180    /// invocations.
181    pub fn as_str(self) -> &'static str {
182        match self {
183            Self::Gpt56Sol => "gpt-5.6-sol",
184            Self::Gpt56Terra => "gpt-5.6-terra",
185            Self::Gpt56Luna => "gpt-5.6-luna",
186            Self::Gpt55 => "gpt-5.5",
187            Self::Gemini3FlashPreview => "gemini-3-flash-preview",
188            Self::Gemini36Flash => "gemini-3.6-flash",
189            Self::Gemini35FlashLite => "gemini-3.5-flash-lite",
190            Self::Gemini31ProPreview => "gemini-3.1-pro-preview",
191            Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
192            Self::ClaudeOpus5 => "claude-opus-5",
193            Self::ClaudeOpus48 => "claude-opus-4-8",
194            Self::ClaudeSonnet5 => "claude-sonnet-5",
195            Self::ClaudeFable5 => "claude-fable-5",
196            Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
197        }
198    }
199
200    /// Returns the model identifier passed to provider transports.
201    ///
202    /// Antigravity and Gemini use the same raw Gemini CLI model ids; callers
203    /// route those ids through the provider stored on [`AgentSelection`].
204    pub fn provider_model_str(self) -> &'static str {
205        self.as_str()
206    }
207
208    /// Parses one persisted model identifier and upgrades retired aliases.
209    ///
210    /// Stored retired Gemini, Claude, and Codex aliases are migrated forward
211    /// so existing projects and sessions continue loading after model
212    /// removals.
213    /// Raw `gemini-*` ids parse to shared Gemini model variants; persisted
214    /// session agents decide whether Gemini or Antigravity owns the session.
215    ///
216    /// # Errors
217    /// Returns an error when `value` is not a known current or retired model
218    /// identifier.
219    pub fn parse_persisted(value: &str) -> Result<Self, String> {
220        match value {
221            "gemini-3.5-flash" => Ok(Self::Gemini36Flash),
222            "gemini-3.1-flash-lite-preview" => Ok(Self::Gemini35FlashLite),
223            "claude-opus-4-6" | "claude-opus-4-7" => Ok(Self::ClaudeOpus48),
224            "claude-sonnet-4-6" => Ok(Self::ClaudeSonnet5),
225            "gpt-5.4-mini" => Ok(Self::Gpt56Luna),
226            "gpt-5.4" => Ok(Self::Gpt55),
227            _ => value.parse(),
228        }
229    }
230}
231
232/// Parses one persisted session agent/model pair without deriving the agent
233/// from the model when the saved agent kind is available.
234///
235/// Existing databases did not persist `agent`, so rows with a missing or
236/// invalid agent value fall back to a compatibility provider inferred from
237/// the persisted model string. Ambiguous raw `gemini-*` rows default to
238/// Antigravity because direct Gemini support had been removed before the new
239/// `agent` column was introduced.
240pub fn parse_persisted_session_agent_model(
241    agent_value: Option<&str>,
242    model_value: &str,
243) -> AgentSelection {
244    let parsed_agent = agent_value
245        .filter(|value| !value.trim().is_empty())
246        .and_then(|value| value.parse::<AgentKind>().ok());
247
248    if let Some(agent_kind) = parsed_agent {
249        return parse_model_for_persisted_agent(agent_kind, model_value);
250    }
251
252    let model = AgentModel::parse_persisted(model_value)
253        .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
254    let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
255
256    AgentSelection::new(agent_kind, model)
257}
258
259/// Parses a persisted model using the already persisted agent kind as the
260/// source of truth for provider ownership.
261fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
262    if let Some(model) = agent_kind.parse_model(model_value) {
263        return AgentSelection::new(agent_kind, model);
264    }
265
266    if let Ok(model) = AgentModel::parse_persisted(model_value)
267        && agent_kind.supports_model(model)
268    {
269        return AgentSelection::new(agent_kind, model);
270    }
271
272    AgentSelection::new(agent_kind, agent_kind.default_model())
273}
274
275/// Returns the provider used for legacy rows that predate explicit session
276/// agent persistence.
277fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
278    if model_value.starts_with("claude-") {
279        return AgentKind::Claude;
280    }
281
282    if model_value.starts_with("gpt-") {
283        return AgentKind::Codex;
284    }
285
286    if model_value.starts_with("gemini-") {
287        return AgentKind::Antigravity;
288    }
289
290    AgentKind::ALL
291        .iter()
292        .copied()
293        .find(|agent_kind| agent_kind.supports_model(model))
294        .unwrap_or(AgentKind::Antigravity)
295}
296
297/// Returns all selectable models owned by the provided agent kinds in stable
298/// settings and slash-menu order.
299#[must_use]
300pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
301    let mut models = Vec::new();
302    for model in agent_kinds
303        .iter()
304        .flat_map(|agent_kind| agent_kind.models())
305        .copied()
306    {
307        if !models.contains(&model) {
308            models.push(model);
309        }
310    }
311
312    models
313}
314
315/// Resolves one model against the currently available agent kinds.
316///
317/// When `model` is unsupported by every available provider, this prefers
318/// `fallback_model` when any available provider supports it and otherwise falls
319/// back to the first available provider default in `agent_kinds`. When no
320/// providers are available, it returns `fallback_model` unchanged.
321#[must_use]
322pub fn resolve_model_for_available_agent_kinds(
323    model: AgentModel,
324    agent_kinds: &[AgentKind],
325    fallback_model: AgentModel,
326) -> AgentModel {
327    if agent_kinds
328        .iter()
329        .any(|agent_kind| agent_kind.supports_model(model))
330    {
331        return model;
332    }
333
334    if agent_kinds
335        .iter()
336        .any(|agent_kind| agent_kind.supports_model(fallback_model))
337    {
338        return fallback_model;
339    }
340
341    agent_kinds
342        .first()
343        .copied()
344        .map_or(fallback_model, AgentKind::default_model)
345}
346
347/// Resolves a provider kind that can run `model` from the available provider
348/// list.
349///
350/// Shared Gemini model ids can be run by both Gemini and Antigravity. This
351/// helper uses the order of `agent_kinds` as the tie-breaker and returns
352/// `fallback_agent_kind` when no available provider supports `model`.
353#[must_use]
354pub fn resolve_agent_kind_for_model(
355    model: AgentModel,
356    agent_kinds: &[AgentKind],
357    fallback_agent_kind: AgentKind,
358) -> AgentKind {
359    agent_kinds
360        .iter()
361        .copied()
362        .find(|agent_kind| agent_kind.supports_model(model))
363        .unwrap_or(fallback_agent_kind)
364}
365
366/// Resolves an [`AgentSelection`] for a model-only setting.
367///
368/// `preferred_agent_kind` is kept when it can run `model`, which preserves the
369/// current session provider for shared Gemini model ids. Otherwise the
370/// available provider order decides the owning provider, falling back to
371/// `preferred_agent_kind` when no available provider supports the model.
372#[must_use]
373pub fn resolve_agent_selection_for_model(
374    model: AgentModel,
375    preferred_agent_kind: AgentKind,
376    agent_kinds: &[AgentKind],
377) -> AgentSelection {
378    let agent_kind = if preferred_agent_kind.supports_model(model) {
379        preferred_agent_kind
380    } else {
381        resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
382    };
383
384    AgentSelection::new(agent_kind, model)
385}
386
387/// Resolves the agent kind used for prompt-side `/model` selection.
388///
389/// This preserves `session_agent_kind` when that backend is still available
390/// and otherwise falls back to the first available backend. When no backends
391/// are available, it returns `None`.
392#[must_use]
393pub fn resolve_prompt_model_agent_kind(
394    session_agent_kind: AgentKind,
395    agent_kinds: &[AgentKind],
396) -> Option<AgentKind> {
397    if agent_kinds.contains(&session_agent_kind) {
398        return Some(session_agent_kind);
399    }
400
401    agent_kinds.first().copied()
402}
403
404impl ReasoningLevel {
405    /// All selectable reasoning-effort levels in UI display order.
406    pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
407
408    /// Returns the stable persisted identifier for this level.
409    ///
410    /// This value is stored in the database and remains independent from any
411    /// provider-specific transport string changes.
412    pub fn as_str(self) -> &'static str {
413        match self {
414            Self::Low => "low",
415            Self::Medium => "medium",
416            Self::High => "high",
417            Self::XHigh => "xhigh",
418            Self::Max => "max",
419        }
420    }
421
422    /// Returns the Codex reasoning-effort identifier for this level.
423    pub fn codex(self) -> &'static str {
424        match self {
425            Self::Low => "low",
426            Self::Medium => "medium",
427            Self::High => "high",
428            Self::XHigh => "xhigh",
429            Self::Max => "max",
430        }
431    }
432
433    /// Returns the Antigravity `--effort` value for this level.
434    ///
435    /// Antigravity accepts `low`, `medium`, and `high`, so higher generic
436    /// reasoning levels map to its highest supported value.
437    pub fn antigravity(self) -> &'static str {
438        match self {
439            Self::Low => "low",
440            Self::Medium => "medium",
441            Self::High | Self::XHigh | Self::Max => "high",
442        }
443    }
444
445    /// Returns the Claude `--effort` value for this level.
446    ///
447    /// Maps `XHigh` and `Max` to `"max"`, which is currently only supported on
448    /// `claude-opus-5` and `claude-opus-4-8`. The Claude CLI enforces this
449    /// restriction and will surface an error for other models.
450    pub fn claude(self) -> &'static str {
451        match self {
452            Self::Low => "low",
453            Self::Medium => "medium",
454            Self::High => "high",
455            Self::XHigh | Self::Max => "max",
456        }
457    }
458
459    /// Returns a short UI description for this reasoning level.
460    pub fn description(self) -> &'static str {
461        match self {
462            Self::Low => "Fastest responses with lighter reasoning.",
463            Self::Medium => "Balanced speed and reasoning depth.",
464            Self::High => "Deeper reasoning for tougher tasks.",
465            Self::XHigh => "Extra-high reasoning for complex tasks.",
466            Self::Max => "Maximum reasoning effort for the hardest tasks.",
467        }
468    }
469}
470
471impl FromStr for ReasoningLevel {
472    type Err = String;
473
474    fn from_str(value: &str) -> Result<Self, Self::Err> {
475        match value {
476            "low" => Ok(Self::Low),
477            "medium" => Ok(Self::Medium),
478            "high" => Ok(Self::High),
479            "xhigh" => Ok(Self::XHigh),
480            "max" => Ok(Self::Max),
481            other => Err(format!("unknown reasoning level: {other}")),
482        }
483    }
484}
485
486impl FromStr for AgentModel {
487    type Err = String;
488
489    fn from_str(value: &str) -> Result<Self, Self::Err> {
490        match value {
491            "gemini-3-flash-preview" => Ok(Self::Gemini3FlashPreview),
492            "gemini-3.6-flash" => Ok(Self::Gemini36Flash),
493            "gemini-3.5-flash-lite" => Ok(Self::Gemini35FlashLite),
494            "gemini-3.1-pro-preview" => Ok(Self::Gemini31ProPreview),
495            "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
496            "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
497            "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
498            "gpt-5.5" => Ok(Self::Gpt55),
499            "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
500            "claude-opus-5" => Ok(Self::ClaudeOpus5),
501            "claude-opus-4-8" => Ok(Self::ClaudeOpus48),
502            "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
503            "claude-fable-5" => Ok(Self::ClaudeFable5),
504            "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
505            other => Err(format!("unknown model: {other}")),
506        }
507    }
508}
509
510impl AgentSelectionMetadata for AgentModel {
511    fn name(&self) -> &'static str {
512        (*self).provider_model_str()
513    }
514
515    fn description(&self) -> &'static str {
516        match self {
517            Self::Gemini31ProPreview => "Higher-quality Gemini model for deeper reasoning.",
518            Self::Gemini36Flash => "Fast Gemini model for agentic and multimodal tasks.",
519            Self::Gemini35FlashLite => {
520                "Lightweight Gemini model for fast, cost-conscious workloads."
521            }
522            Self::Gemini3FlashPreview => "Fast Gemini model for quick iterations.",
523            Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
524            Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
525            Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
526            Self::Gpt55 => "Newer Codex model with stronger coding performance when available.",
527            Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
528            Self::ClaudeOpus5 => "Latest Claude Opus model for complex tasks.",
529            Self::ClaudeOpus48 => "Claude Opus 4.8 model for complex tasks.",
530            Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
531            Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
532            Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
533        }
534    }
535}
536
537impl AgentKind {
538    /// All available agent kinds, in display order.
539    pub const ALL: &[AgentKind] = &[
540        AgentKind::Gemini,
541        AgentKind::Antigravity,
542        AgentKind::Claude,
543        AgentKind::Codex,
544    ];
545
546    /// Returns the provider CLI executable name.
547    pub fn executable_name(self) -> &'static str {
548        match self {
549            Self::Antigravity => "agy",
550            Self::Gemini => "gemini",
551            Self::Claude => "claude",
552            Self::Codex => "codex",
553        }
554    }
555
556    /// Returns the default model for this agent kind.
557    pub fn default_model(self) -> AgentModel {
558        match self {
559            Self::Antigravity | Self::Gemini => AgentModel::Gemini31ProPreview,
560            Self::Claude => AgentModel::ClaudeFable5,
561            Self::Codex => AgentModel::Gpt56Sol,
562        }
563    }
564
565    /// Returns the model string when it belongs to this agent kind.
566    pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
567        if !self.supports_model(model) {
568            return None;
569        }
570
571        Some(model.as_str())
572    }
573
574    /// Returns the curated model list for this agent kind.
575    pub fn models(self) -> &'static [AgentModel] {
576        const ANTIGRAVITY_MODELS: &[AgentModel] = &[
577            AgentModel::Gemini31ProPreview,
578            AgentModel::Gemini36Flash,
579            AgentModel::Gemini35FlashLite,
580            AgentModel::Gemini3FlashPreview,
581        ];
582        const GEMINI_MODELS: &[AgentModel] = &[
583            AgentModel::Gemini31ProPreview,
584            AgentModel::Gemini36Flash,
585            AgentModel::Gemini35FlashLite,
586            AgentModel::Gemini3FlashPreview,
587        ];
588        const CLAUDE_MODELS: &[AgentModel] = &[
589            AgentModel::ClaudeFable5,
590            AgentModel::ClaudeOpus5,
591            AgentModel::ClaudeOpus48,
592            AgentModel::ClaudeSonnet5,
593            AgentModel::ClaudeHaiku4520251001,
594        ];
595        const CODEX_MODELS: &[AgentModel] = &[
596            AgentModel::Gpt56Sol,
597            AgentModel::Gpt56Terra,
598            AgentModel::Gpt56Luna,
599            AgentModel::Gpt55,
600            AgentModel::Gpt53CodexSpark,
601        ];
602
603        match self {
604            Self::Antigravity => ANTIGRAVITY_MODELS,
605            Self::Gemini => GEMINI_MODELS,
606            Self::Claude => CLAUDE_MODELS,
607            Self::Codex => CODEX_MODELS,
608        }
609    }
610
611    /// Parses a provider-specific model string for this agent kind.
612    pub fn parse_model(self, value: &str) -> Option<AgentModel> {
613        let model = value.parse::<AgentModel>().ok()?;
614        if !self.supports_model(model) {
615            return None;
616        }
617
618        Some(model)
619    }
620
621    /// Returns whether this provider can run the given model.
622    pub fn supports_model(self, model: AgentModel) -> bool {
623        self.models().contains(&model)
624    }
625}
626
627impl AgentSelectionMetadata for AgentKind {
628    fn name(&self) -> &'static str {
629        match self {
630            Self::Antigravity => "antigravity",
631            Self::Gemini => "gemini",
632            Self::Claude => "claude",
633            Self::Codex => "codex",
634        }
635    }
636
637    fn description(&self) -> &'static str {
638        match self {
639            Self::Antigravity => "Google Antigravity CLI agent.",
640            Self::Gemini => "Google Gemini CLI agent.",
641            Self::Claude => "Anthropic Claude Code agent.",
642            Self::Codex => "OpenAI Codex CLI agent.",
643        }
644    }
645}
646
647impl AgentSelectionMetadata for ReasoningLevel {
648    fn name(&self) -> &'static str {
649        (*self).as_str()
650    }
651
652    fn description(&self) -> &'static str {
653        (*self).description()
654    }
655}
656
657impl fmt::Display for AgentKind {
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        write!(f, "{}", self.name())
660    }
661}
662
663impl FromStr for AgentKind {
664    type Err = String;
665
666    fn from_str(s: &str) -> Result<Self, Self::Err> {
667        match s.to_lowercase().as_str() {
668            "antigravity" | "agy" => Ok(Self::Antigravity),
669            "gemini" => Ok(Self::Gemini),
670            "claude" => Ok(Self::Claude),
671            "codex" => Ok(Self::Codex),
672            other => Err(format!("unknown agent kind: {other}")),
673        }
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    #[test]
682    /// Ensures model parsing is constrained to the selected provider.
683    fn test_parse_model_returns_none_for_models_from_other_providers() {
684        // Arrange
685        let claude_kind = AgentKind::Claude;
686        let antigravity_model = AgentModel::Gemini3FlashPreview.as_str();
687
688        // Act
689        let parsed_model = claude_kind.parse_model(antigravity_model);
690
691        // Assert
692        assert_eq!(parsed_model, None);
693    }
694
695    #[test]
696    /// Ensures current GPT Codex model ids parse as Codex models.
697    fn test_parse_model_parses_current_codex_models() {
698        // Arrange
699        let codex_kind = AgentKind::Codex;
700
701        // Act
702        let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
703        let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
704        let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
705        let parsed_55 = codex_kind.parse_model("gpt-5.5");
706        let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
707
708        // Assert
709        assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
710        assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
711        assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
712        assert_eq!(parsed_55, Some(AgentModel::Gpt55));
713        assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
714    }
715
716    #[test]
717    /// Ensures Antigravity parses raw Gemini model ids as shared Gemini model
718    /// selections.
719    fn test_parse_model_parses_antigravity() {
720        // Arrange
721        let antigravity_kind = AgentKind::Antigravity;
722
723        // Act
724        let parsed_model = antigravity_kind.parse_model("gemini-3.6-flash");
725
726        // Assert
727        assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
728    }
729
730    #[test]
731    /// Ensures direct Gemini parses the same raw Gemini model ids as
732    /// Antigravity.
733    fn test_parse_model_parses_direct_gemini() {
734        // Arrange
735        let gemini_kind = AgentKind::Gemini;
736
737        // Act
738        let parsed_model = gemini_kind.parse_model("gemini-3.6-flash");
739
740        // Assert
741        assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
742    }
743
744    #[test]
745    /// Ensures current Gemini models expose their user-facing descriptions.
746    fn test_current_gemini_models_have_current_descriptions() {
747        // Arrange
748        let models = [AgentModel::Gemini36Flash, AgentModel::Gemini35FlashLite];
749
750        // Act
751        let descriptions = models.map(|model| model.description());
752
753        // Assert
754        assert_eq!(
755            descriptions,
756            [
757                "Fast Gemini model for agentic and multimodal tasks.",
758                "Lightweight Gemini model for fast, cost-conscious workloads.",
759            ]
760        );
761    }
762
763    #[test]
764    /// Ensures retired GPT model ids no longer parse as selectable models.
765    fn test_parse_model_rejects_retired_gpt_models() {
766        // Arrange
767        let codex_kind = AgentKind::Codex;
768
769        // Act
770        let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
771        let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
772
773        // Assert
774        assert_eq!(parsed_gpt_54, None);
775        assert_eq!(parsed_gpt_54_mini, None);
776    }
777
778    #[test]
779    /// Ensures retired Claude aliases no longer parse as selectable
780    /// models.
781    fn test_parse_model_rejects_retired_claude_aliases() {
782        // Arrange
783        let claude_kind = AgentKind::Claude;
784
785        // Act
786        let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
787        let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
788        let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
789
790        // Assert
791        assert_eq!(parsed_opus_46, None);
792        assert_eq!(parsed_opus_47, None);
793        assert_eq!(parsed_sonnet_46, None);
794    }
795
796    #[test]
797    /// Ensures current Claude model ids parse as supported Claude models.
798    fn test_parse_model_parses_current_claude_models() {
799        // Arrange
800        let claude_kind = AgentKind::Claude;
801
802        // Act
803        let parsed_opus_5 = claude_kind.parse_model("claude-opus-5");
804        let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
805        let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
806        let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
807        let opus_5_id = AgentModel::ClaudeOpus5.as_str();
808        let opus_5_description = AgentModel::ClaudeOpus5.description();
809        let opus_48_description = AgentModel::ClaudeOpus48.description();
810
811        // Assert
812        assert_eq!(parsed_opus_5, Some(AgentModel::ClaudeOpus5));
813        assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
814        assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
815        assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
816        assert_eq!(opus_5_id, "claude-opus-5");
817        assert_eq!(
818            opus_5_description,
819            "Latest Claude Opus model for complex tasks."
820        );
821        assert_eq!(
822            opus_48_description,
823            "Claude Opus 4.8 model for complex tasks."
824        );
825    }
826
827    #[test]
828    /// Ensures persisted model ids parse or migrate to supported
829    /// models.
830    fn test_parse_persisted_handles_supported_and_retired_models() {
831        // Arrange
832
833        // Act
834        let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
835        let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
836        let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
837        let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
838        let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
839        let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
840        let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
841        let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
842        let parsed_gemini_31_flash_lite_preview =
843            AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
844
845        // Assert
846        assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus48));
847        assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus48));
848        assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
849        assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
850        assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
851        assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt55));
852        assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini36Flash));
853        assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini36Flash));
854        assert_eq!(
855            parsed_gemini_31_flash_lite_preview,
856            Ok(AgentModel::Gemini35FlashLite)
857        );
858    }
859
860    #[test]
861    /// Ensures persisted session agent values constrain model parsing instead
862    /// of deriving provider ownership from the model string.
863    fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
864        // Arrange
865
866        // Act
867        let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.6-flash");
868
869        // Assert
870        assert_eq!(selection.kind(), AgentKind::Codex);
871        assert_eq!(selection.model(), AgentKind::Codex.default_model());
872    }
873
874    #[test]
875    /// Ensures Antigravity-persisted sessions keep Antigravity ownership for
876    /// raw Gemini model ids shared with the direct Gemini backend.
877    fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
878        // Arrange
879
880        // Act
881        let selection =
882            parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.6-flash");
883
884        // Assert
885        assert_eq!(selection.kind(), AgentKind::Antigravity);
886        assert_eq!(selection.model(), AgentModel::Gemini36Flash);
887    }
888
889    #[test]
890    /// Ensures retired Gemini models migrate while preserving the saved
891    /// Google provider.
892    fn test_parse_persisted_session_agent_model_migrates_retired_saved_google_models() {
893        // Arrange
894        let persisted_selections = [
895            ("gemini", "gemini-3.5-flash"),
896            ("gemini", "gemini-3.1-flash-lite-preview"),
897            ("antigravity", "gemini-3.5-flash"),
898            ("antigravity", "gemini-3.1-flash-lite-preview"),
899        ];
900
901        // Act
902        let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
903            parse_persisted_session_agent_model(Some(agent_value), model_value)
904        });
905
906        // Assert
907        assert_eq!(
908            migrated_selections,
909            [
910                AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini36Flash),
911                AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
912                AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini36Flash),
913                AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
914            ]
915        );
916    }
917
918    #[test]
919    /// Ensures older persisted rows without `agent` still load through the
920    /// legacy model-derived compatibility path.
921    fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
922        // Arrange
923
924        // Act
925        let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
926
927        // Assert
928        assert_eq!(selection.kind(), AgentKind::Claude);
929        assert_eq!(selection.model(), AgentModel::ClaudeOpus48);
930    }
931
932    #[test]
933    /// Ensures older `gemini-*` rows without `agent` keep the post-removal
934    /// Antigravity compatibility default.
935    fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
936        // Arrange
937
938        // Act
939        let selection = parse_persisted_session_agent_model(None, "gemini-3.6-flash");
940
941        // Assert
942        assert_eq!(selection.kind(), AgentKind::Antigravity);
943        assert_eq!(selection.model(), AgentModel::Gemini36Flash);
944    }
945
946    #[test]
947    /// Ensures retired Gemini models in rows without a saved provider migrate
948    /// through the legacy Antigravity compatibility path.
949    fn test_parse_persisted_session_agent_model_migrates_retired_legacy_gemini_rows() {
950        // Arrange
951
952        // Act
953        let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
954        let migrated_flash_lite =
955            parse_persisted_session_agent_model(None, "gemini-3.1-flash-lite-preview");
956
957        // Assert
958        assert_eq!(
959            migrated_flash,
960            AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini36Flash)
961        );
962        assert_eq!(
963            migrated_flash_lite,
964            AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
965        );
966    }
967
968    #[test]
969    /// Ensures reasoning-level parsing accepts all supported persisted values.
970    fn test_reasoning_level_from_str_parses_supported_values() {
971        // Arrange
972
973        // Act
974        let low_level = "low".parse::<ReasoningLevel>();
975        let medium_level = "medium".parse::<ReasoningLevel>();
976        let high_level = "high".parse::<ReasoningLevel>();
977        let xhigh_level = "xhigh".parse::<ReasoningLevel>();
978        let max_level = "max".parse::<ReasoningLevel>();
979
980        // Assert
981        assert_eq!(low_level, Ok(ReasoningLevel::Low));
982        assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
983        assert_eq!(high_level, Ok(ReasoningLevel::High));
984        assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
985        assert_eq!(max_level, Ok(ReasoningLevel::Max));
986    }
987
988    #[test]
989    /// Ensures unsupported reasoning values return a parse error.
990    fn test_reasoning_level_from_str_rejects_unknown_values() {
991        // Arrange
992
993        // Act
994        let parse_result = "minimal".parse::<ReasoningLevel>();
995
996        // Assert
997        assert!(parse_result.is_err());
998    }
999
1000    #[test]
1001    /// Ensures Codex models are supported by Codex only.
1002    fn test_codex_models_are_supported_by_codex() {
1003        // Arrange
1004        let models = [
1005            AgentModel::Gpt56Sol,
1006            AgentModel::Gpt56Terra,
1007            AgentModel::Gpt56Luna,
1008            AgentModel::Gpt55,
1009            AgentModel::Gpt53CodexSpark,
1010        ];
1011
1012        // Act
1013        let supported = models.map(|model| AgentKind::Codex.supports_model(model));
1014        let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
1015
1016        // Assert
1017        assert_eq!(supported, [true; 5]);
1018        assert_eq!(unsupported, [false; 5]);
1019    }
1020
1021    #[test]
1022    /// Ensures Claude models are supported by Claude only.
1023    fn test_claude_models_are_supported_by_claude() {
1024        // Arrange
1025        let models = [
1026            AgentModel::ClaudeOpus5,
1027            AgentModel::ClaudeOpus48,
1028            AgentModel::ClaudeSonnet5,
1029            AgentModel::ClaudeFable5,
1030            AgentModel::ClaudeHaiku4520251001,
1031        ];
1032
1033        // Act
1034        let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1035        let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1036
1037        // Assert
1038        assert_eq!(supported, [true; 5]);
1039        assert_eq!(unsupported, [false; 5]);
1040    }
1041
1042    #[test]
1043    /// Ensures shared Gemini models are supported by both Google providers.
1044    fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1045        // Arrange
1046        let models = [
1047            AgentModel::Gemini31ProPreview,
1048            AgentModel::Gemini36Flash,
1049            AgentModel::Gemini35FlashLite,
1050            AgentModel::Gemini3FlashPreview,
1051        ];
1052
1053        // Act
1054        let antigravity_supported =
1055            models.map(|model| AgentKind::Antigravity.supports_model(model));
1056        let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1057        let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1058
1059        // Assert
1060        assert_eq!(antigravity_supported, [true; 4]);
1061        assert_eq!(gemini_supported, [true; 4]);
1062        assert_eq!(codex_supported, [false; 4]);
1063    }
1064
1065    #[test]
1066    /// Ensures Antigravity model selections pass raw Gemini ids to provider
1067    /// transports.
1068    fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1069        // Arrange
1070        let model = AgentModel::Gemini35FlashLite;
1071
1072        // Act
1073        let persisted_model = model.as_str();
1074        let provider_model = model.provider_model_str();
1075
1076        // Assert
1077        assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1078        assert_eq!(provider_model, "gemini-3.5-flash-lite");
1079    }
1080
1081    #[test]
1082    /// Ensures `ReasoningLevel::claude()` maps all levels to the correct
1083    /// Claude `--effort` values, including the highest generic levels.
1084    fn test_reasoning_level_claude_maps_all_levels() {
1085        // Arrange / Act / Assert
1086        assert_eq!(ReasoningLevel::Low.claude(), "low");
1087        assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1088        assert_eq!(ReasoningLevel::High.claude(), "high");
1089        assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1090        assert_eq!(ReasoningLevel::Max.claude(), "max");
1091    }
1092
1093    #[test]
1094    /// Ensures Antigravity reasoning values stay within the CLI's accepted
1095    /// `low`, `medium`, and `high` effort levels.
1096    fn test_reasoning_level_antigravity_maps_all_levels() {
1097        // Arrange / Act / Assert
1098        assert_eq!(ReasoningLevel::Low.antigravity(), "low");
1099        assert_eq!(ReasoningLevel::Medium.antigravity(), "medium");
1100        assert_eq!(ReasoningLevel::High.antigravity(), "high");
1101        assert_eq!(ReasoningLevel::XHigh.antigravity(), "high");
1102        assert_eq!(ReasoningLevel::Max.antigravity(), "high");
1103    }
1104
1105    #[test]
1106    /// Ensures Codex reasoning values include the distinct `max` effort.
1107    fn test_reasoning_level_codex_maps_all_levels() {
1108        // Arrange / Act / Assert
1109        assert_eq!(ReasoningLevel::Low.codex(), "low");
1110        assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1111        assert_eq!(ReasoningLevel::High.codex(), "high");
1112        assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1113        assert_eq!(ReasoningLevel::Max.codex(), "max");
1114    }
1115
1116    #[test]
1117    /// Ensures persisted reasoning identifiers stay stable even if provider
1118    /// transport names change in the future.
1119    fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1120        // Arrange / Act / Assert
1121        assert_eq!(ReasoningLevel::Low.as_str(), "low");
1122        assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1123        assert_eq!(ReasoningLevel::High.as_str(), "high");
1124        assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1125        assert_eq!(ReasoningLevel::Max.as_str(), "max");
1126    }
1127
1128    #[test]
1129    /// Ensures selectable-model ordering follows the provided provider order.
1130    fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1131        // Arrange
1132        let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1133
1134        // Act
1135        let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1136
1137        // Assert
1138        assert_eq!(
1139            selectable_models,
1140            vec![
1141                AgentModel::Gpt56Sol,
1142                AgentModel::Gpt56Terra,
1143                AgentModel::Gpt56Luna,
1144                AgentModel::Gpt55,
1145                AgentModel::Gpt53CodexSpark,
1146                AgentModel::Gemini31ProPreview,
1147                AgentModel::Gemini36Flash,
1148                AgentModel::Gemini35FlashLite,
1149                AgentModel::Gemini3FlashPreview,
1150            ]
1151        );
1152    }
1153
1154    #[test]
1155    /// Ensures shared Gemini models appear once even when both Google
1156    /// providers are available.
1157    fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1158        // Arrange
1159        let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1160
1161        // Act
1162        let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1163
1164        // Assert
1165        assert_eq!(
1166            selectable_models,
1167            vec![
1168                AgentModel::Gemini31ProPreview,
1169                AgentModel::Gemini36Flash,
1170                AgentModel::Gemini35FlashLite,
1171                AgentModel::Gemini3FlashPreview,
1172            ]
1173        );
1174    }
1175
1176    #[test]
1177    /// Ensures unavailable models fall back to an available preferred model
1178    /// when possible.
1179    fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1180        // Arrange
1181        let unavailable_model = AgentModel::ClaudeOpus48;
1182        let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1183        let fallback_model = AgentModel::Gpt55;
1184
1185        // Act
1186        let resolved_model = resolve_model_for_available_agent_kinds(
1187            unavailable_model,
1188            &available_agent_kinds,
1189            fallback_model,
1190        );
1191
1192        // Assert
1193        assert_eq!(resolved_model, AgentModel::Gpt55);
1194    }
1195
1196    #[test]
1197    /// Ensures unavailable models fall back to the first available provider
1198    /// default when the preferred fallback is also unavailable.
1199    fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1200        // Arrange
1201        let unavailable_model = AgentModel::ClaudeOpus48;
1202        let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1203        let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1204
1205        // Act
1206        let resolved_model = resolve_model_for_available_agent_kinds(
1207            unavailable_model,
1208            &available_agent_kinds,
1209            unavailable_fallback_model,
1210        );
1211
1212        // Assert
1213        assert_eq!(resolved_model, AgentKind::Codex.default_model());
1214    }
1215
1216    #[test]
1217    /// Ensures model-only settings preserve a preferred provider when it can
1218    /// run the selected model.
1219    fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1220        // Arrange
1221        let model = AgentModel::Gemini36Flash;
1222        let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1223
1224        // Act
1225        let resolved_selection = resolve_agent_selection_for_model(
1226            model,
1227            AgentKind::Antigravity,
1228            &available_agent_kinds,
1229        );
1230
1231        // Assert
1232        assert_eq!(
1233            resolved_selection,
1234            AgentSelection::new(AgentKind::Antigravity, model)
1235        );
1236    }
1237
1238    #[test]
1239    /// Ensures model-only settings use available provider order when the
1240    /// preferred provider cannot run the selected model.
1241    fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1242        // Arrange
1243        let model = AgentModel::Gemini36Flash;
1244        let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1245
1246        // Act
1247        let resolved_selection =
1248            resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1249
1250        // Assert
1251        assert_eq!(
1252            resolved_selection,
1253            AgentSelection::new(AgentKind::Gemini, model)
1254        );
1255    }
1256
1257    #[test]
1258    /// Ensures prompt model selection keeps the current backend when it is
1259    /// still available locally.
1260    fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1261        // Arrange
1262        let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1263
1264        // Act
1265        let resolved_agent_kind =
1266            resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1267
1268        // Assert
1269        assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1270    }
1271
1272    #[test]
1273    /// Ensures prompt model selection falls back to the first locally
1274    /// available backend when the current backend is unavailable.
1275    fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1276        // Arrange
1277        let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1278
1279        // Act
1280        let resolved_agent_kind =
1281            resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1282
1283        // Assert
1284        assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1285    }
1286}