Skip to main content

agent_exec/compress/
mod.rs

1use serde::{Deserialize, Serialize};
2
3mod generic;
4mod language;
5mod route;
6mod util;
7
8use route::{DetectedKind, route};
9use util::{CompressionCandidate, guard_expansion};
10
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
12#[serde(rename_all = "lowercase")]
13#[value(rename_all = "lowercase")]
14pub enum CompressionMode {
15    Off,
16    #[default]
17    Route,
18    Errors,
19    Tests,
20    Logs,
21    Git,
22    Json,
23    Summary,
24}
25
26impl CompressionMode {
27    pub fn as_str(self) -> &'static str {
28        match self {
29            Self::Off => "off",
30            Self::Route => "route",
31            Self::Errors => "errors",
32            Self::Tests => "tests",
33            Self::Logs => "logs",
34            Self::Git => "git",
35            Self::Json => "json",
36            Self::Summary => "summary",
37        }
38    }
39}
40
41#[derive(Debug)]
42pub struct CompressionInput<'a> {
43    pub command: &'a [String],
44    pub stdout: &'a str,
45    pub stderr: &'a str,
46    pub stdout_original_bytes: u64,
47    pub stderr_original_bytes: u64,
48    pub mode: CompressionMode,
49}
50
51pub fn resolve_cli_mode(
52    compress: Option<CompressionMode>,
53    rtk: Option<CompressionMode>,
54) -> Result<Option<CompressionMode>, String> {
55    match (compress, rtk) {
56        (Some(a), Some(b)) if a != b => Err(format!(
57            "--compress {} conflicts with --rtk {}",
58            a.as_str(),
59            b.as_str()
60        )),
61        (Some(a), _) | (_, Some(a)) => Ok(Some(a)),
62        (None, None) => Ok(None),
63    }
64}
65
66pub fn compress(input: CompressionInput<'_>) -> Option<crate::schema::CompressionData> {
67    if input.mode == CompressionMode::Off {
68        return None;
69    }
70
71    let kind = if input.mode == CompressionMode::Route {
72        route(input.command, input.stdout, input.stderr).kind
73    } else {
74        mode_kind(input.mode)
75    };
76
77    let candidate = language::compress_kind(kind, input.stdout, input.stderr)
78        .unwrap_or_else(|| generic::compress_kind(kind, input.stdout, input.stderr));
79    Some(guard_expansion(
80        into_data(candidate, &input, kind),
81        input.stdout,
82        input.stderr,
83    ))
84}
85
86fn into_data(
87    candidate: CompressionCandidate,
88    input: &CompressionInput<'_>,
89    kind: DetectedKind,
90) -> crate::schema::CompressionData {
91    crate::schema::CompressionData {
92        mode: input.mode.as_str().to_string(),
93        applied: true,
94        detected_kind: kind.as_str().to_string(),
95        stdout_compressed_bytes: candidate.stdout.len() as u64,
96        stderr_compressed_bytes: candidate.stderr.len() as u64,
97        stdout_original_bytes: input.stdout_original_bytes,
98        stderr_original_bytes: input.stderr_original_bytes,
99        omitted: candidate.omitted,
100        strategy: candidate.strategy,
101        stdout: candidate.stdout,
102        stderr: candidate.stderr,
103    }
104}
105
106fn mode_kind(mode: CompressionMode) -> DetectedKind {
107    match mode {
108        CompressionMode::Off | CompressionMode::Route => DetectedKind::Summary,
109        CompressionMode::Errors => DetectedKind::Errors,
110        CompressionMode::Tests => DetectedKind::Tests,
111        CompressionMode::Logs => DetectedKind::Logs,
112        CompressionMode::Git => DetectedKind::Git,
113        CompressionMode::Json => DetectedKind::Json,
114        CompressionMode::Summary => DetectedKind::Summary,
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn conflicting_cli_modes_are_rejected() {
124        let err = resolve_cli_mode(Some(CompressionMode::Errors), Some(CompressionMode::Logs))
125            .unwrap_err();
126        assert!(err.contains("conflicts"));
127    }
128
129    #[test]
130    fn logs_mode_deduplicates_lines() {
131        let raw = format!("{}other\n", "same\n".repeat(20));
132        let data = compress(CompressionInput {
133            command: &[],
134            stdout: &raw,
135            stderr: "",
136            stdout_original_bytes: raw.len() as u64,
137            stderr_original_bytes: 0,
138            mode: CompressionMode::Logs,
139        })
140        .unwrap();
141        assert!(data.applied);
142        assert!(data.stdout.contains("repeated 20x"));
143        assert!(data.stdout.len() < raw.len());
144    }
145
146    #[test]
147    fn expansion_guard_suppresses_larger_candidate() {
148        let raw = "same\nsame\nother\n";
149        let data = compress(CompressionInput {
150            command: &[],
151            stdout: raw,
152            stderr: "",
153            stdout_original_bytes: raw.len() as u64,
154            stderr_original_bytes: 0,
155            mode: CompressionMode::Logs,
156        })
157        .unwrap();
158        assert!(!data.applied);
159        assert_eq!(data.stdout, "");
160        assert_eq!(data.stderr, "");
161        assert_eq!(data.stdout_compressed_bytes, 0);
162        assert_eq!(data.stderr_compressed_bytes, 0);
163        assert_eq!(data.strategy, vec!["expansion-guard"]);
164    }
165}