use af_agent_session::{
ContentBlock, Event, InteractionResolution, SessionEvent, ToolAuthorizationStatus,
};
use af_llm::{ChatMessage, Role};
use serde_json::Value;
use crate::PlannedCall;
pub(super) fn content_text(content: &[ContentBlock]) -> String {
content
.iter()
.map(|block| match block {
ContentBlock::Text { text } => text.clone(),
ContentBlock::Resource {
resource_id,
media_type,
} => format!("[resource id={resource_id} media_type={media_type}]"),
ContentBlock::Data { slot, value } => format!("[data slot={slot}] {value}"),
ContentBlock::Citation {
resource_id,
label,
uri,
excerpt,
} => format!(
"[citation id={resource_id} label={label} uri={uri}] {}",
excerpt.as_deref().unwrap_or_default()
),
})
.collect::<Vec<_>>()
.join("\n")
}
pub(super) fn tool_message(call: &PlannedCall, value: Value) -> ChatMessage {
ChatMessage {
role: Role::Tool,
content: Some(value.to_string()),
tool_calls: None,
tool_call_id: Some(call.transcript_id.clone()),
name: Some(af_agent::model_tool_name(&call.name)),
}
}
pub(super) fn interaction_resolution_for_call(
events: &[SessionEvent],
run_id: &str,
call_id: &str,
source_event_seq: u64,
) -> Option<InteractionResolution> {
events.iter().rev().find_map(|event| match &event.event {
Event::InteractionResolved {
run_id: event_run_id,
interaction_id,
resolution,
..
} if event_run_id == run_id => {
let request = events.iter().find_map(|candidate| match &candidate.event {
Event::InteractionRequested {
run_id: request_run_id,
interaction_id: request_id,
payload,
..
} if request_run_id == run_id && request_id == interaction_id => Some(payload),
_ => None,
})?;
(request.get("call_id").and_then(Value::as_str) == Some(call_id)
&& request.get("source_event_seq").and_then(Value::as_u64)
== Some(source_event_seq))
.then_some(*resolution)
}
_ => None,
})
}
pub(super) fn tool_authorization_for_call(
events: &[SessionEvent],
run_id: &str,
call_id: &str,
) -> Option<ToolAuthorizationStatus> {
events.iter().rev().find_map(|event| match &event.event {
Event::ToolAuthorization {
run_id: event_run_id,
call_id: event_call_id,
status,
..
} if event_run_id == run_id && event_call_id == call_id => Some(*status),
_ => None,
})
}
pub(super) fn transcript_from_events(events: &[SessionEvent]) -> Vec<ChatMessage> {
let mut messages = Vec::new();
for envelope in events {
match &envelope.event {
Event::SummaryReplaced { summary, .. } => {
messages.clear();
messages.push(ChatMessage::system(format!(
"Conversation summary:\n{summary}"
)));
}
Event::UserMessage { content, .. } => {
messages.push(ChatMessage::user(content_text(content)))
}
Event::AssistantMessage { content, .. } => {
messages.push(ChatMessage::assistant(content_text(content)))
}
Event::AssistantToolCalls { content, calls, .. } => messages.push(ChatMessage {
role: Role::Assistant,
content: content.clone(),
tool_calls: Some(
calls
.iter()
.map(|call| af_llm::ToolCall {
id: call.call_id.clone(),
kind: "function".into(),
function: af_llm::FunctionCall {
name: af_agent::model_tool_name(&call.tool),
arguments: call.arguments.to_string(),
},
})
.collect(),
),
tool_call_id: None,
name: None,
}),
Event::ToolResult {
call_id, result, ..
} => messages.push(ChatMessage {
role: Role::Tool,
content: Some(result.to_string()),
tool_calls: None,
tool_call_id: Some(call_id.clone()),
name: None,
}),
Event::ToolResultsPruned { call_ids, .. } => {
for message in messages.iter_mut().filter(|message| {
message.role == Role::Tool
&& message
.tool_call_id
.as_ref()
.is_some_and(|id| call_ids.contains(id))
}) {
let content = message.content.as_deref().unwrap_or_default();
message.content = Some(format!(
"{}… [tool result pruned]",
content.chars().take(512).collect::<String>()
));
}
}
Event::InteractionResolved {
resolution: InteractionResolution::Answered,
payload,
..
} => messages.push(ChatMessage::user(format!(
"User answered the pending question: {payload}"
))),
_ => {}
}
}
messages
}