Skip to main content

clawft_types/
delegation.rs

1//! Delegation configuration types.
2//!
3//! Controls how tasks are dispatched between local execution, Claude AI,
4//! and Claude Flow orchestration. Rules use regex patterns to match task
5//! descriptions and route them to the appropriate target.
6
7use serde::{Deserialize, Serialize};
8
9// ── DelegationConfig ────────────────────────────────────────────────────
10
11/// Root configuration for task delegation routing.
12///
13/// When a task arrives, rules are evaluated in order. The first matching
14/// rule determines the target. If no rule matches, the `Auto` target is
15/// used (which applies a complexity heuristic).
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DelegationConfig {
18    /// Whether Claude AI delegation is enabled.
19    #[serde(default)]
20    pub claude_enabled: bool,
21
22    /// Claude model identifier (e.g. `"claude-sonnet-4-20250514"`).
23    #[serde(default = "default_delegation_model", alias = "claudeModel")]
24    pub claude_model: String,
25
26    /// Maximum conversation turns per delegated task.
27    #[serde(default = "default_max_turns", alias = "maxTurns")]
28    pub max_turns: u32,
29
30    /// Maximum tokens per Claude response.
31    #[serde(default = "default_max_tokens", alias = "maxTokens")]
32    pub max_tokens: u32,
33
34    /// Whether Claude Flow orchestration is enabled.
35    #[serde(default, alias = "claudeFlowEnabled")]
36    pub claude_flow_enabled: bool,
37
38    /// Ordered list of routing rules. First match wins.
39    #[serde(default)]
40    pub rules: Vec<DelegationRule>,
41
42    /// Tool names that should never be delegated.
43    #[serde(default, alias = "excludedTools")]
44    pub excluded_tools: Vec<String>,
45}
46
47fn default_delegation_model() -> String {
48    "claude-sonnet-4-20250514".into()
49}
50
51fn default_max_turns() -> u32 {
52    10
53}
54
55fn default_max_tokens() -> u32 {
56    4096
57}
58
59impl Default for DelegationConfig {
60    fn default() -> Self {
61        Self {
62            claude_enabled: true, // Gracefully degrades if no API key
63            claude_model: default_delegation_model(),
64            max_turns: default_max_turns(),
65            max_tokens: default_max_tokens(),
66            claude_flow_enabled: false, // Stays false until Flow fully wired
67            rules: Vec::new(),
68            excluded_tools: Vec::new(),
69        }
70    }
71}
72
73// ── DelegationRule ──────────────────────────────────────────────────────
74
75/// A single routing rule that maps a regex pattern to a delegation target.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DelegationRule {
78    /// Regex pattern matched against the task description.
79    pub pattern: String,
80
81    /// Where to route matching tasks.
82    pub target: DelegationTarget,
83}
84
85// ── DelegationTarget ────────────────────────────────────────────────────
86
87/// Where a task should be executed.
88///
89/// Serializes to snake_case (`"local"`, `"claude"`, `"flow"`, `"auto"`).
90/// For backward compatibility, old PascalCase values (`"Local"`, `"Claude"`,
91/// `"Flow"`, `"Auto"`) are accepted on deserialization via serde aliases.
92#[non_exhaustive]
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum DelegationTarget {
96    /// Execute locally (built-in tool pipeline).
97    #[serde(alias = "Local")]
98    Local,
99    /// Delegate to Claude AI.
100    #[serde(alias = "Claude")]
101    Claude,
102    /// Delegate to Claude Flow orchestration.
103    #[serde(alias = "Flow")]
104    Flow,
105    /// Automatically decide based on complexity heuristics.
106    #[serde(alias = "Auto")]
107    #[default]
108    Auto,
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn delegation_config_defaults() {
117        let cfg = DelegationConfig::default();
118        assert!(cfg.claude_enabled); // M3: defaults to true, degrades gracefully
119        assert_eq!(cfg.claude_model, "claude-sonnet-4-20250514");
120        assert_eq!(cfg.max_turns, 10);
121        assert_eq!(cfg.max_tokens, 4096);
122        assert!(!cfg.claude_flow_enabled);
123        assert!(cfg.rules.is_empty());
124        assert!(cfg.excluded_tools.is_empty());
125    }
126
127    #[test]
128    fn delegation_config_serde_roundtrip() {
129        let cfg = DelegationConfig {
130            claude_enabled: true,
131            claude_model: "claude-opus-4-20250514".into(),
132            max_turns: 5,
133            max_tokens: 2048,
134            claude_flow_enabled: true,
135            rules: vec![
136                DelegationRule {
137                    pattern: r"(?i)deploy".into(),
138                    target: DelegationTarget::Flow,
139                },
140                DelegationRule {
141                    pattern: r"(?i)simple.*query".into(),
142                    target: DelegationTarget::Local,
143                },
144            ],
145            excluded_tools: vec!["shell_exec".into()],
146        };
147
148        let json = serde_json::to_string(&cfg).unwrap();
149        let restored: DelegationConfig = serde_json::from_str(&json).unwrap();
150
151        assert_eq!(restored.claude_enabled, cfg.claude_enabled);
152        assert_eq!(restored.claude_model, cfg.claude_model);
153        assert_eq!(restored.max_turns, cfg.max_turns);
154        assert_eq!(restored.max_tokens, cfg.max_tokens);
155        assert_eq!(restored.claude_flow_enabled, cfg.claude_flow_enabled);
156        assert_eq!(restored.rules.len(), 2);
157        assert_eq!(restored.rules[0].pattern, r"(?i)deploy");
158        assert_eq!(restored.rules[0].target, DelegationTarget::Flow);
159        assert_eq!(restored.rules[1].target, DelegationTarget::Local);
160        assert_eq!(restored.excluded_tools, vec!["shell_exec"]);
161    }
162
163    #[test]
164    fn delegation_config_from_empty_json() {
165        let cfg: DelegationConfig = serde_json::from_str("{}").unwrap();
166        assert!(!cfg.claude_enabled);
167        assert_eq!(cfg.claude_model, "claude-sonnet-4-20250514");
168        assert_eq!(cfg.max_turns, 10);
169        assert_eq!(cfg.max_tokens, 4096);
170        assert!(!cfg.claude_flow_enabled);
171        assert!(cfg.rules.is_empty());
172        assert!(cfg.excluded_tools.is_empty());
173    }
174
175    #[test]
176    fn delegation_config_camel_case_aliases() {
177        let json = r#"{
178            "claudeModel": "test-model",
179            "maxTurns": 3,
180            "maxTokens": 1024,
181            "claudeFlowEnabled": true,
182            "excludedTools": ["dangerous_tool"]
183        }"#;
184        let cfg: DelegationConfig = serde_json::from_str(json).unwrap();
185        assert_eq!(cfg.claude_model, "test-model");
186        assert_eq!(cfg.max_turns, 3);
187        assert_eq!(cfg.max_tokens, 1024);
188        assert!(cfg.claude_flow_enabled);
189        assert_eq!(cfg.excluded_tools, vec!["dangerous_tool"]);
190    }
191
192    #[test]
193    fn delegation_target_serializes_snake_case() {
194        let targets = [
195            (DelegationTarget::Local, "\"local\""),
196            (DelegationTarget::Claude, "\"claude\""),
197            (DelegationTarget::Flow, "\"flow\""),
198            (DelegationTarget::Auto, "\"auto\""),
199        ];
200        for (target, expected_json) in &targets {
201            let json = serde_json::to_string(target).unwrap();
202            assert_eq!(&json, expected_json);
203            let restored: DelegationTarget = serde_json::from_str(&json).unwrap();
204            assert_eq!(restored, *target);
205        }
206    }
207
208    #[test]
209    fn delegation_target_deserializes_legacy_pascal_case() {
210        // Backward compat: old PascalCase values still deserialize.
211        let cases = [
212            ("\"Local\"", DelegationTarget::Local),
213            ("\"Claude\"", DelegationTarget::Claude),
214            ("\"Flow\"", DelegationTarget::Flow),
215            ("\"Auto\"", DelegationTarget::Auto),
216        ];
217        for (json, expected) in &cases {
218            let restored: DelegationTarget = serde_json::from_str(json).unwrap();
219            assert_eq!(restored, *expected);
220        }
221    }
222
223    #[test]
224    fn delegation_target_default_is_auto() {
225        assert_eq!(DelegationTarget::default(), DelegationTarget::Auto);
226    }
227}