200 lines
6.1 KiB
Rust
200 lines
6.1 KiB
Rust
use serde_json::Value;
|
|
|
|
pub(crate) fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
|
let z = days + 719_468;
|
|
let era = if z >= 0 {
|
|
z / 146_097
|
|
} else {
|
|
(z - 146_096) / 146_097
|
|
};
|
|
let doe = (z - era * 146_097) as u64; // [0, 146_096]
|
|
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
|
|
let y = yoe as i64 + era * 400;
|
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
|
|
let mp = (5 * doy + 2) / 153; // [0, 11]
|
|
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
|
let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
|
|
let y = y + i64::from(m <= 2);
|
|
(y as i32, m as u32, d as u32)
|
|
}
|
|
|
|
pub(crate) fn indent_block(value: &str, spaces: usize) -> String {
|
|
let indent = " ".repeat(spaces);
|
|
value
|
|
.lines()
|
|
.map(|line| format!("{indent}{line}"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
pub(crate) fn truncate_for_prompt(value: &str, limit: usize) -> String {
|
|
if value.chars().count() <= limit {
|
|
value.trim().to_string()
|
|
} else {
|
|
let truncated = value.chars().take(limit).collect::<String>();
|
|
format!("{}\n…[truncated]", truncated.trim_end())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn extract_tool_path(parsed: &Value) -> String {
|
|
parsed
|
|
.get("file_path")
|
|
.or_else(|| parsed.get("filePath"))
|
|
.or_else(|| parsed.get("path"))
|
|
.and_then(|value| value.as_str())
|
|
.unwrap_or("?")
|
|
.to_string()
|
|
}
|
|
|
|
pub(crate) fn first_visible_line(text: &str) -> &str {
|
|
text.lines().find(|l| !l.trim().is_empty()).unwrap_or("")
|
|
}
|
|
|
|
pub(crate) fn final_assistant_text(summary: &runtime::TurnSummary) -> String {
|
|
summary
|
|
.assistant_messages
|
|
.last()
|
|
.map(|message| {
|
|
message
|
|
.blocks
|
|
.iter()
|
|
.filter_map(|block| match block {
|
|
runtime::ContentBlock::Text { text } => Some(text.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("")
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn collect_tool_uses(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
|
|
summary
|
|
.assistant_messages
|
|
.iter()
|
|
.flat_map(|message| message.blocks.iter())
|
|
.filter_map(|block| match block {
|
|
runtime::ContentBlock::ToolUse { id, name, input } => Some(serde_json::json!({
|
|
"id": id,
|
|
"name": name,
|
|
"input": input,
|
|
})),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn collect_tool_results(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
|
|
summary
|
|
.tool_results
|
|
.iter()
|
|
.flat_map(|message| message.blocks.iter())
|
|
.filter_map(|block| match block {
|
|
runtime::ContentBlock::ToolResult {
|
|
tool_use_id,
|
|
tool_name,
|
|
output,
|
|
is_error,
|
|
} => Some(serde_json::json!({
|
|
"tool_use_id": tool_use_id,
|
|
"tool_name": tool_name,
|
|
"output": output,
|
|
"is_error": is_error,
|
|
})),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn collect_prompt_cache_events(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
|
|
summary
|
|
.prompt_cache_events
|
|
.iter()
|
|
.map(|event| {
|
|
serde_json::json!({
|
|
"unexpected": event.unexpected,
|
|
"reason": event.reason,
|
|
"previous_cache_read_input_tokens": event.previous_cache_read_input_tokens,
|
|
"current_cache_read_input_tokens": event.current_cache_read_input_tokens,
|
|
"token_drop": event.token_drop,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn sanitize_generated_message(value: &str) -> String {
|
|
value.trim().trim_matches('`').trim().replace("\r\n", "\n")
|
|
}
|
|
|
|
pub(crate) fn parse_titled_body(value: &str) -> Option<(String, String)> {
|
|
let normalized = sanitize_generated_message(value);
|
|
let title = normalized
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("TITLE:").map(str::trim))?;
|
|
let body_start = normalized.find("BODY:")?;
|
|
let body = normalized[body_start + "BODY:".len()..].trim();
|
|
Some((title.to_string(), body.to_string()))
|
|
}
|
|
|
|
pub(crate) fn truncate_output_for_display(content: &str, max_lines: usize, max_chars: usize) -> String {
|
|
let original = content.trim_end_matches('\n');
|
|
if original.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let mut preview_lines = Vec::new();
|
|
let mut used_chars = 0usize;
|
|
let mut truncated = false;
|
|
|
|
for (index, line) in original.lines().enumerate() {
|
|
if index >= max_lines {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
let newline_cost = usize::from(!preview_lines.is_empty());
|
|
let available = max_chars.saturating_sub(used_chars + newline_cost);
|
|
if available == 0 {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
let line_chars = line.chars().count();
|
|
if line_chars > available {
|
|
preview_lines.push(line.chars().take(available).collect::<String>());
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
preview_lines.push(line.to_string());
|
|
used_chars += newline_cost + line_chars;
|
|
}
|
|
|
|
let mut preview = preview_lines.join("\n");
|
|
if truncated {
|
|
if !preview.is_empty() {
|
|
preview.push('\n');
|
|
}
|
|
preview.push_str(super::DISPLAY_TRUNCATION_NOTICE);
|
|
}
|
|
preview
|
|
}
|
|
|
|
pub(crate) fn summarize_tool_payload(payload: &str) -> String {
|
|
let compact = match serde_json::from_str::<Value>(payload) {
|
|
Ok(value) => value.to_string(),
|
|
Err(_) => payload.trim().to_string(),
|
|
};
|
|
truncate_for_summary(&compact, 96)
|
|
}
|
|
|
|
pub(crate) fn truncate_for_summary(value: &str, limit: usize) -> String {
|
|
let mut chars = value.chars();
|
|
let truncated = chars.by_ref().take(limit).collect::<String>();
|
|
if chars.next().is_some() {
|
|
format!("{truncated}…")
|
|
} else {
|
|
truncated
|
|
}
|
|
}
|