use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RuleOrigin {
Builtin,
User,
Project,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleMatch {
#[serde(default)]
pub tool_names: Option<Vec<String>>,
#[serde(default)]
pub argv0: Option<Vec<String>>,
#[serde(default)]
pub argv_includes: Option<Vec<Vec<String>>>,
#[serde(default)]
pub argv_includes_any: Option<Vec<Vec<String>>>,
#[serde(default)]
pub command_includes: Option<Vec<String>>,
#[serde(default)]
pub command_includes_any: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleFilters {
#[serde(default)]
pub skip_patterns: Option<Vec<String>>,
#[serde(default)]
pub keep_patterns: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleTransforms {
#[serde(default)]
pub strip_ansi: Option<bool>,
#[serde(default)]
pub trim_empty_edges: Option<bool>,
#[serde(default)]
pub dedupe_adjacent: Option<bool>,
#[serde(default)]
pub pretty_print_json: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleSummarize {
#[serde(default)]
pub head: Option<usize>,
#[serde(default)]
pub tail: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleCounter {
pub name: String,
pub pattern: String,
#[serde(default)]
pub flags: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleOutputMatch {
pub pattern: String,
pub message: String,
#[serde(default)]
pub flags: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleFailure {
#[serde(default)]
pub preserve_on_failure: Option<bool>,
#[serde(default)]
pub head: Option<usize>,
#[serde(default)]
pub tail: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonRule {
pub id: String,
pub family: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub priority: Option<i32>,
#[serde(default)]
pub on_empty: Option<String>,
#[serde(default)]
pub match_output: Option<Vec<RuleOutputMatch>>,
#[serde(default)]
pub counter_source: Option<CounterSource>,
pub r#match: RuleMatch,
#[serde(default)]
pub filters: Option<RuleFilters>,
#[serde(default)]
pub transforms: Option<RuleTransforms>,
#[serde(default)]
pub summarize: Option<RuleSummarize>,
#[serde(default)]
pub counters: Option<Vec<RuleCounter>>,
#[serde(default)]
pub failure: Option<RuleFailure>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum CounterSource {
PostKeep,
PreKeep,
}
#[derive(Debug, Clone)]
pub struct CompiledCounter {
pub name: String,
pub pattern: regex::Regex,
}
#[derive(Debug, Clone)]
pub struct CompiledOutputMatch {
pub pattern: regex::Regex,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct CompiledParts {
pub skip_patterns: Vec<regex::Regex>,
pub keep_patterns: Vec<regex::Regex>,
pub counters: Vec<CompiledCounter>,
pub output_matches: Vec<CompiledOutputMatch>,
}
#[derive(Debug, Clone)]
pub struct CompiledRule {
pub rule: JsonRule,
pub source: RuleOrigin,
pub path: String,
pub compiled: CompiledParts,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolExecutionInput {
pub tool_name: String,
#[serde(default)]
pub tool_call_id: Option<String>,
#[serde(default)]
pub run_id: Option<String>,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub argv: Option<Vec<String>>,
#[serde(default)]
pub args: Option<HashMap<String, serde_json::Value>>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub partial: Option<bool>,
#[serde(default)]
pub stdout: Option<String>,
#[serde(default)]
pub stderr: Option<String>,
#[serde(default)]
pub combined_text: Option<String>,
#[serde(default)]
pub exit_code: Option<i32>,
#[serde(default)]
pub started_at: Option<f64>,
#[serde(default)]
pub finished_at: Option<f64>,
#[serde(default)]
pub duration_ms: Option<f64>,
#[serde(default)]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReduceOptions {
#[serde(default)]
pub classifier: Option<String>,
#[serde(default)]
pub max_inline_chars: Option<usize>,
#[serde(default)]
pub raw: Option<bool>,
#[serde(default)]
pub cwd: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReductionStats {
pub raw_chars: usize,
pub reduced_chars: usize,
pub ratio: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassificationResult {
pub family: String,
pub confidence: f64,
#[serde(default)]
pub matched_reducer: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompactResult {
pub inline_text: String,
#[serde(default)]
pub preview_text: Option<String>,
#[serde(default)]
pub facts: Option<HashMap<String, usize>>,
pub stats: ReductionStats,
pub classification: ClassificationResult,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AgentTokenjuiceCompression {
#[default]
Auto,
Full,
Light,
Off,
}
impl AgentTokenjuiceCompression {
pub fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Full => "full",
Self::Light => "light",
Self::Off => "off",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ContentKind {
Json,
Code,
Log,
Search,
Diff,
Html,
PlainText,
}
impl ContentKind {
pub fn as_str(self) -> &'static str {
match self {
ContentKind::Json => "json",
ContentKind::Code => "code",
ContentKind::Log => "log",
ContentKind::Search => "search",
ContentKind::Diff => "diff",
ContentKind::Html => "html",
ContentKind::PlainText => "plain_text",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ContentHint {
pub mime: Option<String>,
pub extension: Option<String>,
pub source_tool: Option<String>,
pub query: Option<String>,
pub explicit: Option<ContentKind>,
}
impl ContentHint {
pub fn for_tool(tool_name: impl Into<String>) -> Self {
Self {
source_tool: Some(tool_name.into()),
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CompressorKind {
SmartCrusher,
Code,
Log,
Search,
Diff,
Html,
MlText,
Generic,
None,
}
impl CompressorKind {
pub fn as_str(self) -> &'static str {
match self {
CompressorKind::SmartCrusher => "smartcrusher",
CompressorKind::Code => "code",
CompressorKind::Log => "log",
CompressorKind::Search => "search",
CompressorKind::Diff => "diff",
CompressorKind::Html => "html",
CompressorKind::MlText => "ml_text",
CompressorKind::Generic => "generic",
CompressorKind::None => "none",
}
}
}
#[derive(Debug, Clone)]
pub struct CompressInput<'a> {
pub content: &'a str,
pub kind: ContentKind,
pub hint: &'a ContentHint,
pub exit_code: Option<i32>,
pub command: Option<String>,
pub argv: Option<Vec<String>>,
pub original_bytes: usize,
}
#[derive(Debug, Clone)]
pub struct CompressOutput {
pub text: String,
pub lossy: bool,
pub kind: CompressorKind,
pub facts: Option<HashMap<String, usize>>,
}
impl CompressOutput {
pub fn reformatted(text: String, kind: CompressorKind) -> Self {
Self {
text,
lossy: false,
kind,
facts: None,
}
}
pub fn lossy(text: String, kind: CompressorKind) -> Self {
Self {
text,
lossy: true,
kind,
facts: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CompressOptions {
pub router_enabled: bool,
pub ccr_enabled: bool,
pub search_enabled: bool,
pub code_enabled: bool,
pub html_enabled: bool,
pub ml_text_enabled: bool,
pub min_bytes_to_compress: usize,
pub ccr_min_tokens: usize,
pub max_inline_chars: Option<usize>,
}
impl Default for CompressOptions {
fn default() -> Self {
Self {
router_enabled: true,
ccr_enabled: true,
search_enabled: true,
code_enabled: true,
html_enabled: true,
ml_text_enabled: false,
min_bytes_to_compress: 2048,
ccr_min_tokens: 500,
max_inline_chars: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CompressedOutput {
pub text: String,
pub content_kind: ContentKind,
pub compressor: CompressorKind,
pub lossy: bool,
pub applied: bool,
pub ccr_token: Option<String>,
pub original_bytes: usize,
pub compacted_bytes: usize,
}
impl CompressedOutput {
pub fn passthrough(content: String, kind: ContentKind) -> Self {
let len = content.len();
Self {
text: content,
content_kind: kind,
compressor: CompressorKind::None,
lossy: false,
applied: false,
ccr_token: None,
original_bytes: len,
compacted_bytes: len,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleFixture {
pub input: ToolExecutionInput,
pub expected_output: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub options: Option<ReduceOptions>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_profile_labels_are_stable() {
assert_eq!(AgentTokenjuiceCompression::Auto.as_str(), "auto");
assert_eq!(AgentTokenjuiceCompression::Full.as_str(), "full");
assert_eq!(AgentTokenjuiceCompression::Light.as_str(), "light");
assert_eq!(AgentTokenjuiceCompression::Off.as_str(), "off");
}
#[test]
fn content_kind_labels_are_stable() {
let labels = [
(ContentKind::Json, "json"),
(ContentKind::Code, "code"),
(ContentKind::Log, "log"),
(ContentKind::Search, "search"),
(ContentKind::Diff, "diff"),
(ContentKind::Html, "html"),
(ContentKind::PlainText, "plain_text"),
];
for (kind, label) in labels {
assert_eq!(kind.as_str(), label);
}
}
#[test]
fn compressor_kind_labels_are_stable() {
let labels = [
(CompressorKind::SmartCrusher, "smartcrusher"),
(CompressorKind::Code, "code"),
(CompressorKind::Log, "log"),
(CompressorKind::Search, "search"),
(CompressorKind::Diff, "diff"),
(CompressorKind::Html, "html"),
(CompressorKind::MlText, "ml_text"),
(CompressorKind::Generic, "generic"),
(CompressorKind::None, "none"),
];
for (kind, label) in labels {
assert_eq!(kind.as_str(), label);
}
}
#[test]
fn content_hint_for_tool_sets_only_source_tool() {
let hint = ContentHint::for_tool("shell");
assert_eq!(hint.source_tool.as_deref(), Some("shell"));
assert!(hint.mime.is_none());
assert!(hint.extension.is_none());
assert!(hint.query.is_none());
assert!(hint.explicit.is_none());
}
#[test]
fn compress_output_constructors_set_lossiness_and_facts() {
let reformatted = CompressOutput::reformatted("{}".into(), CompressorKind::SmartCrusher);
assert!(!reformatted.lossy);
assert_eq!(reformatted.kind, CompressorKind::SmartCrusher);
assert!(reformatted.facts.is_none());
let lossy = CompressOutput::lossy("partial".into(), CompressorKind::Log);
assert!(lossy.lossy);
assert_eq!(lossy.kind, CompressorKind::Log);
assert!(lossy.facts.is_none());
}
#[test]
fn compressed_output_passthrough_preserves_lengths_and_flags() {
let output = CompressedOutput::passthrough("alpha".into(), ContentKind::PlainText);
assert_eq!(output.text, "alpha");
assert_eq!(output.content_kind, ContentKind::PlainText);
assert_eq!(output.compressor, CompressorKind::None);
assert!(!output.lossy);
assert!(!output.applied);
assert!(output.ccr_token.is_none());
assert_eq!(output.original_bytes, 5);
assert_eq!(output.compacted_bytes, 5);
}
}