wip: pi working

This commit is contained in:
Franklin 2026-02-06 16:54:53 -05:00
parent a6064e7027
commit bef2e84d0c
9 changed files with 1747 additions and 39 deletions

View file

@ -21,6 +21,46 @@ pub struct PiEventConverter {
}
impl PiEventConverter {
pub fn event_value_to_universal(
&mut self,
raw: &Value,
) -> Result<Vec<EventConversion>, String> {
let event_type = raw
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| "missing event type".to_string())?;
let native_session_id = extract_session_id(raw);
let conversions = match event_type {
"message_start" => self.message_start(raw),
"message_update" => self.message_update(raw),
"message_end" => self.message_end(raw),
"tool_execution_start" => self.tool_execution_start(raw),
"tool_execution_update" => self.tool_execution_update(raw),
"tool_execution_end" => self.tool_execution_end(raw),
"agent_start"
| "agent_end"
| "turn_start"
| "turn_end"
| "auto_compaction"
| "auto_compaction_start"
| "auto_compaction_end"
| "auto_retry"
| "auto_retry_start"
| "auto_retry_end"
| "hook_error" => Ok(vec![status_event(event_type, raw)]),
"extension_ui_request" | "extension_ui_response" | "extension_error" => {
Ok(vec![status_event(event_type, raw)])
}
other => Err(format!("unsupported Pi event type: {other}")),
}?;
Ok(conversions
.into_iter()
.map(|conversion| attach_metadata(conversion, &native_session_id, raw))
.collect())
}
fn next_synthetic_message_id(&mut self) -> String {
self.message_counter += 1;
format!("pi_msg_{}", self.message_counter)
@ -72,40 +112,7 @@ impl PiEventConverter {
event: &schema::RpcEvent,
) -> Result<Vec<EventConversion>, String> {
let raw = serde_json::to_value(event).map_err(|err| err.to_string())?;
let event_type = raw
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| "missing event type".to_string())?;
let native_session_id = extract_session_id(&raw);
let conversions = match event_type {
"message_start" => self.message_start(&raw),
"message_update" => self.message_update(&raw),
"message_end" => self.message_end(&raw),
"tool_execution_start" => self.tool_execution_start(&raw),
"tool_execution_update" => self.tool_execution_update(&raw),
"tool_execution_end" => self.tool_execution_end(&raw),
"agent_start"
| "agent_end"
| "turn_start"
| "turn_end"
| "auto_compaction"
| "auto_compaction_start"
| "auto_compaction_end"
| "auto_retry"
| "auto_retry_start"
| "auto_retry_end"
| "hook_error" => Ok(vec![status_event(event_type, &raw)]),
"extension_ui_request" | "extension_ui_response" | "extension_error" => {
Ok(vec![status_event(event_type, &raw)])
}
other => Err(format!("unsupported Pi event type: {other}")),
}?;
Ok(conversions
.into_iter()
.map(|conversion| attach_metadata(conversion, &native_session_id, &raw))
.collect())
self.event_value_to_universal(&raw)
}
fn message_start(&mut self, raw: &Value) -> Result<Vec<EventConversion>, String> {
@ -265,6 +272,8 @@ impl PiEventConverter {
message: Option<&Value>,
) -> EventConversion {
let mut content = message.and_then(parse_message_content).unwrap_or_default();
let failed = message_is_failed(message);
let message_error_text = extract_message_error_text(message);
if let Some(id) = message_id.clone() {
if content.is_empty() {
@ -292,6 +301,12 @@ impl PiEventConverter {
self.message_started.remove(&id);
}
if failed && content.is_empty() {
if let Some(text) = message_error_text {
content.push(ContentPart::Text { text });
}
}
let item = UniversalItem {
item_id: String::new(),
native_item_id: message_id,
@ -299,7 +314,11 @@ impl PiEventConverter {
kind: ItemKind::Message,
role: Some(ItemRole::Assistant),
content,
status: ItemStatus::Completed,
status: if failed {
ItemStatus::Failed
} else {
ItemStatus::Completed
},
};
EventConversion::new(
UniversalEventType::ItemCompleted,
@ -434,6 +453,10 @@ pub fn event_to_universal(event: &schema::RpcEvent) -> Result<Vec<EventConversio
PiEventConverter::default().event_to_universal(event)
}
pub fn event_value_to_universal(raw: &Value) -> Result<Vec<EventConversion>, String> {
PiEventConverter::default().event_value_to_universal(raw)
}
fn attach_metadata(
conversion: EventConversion,
native_session_id: &Option<String>,
@ -584,6 +607,53 @@ fn parse_message_content(message: &Value) -> Option<Vec<ContentPart>> {
Some(parts)
}
fn message_is_failed(message: Option<&Value>) -> bool {
message
.and_then(|value| {
value
.get("stopReason")
.or_else(|| value.get("stop_reason"))
.and_then(Value::as_str)
})
.is_some_and(|reason| reason == "error" || reason == "aborted")
}
fn extract_message_error_text(message: Option<&Value>) -> Option<String> {
let value = message?;
if let Some(text) = value
.get("errorMessage")
.or_else(|| value.get("error_message"))
.and_then(Value::as_str)
{
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
let error = value.get("error")?;
if let Some(text) = error.as_str() {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
if let Some(text) = error
.get("errorMessage")
.or_else(|| error.get("error_message"))
.or_else(|| error.get("message"))
.and_then(Value::as_str)
{
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
None
}
fn content_part_from_value(value: &Value) -> Option<ContentPart> {
if let Some(text) = value.as_str() {
return Some(ContentPart::Text {

View file

@ -262,3 +262,46 @@ fn pi_message_done_completes_without_message_end() {
panic!("expected item event");
}
}
#[test]
fn pi_message_end_error_surfaces_failed_status_and_error_text() {
let mut converter = PiEventConverter::default();
let start_event = parse_event(json!({
"type": "message_start",
"sessionId": "session-1",
"messageId": "msg-err",
"message": {
"role": "assistant",
"content": []
}
}));
let _ = converter
.event_to_universal(&start_event)
.expect("start conversions");
let end_raw = json!({
"type": "message_end",
"sessionId": "session-1",
"messageId": "msg-err",
"message": {
"role": "assistant",
"content": [],
"stopReason": "error",
"errorMessage": "Connection error."
}
});
let end_events = converter
.event_value_to_universal(&end_raw)
.expect("end conversions");
assert_eq!(end_events[0].event_type, UniversalEventType::ItemCompleted);
if let UniversalEventData::Item(item) = &end_events[0].data {
assert_eq!(item.item.status, ItemStatus::Failed);
assert!(
matches!(item.item.content.first(), Some(ContentPart::Text { text }) if text == "Connection error.")
);
} else {
panic!("expected item event");
}
}