use agent_client_protocol::schema as acp;
use crate::{PlanEntry, PlanEntryPriority, PlanEntryStatus, RunEvent, ToolKind, ToolLocation};
pub(crate) fn session_update_to_events(run_id: &str, update: acp::SessionUpdate) -> Vec<RunEvent> {
let rid = || run_id.to_owned();
match update {
acp::SessionUpdate::AgentMessageChunk(chunk) => match text_of(&chunk.content) {
Some(delta) => vec![RunEvent::Text { run_id: rid(), delta }],
None => vec![],
},
acp::SessionUpdate::AgentThoughtChunk(chunk) => match text_of(&chunk.content) {
Some(delta) => vec![RunEvent::Thinking { run_id: rid(), delta }],
None => vec![],
},
acp::SessionUpdate::ToolCall(call) => vec![RunEvent::ToolStart {
run_id: rid(),
tool_call_id: call.tool_call_id.0.to_string(),
title: call.title,
tool_kind: map_kind(call.kind),
locations: map_locations(&call.locations),
raw_input: call.raw_input.as_ref().map(ToString::to_string),
}],
acp::SessionUpdate::ToolCallUpdate(update) => match update.fields.status {
Some(status @ (acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed)) => {
vec![RunEvent::ToolEnd {
run_id: rid(),
tool_call_id: update.tool_call_id.0.to_string(),
ok: matches!(status, acp::ToolCallStatus::Completed),
content: tool_output(&update.fields),
raw_output: update.fields.raw_output.as_ref().map(ToString::to_string),
locations: update.fields.locations.as_deref().map(map_locations).unwrap_or_default(),
}]
}
_ => vec![],
},
acp::SessionUpdate::Plan(plan) => {
let entries = plan
.entries
.into_iter()
.map(|e| PlanEntry {
content: e.content,
status: map_plan_status(e.status),
priority: Some(map_plan_priority(e.priority)),
})
.collect();
vec![RunEvent::Plan { run_id: rid(), entries }]
}
_ => vec![],
}
}
fn text_of(block: &acp::ContentBlock) -> Option<String> {
match block {
acp::ContentBlock::Text(t) => Some(t.text.clone()),
_ => None,
}
}
fn tool_output(fields: &acp::ToolCallUpdateFields) -> Option<String> {
if let Some(blocks) = &fields.content {
let text = blocks.iter().filter_map(tool_content_text).collect::<Vec<_>>().join("\n");
if !text.trim().is_empty() {
return Some(text);
}
}
fields.raw_output.as_ref().map(ToString::to_string)
}
fn tool_content_text(content: &acp::ToolCallContent) -> Option<String> {
match content {
acp::ToolCallContent::Content(c) => text_of(&c.content),
acp::ToolCallContent::Diff(_) => Some("[diff]".to_owned()),
_ => None,
}
}
fn map_locations(locs: &[acp::ToolCallLocation]) -> Vec<ToolLocation> {
locs.iter()
.map(|l| ToolLocation { path: l.path.to_string_lossy().into_owned(), line: l.line })
.collect()
}
fn map_kind(kind: acp::ToolKind) -> ToolKind {
match kind {
acp::ToolKind::Read => ToolKind::Read,
acp::ToolKind::Edit => ToolKind::Edit,
acp::ToolKind::Delete => ToolKind::Delete,
acp::ToolKind::Move => ToolKind::Move,
acp::ToolKind::Search => ToolKind::Search,
acp::ToolKind::Execute => ToolKind::Execute,
acp::ToolKind::Fetch => ToolKind::Fetch,
_ => ToolKind::Other,
}
}
fn map_plan_status(status: acp::PlanEntryStatus) -> PlanEntryStatus {
match status {
acp::PlanEntryStatus::Pending => PlanEntryStatus::Pending,
acp::PlanEntryStatus::InProgress => PlanEntryStatus::InProgress,
acp::PlanEntryStatus::Completed => PlanEntryStatus::Completed,
_ => PlanEntryStatus::Pending,
}
}
fn map_plan_priority(priority: acp::PlanEntryPriority) -> PlanEntryPriority {
match priority {
acp::PlanEntryPriority::High => PlanEntryPriority::High,
acp::PlanEntryPriority::Medium => PlanEntryPriority::Medium,
acp::PlanEntryPriority::Low => PlanEntryPriority::Low,
_ => PlanEntryPriority::Medium,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn chunk(text: &str) -> acp::ContentChunk {
acp::ContentChunk::new(acp::ContentBlock::from(text))
}
#[test]
fn message_vs_thought_chunks_split_correctly() {
let msg = session_update_to_events("r1", acp::SessionUpdate::AgentMessageChunk(chunk("hello")));
assert!(matches!(msg.as_slice(), [RunEvent::Text { delta, .. }] if delta == "hello"));
let thought = session_update_to_events("r1", acp::SessionUpdate::AgentThoughtChunk(chunk("hmm")));
assert!(matches!(thought.as_slice(), [RunEvent::Thinking { delta, .. }] if delta == "hmm"));
let user = session_update_to_events("r1", acp::SessionUpdate::UserMessageChunk(chunk("u")));
assert!(user.is_empty());
}
}