use std::sync::Arc;
use crate::providers::{RequestErrorKind, TokenUsage};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompactReason {
Proactive,
Reactive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyKind {
Turns,
InputTokens,
OutputTokens,
MaxSchemaRetries,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolFailureKind {
ToolNotFound,
ExecutionFailed,
SchemaValidationFailed,
}
#[derive(Debug, Clone)]
pub struct Event {
pub agent_name: String,
pub kind: EventKind,
}
impl Event {
pub(crate) fn new(agent_name: impl Into<String>, kind: EventKind) -> Self {
Self {
agent_name: agent_name.into(),
kind,
}
}
}
#[derive(Debug, Clone)]
pub enum EventKind {
TicketStarted { key: String },
TicketDone { key: String },
TicketFailed { key: String },
RequestStarted { model: String },
RequestFinished { model: String, usage: TokenUsage },
RequestFailed {
kind: RequestErrorKind,
message: String,
},
RequestRetried {
attempt: u32,
max_attempts: u32,
kind: RequestErrorKind,
message: String,
},
TextChunkReceived { content: String },
ToolCallStarted {
tool_name: String,
call_id: String,
input: serde_json::Value,
},
ToolCallFinished {
tool_name: String,
call_id: String,
output: String,
},
ToolCallFailed {
tool_name: String,
call_id: String,
message: String,
kind: ToolFailureKind,
},
PolicyViolated { kind: PolicyKind, limit: u64 },
SchemaRetried {
attempt: u32,
max_attempts: u32,
message: String,
},
CompactionStarted { reason: CompactReason },
CompactionFinished { reason: CompactReason },
CompactionFailed {
reason: CompactReason,
message: String,
},
BlockingLimitExceeded {
estimated_tokens: u64,
threshold_tokens: u64,
},
}
pub fn default_logger() -> Arc<dyn Fn(Event) + Send + Sync> {
Arc::new(|event: Event| {
let agent = &event.agent_name;
match &event.kind {
EventKind::TicketStarted { key } => {
eprintln!("[{agent}] started {key}");
}
EventKind::TicketDone { key } => {
eprintln!("[{agent}] done {key}");
}
EventKind::TicketFailed { key } => {
eprintln!("[{agent}] failed {key}");
}
EventKind::ToolCallStarted {
tool_name, input, ..
} => {
eprintln!("[{agent}] {tool_name}({})", compact_input(input));
}
EventKind::ToolCallFailed {
tool_name,
message,
kind,
..
} => {
eprintln!("[{agent}] {tool_name} failed ({kind:?}): {message}");
}
EventKind::RequestFailed { message, .. } => {
eprintln!("[{agent}] request failed: {message}");
}
EventKind::RequestRetried {
attempt,
max_attempts,
message,
..
} => {
eprintln!("[{agent}] retry {attempt}/{max_attempts}: {message}");
}
EventKind::SchemaRetried {
attempt,
max_attempts,
message,
} => {
eprintln!("[{agent}] schema retry {attempt}/{max_attempts}: {message}");
}
EventKind::PolicyViolated { kind, limit } => {
eprintln!("[{agent}] policy violated: {kind:?} limit={limit}");
}
EventKind::CompactionStarted { reason } => {
eprintln!("[{agent}] compacting context ({reason:?})");
}
EventKind::CompactionFinished { reason } => {
eprintln!("[{agent}] context compacted ({reason:?})");
}
EventKind::CompactionFailed { reason, message } => {
eprintln!("[{agent}] compaction failed ({reason:?}): {message}");
}
EventKind::BlockingLimitExceeded {
estimated_tokens,
threshold_tokens,
} => {
eprintln!(
"[{agent}] blocking limit: estimated {estimated_tokens} tokens >= {threshold_tokens}",
);
}
_ => {}
}
})
}
fn compact_input(input: &serde_json::Value) -> String {
let one_line = input.to_string().replace('\n', " ");
const MAX: usize = 80;
if one_line.chars().count() <= MAX {
one_line
} else {
let cut: String = one_line.chars().take(MAX).collect();
format!("{cut}…")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::TokenUsage;
fn all_variants() -> Vec<EventKind> {
vec![
EventKind::TicketStarted { key: "T-1".into() },
EventKind::TicketDone { key: "T-1".into() },
EventKind::TicketFailed { key: "T-1".into() },
EventKind::RequestStarted { model: "m".into() },
EventKind::RequestFinished {
model: "m".into(),
usage: TokenUsage::default(),
},
EventKind::RequestFailed {
kind: RequestErrorKind::ConnectionFailed,
message: "timeout".into(),
},
EventKind::RequestRetried {
attempt: 1,
max_attempts: 10,
kind: RequestErrorKind::ConnectionFailed,
message: "transient".into(),
},
EventKind::SchemaRetried {
attempt: 1,
max_attempts: 5,
message: "missing required field 'idx'".into(),
},
EventKind::TextChunkReceived {
content: "hello".into(),
},
EventKind::ToolCallStarted {
tool_name: "bash".into(),
call_id: "c1".into(),
input: serde_json::json!({"cmd": "ls"}),
},
EventKind::ToolCallFinished {
tool_name: "bash".into(),
call_id: "c1".into(),
output: "file.txt".into(),
},
EventKind::ToolCallFailed {
tool_name: "bash".into(),
call_id: "c1".into(),
message: "not found".into(),
kind: ToolFailureKind::ToolNotFound,
},
EventKind::ToolCallFailed {
tool_name: "manage_tickets_tool".into(),
call_id: "c2".into(),
message: "Schema validation failed".into(),
kind: ToolFailureKind::SchemaValidationFailed,
},
EventKind::PolicyViolated {
kind: PolicyKind::Turns,
limit: 10,
},
EventKind::PolicyViolated {
kind: PolicyKind::MaxSchemaRetries,
limit: 10,
},
EventKind::CompactionStarted {
reason: CompactReason::Proactive,
},
EventKind::CompactionFinished {
reason: CompactReason::Proactive,
},
EventKind::CompactionFailed {
reason: CompactReason::Reactive,
message: "summarize call failed".into(),
},
EventKind::BlockingLimitExceeded {
estimated_tokens: 197_500,
threshold_tokens: 197_000,
},
]
}
#[test]
fn default_logger_handles_every_variant() {
let logger = default_logger();
for kind in all_variants() {
logger(Event::new("agent", kind));
}
}
}