返回 CodeWhale
harness.rs
根目录 / crates / config / src / harness.rs
1 //! Harness posture + profile config types (#3311).
2 //!
3 //! A *harness posture* is the agent-shaping policy (sub-agent cap, tool
4 //! surface, compaction/cache strategy, safety stance); a *harness profile*
5 //! binds a posture to a provider route + model pattern. Extracted verbatim
6 //! from lib.rs to separate this agent-posture domain from the rest of the
7 //! config schema; re-exported at the crate root so existing paths are
8 //! unchanged. Behavior is identical.
9
10 use std::sync::OnceLock;
11
12 use serde::{Deserialize, Serialize};
13
14 use crate::ProviderKind;
15
16 /// Kinds of built-in harness postures.
17 ///
18 /// A posture names the runtime strategy CodeWhale should use for a
19 /// provider/model route: how much context to preload, how aggressively to lean
20 /// on sub-agents, and how to balance prompt-cache stability against quick
21 /// exploration. Runtime selection is wired in later v0.9 slices; this config
22 /// model intentionally keeps the policy data explicit first.
23 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
24 #[serde(rename_all = "kebab-case")]
25 pub enum HarnessPostureKind {
26 /// Full-featured default: rich constitution, broad tool catalog, and normal
27 /// sub-agent posture.
28 #[default]
29 Standard,
30 /// Cache-heavy: deeper prompt layering and prefix-cache-oriented context.
31 CacheHeavy,
32 /// Lean: smaller starting context, faster compaction, and stronger
33 /// exploration/delegation bias.
34 Lean,
35 /// User-defined posture assembled from explicit knobs below.
36 Custom,
37 }
38
39 /// How this posture should approach compaction and prompt-cache stability.
40 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
41 #[serde(rename_all = "kebab-case")]
42 pub enum HarnessCompactionStrategy {
43 #[default]
44 Default,
45 PrefixCache,
46 Aggressive,
47 }
48
49 /// Which tool catalog shape this posture prefers.
50 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
51 #[serde(rename_all = "kebab-case")]
52 pub enum HarnessToolSurface {
53 #[default]
54 Full,
55 ReadOnly,
56 Auto,
57 }
58
59 /// Safety posture applied when the runtime consumes a harness profile.
60 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
61 #[serde(rename_all = "kebab-case")]
62 pub enum HarnessSafetyPosture {
63 #[default]
64 Standard,
65 Strict,
66 Permissive,
67 }
68
69 /// A concrete harness posture with policy knobs.
70 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
71 #[serde(deny_unknown_fields)]
72 pub struct HarnessPosture {
73 /// Named posture kind.
74 #[serde(default)]
75 pub kind: HarnessPostureKind,
76 /// Maximum number of concurrent sub-agents (0 = runtime default).
77 #[serde(default)]
78 pub max_subagents: usize,
79 /// Prefer search-based/on-demand context over always-on documentation.
80 #[serde(default)]
81 pub prefer_codebase_search: bool,
82 /// Compaction and prompt-cache strategy.
83 #[serde(default)]
84 pub compaction_strategy: HarnessCompactionStrategy,
85 /// Preferred tool catalog shape.
86 #[serde(default)]
87 pub tool_surface: HarnessToolSurface,
88 /// Safety posture for runtime consumers.
89 #[serde(default)]
90 pub safety_posture: HarnessSafetyPosture,
91 }
92
93 impl Default for HarnessPosture {
94 fn default() -> Self {
95 Self {
96 kind: HarnessPostureKind::Standard,
97 max_subagents: 0,
98 prefer_codebase_search: false,
99 compaction_strategy: HarnessCompactionStrategy::default(),
100 tool_surface: HarnessToolSurface::default(),
101 safety_posture: HarnessSafetyPosture::default(),
102 }
103 }
104 }
105
106 impl HarnessPosture {
107 /// A cache-heavy posture tuned for DeepSeek V4 / MiMo-style models.
108 #[must_use]
109 pub fn cache_heavy() -> Self {
110 Self {
111 kind: HarnessPostureKind::CacheHeavy,
112 max_subagents: 10,
113 prefer_codebase_search: false,
114 compaction_strategy: HarnessCompactionStrategy::PrefixCache,
115 tool_surface: HarnessToolSurface::Full,
116 safety_posture: HarnessSafetyPosture::Standard,
117 }
118 }
119
120 /// A lean posture for smaller-context or weaker tool-use models.
121 #[must_use]
122 pub fn lean() -> Self {
123 Self {
124 kind: HarnessPostureKind::Lean,
125 max_subagents: 20,
126 prefer_codebase_search: true,
127 compaction_strategy: HarnessCompactionStrategy::Aggressive,
128 tool_surface: HarnessToolSurface::Full,
129 safety_posture: HarnessSafetyPosture::Standard,
130 }
131 }
132 }
133
134 /// A harness profile binds a posture to a provider route and model pattern.
135 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
136 #[serde(deny_unknown_fields)]
137 pub struct HarnessProfile {
138 /// Provider route this profile applies to, e.g. "deepseek" or
139 /// "xiaomi-mimo".
140 pub provider_route: String,
141 /// Regex or glob pattern for model names, e.g. "deepseek-v4.*".
142 pub model_pattern: String,
143 /// The posture to apply.
144 #[serde(default)]
145 pub posture: HarnessPosture,
146 }
147
148 impl HarnessProfile {
149 /// Return true when this profile applies to the provider/model route.
150 ///
151 /// This is a pure config helper: matching a profile must not mutate runtime
152 /// provider selection, prompts, auth, tools, context, or persisted config.
153 #[must_use]
154 pub fn matches_route(&self, provider_route: &str, model: &str) -> bool {
155 provider_routes_equal(&self.provider_route, provider_route)
156 && wildcard_pattern_matches(&self.model_pattern, model)
157 }
158 }
159
160 /// Built-in profile seeds for common provider/model families.
161 ///
162 /// User-configured profiles are always checked first; these seeds only provide
163 /// a stable resolver result when config has no narrower match.
164 #[must_use]
165 pub fn built_in_harness_profiles() -> &'static [HarnessProfile] {
166 static PROFILES: OnceLock<Vec<HarnessProfile>> = OnceLock::new();
167 PROFILES.get_or_init(|| {
168 vec![
169 HarnessProfile {
170 provider_route: "deepseek".to_string(),
171 model_pattern: "deepseek-v4*".to_string(),
172 posture: HarnessPosture::cache_heavy(),
173 },
174 HarnessProfile {
175 provider_route: "xiaomi-mimo".to_string(),
176 model_pattern: "mimo-v2.5*".to_string(),
177 posture: HarnessPosture::cache_heavy(),
178 },
179 HarnessProfile {
180 provider_route: "arcee".to_string(),
181 model_pattern: "trinity-large-thinking".to_string(),
182 posture: HarnessPosture::cache_heavy(),
183 },
184 HarnessProfile {
185 provider_route: "huggingface".to_string(),
186 model_pattern: "*".to_string(),
187 posture: HarnessPosture::lean(),
188 },
189 HarnessProfile {
190 provider_route: "sglang".to_string(),
191 model_pattern: "*".to_string(),
192 posture: HarnessPosture::lean(),
193 },
194 HarnessProfile {
195 provider_route: "vllm".to_string(),
196 model_pattern: "*".to_string(),
197 posture: HarnessPosture::lean(),
198 },
199 HarnessProfile {
200 provider_route: "ollama".to_string(),
201 model_pattern: "*".to_string(),
202 posture: HarnessPosture::lean(),
203 },
204 ]
205 })
206 }
207
208 fn provider_routes_equal(expected: &str, actual: &str) -> bool {
209 match (ProviderKind::parse(expected), ProviderKind::parse(actual)) {
210 (Some(expected), Some(actual)) => expected == actual,
211 _ => expected.trim().eq_ignore_ascii_case(actual.trim()),
212 }
213 }
214
215 fn wildcard_pattern_matches(pattern: &str, value: &str) -> bool {
216 wildcard_chars_match(
217 &pattern.chars().collect::<Vec<_>>(),
218 &value.chars().collect::<Vec<_>>(),
219 )
220 }
221
222 fn wildcard_chars_match(pattern: &[char], value: &[char]) -> bool {
223 let (mut pattern_idx, mut value_idx) = (0, 0);
224 let mut star_idx: Option<usize> = None;
225 let mut star_value_idx = 0;
226
227 while value_idx < value.len() {
228 if pattern_idx < pattern.len()
229 && (pattern[pattern_idx] == '?' || pattern[pattern_idx] == value[value_idx])
230 {
231 pattern_idx += 1;
232 value_idx += 1;
233 } else if pattern_idx < pattern.len() && pattern[pattern_idx] == '*' {
234 star_idx = Some(pattern_idx);
235 pattern_idx += 1;
236 star_value_idx = value_idx;
237 } else if let Some(star) = star_idx {
238 pattern_idx = star + 1;
239 star_value_idx += 1;
240 value_idx = star_value_idx;
241 } else {
242 return false;
243 }
244 }
245
246 pattern[pattern_idx..].iter().all(|ch| *ch == '*')
247 }
248
248 lines RUST