132 lines
5.5 KiB
Rust
132 lines
5.5 KiB
Rust
|
|
//! Human-readable formatting of raw agent log entries.
|
||
|
|
|
||
|
|
/// Format a single log entry as a human-readable text line.
|
||
|
|
///
|
||
|
|
/// `timestamp` is an ISO 8601 string; `event` is the flattened `AgentEvent`
|
||
|
|
/// value (has `type`, `agent_name`, etc. at the top level).
|
||
|
|
///
|
||
|
|
/// Returns `None` for entries that should be skipped (raw streaming noise,
|
||
|
|
/// trivial status changes, empty output, etc.).
|
||
|
|
pub fn format_log_entry_as_text(timestamp: &str, event: &serde_json::Value) -> Option<String> {
|
||
|
|
let agent_name = event
|
||
|
|
.get("agent_name")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.unwrap_or("?");
|
||
|
|
// Extract HH:MM:SS from ISO 8601 "2026-04-10T12:48:02.123456789+00:00"
|
||
|
|
let ts_short = if timestamp.len() >= 19 {
|
||
|
|
×tamp[11..19]
|
||
|
|
} else {
|
||
|
|
timestamp
|
||
|
|
};
|
||
|
|
let pfx = format!("[{ts_short}][{agent_name}]");
|
||
|
|
|
||
|
|
match event.get("type").and_then(|v| v.as_str()) {
|
||
|
|
Some("output") => {
|
||
|
|
let text = event
|
||
|
|
.get("text")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.unwrap_or("")
|
||
|
|
.trim();
|
||
|
|
if text.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some(format!("{pfx} {text}"))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Some("error") => {
|
||
|
|
let msg = event
|
||
|
|
.get("message")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.unwrap_or("(unknown error)");
|
||
|
|
Some(format!("{pfx} ERROR: {msg}"))
|
||
|
|
}
|
||
|
|
Some("done") => Some(format!("{pfx} DONE")),
|
||
|
|
Some("status") => {
|
||
|
|
// Skip trivial running/started noise
|
||
|
|
let status = event.get("status").and_then(|v| v.as_str()).unwrap_or("?");
|
||
|
|
match status {
|
||
|
|
"running" | "started" => None,
|
||
|
|
_ => Some(format!("{pfx} STATUS: {status}")),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Some("agent_json") => {
|
||
|
|
let data = event.get("data")?;
|
||
|
|
match data.get("type").and_then(|v| v.as_str()) {
|
||
|
|
Some("assistant") => {
|
||
|
|
let mut parts: Vec<String> = Vec::new();
|
||
|
|
if let Some(arr) = data.pointer("/message/content").and_then(|v| v.as_array()) {
|
||
|
|
for item in arr {
|
||
|
|
match item.get("type").and_then(|v| v.as_str()) {
|
||
|
|
Some("text") => {
|
||
|
|
let text = item
|
||
|
|
.get("text")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.unwrap_or("")
|
||
|
|
.trim();
|
||
|
|
if !text.is_empty() {
|
||
|
|
parts.push(format!("{pfx} {text}"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Some("tool_use") => {
|
||
|
|
let name =
|
||
|
|
item.get("name").and_then(|v| v.as_str()).unwrap_or("?");
|
||
|
|
let input = item
|
||
|
|
.get("input")
|
||
|
|
.map(|v| serde_json::to_string(v).unwrap_or_default())
|
||
|
|
.unwrap_or_default();
|
||
|
|
let display = if input.len() > 200 {
|
||
|
|
format!("{}...", &input[..200])
|
||
|
|
} else {
|
||
|
|
input
|
||
|
|
};
|
||
|
|
parts.push(format!("{pfx} TOOL: {name}({display})"));
|
||
|
|
}
|
||
|
|
_ => {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if parts.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some(parts.join("\n"))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Some("user") => {
|
||
|
|
let mut parts: Vec<String> = Vec::new();
|
||
|
|
if let Some(arr) = data.pointer("/message/content").and_then(|v| v.as_array()) {
|
||
|
|
for item in arr {
|
||
|
|
if item.get("type").and_then(|v| v.as_str()) != Some("tool_result") {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let content_str = match item.get("content") {
|
||
|
|
Some(serde_json::Value::String(s)) => s.clone(),
|
||
|
|
Some(v) => v.to_string(),
|
||
|
|
None => String::new(),
|
||
|
|
};
|
||
|
|
let display = if content_str.len() > 500 {
|
||
|
|
format!(
|
||
|
|
"{}... [{} chars total]",
|
||
|
|
&content_str[..500],
|
||
|
|
content_str.len()
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
content_str
|
||
|
|
};
|
||
|
|
if !display.trim().is_empty() {
|
||
|
|
parts.push(format!("{pfx} RESULT: {display}"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if parts.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some(parts.join("\n"))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => None, // Skip stream_event, system init, etc.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|