use a2a_lf::{AgentCard, Message, Part, PartContent, Task, TaskState};
const MAX_CARD_TEXT_BYTES: usize = 2 * 1024;
const MAX_SKILL_TEXT_BYTES: usize = 240;
const MAX_SKILLS: usize = 16;
pub fn remote_output(agent: &str, task: &Task, max_output_bytes: usize) -> String {
let body = task_text(task);
if body.is_empty() {
return String::new();
}
quoted(agent, &body, max_output_bytes)
}
pub fn remote_reply(agent: &str, message: &Message, max_output_bytes: usize) -> String {
quoted(agent, &parts_text(&message.parts), max_output_bytes)
}
pub fn card_summary(card: &AgentCard) -> String {
let mut about = String::new();
let own = card.description.trim();
if !own.is_empty() {
about.push_str(&format!("It describes itself as: {own}\n"));
}
let skills: Vec<String> = card
.skills
.iter()
.take(MAX_SKILLS)
.map(|skill| {
let (text, _) = clip(skill.description.trim(), MAX_SKILL_TEXT_BYTES);
format!("- {}: {text}", skill.name.trim())
})
.collect();
if !skills.is_empty() {
about.push_str("Its skills:\n");
about.push_str(&skills.join("\n"));
about.push('\n');
let more = card.skills.len().saturating_sub(MAX_SKILLS);
if more > 0 {
about.push_str(&format!("- and {more} more\n"));
}
}
let (shown, omitted) = clip(&about, MAX_CARD_TEXT_BYTES);
let mut summary = shown.to_string();
if omitted > 0 {
summary.push_str("…\n");
}
summary
}
pub(crate) fn remote_line(text: &str, max_bytes: usize) -> String {
let line = text
.split(|c: char| c.is_whitespace() || c.is_control())
.filter(|word| !word.is_empty())
.collect::<Vec<_>>()
.join(" ");
match clip(&line, max_bytes) {
(shown, 0) => shown.to_string(),
(shown, _) => format!("{shown}…"),
}
}
fn task_text(task: &Task) -> String {
let artifacts = task.artifacts.as_deref().unwrap_or_default();
let mut sections = Vec::new();
let status_message = task
.status
.message
.as_ref()
.filter(|_| task.status.state != TaskState::Completed || artifacts.is_empty())
.map(|message| parts_text(&message.parts))
.filter(|text| !text.is_empty());
sections.extend(status_message);
let labelled = artifacts.len() > 1;
for artifact in artifacts {
let body = parts_text(&artifact.parts);
let label = artifact
.name
.as_deref()
.filter(|name| !name.trim().is_empty());
match label {
Some(name) => sections.push(format!("[artifact: {name}]\n{body}")),
None if labelled => {
sections.push(format!("[artifact: {}]\n{body}", artifact.artifact_id))
}
None => sections.push(body),
}
}
sections.join("\n\n")
}
fn parts_text(parts: &[Part]) -> String {
parts.iter().map(part_text).collect::<Vec<_>>().join("\n")
}
fn part_text(part: &Part) -> String {
let mut about = String::new();
if let Some(filename) = &part.filename {
about.push_str(&format!(" {filename}"));
}
if let Some(media_type) = &part.media_type {
about.push_str(&format!(" ({media_type})"));
}
match &part.content {
PartContent::Text(text) => text.clone(),
PartContent::Raw(bytes) => {
let size = bytes.len();
let text = part
.media_type
.as_deref()
.filter(|media_type| is_textual(media_type))
.and_then(|_| std::str::from_utf8(bytes).ok());
match text {
Some(text) => format!("[file{about}: {size} bytes]\n{text}"),
None => format!("[file{about}: {size} bytes inline, not shown]"),
}
}
PartContent::Url(url) => format!("[file{about} at {url}]"),
PartContent::Data(value) => {
let json = serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string());
format!("[data{about}]\n{json}")
}
}
}
fn is_textual(media_type: &str) -> bool {
let essence = media_type
.split(';')
.next()
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let Some((kind, subtype)) = essence.split_once('/') else {
return false;
};
if kind == "text" {
return true;
}
let structured = ["json", "xml", "yaml", "x-yaml"];
kind == "application"
&& (structured.contains(&subtype)
|| ["+json", "+xml", "+yaml"]
.iter()
.any(|suffix| subtype.ends_with(suffix)))
}
fn quoted(agent: &str, body: &str, max_output_bytes: usize) -> String {
let (shown, omitted) = clip(body, max_output_bytes);
let mut text = format!("--- begin output from remote agent `{agent}` ---\n");
text.push_str(shown);
if omitted > 0 {
text.push_str(&format!("\n[truncated: {omitted} more bytes not shown]"));
}
text.push_str(&format!("\n--- end output from remote agent `{agent}` ---"));
text
}
fn clip(text: &str, max_bytes: usize) -> (&str, usize) {
if text.len() <= max_bytes {
return (text, 0);
}
let mut end = max_bytes;
while !text.is_char_boundary(end) {
end -= 1;
}
(&text[..end], text.len() - end)
}
#[cfg(test)]
mod tests;