Skip to main content

harness_core/
signal.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6#[non_exhaustive]
7pub enum Severity {
8    /// Informational; agent may ignore.
9    Hint,
10    /// Should fix, but not blocking.
11    Warn,
12    /// Must address before proceeding.
13    Block,
14}
15
16/// A feedback signal from a sensor — **optimised for LLM consumption**.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Signal {
19    pub severity: Severity,
20    pub origin: String,
21    /// Human-readable description of the problem.
22    pub message: String,
23    /// Direct correction instruction for the model (required if `severity == Block`).
24    pub agent_hint: Option<String>,
25    /// Computational fix that bypasses the model — applied in `auto_fix` channel.
26    pub auto_fix: Option<FixPatch>,
27    pub location: Option<CodeSpan>,
28}
29
30impl Signal {
31    pub fn is_blocking(&self) -> bool {
32        matches!(self.severity, Severity::Block)
33    }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CodeSpan {
38    pub path: PathBuf,
39    pub line: u32,
40    pub column: u32,
41    pub length: u32,
42}
43
44/// A direct patch a sensor can apply without going through the model.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[non_exhaustive]
47pub enum FixPatch {
48    /// Replace the entire file content.
49    ReplaceFile { path: PathBuf, content: String },
50    /// Apply a unified diff.
51    UnifiedDiff { diff: String },
52    /// Run a deterministic shell command (e.g. `cargo fmt`).
53    RunCommand {
54        program: String,
55        args: Vec<String>,
56        cwd: Option<PathBuf>,
57    },
58}
59
60/// A bundle of signals, with helpers for the agent loop.
61#[derive(Debug, Default)]
62pub struct SignalSet {
63    pub signals: Vec<Signal>,
64}
65
66impl SignalSet {
67    pub fn new(signals: Vec<Signal>) -> Self {
68        Self { signals }
69    }
70
71    pub fn has_blocking(&self) -> bool {
72        self.signals.iter().any(Signal::is_blocking)
73    }
74
75    /// Partition into (auto-fix patches, signals that still need model attention).
76    pub fn partition_auto_fix(self) -> (Vec<FixPatch>, SignalSet) {
77        let mut patches = Vec::new();
78        let mut remaining = Vec::new();
79        for s in self.signals {
80            if let Some(p) = s.auto_fix.clone() {
81                patches.push(p);
82            } else {
83                remaining.push(s);
84            }
85        }
86        (patches, SignalSet { signals: remaining })
87    }
88
89    pub fn is_clean(&self) -> bool {
90        self.signals.is_empty()
91    }
92}