use std::sync::Arc;
use arrow::array::{ArrayRef, StringBuilder, UInt64Builder};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::error::ArrowError;
use arrow::record_batch::RecordBatch;
use buffa::EnumValue;
use polyc_eventlog::Event;
use polyc_proto::events_decode::try_decode_event_payload;
use polyc_proto::kinds;
use polyc_proto::proto::polychrome::events::v1::TurnFailedEvent;
use polyc_proto::proto::polychrome::harness::v1::TurnFailureKind;
#[derive(Debug, Clone)]
pub(crate) struct TurnFailedRow {
pub partition: String,
pub position: u64,
pub turn_id: Option<String>,
pub failure_kind: String,
pub message: String,
}
#[must_use]
pub(crate) fn schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("partition", DataType::Utf8, false),
Field::new("position", DataType::UInt64, false),
Field::new("turn_id", DataType::Utf8, true),
Field::new("failure_kind", DataType::Utf8, false),
Field::new("message", DataType::Utf8, false),
]))
}
#[must_use]
fn failure_kind_label(kind: EnumValue<TurnFailureKind>) -> &'static str {
match kind.as_known() {
Some(TurnFailureKind::RateLimit) => "rate_limit",
Some(TurnFailureKind::Timeout) => "timeout",
Some(TurnFailureKind::Unavailable) => "unavailable",
Some(TurnFailureKind::Auth) => "auth",
Some(TurnFailureKind::BadRequest) => "bad_request",
Some(TurnFailureKind::Other) => "other",
Some(TurnFailureKind::Unspecified) | None => "unspecified",
}
}
pub(crate) fn decode_turn_failed_batch(rows: &[TurnFailedRow]) -> Result<RecordBatch, ArrowError> {
let mut partition_b = StringBuilder::with_capacity(rows.len(), rows.len() * 8);
let mut position_b = UInt64Builder::with_capacity(rows.len());
let mut turn_id_b = StringBuilder::with_capacity(rows.len(), rows.len() * 36);
let mut failure_kind_b = StringBuilder::with_capacity(rows.len(), rows.len() * 12);
let mut message_b = StringBuilder::with_capacity(rows.len(), rows.len() * 32);
for row in rows {
partition_b.append_value(&row.partition);
position_b.append_value(row.position);
match &row.turn_id {
Some(id) => turn_id_b.append_value(id),
None => turn_id_b.append_null(),
}
failure_kind_b.append_value(&row.failure_kind);
message_b.append_value(&row.message);
}
let columns: Vec<ArrayRef> = vec![
Arc::new(partition_b.finish()),
Arc::new(position_b.finish()),
Arc::new(turn_id_b.finish()),
Arc::new(failure_kind_b.finish()),
Arc::new(message_b.finish()),
];
RecordBatch::try_new(schema(), columns)
}
#[must_use]
pub(crate) fn decode_turn_failed_events(
partition: &str,
events: &[(u64, Event)],
) -> Vec<TurnFailedRow> {
crate::decode::decode_typed_kind_events(
partition,
events,
&[kinds::TURN_FAILED],
"turn_failed",
try_decode_event_payload::<TurnFailedEvent>,
|partition, position, turn_id, event: TurnFailedEvent| TurnFailedRow {
partition,
position,
turn_id,
failure_kind: failure_kind_label(event.kind).to_string(),
message: event.message,
},
)
}
#[cfg(test)]
mod tests {
use arrow::array::Array as _;
use buffa::Message as _;
use uuid::Uuid;
use super::*;
fn sample_turn_failed(kind: TurnFailureKind, message: &str) -> TurnFailedEvent {
TurnFailedEvent {
kind: kind.into(),
message: message.to_string(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
#[test]
fn schema_shape() {
let schema = schema();
let names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
assert_eq!(
names,
vec![
"partition",
"position",
"turn_id",
"failure_kind",
"message"
]
);
let expect = [
("partition", DataType::Utf8, false),
("position", DataType::UInt64, false),
("turn_id", DataType::Utf8, true),
("failure_kind", DataType::Utf8, false),
("message", DataType::Utf8, false),
];
for (field, (name, ty, nullable)) in schema.fields().iter().zip(expect) {
assert_eq!(field.name(), name);
assert_eq!(field.data_type(), &ty);
assert_eq!(field.is_nullable(), nullable);
}
}
#[test]
fn failure_kind_label_covers_every_known_variant() {
assert_eq!(
failure_kind_label(TurnFailureKind::Unspecified.into()),
"unspecified"
);
assert_eq!(
failure_kind_label(TurnFailureKind::RateLimit.into()),
"rate_limit"
);
assert_eq!(
failure_kind_label(TurnFailureKind::Timeout.into()),
"timeout"
);
assert_eq!(
failure_kind_label(TurnFailureKind::Unavailable.into()),
"unavailable"
);
assert_eq!(failure_kind_label(TurnFailureKind::Auth.into()), "auth");
assert_eq!(
failure_kind_label(TurnFailureKind::BadRequest.into()),
"bad_request"
);
assert_eq!(failure_kind_label(TurnFailureKind::Other.into()), "other");
}
#[test]
fn failure_kind_label_maps_an_unknown_wire_value_to_unspecified() {
let unknown: EnumValue<TurnFailureKind> = EnumValue::from(999);
assert_eq!(failure_kind_label(unknown), "unspecified");
}
#[test]
fn decode_turn_failed_batch_round_trips() {
let rows = vec![
TurnFailedRow {
partition: "conv-a".to_string(),
position: 5,
turn_id: None,
failure_kind: "rate_limit".to_string(),
message: "provider throttled the request".to_string(),
},
TurnFailedRow {
partition: "conv-a".to_string(),
position: 9,
turn_id: Some("turn-xyz".to_string()),
failure_kind: "timeout".to_string(),
message: "deadline exceeded".to_string(),
},
];
let batch = decode_turn_failed_batch(&rows).expect("batch build");
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.schema(), schema());
let partition = batch
.column(0)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(partition.value(0), "conv-a");
assert_eq!(partition.value(1), "conv-a");
let position = batch
.column(1)
.as_any()
.downcast_ref::<arrow::array::UInt64Array>()
.unwrap();
assert_eq!(position.values(), &[5, 9]);
let turn_id = batch
.column(2)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert!(turn_id.is_null(0));
assert_eq!(turn_id.value(1), "turn-xyz");
let failure_kind = batch
.column(3)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(failure_kind.value(0), "rate_limit");
assert_eq!(failure_kind.value(1), "timeout");
let message = batch
.column(4)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(message.value(0), "provider throttled the request");
assert_eq!(message.value(1), "deadline exceeded");
}
#[test]
fn decode_turn_failed_events_filters_and_decodes_real_buffa_bytes() {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_2345);
let turn_failed =
sample_turn_failed(TurnFailureKind::Unavailable, "harness pod unreachable");
let bytes = turn_failed.encode_to_vec();
let events = vec![
(1, Event::new(kinds::TURN_START, Vec::new())),
(
2,
Event::new(kinds::tagged(kinds::TURN_FAILED, &turn), bytes),
),
(
3,
Event::new(kinds::USER_MSG, b"not a turn failure".to_vec()),
),
];
let decoded = decode_turn_failed_events("conv-real", &events);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].partition, "conv-real");
assert_eq!(decoded[0].position, 2);
assert_eq!(decoded[0].turn_id, Some(turn.to_string()));
assert_eq!(decoded[0].failure_kind, "unavailable");
assert_eq!(decoded[0].message, "harness pod unreachable");
}
#[test]
fn decode_turn_failed_events_bare_kind_has_no_turn_id() {
let events = vec![(7, Event::new(kinds::TURN_FAILED, Vec::new()))];
let decoded = decode_turn_failed_events("conv-bare", &events);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].partition, "conv-bare");
assert_eq!(decoded[0].position, 7);
assert_eq!(decoded[0].turn_id, None);
}
#[test]
fn empty_payload_turn_failed_event_decodes_to_defaults() {
let events = vec![(1, Event::new(kinds::TURN_FAILED, Vec::new()))];
let decoded = decode_turn_failed_events("conv-empty", &events);
assert_eq!(decoded.len(), 1, "an empty payload must decode, not skip");
assert_eq!(decoded[0].failure_kind, "unspecified");
assert_eq!(decoded[0].message, "");
}
#[test]
fn undecodable_non_empty_payload_is_skipped() {
let events = vec![
(1, Event::new(kinds::TURN_FAILED, vec![0xFF, 0xFE, 0xFD])),
(2, Event::new(kinds::TURN_FAILED, Vec::new())),
];
let decoded = decode_turn_failed_events("conv-corrupt", &events);
assert_eq!(decoded.len(), 1, "only the empty (valid) payload decodes");
assert_eq!(decoded[0].position, 2);
assert_eq!(decoded[0].failure_kind, "unspecified");
assert_eq!(decoded[0].message, "");
}
#[test]
fn turn_failed_event_round_trip_through_decode_and_batch() {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_9999);
let turn_failed = sample_turn_failed(TurnFailureKind::BadRequest, "malformed tool call");
let bytes = turn_failed.encode_to_vec();
let events = vec![(
1,
Event::new(kinds::tagged(kinds::TURN_FAILED, &turn), bytes),
)];
let decoded = decode_turn_failed_events("conv-rt", &events);
let batch = decode_turn_failed_batch(&decoded).expect("batch build");
let partition = batch
.column(0)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(partition.value(0), "conv-rt");
let position = batch
.column(1)
.as_any()
.downcast_ref::<arrow::array::UInt64Array>()
.unwrap();
assert_eq!(position.value(0), 1);
let turn_id = batch
.column(2)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(turn_id.value(0), turn.to_string());
let failure_kind = batch
.column(3)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(failure_kind.value(0), "bad_request");
let message = batch
.column(4)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(message.value(0), "malformed tool call");
}
}