This commit is contained in:
Franklin 2026-02-06 19:16:53 -05:00
parent e2e7f11b9a
commit bd030904bc
12 changed files with 371 additions and 262 deletions

View file

@ -12,6 +12,7 @@ use crate::{
pub struct PiEventConverter {
tool_result_buffers: HashMap<String, String>,
tool_result_started: HashSet<String>,
message_completed: HashSet<String>,
message_errors: HashSet<String>,
message_reasoning: HashMap<String, String>,
message_text: HashMap<String, String>,
@ -121,6 +122,7 @@ impl PiEventConverter {
return Ok(Vec::new());
}
let message_id = self.ensure_message_id(extract_message_id(raw));
self.message_completed.remove(&message_id);
self.message_started.insert(message_id.clone());
let content = message.and_then(parse_message_content).unwrap_or_default();
let entry = self.message_text.entry(message_id.clone()).or_default();
@ -210,15 +212,24 @@ impl PiEventConverter {
self.clear_last_message_id(Some(&message_id));
return Ok(Vec::new());
}
if self.message_completed.contains(&message_id) {
self.clear_last_message_id(Some(&message_id));
return Ok(Vec::new());
}
let message = raw
.get("message")
.or_else(|| assistant_event.get("message"));
let conversion = self.complete_message(Some(message_id.clone()), message);
self.message_completed.insert(message_id.clone());
self.clear_last_message_id(Some(&message_id));
Ok(vec![conversion])
}
"error" => {
let message_id = self.ensure_message_id(message_id);
if self.message_completed.contains(&message_id) {
self.clear_last_message_id(Some(&message_id));
return Ok(Vec::new());
}
let error_text = assistant_event
.get("error")
.or_else(|| raw.get("error"))
@ -228,6 +239,7 @@ impl PiEventConverter {
self.message_text.remove(&message_id);
self.message_errors.insert(message_id.clone());
self.message_started.remove(&message_id);
self.message_completed.insert(message_id.clone());
self.clear_last_message_id(Some(&message_id));
let item = UniversalItem {
item_id: String::new(),
@ -261,7 +273,12 @@ impl PiEventConverter {
self.clear_last_message_id(Some(&message_id));
return Ok(Vec::new());
}
if self.message_completed.contains(&message_id) {
self.clear_last_message_id(Some(&message_id));
return Ok(Vec::new());
}
let conversion = self.complete_message(Some(message_id.clone()), message);
self.message_completed.insert(message_id.clone());
self.clear_last_message_id(Some(&message_id));
Ok(vec![conversion])
}
@ -479,7 +496,7 @@ fn status_event(label: &str, raw: &Value) -> EventConversion {
kind: ItemKind::Status,
role: Some(ItemRole::System),
content: vec![ContentPart::Status {
label: format!("pi.{label}"),
label: pi_status_label(label),
detail,
}],
status: ItemStatus::Completed,
@ -490,6 +507,14 @@ fn status_event(label: &str, raw: &Value) -> EventConversion {
)
}
fn pi_status_label(label: &str) -> String {
match label {
"turn_end" => "turn.completed".to_string(),
"agent_end" => "session.idle".to_string(),
_ => format!("pi.{label}"),
}
}
fn item_delta(message_id: Option<String>, delta: String) -> EventConversion {
EventConversion::new(
UniversalEventType::ItemDelta,

View file

@ -216,6 +216,56 @@ fn pi_unknown_event_returns_error() {
assert!(converter.event_to_universal(&event).is_err());
}
#[test]
fn pi_turn_and_agent_end_emit_terminal_status_labels() {
let mut converter = PiEventConverter::default();
let turn_end = parse_event(json!({
"type": "turn_end",
"sessionId": "session-1"
}));
let turn_events = converter
.event_to_universal(&turn_end)
.expect("turn_end conversions");
assert_eq!(turn_events[0].event_type, UniversalEventType::ItemCompleted);
if let UniversalEventData::Item(item) = &turn_events[0].data {
assert_eq!(item.item.kind, ItemKind::Status);
assert!(
matches!(
item.item.content.first(),
Some(ContentPart::Status { label, .. }) if label == "turn.completed"
),
"turn_end should map to turn.completed status"
);
} else {
panic!("expected item event");
}
let agent_end = parse_event(json!({
"type": "agent_end",
"sessionId": "session-1"
}));
let agent_events = converter
.event_to_universal(&agent_end)
.expect("agent_end conversions");
assert_eq!(
agent_events[0].event_type,
UniversalEventType::ItemCompleted
);
if let UniversalEventData::Item(item) = &agent_events[0].data {
assert_eq!(item.item.kind, ItemKind::Status);
assert!(
matches!(
item.item.content.first(),
Some(ContentPart::Status { label, .. }) if label == "session.idle"
),
"agent_end should map to session.idle status"
);
} else {
panic!("expected item event");
}
}
#[test]
fn pi_message_done_completes_without_message_end() {
let mut converter = PiEventConverter::default();
@ -263,6 +313,63 @@ fn pi_message_done_completes_without_message_end() {
}
}
#[test]
fn pi_message_done_then_message_end_does_not_double_complete() {
let mut converter = PiEventConverter::default();
let start_event = parse_event(json!({
"type": "message_start",
"sessionId": "session-1",
"messageId": "msg-1",
"message": {
"role": "assistant",
"content": [{ "type": "text", "text": "Hello" }]
}
}));
let _ = converter
.event_to_universal(&start_event)
.expect("start conversions");
let update_event = parse_event(json!({
"type": "message_update",
"sessionId": "session-1",
"messageId": "msg-1",
"assistantMessageEvent": { "type": "text_delta", "delta": " world" }
}));
let _ = converter
.event_to_universal(&update_event)
.expect("update conversions");
let done_event = parse_event(json!({
"type": "message_update",
"sessionId": "session-1",
"messageId": "msg-1",
"assistantMessageEvent": { "type": "done" }
}));
let done_events = converter
.event_to_universal(&done_event)
.expect("done conversions");
assert_eq!(done_events.len(), 1);
assert_eq!(done_events[0].event_type, UniversalEventType::ItemCompleted);
let end_event = parse_event(json!({
"type": "message_end",
"sessionId": "session-1",
"messageId": "msg-1",
"message": {
"role": "assistant",
"content": [{ "type": "text", "text": "Hello world" }]
}
}));
let end_events = converter
.event_to_universal(&end_event)
.expect("end conversions");
assert!(
end_events.is_empty(),
"message_end after done should not emit a second completion"
);
}
#[test]
fn pi_message_end_error_surfaces_failed_status_and_error_text() {
let mut converter = PiEventConverter::default();