Skip to main content

ralph_workflow/agents/ccs/
agent_config.rs

1/// Build the final `AgentConfig` from alias config and defaults.
2fn build_ccs_config_from_flags(
3    alias_config: &CcsAliasConfig,
4    defaults: &CcsConfig,
5    cmd: String,
6    env_vars: HashMap<String, String>,
7    display_name: String,
8) -> AgentConfig {
9    let output_flag = alias_config
10        .output_flag
11        .clone()
12        .unwrap_or_else(|| defaults.output_flag.clone());
13    let yolo_flag = alias_config
14        .yolo_flag
15        .clone()
16        .unwrap_or_else(|| defaults.yolo_flag.clone());
17    let verbose_flag = alias_config
18        .verbose_flag
19        .clone()
20        .unwrap_or_else(|| defaults.verbose_flag.clone());
21    // CCS headless behavior: when invoking via the `ccs` wrapper, avoid `-p` because CCS
22    // interprets `-p`/`--prompt` as its own headless delegation mode.
23    // Use Claude's long-form `--print` flag for non-interactive mode instead.
24    // If defaults.print_flag is empty (missing config), fall back to "--print".
25    let print_flag = alias_config.print_flag.clone().unwrap_or_else(|| {
26        let pf = defaults.print_flag.clone();
27        if pf.is_empty() {
28            // Hardcoded safety fallback: use --print to avoid CCS delegation interception
29            "--print".to_string()
30        } else {
31            pf
32        }
33    });
34
35    // Parser selection: alias-specific override takes precedence over CCS default.
36    // This allows users to customize parser per CCS alias if needed.
37    // See function docstring above for detailed explanation.
38    let json_parser = alias_config
39        .json_parser
40        .as_deref()
41        .unwrap_or(&defaults.json_parser);
42    let can_commit = alias_config.can_commit.unwrap_or(defaults.can_commit);
43
44    // Get streaming flag from alias override or defaults
45    let streaming_flag = alias_config
46        .streaming_flag
47        .clone()
48        .unwrap_or_else(|| defaults.streaming_flag.clone());
49
50    // Session continuation flag: prefer alias-specific override, then unified-config CCS defaults.
51    // This is used for XSD retry loops to continue an existing conversation.
52    let session_flag = alias_config
53        .session_flag
54        .clone()
55        .unwrap_or_else(|| defaults.session_flag.clone());
56
57    AgentConfig {
58        cmd, // Uses `claude` directly if found, otherwise falls back to original command
59        output_flag,
60        yolo_flag,
61        verbose_flag,
62        can_commit,
63        json_parser: JsonParserType::parse(json_parser),
64        model_flag: alias_config.model_flag.clone(),
65        print_flag, // Default: --print (safe for `ccs` wrapper); user can override per-alias
66        streaming_flag, // Required for JSON streaming when using -p
67        session_flag, // Session continuation flag for XSD retries
68        env_vars,   // Loaded from CCS settings for the resolved profile, if available
69        display_name: Some(display_name),
70    }
71}
72
73/// Build an `AgentConfig` for CCS, loading credentials and determining command to use.
74///
75/// CCS aliases to use their configured credentials without requiring manual environment variable
76/// configuration, while avoiding hard-coded assumptions about CCS' internal schema.
77#[cfg(any(test, feature = "test-utils"))]
78#[must_use]
79pub fn build_ccs_agent_config(
80    alias_config: &CcsAliasConfig,
81    defaults: &CcsConfig,
82    display_name: String,
83    alias_name: &str,
84) -> AgentConfig {
85    build_ccs_agent_config_impl(alias_config, defaults, display_name, alias_name, |key| {
86        crate::agents::runtime::get_env_var(key)
87    })
88}
89
90#[cfg(not(any(test, feature = "test-utils")))]
91pub fn build_ccs_agent_config(
92    alias_config: &CcsAliasConfig,
93    defaults: &CcsConfig,
94    display_name: String,
95    alias_name: &str,
96) -> AgentConfig {
97    build_ccs_agent_config_impl(alias_config, defaults, display_name, alias_name, |key| {
98        crate::agents::runtime::get_env_var(key)
99    })
100}
101
102#[expect(
103    clippy::print_stderr,
104    reason = "user-facing informative messages for CCS configuration"
105)]
106fn build_ccs_agent_config_impl(
107    alias_config: &CcsAliasConfig,
108    defaults: &CcsConfig,
109    display_name: String,
110    alias_name: &str,
111    get_env_var: impl Fn(&str) -> Option<String>,
112) -> AgentConfig {
113    // Check for CCS_DEBUG env var to enable detailed logging (boundary call)
114    let debug_mode = get_env_var("RALPH_CCS_DEBUG").is_some();
115
116    // Compute env vars and track profile used - functional style
117    let ((env_vars, env_vars_loaded), profile_used_for_env) = if alias_name.is_empty() {
118        ((HashMap::new(), false), None)
119    } else if is_glm_alias(alias_name) {
120        let original_cmd = alias_config.cmd.as_str();
121        let profile =
122            ccs_profile_from_command(original_cmd).unwrap_or_else(|| alias_name.to_string());
123        let result = match load_ccs_env_vars_with_guess(&profile) {
124            Ok((vars, guessed)) => {
125                if let Some(guessed) = guessed {
126                    eprintln!("Info: CCS profile '{profile}' not found; using '{guessed}'");
127                }
128                let loaded = !vars.is_empty();
129                (vars, loaded)
130            }
131            Err(err) => {
132                let suggestions = find_ccs_profile_suggestions(&profile);
133                eprintln!("Warning: failed to load CCS env vars for profile '{profile}': {err}");
134                if !suggestions.is_empty() {
135                    eprintln!("Tip: available/nearby CCS profiles:");
136                    suggestions.iter().for_each(|s| {
137                        eprintln!("  - {s}");
138                    });
139                }
140                (HashMap::new(), false)
141            }
142        };
143        (result, Some(profile))
144    } else {
145        // Non-GLM CCS aliases must execute `ccs ...` directly.
146        // Do not inject GLM/Anthropic-style env vars for other providers.
147        ((HashMap::new(), false), None)
148    };
149
150    // Determine the command to use
151    let cmd = resolve_ccs_command(
152        alias_config,
153        alias_name,
154        env_vars_loaded,
155        profile_used_for_env.as_ref(),
156        debug_mode,
157    );
158
159    // Build the final AgentConfig
160    build_ccs_config_from_flags(alias_config, defaults, cmd.command, env_vars, display_name)
161}
162
163/// CCS alias resolver that can be used by the agent registry.
164#[derive(Debug, Clone, Default)]
165pub struct CcsAliasResolver {
166    aliases: HashMap<String, CcsAliasConfig>,
167    defaults: CcsConfig,
168}
169
170impl CcsAliasResolver {
171    /// Create a new CCS alias resolver with the given aliases.
172    #[must_use]
173    pub const fn new(aliases: HashMap<String, CcsAliasConfig>, defaults: CcsConfig) -> Self {
174        Self { aliases, defaults }
175    }
176
177    /// Create an empty resolver (no aliases configured).
178    #[must_use]
179    pub fn empty() -> Self {
180        Self::default()
181    }
182
183    /// Try to resolve an agent name as a CCS reference.
184    ///
185    /// Returns `Some(AgentConfig)` if the name is a valid CCS reference.
186    /// For known aliases (or default `ccs`), uses the configured command.
187    /// For unknown aliases (e.g., `ccs/random`), generates a default CCS config
188    /// to allow direct CCS execution without configuration.
189    /// Returns `None` if the name is not a CCS reference (doesn't start with "ccs").
190    #[must_use]
191    pub fn try_resolve(&self, agent_name: &str) -> Option<AgentConfig> {
192        let alias = parse_ccs_ref(agent_name)?;
193        // Try to resolve from configured aliases
194        if let Some(config) = resolve_ccs_agent(alias, &self.aliases, &self.defaults) {
195            return Some(config);
196        }
197        // For unknown CCS aliases, generate a default config for direct execution
198        // This allows commands like `ccs random` to work without pre-configuration
199        let cmd = CcsAliasConfig {
200            cmd: format!("ccs {alias}"),
201            ..CcsAliasConfig::default()
202        };
203        let display_name = format!("ccs-{alias}");
204        Some(build_ccs_agent_config(
205            &cmd,
206            &self.defaults,
207            display_name,
208            alias,
209        ))
210    }
211}