use crate::providers::{MessageRole, ProviderConversationItem};
use serde_json::{Value, json};
use std::collections::HashMap;
pub(crate) fn excerpt(text: &str, max_bytes: usize) -> String {
if text.len() <= max_bytes {
return text.to_string();
}
const MARKER: &str = "\n[content omitted]\n";
if max_bytes < MARKER.len() {
return String::new();
}
let available = max_bytes - MARKER.len();
let mut head = available / 2;
while !text.is_char_boundary(head) {
head -= 1;
}
let mut tail = text.len() - (available - available / 2);
while !text.is_char_boundary(tail) {
tail += 1;
}
format!("{}{MARKER}{}", &text[..head], &text[tail..])
}
pub(crate) fn conversation_evidence<'a>(
items: impl IntoIterator<Item = &'a ProviderConversationItem>,
max_bytes: usize,
) -> Value {
let items = items.into_iter().collect::<Vec<_>>();
let calls = items
.iter()
.filter_map(|item| match item {
ProviderConversationItem::ResponseItem(value)
if value["type"].as_str() == Some("function_call") =>
{
Some((value["call_id"].as_str()?, value))
}
_ => None,
})
.collect::<HashMap<_, _>>();
let text_limit = (max_bytes / 4).clamp(128, 4096);
let mut records = Vec::new();
let mut omitted = 0;
let mut truncated = 0;
for item in items.iter().rev() {
let (record, was_truncated) = match item {
ProviderConversationItem::Message(message)
if matches!(message.role, MessageRole::User | MessageRole::Assistant) =>
{
(
json!({
"kind": if message.role == MessageRole::User { "user_request" } else { "assistant_statement" },
"text": excerpt(&message.content, text_limit),
"truncated": message.content.len() > text_limit,
}),
message.content.len() > text_limit,
)
}
ProviderConversationItem::ToolResult(result) => {
let call = calls.get(result.call_id.as_str());
let arguments = call.map(|value| match &value["arguments"] {
Value::String(text) => text.clone(),
value => value.to_string(),
});
let cut = result.output.len() > text_limit
|| arguments
.as_ref()
.is_some_and(|text| text.len() > text_limit);
(
json!({
"kind": "tool_receipt",
"call_id": result.call_id,
"tool": result.tool_name,
"arguments": arguments.as_ref().map(|text| excerpt(text, text_limit)),
"success": result.success,
"output": excerpt(&result.output, text_limit),
"truncated": cut,
}),
cut,
)
}
_ => continue,
};
records.push(record);
let candidate = json!({"records": records, "omitted_records": omitted, "truncated_records": truncated + usize::from(was_truncated)});
if candidate.to_string().len().saturating_add(64) > max_bytes {
records.pop();
omitted += 1;
} else {
truncated += usize::from(was_truncated);
}
}
records.reverse();
json!({"records": records, "omitted_records": omitted, "truncated_records": truncated})
}