173 lines
4.3 KiB
Rust
173 lines
4.3 KiB
Rust
use std::collections::HashMap;
|
|||
|
|
use crate::app::state::rest::AppStateRest;
|
||
|
|
use crate::app::state::types::{AgentMode, Origin, Toast, ToastKind};
|
||
|
|
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<()> {
|
||
|
|
let _origin = Origin::Reviewer;
|
||
|
|
state.push_transcript(crate::app::state::rest::ChatMessageDisplay::new(
|
||
|
|
crate::dto::chat::message::Role::System,
|
||
|
|
"review triggered".to_string(),
|
||
|
|
));
|
||
|
|
Ok(())
|
||
|
|
}
|