Skip to main content

gba_core/
config.rs

1//! Configuration types for the GBA core engine.
2//!
3//! This module defines configuration types for tasks and the engine,
4//! including task-specific settings loaded from `config.yml` files.
5
6use std::path::PathBuf;
7
8use claude_agent_sdk_rs::PermissionMode;
9use serde::{Deserialize, Serialize};
10use typed_builder::TypedBuilder;
11
12use gba_pm::PromptManager;
13
14/// Permission mode for task execution.
15///
16/// This wraps the SDK's `PermissionMode` to allow deserialization from YAML.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum TaskPermissionMode {
20    /// Accept edits but prompt for other operations.
21    AcceptEdits,
22    /// Bypass all permission prompts.
23    BypassPermissions,
24    /// Default behavior (prompt for all operations).
25    Default,
26}
27
28impl From<TaskPermissionMode> for PermissionMode {
29    fn from(mode: TaskPermissionMode) -> Self {
30        match mode {
31            TaskPermissionMode::AcceptEdits => PermissionMode::AcceptEdits,
32            TaskPermissionMode::BypassPermissions => PermissionMode::BypassPermissions,
33            TaskPermissionMode::Default => PermissionMode::Default,
34        }
35    }
36}
37
38/// Task configuration loaded from `tasks/<kind>/config.yml`.
39///
40/// This configuration determines how the Claude agent is configured
41/// for a specific task type.
42///
43/// # Example YAML
44///
45/// ```yaml
46/// preset: true
47/// tools: []
48/// disallowedTools:
49///   - Write
50///   - Edit
51/// permissionMode: bypass_permissions
52/// ```
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55#[non_exhaustive]
56pub struct TaskConfig {
57    /// Whether to use the `claude_code` preset for the system prompt.
58    ///
59    /// When `true`, the system prompt from `system.j2` is appended to
60    /// the preset. When `false`, the system prompt is used directly.
61    #[serde(default)]
62    pub preset: bool,
63
64    /// List of allowed tools.
65    ///
66    /// An empty list means all tools are allowed.
67    #[serde(default)]
68    pub tools: Vec<String>,
69
70    /// List of disallowed tools.
71    ///
72    /// An empty list means no tools are explicitly disallowed.
73    #[serde(default)]
74    pub disallowed_tools: Vec<String>,
75
76    /// Permission mode for this task.
77    ///
78    /// Controls how the agent handles permission prompts.
79    /// Defaults to `bypass_permissions` for automated execution.
80    #[serde(default)]
81    pub permission_mode: Option<TaskPermissionMode>,
82}
83
84/// Engine configuration for creating an [`Engine`](crate::Engine) instance.
85///
86/// This configuration specifies the working directory, prompt manager,
87/// and optional Claude agent options.
88#[derive(TypedBuilder)]
89#[builder(doc)]
90pub struct EngineConfig<'a> {
91    /// Working directory for the engine.
92    ///
93    /// This is typically the root of the repository where GBA is running.
94    #[builder(setter(into))]
95    pub workdir: PathBuf,
96
97    /// Prompt manager instance containing loaded templates.
98    pub prompts: PromptManager<'a>,
99
100    /// Optional Claude agent options to merge with task-specific options.
101    #[builder(default)]
102    pub agent_options: Option<claude_agent_sdk_rs::ClaudeAgentOptions>,
103}
104
105impl std::fmt::Debug for EngineConfig<'_> {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("EngineConfig")
108            .field("workdir", &self.workdir)
109            .field("prompts", &self.prompts)
110            .field("agent_options", &"<ClaudeAgentOptions>")
111            .finish()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_should_deserialize_task_config_with_defaults() {
121        let yaml = "preset: true";
122        let config: TaskConfig = serde_yaml::from_str(yaml).unwrap();
123
124        assert!(config.preset);
125        assert!(config.tools.is_empty());
126        assert!(config.disallowed_tools.is_empty());
127        assert!(config.permission_mode.is_none());
128    }
129
130    #[test]
131    fn test_should_deserialize_task_config_with_permission_mode() {
132        let yaml = r#"
133preset: true
134permissionMode: bypassPermissions
135"#;
136        let config: TaskConfig = serde_yaml::from_str(yaml).unwrap();
137
138        assert!(config.preset);
139        assert_eq!(
140            config.permission_mode,
141            Some(TaskPermissionMode::BypassPermissions)
142        );
143    }
144
145    #[test]
146    fn test_should_deserialize_task_config_with_tools() {
147        let yaml = r#"
148preset: true
149tools:
150  - Read
151  - Write
152disallowedTools:
153  - Bash
154"#;
155        let config: TaskConfig = serde_yaml::from_str(yaml).unwrap();
156
157        assert!(config.preset);
158        assert_eq!(config.tools, vec!["Read", "Write"]);
159        assert_eq!(config.disallowed_tools, vec!["Bash"]);
160    }
161
162    #[test]
163    fn test_should_deserialize_review_task_config() {
164        let yaml = r#"
165preset: true
166tools: []
167disallowedTools:
168  - Write
169  - Edit
170  - NotebookEdit
171"#;
172        let config: TaskConfig = serde_yaml::from_str(yaml).unwrap();
173
174        assert!(config.preset);
175        assert!(config.tools.is_empty());
176        assert_eq!(
177            config.disallowed_tools,
178            vec!["Write", "Edit", "NotebookEdit"]
179        );
180    }
181
182    #[test]
183    fn test_should_build_engine_config() {
184        let prompts = PromptManager::new();
185        let config = EngineConfig::builder()
186            .workdir("/tmp/test")
187            .prompts(prompts)
188            .build();
189
190        assert_eq!(config.workdir, PathBuf::from("/tmp/test"));
191        assert!(config.agent_options.is_none());
192    }
193}