use std::collections::BTreeSet;
use polyc_eventlog_model::Event;
use polyc_proto::kinds;
use polyc_proto::proto::polychrome::agent::v1::{Message, content};
#[must_use]
pub fn withheld_turn_ids(events: &[Event]) -> BTreeSet<uuid::Uuid> {
events
.iter()
.filter_map(|ev| {
let (base, turn) = kinds::parse(&ev.kind);
(base == kinds::TURN_TEXT_WITHHELD)
.then_some(turn)
.flatten()
})
.collect()
}
pub fn withhold_paused_turn_text(events: &mut [Event], withheld: &BTreeSet<uuid::Uuid>) {
withhold_each(events.iter_mut(), withheld);
}
#[must_use]
pub fn withheld_turn_ids_positioned(events: &[(u64, Event)]) -> BTreeSet<uuid::Uuid> {
events
.iter()
.filter_map(|(_, ev)| {
let (base, turn) = kinds::parse(&ev.kind);
(base == kinds::TURN_TEXT_WITHHELD)
.then_some(turn)
.flatten()
})
.collect()
}
pub fn withhold_paused_turn_text_positioned(
events: &mut [(u64, Event)],
withheld: &BTreeSet<uuid::Uuid>,
) {
withhold_each(events.iter_mut().map(|(_, ev)| ev), withheld);
}
fn withhold_each<'a>(events: impl Iterator<Item = &'a mut Event>, withheld: &BTreeSet<uuid::Uuid>) {
if withheld.is_empty() {
return;
}
for ev in events {
let (base, turn) = kinds::parse(&ev.kind);
if base != kinds::OUTPUT_MSG || !turn.is_some_and(|t| withheld.contains(&t)) {
continue;
}
let Some(mut message) =
polyc_proto::events_decode::try_decode_event_payload::<Message>(&ev.payload).ok()
else {
continue;
};
let is_model_text = message.role == "model"
&& matches!(
message.content.as_option().and_then(|c| c.r#type.as_ref()),
Some(content::Type::Text(_))
);
if !is_model_text || message.internal_only {
continue;
}
message.internal_only = true;
ev.payload = buffa::Message::encode_to_vec(&message);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use polyc_proto::proto::polychrome::agent::v1::{Content, TextContent};
fn text(turn: &uuid::Uuid, body: &str, role: &str) -> Event {
let message = Message {
role: role.to_owned(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::Text(Box::new(TextContent {
text: body.to_owned(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}))),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}),
internal_only: false,
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
Event::new(
kinds::tagged(kinds::OUTPUT_MSG, turn),
buffa::Message::encode_to_vec(&message),
)
}
fn decoded(ev: &Event) -> Message {
polyc_proto::events_decode::try_decode_event_payload::<Message>(&ev.payload)
.expect("the rewritten payload still decodes")
}
#[test]
fn a_marker_names_its_turn_and_carries_nothing() {
let turn = uuid::Uuid::now_v7();
let marker = Event::new(kinds::tagged(kinds::TURN_TEXT_WITHHELD, &turn), Vec::new());
assert!(
marker.payload.is_empty(),
"the marker must carry no content of its own"
);
assert_eq!(withheld_turn_ids(&[marker]), [turn].into_iter().collect());
}
#[test]
fn only_the_named_turns_model_text_is_withheld() {
let paused = uuid::Uuid::now_v7();
let other = uuid::Uuid::now_v7();
let mut events = vec![
text(&paused, "pending your approval", "model"),
text(&paused, "a tool said so", "tool"),
text(&other, "an unrelated turn", "model"),
];
withhold_paused_turn_text(&mut events, &[paused].into_iter().collect());
assert!(
decoded(&events[0]).internal_only,
"the paused turn's model narration is withheld"
);
assert!(
!decoded(&events[1]).internal_only,
"a tool result is not narration and stays visible"
);
assert!(
!decoded(&events[2]).internal_only,
"another turn's narration is untouched"
);
assert_eq!(
decoded(&events[0]).content.as_option().and_then(|c| {
match c.r#type.as_ref() {
Some(content::Type::Text(t)) => Some(t.text.clone()),
_ => None,
}
}),
Some("pending your approval".to_owned()),
"withholding marks the message; it never blanks the text an admin \
and the resumed prompt still need"
);
}
#[test]
fn no_marker_changes_nothing() {
let turn = uuid::Uuid::now_v7();
let mut events = vec![text(&turn, "ordinary narration", "model")];
let before = events[0].payload.clone();
withhold_paused_turn_text(&mut events, &BTreeSet::new());
assert_eq!(events[0].payload, before);
}
}