use chrono::Utc;
use serde::Serialize;
use std::path::PathBuf;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ToolErrorKind {
Hard,
Soft,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ToolErrorRecord {
pub ts: String,
pub session_id: String,
pub round: usize,
pub tool_name: String,
pub tool_call_id: String,
pub args_preview: String,
pub error_kind: ToolErrorKind,
pub error_message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_snippet: Option<String>,
}
const PREVIEW_MAX_CHARS: usize = 512;
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let truncated: String = s.chars().take(max).collect();
format!("{truncated}...")
}
}
fn error_log_path() -> PathBuf {
bamboo_infrastructure::paths::bamboo_dir().join("tool_errors.jsonl")
}
pub(crate) async fn append_tool_error(record: ToolErrorRecord) {
let path = error_log_path();
let line = match serde_json::to_string(&record) {
Ok(json) => format!("{json}\n"),
Err(e) => {
tracing::warn!("Failed to serialize tool error record: {e}");
return;
}
};
match OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await
{
Ok(mut file) => {
if let Err(e) = file.write_all(line.as_bytes()).await {
tracing::warn!(
"Failed to write tool error record to {}: {e}",
path.display()
);
}
}
Err(e) => {
tracing::warn!("Failed to open tool error log {}: {e}", path.display());
}
}
}
pub(crate) fn hard_error_record(
session_id: &str,
round: usize,
tool_name: &str,
tool_call_id: &str,
arguments: &str,
error_message: &str,
) -> ToolErrorRecord {
ToolErrorRecord {
ts: Utc::now().to_rfc3339(),
session_id: session_id.to_string(),
round,
tool_name: tool_name.to_string(),
tool_call_id: tool_call_id.to_string(),
args_preview: truncate(arguments, PREVIEW_MAX_CHARS),
error_kind: ToolErrorKind::Hard,
error_message: error_message.to_string(),
result_snippet: None,
}
}
pub(crate) fn soft_failure_record(
session_id: &str,
round: usize,
tool_name: &str,
tool_call_id: &str,
arguments: &str,
result_text: &str,
) -> ToolErrorRecord {
ToolErrorRecord {
ts: Utc::now().to_rfc3339(),
session_id: session_id.to_string(),
round,
tool_name: tool_name.to_string(),
tool_call_id: tool_call_id.to_string(),
args_preview: truncate(arguments, PREVIEW_MAX_CHARS),
error_kind: ToolErrorKind::Soft,
error_message: truncate(result_text, PREVIEW_MAX_CHARS),
result_snippet: Some(truncate(result_text, PREVIEW_MAX_CHARS)),
}
}