Crate renames: - zesdex-entities::seaorm → domain (misleading name, no SeaORM used) - zesdex-dto → merged into zesdex-entities (100% re-exports) - zesdex-libs → zesdex-infra (vague name) Module renames: - app/harness → guard (misleading: safety gatekeeper, not test harness) - runtime/commands → action_dispatch (name clashed with controller/command) - resources → prompts (embedded prompt text, not general resources) - tool/seqthink → sequential_think (unreadable abbreviation) - msglog/query → insert (module only inserts, never queries) Dead code removal: - app/mode/help.rs (orphaned — not declared in mod.rs) - app/mode/loading.rs (orphaned — not declared in mod.rs) File splitting (71 new files, avg ~115 lines/file): - app/runtime/actions/: 1→8 files (was 2030 lines) - view/overlays/: 1→16 files (was 1167 lines) - tool/lsp/: 1→8 per-tool files (was 909 lines) - main.rs: 1→5 files (session, daemon, attach, event_loop) - workflow/engine + hive_mind: 2→10 files - subagent/engine + auto: 2→9 files - lsp/provisioner: 1→5 files - review/: 1→6 files - guard/: 1→2 files (extracted patterns) - state/misc: 1→3 files (input, scroll) - mcp/: 1→3 files (transport, adapter) - stream/json_repair extracted from turn.rs DRY: - Pattern constants (STUB_PATTERNS etc) in guard/patterns shared with subagent - 3 near-identical background spawners → 1 generic + thin wrappers - Shared spawn_subagent_with_drain() extracted - Shared create_session() in main - write_osc52 deduplicated Bug fixes: - archive_message(): sess.db → db (wrong variable name) - execute_one_tool(): wrong parameter name - check_credential_read() function was missing (restored from test expectations)
188 lines
6.9 KiB
Rust
188 lines
6.9 KiB
Rust
//! Adaptive quality-review triggering, build/test probing, staleness
|
|
//! sweeps for stored lessons, and the pending-lesson approval workflow.
|
|
|
|
pub mod pending;
|
|
pub mod probe;
|
|
pub mod prompt;
|
|
pub mod staleness;
|
|
pub mod types;
|
|
|
|
pub use pending::{load_pending_lessons, process_pending_lessons, resolve_pending_lesson};
|
|
pub use staleness::maybe_run_staleness_sweep;
|
|
pub use types::{Confidence, LessonScope};
|
|
|
|
use crate::app::state::rest::AppStateRest;
|
|
use crate::app::state::runtime::TurnEvent;
|
|
use crate::app::state::types::{Origin, Toast, ToastKind};
|
|
use crate::app::subagent::context::build_subagent_context;
|
|
use crate::app::subagent::engine::run_subagent;
|
|
use crate::app::subagent::event::SubagentEvent;
|
|
use crate::app::subagent::spawn::{spawn_subagent_with_drain, AgentDefinition};
|
|
|
|
/// Decide whether an adaptive quality review should fire for this turn.
|
|
///
|
|
/// Flow: only `Origin::Main` turns are eligible → require review enabled
|
|
/// in settings → fire every 5th edit unconditionally → otherwise, once
|
|
/// `consecutive_empty_reviews` reaches `adaptive_review_max_skip` (min 2),
|
|
/// fire on an exponentially growing skip interval (2^n, capped at 2^10)
|
|
/// to avoid reviewing every single edit once reviews keep coming back empty.
|
|
///
|
|
/// Why: balances review usefulness against wasted subagent calls when
|
|
/// reviews consistently find nothing.
|
|
///
|
|
/// Return: `true` if a review should be triggered this turn.
|
|
pub fn should_trigger_review(state: &AppStateRest, origin: Origin) -> bool {
|
|
if origin != Origin::Main {
|
|
return false;
|
|
}
|
|
let Some(runtime) = &state.session_runtime else {
|
|
return false;
|
|
};
|
|
if !state.settings.flags.review_enabled {
|
|
return false;
|
|
}
|
|
if runtime.edit_count > 0 && runtime.edit_count % 5 == 0 {
|
|
return true;
|
|
}
|
|
let base: u32 = state.settings.adaptive_review_max_skip.max(2);
|
|
let consecutive = runtime.consecutive_empty_reviews;
|
|
if consecutive >= base {
|
|
let skip = 1u32 << (consecutive - base).min(10);
|
|
if runtime.edit_count > 0 && (runtime.edit_count % skip == 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Spawn a background quality-review subagent for the current session.
|
|
///
|
|
/// Flow: build a "quality-reviewer" subagent context → probe build/test
|
|
/// status via `probe_build_test` to give the reviewer a real pass/fail
|
|
/// signal → compose a system prompt embedding the probe result and lesson
|
|
/// tagging instructions → spawn a thread running `run_subagent` → on
|
|
/// completion, push a `TurnEvent::SystemNote` with the verdict's first
|
|
/// line (or error) → push an "in progress" toast immediately.
|
|
///
|
|
/// Why: runs on a plain OS thread (not tokio) so it doesn't block the
|
|
/// async event loop; communicates its result back via `turn_events`
|
|
/// rather than a channel receiver (the `_rx` half is intentionally unused).
|
|
///
|
|
/// Return: `Ok(())` once the review has been kicked off; errors only
|
|
/// propagate from constructing the subagent context, not from the review
|
|
/// itself (that failure is reported via a `SystemNote` instead).
|
|
pub fn trigger_review(state: &mut AppStateRest) {
|
|
state.misc.lesson_running = true;
|
|
|
|
if let Some(workspace) = state.workspace_roots.first() {
|
|
let gitignore_path = workspace.join(".gitignore");
|
|
let content = std::fs::read_to_string(&gitignore_path).unwrap_or_default();
|
|
if !content.contains("docs/lesson") {
|
|
use std::io::Write;
|
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&gitignore_path)
|
|
{
|
|
let prefix = if content.is_empty() || content.ends_with('\n') {
|
|
""
|
|
} else {
|
|
"\n"
|
|
};
|
|
let _ = writeln!(file, "{prefix}docs/lesson/");
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut def = AgentDefinition::new("lesson-generator".to_string(), "reviewer".to_string());
|
|
// Explicitly allow write_file for docs/lesson
|
|
def.allowed_tools = Some(vec![
|
|
"read".to_string(),
|
|
"write".to_string(),
|
|
"grep".to_string(),
|
|
"glob".to_string(),
|
|
]);
|
|
|
|
let mut ctx = build_subagent_context(&def);
|
|
ctx.session_dir.clone_from(&state.session_dir);
|
|
ctx.workspaces.clone_from(&state.workspace_roots);
|
|
|
|
let probe_result = probe::probe_build_test(
|
|
&state.workspace_roots,
|
|
state.settings.verify_command.as_deref(),
|
|
state.settings.verify_timeout_ms,
|
|
);
|
|
|
|
let probe_note = match &probe_result {
|
|
Some(r) => {
|
|
if r.passed {
|
|
format!("Build/test verification passed ({}).", r.command)
|
|
} else if r.timed_out {
|
|
format!("Build/test verification timed out ({}).", r.command)
|
|
} else {
|
|
format!(
|
|
"Build/test verification failed ({}). Output: {}",
|
|
r.command, r.output
|
|
)
|
|
}
|
|
}
|
|
None => "No build/test probe matched.".to_string(),
|
|
};
|
|
|
|
ctx.system_prompt = prompt::compose_review_prompt(state, &probe_note);
|
|
|
|
let turn_events_for_drain = state.turn_events.clone();
|
|
let (tx, _drain_thread) = spawn_subagent_with_drain(move |event| {
|
|
match &event {
|
|
SubagentEvent::ToolCall { tool, .. } => {
|
|
tracing::debug!("[review] tool call: {}", tool)
|
|
}
|
|
SubagentEvent::ToolResult { tool, .. } => {
|
|
tracing::debug!("[review] tool result: {}", tool)
|
|
}
|
|
SubagentEvent::StepCompleted { .. } => tracing::trace!("[review] step completed"),
|
|
SubagentEvent::StepFailed { step, error } => {
|
|
tracing::warn!("[review] step {} failed: {}", step, error)
|
|
}
|
|
SubagentEvent::Progress(_) => {}
|
|
SubagentEvent::Completed => tracing::debug!("[review] completed"),
|
|
SubagentEvent::Usage {
|
|
tokens_in,
|
|
tokens_out,
|
|
} => {
|
|
if let Ok(mut q) = turn_events_for_drain.lock() {
|
|
q.push_back(TurnEvent::ReviewUsage {
|
|
tokens_in: *tokens_in,
|
|
tokens_out: *tokens_out,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
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!("Lesson created: {first_line}")
|
|
}
|
|
Err(e) => format!("Lesson generation 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,
|
|
"Generating lesson...".to_string(),
|
|
));
|
|
}
|