use crate::agent::agent_loop::tool_input_repair::RepairStatsSnapshot;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GateSource {
AwaitingUser,
Hook,
ResumeAfterFailure,
Verifier,
Critic,
Goal,
Todo,
OpenIssues,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BoundaryNudge {
TrackWork,
FastVerify,
ProgressStall,
ProgressBudget,
ProgressPrologue,
FileTouch,
ReflectionCheckpoint,
SafeState,
None,
}
impl GateSource {
fn index(self) -> usize {
match self {
GateSource::AwaitingUser => 0,
GateSource::Hook => 1,
GateSource::ResumeAfterFailure => 2,
GateSource::Verifier => 3,
GateSource::Critic => 4,
GateSource::Goal => 5,
GateSource::Todo => 6,
GateSource::OpenIssues => 7,
GateSource::None => 8,
}
}
}
impl BoundaryNudge {
fn index(self) -> usize {
match self {
BoundaryNudge::TrackWork => 0,
BoundaryNudge::FastVerify => 1,
BoundaryNudge::ProgressStall => 2,
BoundaryNudge::ProgressBudget => 3,
BoundaryNudge::ProgressPrologue => 4,
BoundaryNudge::FileTouch => 5,
BoundaryNudge::ReflectionCheckpoint => 6,
BoundaryNudge::SafeState => 7,
BoundaryNudge::None => 8,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct GateTally {
gates: [u32; 9],
nudges: [u32; 9],
turns: u32,
tool_calls: u32,
errored_tool_calls: u32,
final_verification: Option<crate::agent::agent_loop::verifier::VerificationStatus>,
repairs: Option<RepairStatsSnapshot>,
capability_tier: Option<super::capability::CapabilityTier>,
scavenged_calls: u32,
hallucinated_tool_names: u32,
storm_suppressions: u32,
max_failure_streak: u32,
}
impl GateTally {
pub fn new() -> Self {
Self::default()
}
pub fn record_gate(&mut self, gate: GateSource) {
if gate == GateSource::None {
return;
}
self.gates[gate.index()] += 1;
}
pub fn record_nudge(&mut self, nudge: BoundaryNudge) {
if nudge == BoundaryNudge::None {
return;
}
self.nudges[nudge.index()] += 1;
}
pub fn record_turn(&mut self) {
self.turns += 1;
}
pub fn record_tool_call(&mut self, is_error: bool) {
self.tool_calls += 1;
if is_error {
self.errored_tool_calls += 1;
}
}
pub fn set_verification(
&mut self,
status: Option<crate::agent::agent_loop::verifier::VerificationStatus>,
) {
self.final_verification = status;
}
pub fn set_capability_tier(&mut self, tier: Option<super::capability::CapabilityTier>) {
self.capability_tier = tier;
}
pub fn set_repairs(&mut self, snapshot: Option<RepairStatsSnapshot>) {
self.repairs = snapshot;
}
pub fn record_scavenged_call(&mut self) {
self.scavenged_calls += 1;
}
#[allow(dead_code)]
pub fn record_hallucinated_tool_name(&mut self) {
self.hallucinated_tool_names += 1;
}
pub fn record_storm_suppression(&mut self) {
self.storm_suppressions += 1;
}
pub fn record_failure_streak(&mut self, current: u32) {
self.max_failure_streak = self.max_failure_streak.max(current);
}
}
#[allow(dead_code)]
impl GateTally {
pub fn gate_count(&self, gate: GateSource) -> u32 {
self.gates[gate.index()]
}
pub fn nudge_count(&self, nudge: BoundaryNudge) -> u32 {
self.nudges[nudge.index()]
}
pub fn turns(&self) -> u32 {
self.turns
}
pub fn tool_calls(&self) -> u32 {
self.tool_calls
}
pub fn errored_tool_calls(&self) -> u32 {
self.errored_tool_calls
}
pub fn scavenged_calls(&self) -> u32 {
self.scavenged_calls
}
pub fn hallucinated_tool_names(&self) -> u32 {
self.hallucinated_tool_names
}
pub fn storm_suppressions(&self) -> u32 {
self.storm_suppressions
}
pub fn max_failure_streak(&self) -> u32 {
self.max_failure_streak
}
pub fn emit(&self) {
let final_verification = match self.final_verification {
Some(status) => format!("{status:?}"),
None => "none".to_string(),
};
let repairs = &self.repairs;
let capability_tier = self.capability_tier.map_or("none", |t| t.as_str());
tracing::info!(
target: "dirge::gates",
capability_tier = %capability_tier,
turns = self.turns,
tool_calls = self.tool_calls,
errored_tool_calls = self.errored_tool_calls,
final_verification = %final_verification,
gate_awaiting_user = self.gates[GateSource::AwaitingUser.index()],
gate_hook = self.gates[GateSource::Hook.index()],
gate_resume_after_failure = self.gates[GateSource::ResumeAfterFailure.index()],
gate_verifier = self.gates[GateSource::Verifier.index()],
gate_critic = self.gates[GateSource::Critic.index()],
gate_goal = self.gates[GateSource::Goal.index()],
gate_todo = self.gates[GateSource::Todo.index()],
gate_open_issues = self.gates[GateSource::OpenIssues.index()],
nudge_track_work = self.nudges[BoundaryNudge::TrackWork.index()],
nudge_fast_verify = self.nudges[BoundaryNudge::FastVerify.index()],
nudge_progress_stall = self.nudges[BoundaryNudge::ProgressStall.index()],
nudge_progress_budget = self.nudges[BoundaryNudge::ProgressBudget.index()],
nudge_progress_prologue = self.nudges[BoundaryNudge::ProgressPrologue.index()],
nudge_file_touch = self.nudges[BoundaryNudge::FileTouch.index()],
nudge_reflection_checkpoint = self.nudges[BoundaryNudge::ReflectionCheckpoint.index()],
nudge_safe_state = self.nudges[BoundaryNudge::SafeState.index()],
scavenged_calls = self.scavenged_calls,
hallucinated_tool_names = self.hallucinated_tool_names,
storm_suppressions = self.storm_suppressions,
max_failure_streak = self.max_failure_streak,
repair_null_stripped = repairs.as_ref().map_or(0, |s| s.null_stripped),
repair_json_string_to_array = repairs.as_ref().map_or(0, |s| s.json_string_to_array),
repair_object_to_array = repairs.as_ref().map_or(0, |s| s.object_to_array),
repair_bare_string_to_array = repairs.as_ref().map_or(0, |s| s.bare_string_to_array),
repair_md_link_unwrapped = repairs.as_ref().map_or(0, |s| s.md_link_unwrapped),
repair_truncation_fixed = repairs.as_ref().map_or(0, |s| s.truncation_fixed),
repair_invalid = repairs.as_ref().map_or(0, |s| s.invalid),
repair_total_successful = repairs.as_ref().map_or(0, |s| s.total_successful()),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::agent_loop::verifier::VerificationStatus;
#[test]
fn counts_each_gate_variant_separately() {
let mut tally = GateTally::new();
for gate in [
GateSource::AwaitingUser,
GateSource::Hook,
GateSource::ResumeAfterFailure,
GateSource::Verifier,
GateSource::Critic,
GateSource::Goal,
GateSource::Todo,
GateSource::OpenIssues,
] {
tally.record_gate(gate);
}
tally.record_gate(GateSource::Verifier);
assert_eq!(tally.gate_count(GateSource::AwaitingUser), 1);
assert_eq!(tally.gate_count(GateSource::Hook), 1);
assert_eq!(tally.gate_count(GateSource::ResumeAfterFailure), 1);
assert_eq!(tally.gate_count(GateSource::Verifier), 2);
assert_eq!(tally.gate_count(GateSource::Critic), 1);
assert_eq!(tally.gate_count(GateSource::Goal), 1);
assert_eq!(tally.gate_count(GateSource::Todo), 1);
assert_eq!(tally.gate_count(GateSource::OpenIssues), 1);
assert_eq!(tally.gate_count(GateSource::None), 0);
}
#[test]
fn counts_each_nudge_variant_separately() {
let mut tally = GateTally::new();
for nudge in [
BoundaryNudge::TrackWork,
BoundaryNudge::FastVerify,
BoundaryNudge::ProgressStall,
BoundaryNudge::ProgressBudget,
BoundaryNudge::ProgressPrologue,
BoundaryNudge::FileTouch,
BoundaryNudge::ReflectionCheckpoint,
BoundaryNudge::SafeState,
] {
tally.record_nudge(nudge);
}
tally.record_nudge(BoundaryNudge::ProgressStall);
assert_eq!(tally.nudge_count(BoundaryNudge::TrackWork), 1);
assert_eq!(tally.nudge_count(BoundaryNudge::FastVerify), 1);
assert_eq!(tally.nudge_count(BoundaryNudge::ProgressStall), 2);
assert_eq!(tally.nudge_count(BoundaryNudge::ProgressBudget), 1);
assert_eq!(tally.nudge_count(BoundaryNudge::FileTouch), 1);
assert_eq!(tally.nudge_count(BoundaryNudge::ReflectionCheckpoint), 1);
assert_eq!(tally.nudge_count(BoundaryNudge::SafeState), 1);
assert_eq!(tally.nudge_count(BoundaryNudge::None), 0);
}
#[test]
fn none_gate_is_a_noop() {
let mut tally = GateTally::new();
tally.record_gate(GateSource::None);
tally.record_gate(GateSource::None);
assert_eq!(tally.gate_count(GateSource::None), 0);
assert_eq!(tally.gate_count(GateSource::Goal), 0);
}
#[test]
fn none_nudge_is_a_noop() {
let mut tally = GateTally::new();
tally.record_nudge(BoundaryNudge::None);
tally.record_nudge(BoundaryNudge::None);
assert_eq!(tally.nudge_count(BoundaryNudge::None), 0);
assert_eq!(tally.nudge_count(BoundaryNudge::FileTouch), 0);
}
#[test]
fn turns_are_counted() {
let mut tally = GateTally::new();
tally.record_turn();
tally.record_turn();
tally.record_turn();
assert_eq!(tally.turns(), 3);
}
#[test]
fn tool_call_errors_are_counted_separately() {
let mut tally = GateTally::new();
tally.record_tool_call(false);
tally.record_tool_call(false);
tally.record_tool_call(true);
assert_eq!(tally.tool_calls(), 3);
assert_eq!(tally.errored_tool_calls(), 1);
}
#[test]
fn verification_status_round_trips() {
let mut tally = GateTally::new();
assert!(tally.final_verification.is_none());
tally.set_verification(Some(VerificationStatus::VerifiedGreen));
assert_eq!(
tally.final_verification,
Some(VerificationStatus::VerifiedGreen)
);
tally.set_verification(None);
assert!(tally.final_verification.is_none());
}
#[test]
fn scavenged_calls_are_counted() {
let mut tally = GateTally::new();
tally.record_scavenged_call();
tally.record_scavenged_call();
tally.record_scavenged_call();
assert_eq!(tally.scavenged_calls(), 3);
}
#[test]
fn repair_snapshot_round_trips() {
let mut tally = GateTally::new();
assert!(tally.repairs.is_none());
let snapshot = RepairStatsSnapshot {
truncation_fixed: 2,
invalid: 1,
..Default::default()
};
tally.set_repairs(Some(snapshot.clone()));
assert_eq!(tally.repairs, Some(snapshot));
tally.set_repairs(None);
assert!(tally.repairs.is_none());
}
#[test]
fn hallucinated_tool_names_are_counted() {
let mut tally = GateTally::new();
tally.record_hallucinated_tool_name();
assert_eq!(tally.hallucinated_tool_names(), 1);
}
#[test]
fn storm_suppressions_are_counted() {
let mut tally = GateTally::new();
tally.record_storm_suppression();
tally.record_storm_suppression();
assert_eq!(tally.storm_suppressions(), 2);
}
#[test]
fn max_failure_streak_keeps_peak() {
let mut tally = GateTally::new();
tally.record_failure_streak(3);
tally.record_failure_streak(5);
tally.record_failure_streak(2);
assert_eq!(tally.max_failure_streak(), 5);
}
}