use std::collections::{BTreeMap, BTreeSet};
use polyc_eventlog_model::{Event, TrustTag};
use polyc_proto::proto::polychrome::agent::v1::Message;
use polyc_proto::{events_decode::try_decode_event_payload, kinds};
use crate::committed::committed_turn_ids;
use crate::excision::{excised_positions, strip_excised, verified_excisions_matching};
use crate::message_content::{MessageContent, fold_message_content};
use crate::withholding::{withheld_turn_ids_positioned, withhold_paused_turn_text_positioned};
const MESSAGE_KIND_BASES: [&str; 2] = [kinds::USER_MSG, kinds::OUTPUT_MSG];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommittedTurnFact {
pub turn_id: uuid::Uuid,
pub first_position: u64,
pub start_position: u64,
pub complete_position: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConversationMessageFact {
pub position: u64,
pub turn_id: uuid::Uuid,
pub role: String,
pub internal_only: bool,
pub text: String,
pub trust: TrustTag,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ConversationCoreFacts {
pub turns: Vec<CommittedTurnFact>,
pub messages: Vec<ConversationMessageFact>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConversationCoreError {
#[error("position {position} carries a {kind} payload that is not a message: {reason}")]
UndecodableMessage {
position: u64,
kind: &'static str,
reason: String,
},
#[error("position {position} appears more than once in one source prefix")]
RepeatedPosition {
position: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PreparedSource {
pub excised: BTreeSet<u64>,
pub withheld: BTreeSet<uuid::Uuid>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LifecycleBarrier {
Excision,
}
#[must_use]
pub fn lifecycle_barrier(events: &[(u64, Event)]) -> Option<LifecycleBarrier> {
events
.iter()
.any(|(_, event)| kinds::base(&event.kind) == kinds::TAINT_EXCISION)
.then_some(LifecycleBarrier::Excision)
}
pub fn prepare_conversation_core(events: &mut [(u64, Event)], partition: &str) -> PreparedSource {
let excisions = verified_excisions_matching(events, partition, |excision| {
format!("conv-{}", excision.conversation_id) == partition
});
let excised = excised_positions(events, &excisions);
strip_excised(events, &excised);
let withheld = withheld_turn_ids_positioned(events);
if !withheld.is_empty() {
withhold_paused_turn_text_positioned(events, &withheld);
}
PreparedSource { excised, withheld }
}
pub fn fold_conversation_core(
events: &[(u64, Event)],
) -> Result<ConversationCoreFacts, ConversationCoreError> {
let mut seen: BTreeSet<u64> = BTreeSet::new();
for (position, _) in events {
if !seen.insert(*position) {
return Err(ConversationCoreError::RepeatedPosition {
position: *position,
});
}
}
let committed = committed_turn_ids(events.iter().map(|(_, event)| event));
let mut boundaries: BTreeMap<uuid::Uuid, CommittedTurnFact> = BTreeMap::new();
let mut messages = Vec::new();
for (position, event) in events {
let (base, turn_uuid) = kinds::parse(&event.kind);
let Some(turn_id) = turn_uuid.filter(|id| committed.contains(id)) else {
continue;
};
let fact = boundaries.entry(turn_id).or_insert(CommittedTurnFact {
turn_id,
first_position: *position,
start_position: u64::MAX,
complete_position: 0,
});
fact.first_position = fact.first_position.min(*position);
if base == kinds::TURN_START {
fact.start_position = fact.start_position.min(*position);
}
if base == kinds::TURN_COMPLETE {
fact.complete_position = fact.complete_position.max(*position);
}
if !MESSAGE_KIND_BASES.contains(&base) {
continue;
}
if event.payload.is_empty() {
continue;
}
let message = try_decode_event_payload::<Message>(&event.payload).map_err(|error| {
ConversationCoreError::UndecodableMessage {
position: *position,
kind: if base == kinds::USER_MSG {
kinds::USER_MSG
} else {
kinds::OUTPUT_MSG
},
reason: error.to_string(),
}
})?;
let folded = fold_message_content(&message, *position, None, event.trust.as_str());
let MessageContent::Text(text) = folded.content else {
continue;
};
messages.push(ConversationMessageFact {
position: *position,
turn_id,
role: message.role,
internal_only: message.internal_only,
text: text.text,
trust: event.trust,
});
}
Ok(ConversationCoreFacts {
turns: boundaries.into_values().collect(),
messages,
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use buffa::Message as _;
use polyc_proto::proto::polychrome::agent::v1::{Content, Message, TextContent, content};
use super::*;
fn turn() -> uuid::Uuid {
uuid::Uuid::from_u128(0x1111_2222_3333_4444_5555_6666_7777_8888)
}
fn other_turn() -> uuid::Uuid {
uuid::Uuid::from_u128(0x9999_aaaa_bbbb_cccc_dddd_eeee_ffff_0000)
}
fn marker(base: &str, id: uuid::Uuid) -> Event {
Event::new(kinds::tagged(base, &id), Vec::new())
}
fn text_payload(text: &str) -> Vec<u8> {
Message {
role: "model".into(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::Text(Box::new(TextContent {
text: text.to_owned(),
..Default::default()
}))),
..Default::default()
}),
internal_only: false,
..Default::default()
}
.encode_to_vec()
}
fn message(base: &str, id: uuid::Uuid, text: &str, trust: TrustTag) -> Event {
Event::with_trust(kinds::tagged(base, &id), text_payload(text), trust)
}
fn committed_pair(id: uuid::Uuid, start: u64, complete: u64) -> Vec<(u64, Event)> {
vec![
(start, marker(kinds::TURN_START, id)),
(complete, marker(kinds::TURN_COMPLETE, id)),
]
}
#[test]
fn both_markers_are_required() {
let events = vec![(1, marker(kinds::TURN_START, turn()))];
let facts = fold_conversation_core(&events).unwrap();
assert!(facts.turns.is_empty());
}
#[test]
fn duplicate_markers_choose_minimum_start_and_maximum_completion() {
let mut events = committed_pair(turn(), 5, 9);
events.push((3, marker(kinds::TURN_START, turn())));
events.push((11, marker(kinds::TURN_COMPLETE, turn())));
events.sort_by_key(|(position, _)| *position);
let facts = fold_conversation_core(&events).unwrap();
let [only] = facts.turns.as_slice() else {
panic!("one committed turn: {:?}", facts.turns);
};
assert_eq!(only.start_position, 3);
assert_eq!(only.complete_position, 11);
assert_eq!(only.first_position, 3);
}
#[test]
fn first_position_precedes_both_boundary_positions() {
let mut events = vec![(
2,
message(kinds::USER_MSG, turn(), "hi", TrustTag::TrustedUser),
)];
events.extend(committed_pair(turn(), 4, 8));
let facts = fold_conversation_core(&events).unwrap();
let [only] = facts.turns.as_slice() else {
panic!("one committed turn");
};
assert_eq!(only.first_position, 2);
assert_eq!(only.start_position, 4);
assert_eq!(only.complete_position, 8);
}
#[test]
fn message_rows_carry_the_real_journal_position() {
let mut events = committed_pair(turn(), 100, 400);
events.push((
250,
message(kinds::OUTPUT_MSG, turn(), "spoke", TrustTag::Unspecified),
));
events.sort_by_key(|(position, _)| *position);
let facts = fold_conversation_core(&events).unwrap();
let [only] = facts.messages.as_slice() else {
panic!("one message row");
};
assert_eq!(only.position, 250);
assert_eq!(only.turn_id, turn());
assert_eq!(only.role, "model");
assert!(!only.internal_only);
assert_eq!(only.text, "spoke");
assert_eq!(only.trust, TrustTag::Unspecified);
}
#[test]
fn an_empty_text_block_still_emits_one_row() {
let mut events = committed_pair(turn(), 1, 3);
events.insert(
1,
(
2,
message(kinds::OUTPUT_MSG, turn(), "", TrustTag::Unspecified),
),
);
let facts = fold_conversation_core(&events).unwrap();
assert_eq!(facts.messages.len(), 1);
assert_eq!(facts.messages[0].text, "");
}
#[test]
fn an_uncommitted_turns_text_emits_no_row() {
let events = vec![
(1, marker(kinds::TURN_START, turn())),
(
2,
message(
kinds::USER_MSG,
turn(),
"never committed",
TrustTag::TrustedUser,
),
),
];
let facts = fold_conversation_core(&events).unwrap();
assert!(facts.messages.is_empty());
assert!(facts.turns.is_empty());
}
#[test]
fn a_message_with_no_turn_suffix_emits_no_row() {
let mut events = committed_pair(turn(), 1, 3);
events.insert(
1,
(
2,
Event::new(kinds::USER_MSG.to_owned(), text_payload("orphan")),
),
);
let facts = fold_conversation_core(&events).unwrap();
assert!(facts.messages.is_empty());
}
#[test]
fn one_partitions_marker_cannot_complete_another_partitions_turn() {
let first = vec![(1, marker(kinds::TURN_START, turn()))];
let second = vec![(1, marker(kinds::TURN_COMPLETE, turn()))];
assert!(fold_conversation_core(&first).unwrap().turns.is_empty());
assert!(fold_conversation_core(&second).unwrap().turns.is_empty());
}
#[test]
fn an_undecodable_relevant_payload_refuses_the_fold() {
let mut events = committed_pair(turn(), 1, 3);
events.insert(
1,
(
2,
Event::new(kinds::tagged(kinds::OUTPUT_MSG, &turn()), vec![0xff; 8]),
),
);
let error = fold_conversation_core(&events).unwrap_err();
assert!(matches!(
error,
ConversationCoreError::UndecodableMessage { position: 2, .. }
));
}
#[test]
fn an_undecodable_irrelevant_payload_is_ignored() {
let mut events = committed_pair(turn(), 1, 3);
events.insert(1, (2, Event::new("usage".to_owned(), vec![0xff; 8])));
assert!(fold_conversation_core(&events).is_ok());
}
#[test]
fn a_repeated_position_refuses_the_fold() {
let events = vec![
(1, marker(kinds::TURN_START, turn())),
(1, marker(kinds::TURN_COMPLETE, turn())),
];
assert!(matches!(
fold_conversation_core(&events).unwrap_err(),
ConversationCoreError::RepeatedPosition { position: 1 }
));
}
#[test]
fn rows_are_ordered_by_turn_id_and_position() {
let mut events = committed_pair(other_turn(), 1, 2);
events.extend(committed_pair(turn(), 3, 6));
events.push((
5,
message(kinds::OUTPUT_MSG, turn(), "b", TrustTag::Unspecified),
));
events.push((
4,
message(kinds::USER_MSG, turn(), "a", TrustTag::TrustedUser),
));
events.sort_by_key(|(position, _)| *position);
let facts = fold_conversation_core(&events).unwrap();
let ids: Vec<_> = facts.turns.iter().map(|turn| turn.turn_id).collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(ids, sorted);
let positions: Vec<_> = facts.messages.iter().map(|m| m.position).collect();
assert_eq!(positions, vec![4, 5]);
}
#[test]
fn the_same_prefix_folds_to_the_same_rows() {
let mut events = committed_pair(turn(), 1, 4);
events.push((
2,
message(kinds::USER_MSG, turn(), "ask", TrustTag::TrustedUser),
));
events.push((
3,
message(kinds::OUTPUT_MSG, turn(), "answer", TrustTag::Unspecified),
));
events.sort_by_key(|(position, _)| *position);
assert_eq!(
fold_conversation_core(&events).unwrap(),
fold_conversation_core(&events).unwrap()
);
}
#[test]
fn text_matches_the_search_fold_wherever_that_fold_emits_a_row() {
let mut events = committed_pair(turn(), 1, 5);
events.push((
2,
message(kinds::USER_MSG, turn(), "ask", TrustTag::TrustedUser),
));
events.push((
3,
message(kinds::OUTPUT_MSG, turn(), "", TrustTag::Unspecified),
));
events.push((
4,
message(kinds::OUTPUT_MSG, turn(), "answer", TrustTag::Unspecified),
));
events.sort_by_key(|(position, _)| *position);
let bare: Vec<Event> = events.iter().map(|(_, event)| event.clone()).collect();
let search = crate::committed::committed_message_facts(&bare);
let projected = fold_conversation_core(&events).unwrap();
let search_pairs: Vec<(String, String)> = search
.iter()
.map(|fact| (fact.turn_id.clone(), fact.text.clone()))
.collect();
let projected_pairs: Vec<(String, String)> = projected
.messages
.iter()
.filter(|row| !row.text.is_empty())
.map(|row| (row.turn_id.to_string(), row.text.clone()))
.collect();
assert_eq!(search_pairs, projected_pairs);
assert_eq!(projected.messages.len(), search.len() + 1);
}
#[test]
fn withholding_flips_internal_only_before_the_fold_emits_a_row() {
let mut events = committed_pair(turn(), 1, 4);
events.push((
2,
message(
kinds::OUTPUT_MSG,
turn(),
"narration",
TrustTag::Unspecified,
),
));
events.push((3, marker(kinds::TURN_TEXT_WITHHELD, turn())));
events.sort_by_key(|(position, _)| *position);
let before = fold_conversation_core(&events).unwrap();
assert!(!before.messages[0].internal_only);
let prepared = prepare_conversation_core(&mut events, "conv-x");
assert!(prepared.withheld.contains(&turn()));
let after = fold_conversation_core(&events).unwrap();
assert!(after.messages[0].internal_only);
assert_eq!(after.messages[0].text, "narration");
}
#[test]
fn preparing_reports_the_positions_it_did_not_remove() {
let mut events = committed_pair(turn(), 1, 3);
events.insert(
1,
(
2,
message(kinds::OUTPUT_MSG, turn(), "kept", TrustTag::Unspecified),
),
);
let prepared = prepare_conversation_core(&mut events, "conv-x");
assert!(prepared.excised.is_empty());
assert!(prepared.withheld.is_empty());
assert_eq!(fold_conversation_core(&events).unwrap().messages.len(), 1);
}
}