2026-07-11 13:16:10 +07:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use crate::app::state::rest::AppStateRest;
|
2026-07-11 20:21:59 +07:00
|
|
|
use crate::app::state::runtime::TurnEvent;
|
2026-07-11 13:16:10 +07:00
|
|
|
use crate::app::state::types::{AgentMode, Origin, Toast, ToastKind};
|
2026-07-11 20:21:59 +07:00
|
|
|
use crate::app::subagent::context::build_subagent_context;
|
|
|
|
|
use crate::app::subagent::engine::run_subagent;
|
|
|
|
|
use crate::app::subagent::spawn::AgentDefinition;
|
2026-07-11 13:16:10 +07:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
|
|
|
pub enum Confidence {
|
|
|
|
|
Human,
|
|
|
|
|
Verified,
|
|
|
|
|
Unverified,
|
|
|
|
|
Auto,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
|
|
|
pub enum LessonLifecycle {
|
|
|
|
|
New,
|
|
|
|
|
Active,
|
|
|
|
|
Stale,
|
|
|
|
|
Contradicted,
|
|
|
|
|
Superseded,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
|
|
|
pub enum LessonScope {
|
|
|
|
|
Project,
|
|
|
|
|
Global,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Provenance {
|
|
|
|
|
pub session_turn: String,
|
|
|
|
|
pub session_id: String,
|
|
|
|
|
pub reviewer: Origin,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Lesson {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub content: String,
|
|
|
|
|
pub confidence: Confidence,
|
|
|
|
|
pub outcome: Option<String>,
|
|
|
|
|
pub lifecycle: LessonLifecycle,
|
|
|
|
|
pub scope: LessonScope,
|
|
|
|
|
pub contradiction_with: Option<String>,
|
|
|
|
|
pub provenance: Provenance,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum ViolationEscalation {
|
|
|
|
|
None,
|
|
|
|
|
Warning,
|
|
|
|
|
Escalate,
|
|
|
|
|
Block,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum ShadowStatus {
|
|
|
|
|
Trial,
|
|
|
|
|
Graduated,
|
|
|
|
|
Rejected,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ShadowCheck {
|
|
|
|
|
pub pattern: String,
|
|
|
|
|
pub trial_window: u32,
|
|
|
|
|
pub trial_count: u32,
|
|
|
|
|
pub trial_passed: u32,
|
|
|
|
|
pub status: ShadowStatus,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ReviewSystem {
|
|
|
|
|
pub pending: bool,
|
|
|
|
|
pub queue_capacity: usize,
|
|
|
|
|
pub repeated_violations: HashMap<String, u32>,
|
|
|
|
|
pub shadow_violations: Vec<ShadowCheck>,
|
|
|
|
|
pub violation_window: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ReviewSystem {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
ReviewSystem {
|
|
|
|
|
pending: false,
|
|
|
|
|
queue_capacity: 1,
|
|
|
|
|
repeated_violations: HashMap::new(),
|
|
|
|
|
shadow_violations: Vec::new(),
|
|
|
|
|
violation_window: 10,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn reset(&mut self) {
|
|
|
|
|
self.pending = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn check_escalation(&self, pattern: &str) -> ViolationEscalation {
|
|
|
|
|
let count = self.repeated_violations.get(pattern).copied().unwrap_or(0);
|
|
|
|
|
match count {
|
|
|
|
|
0 | 1 => ViolationEscalation::None,
|
|
|
|
|
2 => ViolationEscalation::Warning,
|
|
|
|
|
3 | 4 => ViolationEscalation::Escalate,
|
|
|
|
|
_ => ViolationEscalation::Block,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn increment_violation(&mut self, pattern: &str) -> ViolationEscalation {
|
|
|
|
|
let entry = self.repeated_violations.entry(pattern.to_string()).or_insert(0);
|
|
|
|
|
*entry += 1;
|
|
|
|
|
self.check_escalation(pattern)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn should_skip_review(consecutive_empty: u32) -> bool {
|
|
|
|
|
consecutive_empty >= 3
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn create_pending_lesson(name: &str, content: &str, provenance: Provenance) -> Lesson {
|
|
|
|
|
Lesson {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
content: content.to_string(),
|
|
|
|
|
confidence: Confidence::Unverified,
|
|
|
|
|
outcome: None,
|
|
|
|
|
lifecycle: LessonLifecycle::New,
|
|
|
|
|
scope: LessonScope::Project,
|
|
|
|
|
contradiction_with: None,
|
|
|
|
|
provenance,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn apply_lesson_calibration(state: &mut AppStateRest, lesson: &Lesson) {
|
|
|
|
|
if lesson.confidence != Confidence::Unverified {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if state.mode.auto_approve() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
state.push_toast(Toast::new(
|
|
|
|
|
ToastKind::Lesson,
|
|
|
|
|
format!("Lesson '{}' is pending review. Keep or discard?", lesson.name),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn should_trigger_review(state: &AppStateRest, origin: Origin) -> bool {
|
|
|
|
|
if state.mode == AgentMode::Plan {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if origin != Origin::Main {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let runtime = match &state.session_runtime {
|
|
|
|
|
Some(r) => r,
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
if !state.settings.review_enabled {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if runtime.edit_count > 0 && runtime.edit_count % 5 == 0 {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if runtime.consecutive_empty_reviews >= state.settings.adaptive_review_max_skip {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> {
|
2026-07-11 20:21:59 +07:00
|
|
|
let def = AgentDefinition::new(
|
|
|
|
|
"quality-reviewer".to_string(),
|
|
|
|
|
"reviewer".to_string(),
|
|
|
|
|
);
|
|
|
|
|
let mut ctx = build_subagent_context(def);
|
|
|
|
|
ctx.session_dir = state.session_dir.clone();
|
|
|
|
|
ctx.system_prompt = format!(
|
|
|
|
|
"You are a code quality reviewer. Review the recent code changes \
|
|
|
|
|
for correctness, security, and adherence to best practices. \
|
|
|
|
|
Use read-only tools (read, grep, glob, recall, remember) to \
|
|
|
|
|
inspect the session files and provide a concise review verdict. \
|
|
|
|
|
Session directory: {:?}",
|
|
|
|
|
state.session_dir
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let (tx, _rx) = tokio::sync::mpsc::channel(32);
|
|
|
|
|
let turn_events = state.turn_events.clone();
|
|
|
|
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
let result = run_subagent(ctx, tx);
|
|
|
|
|
let message = match result {
|
|
|
|
|
Ok(verdict) => {
|
|
|
|
|
let first_line = verdict.lines().next().unwrap_or(&verdict);
|
|
|
|
|
format!("Quality review: {}", first_line)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => format!("Quality review failed: {}", e),
|
|
|
|
|
};
|
|
|
|
|
if let Ok(mut q) = turn_events.lock() {
|
|
|
|
|
q.push_back(TurnEvent::SystemNote {
|
|
|
|
|
kind: "review".to_string(),
|
|
|
|
|
message,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
state.push_toast(Toast::new(
|
|
|
|
|
ToastKind::Info,
|
|
|
|
|
"Quality review triggered".to_string(),
|
2026-07-11 13:16:10 +07:00
|
|
|
));
|
2026-07-11 20:21:59 +07:00
|
|
|
|
2026-07-11 13:16:10 +07:00
|
|
|
Ok(())
|
|
|
|
|
}
|