1use std::collections::HashSet;
35
36use async_trait::async_trait;
37use car_eventlog::harness_adapt::{HarnessIntervention, InterventionLayer};
38use car_memgine::harness_evolution::{
39 mutation_fingerprint, ChangeContract, Governance, HarnessComponent, HarnessConfig,
40 HarnessConfigPatch, HarnessMutation, PromotionDecision,
41};
42use car_policy::permission::ApprovalDecision;
43
44use super::ab_loop::{AbFixer, FixResult};
45
46pub fn mutations_from_interventions(
56 interventions: &[HarnessIntervention],
57 current: &HarnessConfig,
58) -> Vec<HarnessMutation> {
59 let mut out = Vec::new();
60 let mut seen = HashSet::new();
61 for iv in interventions {
62 if iv.layer == InterventionLayer::EnvironmentContract {
73 if let Some(mutation) = prompt_overlay_mutation(iv, current) {
74 if seen.insert(mutation_fingerprint(&mutation)) {
75 out.push(mutation);
76 }
77 }
78 continue;
79 }
80 if iv.layer != InterventionLayer::TrajectoryRegulation {
81 continue;
82 }
83 let bump = (iv.evidence_count as u32).clamp(1, 4);
85 let trig = iv.trigger.to_lowercase();
86 let (component, patch, predicted) = if trig.contains("retried") {
87 let target = current.max_retries.saturating_add(bump);
88 (
89 HarnessComponent::RetryConfig,
90 HarnessConfigPatch {
91 max_retries: Some(target),
92 ..Default::default()
93 },
94 format!(
95 "raise max_retries {} → {} to absorb retry thrash on '{}'",
96 current.max_retries, target, iv.target
97 ),
98 )
99 } else {
100 let target = current.planning_max_replans.saturating_add(bump);
101 (
102 HarnessComponent::PlanningConfig,
103 HarnessConfigPatch {
104 planning_max_replans: Some(target),
105 ..Default::default()
106 },
107 format!(
108 "raise planning_max_replans {} → {} for recurring failure on '{}'",
109 current.planning_max_replans, target, iv.target
110 ),
111 )
112 };
113 let mutation = HarnessMutation {
114 id: format!("ab:{}:{}", component_slug(component), iv.target),
115 contract: ChangeContract {
116 component,
117 target_failure: iv.trigger.clone(),
118 predicted_improvement: predicted,
119 invariants: vec![
120 "no new tool, permission, or validator surface".to_string(),
121 "coder's contract remains the trust boundary".to_string(),
122 ],
123 falsifying_eval: "the next coder A/B round's paired pass-rate does not improve"
124 .to_string(),
125 rollback: "apply the inverse patch (restore the prior knob value)".to_string(),
126 },
127 rationale: format!(
128 "coder A/B attribution: {} (evidence {})",
129 iv.intervention, iv.evidence_count
130 ),
131 patch: Some(patch),
132 };
133 if seen.insert(mutation_fingerprint(&mutation)) {
134 out.push(mutation);
135 }
136 }
137 out
138}
139
140fn overlay_guidance_line(iv: &HarnessIntervention) -> String {
148 format!(
149 "- {} (recurring: {})",
150 iv.intervention.trim(),
151 iv.trigger.trim()
152 )
153}
154
155fn prompt_overlay_mutation(
163 iv: &HarnessIntervention,
164 current: &HarnessConfig,
165) -> Option<HarnessMutation> {
166 let line = overlay_guidance_line(iv);
167 let existing = current.prompt_overlay.clone().unwrap_or_default();
168 if existing.contains(&line) {
169 return None;
170 }
171 let next = if existing.trim().is_empty() {
172 line.clone()
173 } else {
174 format!("{existing}\n{line}")
175 };
176
177 Some(HarnessMutation {
178 id: format!("ab:prompt:{}", iv.target),
179 contract: ChangeContract {
180 component: HarnessComponent::Prompt,
181 target_failure: iv.trigger.clone(),
182 predicted_improvement: format!(
183 "add prompt guidance for the recurring pattern on '{}'",
184 iv.target
185 ),
186 invariants: vec![
187 "the base prompt is unchanged; guidance is appended only".to_string(),
188 "no new tool, permission, or validator surface".to_string(),
189 "coder's contract remains the trust boundary".to_string(),
190 ],
191 falsifying_eval: "the next coder A/B round's paired pass-rate does not improve"
192 .to_string(),
193 rollback: "apply the inverse patch (restore the prior overlay, or clear it)"
194 .to_string(),
195 },
196 rationale: format!(
197 "coder A/B attribution: {} (evidence {})",
198 iv.intervention, iv.evidence_count
199 ),
200 patch: Some(HarnessConfigPatch {
201 prompt_overlay: Some(next),
202 ..Default::default()
203 }),
204 })
205}
206
207fn component_slug(c: HarnessComponent) -> &'static str {
208 match c {
209 HarnessComponent::RetryConfig => "retry",
210 HarnessComponent::PlanningConfig => "planning",
211 HarnessComponent::ToolSchema => "tool_schema",
212 HarnessComponent::RetrievalPolicy => "retrieval",
213 HarnessComponent::ContextBudget => "context",
214 HarnessComponent::WorkflowTopology => "topology",
215 HarnessComponent::PermissionRule => "permission",
216 HarnessComponent::Validator => "validator",
217 HarnessComponent::Prompt => "prompt",
218 }
219}
220
221#[async_trait]
227pub trait HarnessApply: Send + Sync {
228 async fn approval(&self, fingerprint: &str) -> Option<ApprovalDecision>;
229 async fn apply(&self, mutation: &HarnessMutation, governance: Governance)
230 -> Result<(), String>;
231}
232
233pub struct EvolutionAbFixer<A: HarnessApply> {
238 pub current: HarnessConfig,
239 pub backend: A,
240 pub optimistic: bool,
241}
242
243#[async_trait]
244impl<A: HarnessApply> AbFixer for EvolutionAbFixer<A> {
245 async fn apply(&self, interventions: &[HarnessIntervention]) -> FixResult {
246 let mutations = mutations_from_interventions(interventions, &self.current);
247 let patchless = interventions
248 .iter()
249 .filter(|i| i.layer != InterventionLayer::TrajectoryRegulation)
250 .count();
251 if mutations.is_empty() {
252 return FixResult {
253 applied: false,
254 note: format!(
255 "no auto-applicable budget knob among {} intervention(s); {} need human design (prompt/validator/permission changes are code, not knobs)",
256 interventions.len(),
257 patchless
258 ),
259 };
260 }
261 let mut applied = 0usize;
262 let mut pending = 0usize;
263 let mut blocked = 0usize;
264 for m in &mutations {
265 let fp = mutation_fingerprint(m);
266 match self.backend.approval(&fp).await {
267 Some(ApprovalDecision::Rejected) => blocked += 1,
268 Some(ApprovalDecision::Approved) => {
269 if self
270 .backend
271 .apply(m, Governance::HumanApproved)
272 .await
273 .is_ok()
274 {
275 applied += 1;
276 } else {
277 blocked += 1;
278 }
279 }
280 None => {
281 if self.optimistic && !m.requires_human_approval() {
285 let gov = Governance::Promoted(PromotionDecision::Promote {
286 reason:
287 "non-safety budget bump; the next A/B round regression-gates it"
288 .into(),
289 });
290 if self.backend.apply(m, gov).await.is_ok() {
291 applied += 1;
292 } else {
293 pending += 1;
294 }
295 } else {
296 pending += 1;
297 }
298 }
299 }
300 }
301 FixResult {
302 applied: applied > 0,
303 note: format!(
304 "{applied} applied, {pending} pending approval, {blocked} blocked ({} mutation(s), {patchless} non-knob intervention(s) deferred to human design)",
305 mutations.len()
306 ),
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::collections::HashMap;
315 use std::sync::Mutex;
316
317 fn iv(
318 layer: InterventionLayer,
319 target: &str,
320 trigger: &str,
321 evidence: usize,
322 ) -> HarnessIntervention {
323 HarnessIntervention {
324 layer,
325 target: target.into(),
326 trigger: trigger.into(),
327 intervention: "do the thing".into(),
328 evidence_count: evidence,
329 }
330 }
331
332 #[test]
337 fn bridge_maps_trajectory_onto_knobs_and_environment_contract_onto_the_prompt() {
338 let cur = HarnessConfig::default(); let ivs = vec![
340 iv(
341 InterventionLayer::TrajectoryRegulation,
342 "run_command",
343 "action 'run_command' retried 3×",
344 3,
345 ),
346 iv(
347 InterventionLayer::TrajectoryRegulation,
348 "proposal:p1",
349 "replanning exhausted 2× for proposal 'p1'",
350 2,
351 ),
352 iv(
354 InterventionLayer::EnvironmentContract,
355 "edit_file",
356 "rejected before execution 2×",
357 2,
358 ),
359 iv(
361 InterventionLayer::ActionRealization,
362 "tool_x",
363 "no tool 'tool_x'",
364 4,
365 ),
366 ];
367 let muts = mutations_from_interventions(&ivs, &cur);
368 assert_eq!(
369 muts.len(),
370 3,
371 "two trajectory knobs plus one prompt overlay; ActionRealization \
372 still has nothing to turn"
373 );
374 assert_eq!(
375 muts.iter()
376 .filter(|m| m.contract.component == HarnessComponent::Prompt)
377 .count(),
378 1
379 );
380 let retry = muts
382 .iter()
383 .find(|m| m.contract.component == HarnessComponent::RetryConfig)
384 .unwrap();
385 assert_eq!(retry.patch.as_ref().unwrap().max_retries, Some(6));
386 let plan = muts
388 .iter()
389 .find(|m| m.contract.component == HarnessComponent::PlanningConfig)
390 .unwrap();
391 assert_eq!(plan.patch.as_ref().unwrap().planning_max_replans, Some(4));
392 assert!(muts
394 .iter()
395 .all(|m| m.contract.falsifying_eval.contains("A/B")));
396 }
397
398 #[test]
399 fn bridge_dedups_identical_bumps() {
400 let cur = HarnessConfig::default();
401 let ivs = vec![
404 iv(
405 InterventionLayer::TrajectoryRegulation,
406 "t",
407 "action 't' failed 2×",
408 2,
409 ),
410 iv(
411 InterventionLayer::TrajectoryRegulation,
412 "t",
413 "action 't' failed 2×",
414 2,
415 ),
416 ];
417 assert_eq!(mutations_from_interventions(&ivs, &cur).len(), 1);
418 }
419
420 struct FakeApply {
422 decisions: HashMap<String, ApprovalDecision>,
423 applied: Mutex<Vec<String>>,
424 fail_apply: bool,
425 }
426 #[async_trait]
427 impl HarnessApply for FakeApply {
428 async fn approval(&self, fp: &str) -> Option<ApprovalDecision> {
429 self.decisions.get(fp).cloned()
430 }
431 async fn apply(&self, m: &HarnessMutation, _g: Governance) -> Result<(), String> {
432 if self.fail_apply {
433 return Err("apply failed".into());
434 }
435 self.applied.lock().unwrap().push(mutation_fingerprint(m));
436 Ok(())
437 }
438 }
439
440 fn traj(target: &str, evidence: usize) -> HarnessIntervention {
441 iv(
442 InterventionLayer::TrajectoryRegulation,
443 target,
444 &format!("action '{target}' failed {evidence}×"),
445 evidence,
446 )
447 }
448
449 #[tokio::test]
450 async fn optimistic_applies_non_safety_budget_bumps() {
451 let fixer = EvolutionAbFixer {
452 current: HarnessConfig::default(),
453 backend: FakeApply {
454 decisions: HashMap::new(),
455 applied: Mutex::new(vec![]),
456 fail_apply: false,
457 },
458 optimistic: true,
459 };
460 let r = fixer.apply(&[traj("a", 2), traj("b", 1)]).await;
461 assert!(r.applied, "{}", r.note);
462 assert_eq!(fixer.backend.applied.lock().unwrap().len(), 2);
463 }
464
465 #[tokio::test]
466 async fn non_optimistic_leaves_everything_pending() {
467 let fixer = EvolutionAbFixer {
468 current: HarnessConfig::default(),
469 backend: FakeApply {
470 decisions: HashMap::new(),
471 applied: Mutex::new(vec![]),
472 fail_apply: false,
473 },
474 optimistic: false,
475 };
476 let r = fixer.apply(&[traj("a", 2)]).await;
477 assert!(!r.applied);
478 assert!(r.note.contains("1 pending"), "{}", r.note);
479 assert!(fixer.backend.applied.lock().unwrap().is_empty());
480 }
481
482 #[tokio::test]
483 async fn ledger_approval_and_rejection_are_honored() {
484 let cur = HarnessConfig::default();
485 let approved = &mutations_from_interventions(&[traj("a", 2)], &cur)[0];
486 let rejected = &mutations_from_interventions(&[traj("b", 3)], &cur)[0];
487 let mut decisions = HashMap::new();
488 decisions.insert(mutation_fingerprint(approved), ApprovalDecision::Approved);
489 decisions.insert(mutation_fingerprint(rejected), ApprovalDecision::Rejected);
490 let fixer = EvolutionAbFixer {
491 current: cur,
492 backend: FakeApply {
493 decisions,
494 applied: Mutex::new(vec![]),
495 fail_apply: false,
496 },
497 optimistic: false,
499 };
500 let r = fixer.apply(&[traj("a", 2), traj("b", 3)]).await;
501 assert!(r.applied);
502 assert!(
503 r.note.contains("1 applied") && r.note.contains("1 blocked"),
504 "{}",
505 r.note
506 );
507 assert_eq!(fixer.backend.applied.lock().unwrap().len(), 1);
508 }
509
510 #[tokio::test]
511 async fn no_knob_interventions_report_not_applied() {
512 let fixer = EvolutionAbFixer {
513 current: HarnessConfig::default(),
514 backend: FakeApply {
515 decisions: HashMap::new(),
516 applied: Mutex::new(vec![]),
517 fail_apply: false,
518 },
519 optimistic: true,
520 };
521 let r = fixer
522 .apply(&[iv(
523 InterventionLayer::EnvironmentContract,
524 "x",
525 "rejected 2×",
526 2,
527 )])
528 .await;
529 assert!(!r.applied);
530 assert!(r.note.contains("human design"), "{}", r.note);
531 }
532}
533
534#[cfg(test)]
535mod prompt_overlay_tests {
536 use super::*;
537
538 fn intervention(layer: InterventionLayer, target: &str) -> HarnessIntervention {
539 HarnessIntervention {
540 layer,
541 target: target.to_string(),
542 trigger: "model proposed a denied git commit".to_string(),
543 intervention: "State that committing is never the agent's job".to_string(),
544 evidence_count: 3,
545 }
546 }
547
548 #[test]
552 fn an_environment_contract_diagnosis_proposes_a_prompt_overlay() {
553 let ivs = [intervention(
554 InterventionLayer::EnvironmentContract,
555 "proposal:1",
556 )];
557 let muts = mutations_from_interventions(&ivs, &HarnessConfig::default());
558
559 assert_eq!(muts.len(), 1);
560 assert_eq!(muts[0].contract.component, HarnessComponent::Prompt);
561 let overlay = muts[0]
562 .patch
563 .as_ref()
564 .unwrap()
565 .prompt_overlay
566 .as_ref()
567 .unwrap();
568 assert!(overlay.contains("State that committing is never the agent's job"));
569 assert!(overlay.contains("recurring:"));
570 }
571
572 #[test]
575 fn a_prompt_mutation_requires_human_approval() {
576 let ivs = [intervention(InterventionLayer::EnvironmentContract, "p")];
577 let muts = mutations_from_interventions(&ivs, &HarnessConfig::default());
578 assert!(
579 muts[0].requires_human_approval(),
580 "a prompt change must route through the ledger, not the optimistic path"
581 );
582 }
583
584 #[test]
587 fn a_proposal_appends_to_the_existing_overlay() {
588 let current = HarnessConfig {
589 prompt_overlay: Some("- Existing approved guidance (recurring: x)".into()),
590 ..Default::default()
591 };
592 let ivs = [intervention(InterventionLayer::EnvironmentContract, "p")];
593 let muts = mutations_from_interventions(&ivs, ¤t);
594
595 let overlay = muts[0]
596 .patch
597 .as_ref()
598 .unwrap()
599 .prompt_overlay
600 .clone()
601 .unwrap();
602 assert!(overlay.contains("Existing approved guidance"));
603 assert!(overlay.contains("State that committing is never the agent's job"));
604 }
605
606 #[test]
608 fn an_already_present_line_proposes_nothing() {
609 let iv = intervention(InterventionLayer::EnvironmentContract, "p");
610 let first =
611 mutations_from_interventions(std::slice::from_ref(&iv), &HarnessConfig::default());
612 let applied = HarnessConfig {
613 prompt_overlay: first[0].patch.as_ref().unwrap().prompt_overlay.clone(),
614 ..Default::default()
615 };
616 assert!(
617 mutations_from_interventions(std::slice::from_ref(&iv), &applied).is_empty(),
618 "the same diagnosis must not propose the same line twice"
619 );
620 }
621
622 #[test]
625 fn trajectory_diagnoses_still_propose_budget_bumps() {
626 let mut iv = intervention(InterventionLayer::TrajectoryRegulation, "action:9");
627 iv.trigger = "action retried repeatedly".into();
628 let muts = mutations_from_interventions(&[iv], &HarnessConfig::default());
629 assert_eq!(muts[0].contract.component, HarnessComponent::RetryConfig);
630 }
631}