use super::*;
use crate::parse::SpineRow;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ChainNode<'a> {
Full(&'a Record),
Spine(&'a SpineRow),
}
impl<'a> ChainNode<'a> {
pub(crate) fn full(self) -> Option<&'a Record> {
match self {
ChainNode::Full(r) => Some(r),
ChainNode::Spine(_) => None,
}
}
pub(crate) fn kind(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.r#type.as_deref(),
ChainNode::Spine(s) => Some(s.kind_str()),
}
}
pub(crate) fn subtype(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.subtype.as_deref(),
ChainNode::Spine(s) => s.subtype(),
}
}
pub(crate) fn uuid(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.uuid.as_deref(),
ChainNode::Spine(s) => s.uuid.as_deref(),
}
}
pub(crate) fn parent_uuid(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.parent_uuid.as_deref(),
ChainNode::Spine(s) => s.parent_uuid.as_deref(),
}
}
pub(crate) fn logical_parent_uuid(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.logical_parent_uuid.as_deref(),
ChainNode::Spine(s) => s.logical_parent_uuid(),
}
}
pub(crate) fn leaf_uuid(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.leaf_uuid.as_deref(),
ChainNode::Spine(s) => s.leaf_uuid(),
}
}
pub(crate) fn explicit(self) -> Option<bool> {
match self {
ChainNode::Full(r) => r.explicit,
ChainNode::Spine(s) => s.explicit(),
}
}
pub(crate) fn is_sidechain(self) -> Option<bool> {
match self {
ChainNode::Full(r) => r.is_sidechain,
ChainNode::Spine(s) => s.is_sidechain,
}
}
pub(crate) fn timestamp(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.timestamp.as_deref(),
ChainNode::Spine(s) => s.timestamp.as_deref(),
}
}
pub(crate) fn compact_metadata(self) -> Option<&'a serde_json::Value> {
match self {
ChainNode::Full(r) => r.compact_metadata.as_ref(),
ChainNode::Spine(s) => s.compact_metadata(),
}
}
pub(crate) fn message_id(self) -> Option<&'a str> {
match self {
ChainNode::Full(r) => r.message.as_ref().and_then(|m| m.id.as_deref()),
ChainNode::Spine(_) => None,
}
}
pub(crate) fn has_tool_result(self) -> bool {
match self {
ChainNode::Full(r) => r
.blocks()
.is_some_and(|bs| bs.iter().any(|x| matches!(x, Block::ToolResult { .. }))),
ChainNode::Spine(_) => false,
}
}
pub(crate) fn is_elicitation_marker(self) -> bool {
match self {
ChainNode::Full(r) => r.is_elicitation_marker(),
ChainNode::Spine(_) => false,
}
}
pub(crate) fn opens_turn(self) -> bool {
match self {
ChainNode::Full(r) => r.r#type.as_deref() == Some("user") && r.opens_turn(),
ChainNode::Spine(_) => false,
}
}
pub(crate) fn is_compact_boundary(self) -> bool {
self.kind() == Some("system") && self.subtype() == Some("compact_boundary")
}
pub(crate) fn summary_compaction_mode(self) -> Option<SummarizeMode> {
match self {
ChainNode::Full(r) => r.summary_compaction_mode(),
ChainNode::Spine(_) => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Slot {
pub(crate) first: usize,
pub(crate) last: usize,
}
pub(crate) struct Builder<'a> {
pub(crate) recs: Vec<ChainNode<'a>>,
pub(crate) admit: Vec<bool>,
pub(crate) parent: Vec<Option<&'a str>>,
pub(crate) map: HashMap<&'a str, Slot>,
pub(crate) removed: Vec<bool>,
pub(crate) on_chain: Vec<bool>,
pub(crate) precut: Vec<bool>,
pub(crate) visited: Vec<bool>,
pub(crate) replay_of: HashMap<usize, usize>,
pub(crate) chain_child: HashMap<&'a str, usize>,
pub(crate) ts_sorted: Option<Vec<(i64, usize)>>,
pub(crate) opens: Vec<bool>,
pub(crate) child_off: Vec<usize>,
pub(crate) child_at: Vec<usize>,
}
pub(crate) fn chain_admits(r: ChainNode<'_>) -> bool {
if r.is_elicitation_marker() {
return false;
}
if r.uuid().is_none_or(str::is_empty) {
return false;
}
matches!(
r.kind(),
Some("user" | "assistant" | "attachment" | "system")
)
}
impl<'a> Builder<'a> {
pub(crate) fn new<T>(records: &'a [T], node: &impl Fn(&T) -> ChainNode<'_>) -> Builder<'a> {
let recs: Vec<ChainNode<'a>> = records.iter().map(node).collect();
let n = recs.len();
let mut admit = Vec::with_capacity(n);
let mut parent: Vec<Option<&'a str>> = Vec::with_capacity(n);
let mut map: HashMap<&'a str, Slot> = HashMap::with_capacity(n);
let mut replay_of: HashMap<usize, usize> = HashMap::new();
for (i, r) in recs.iter().copied().enumerate() {
let ok = chain_admits(r);
admit.push(ok);
parent.push(r.parent_uuid().filter(|s| !s.is_empty()));
if !ok {
continue;
}
let Some(uuid) = r.uuid() else {
continue;
};
match map.get_mut(uuid) {
Some(slot) => {
replay_of.insert(slot.last, i);
slot.last = i;
}
None => {
map.insert(uuid, Slot { first: i, last: i });
}
}
}
for (i, target) in &mut replay_of {
if let Some(last) = recs[*i].uuid().and_then(|u| map.get(u)) {
*target = last.last;
}
}
let opens = recs.iter().map(|r| r.opens_turn()).collect();
Builder {
recs,
admit,
parent,
map,
removed: vec![false; n],
on_chain: vec![false; n],
precut: vec![false; n],
visited: vec![false; n],
replay_of,
chain_child: HashMap::new(),
ts_sorted: None,
opens,
child_off: Vec::new(),
child_at: Vec::new(),
}
}
pub(crate) fn index_children(&mut self) {
let n = self.len();
let mut parent_of: Vec<Option<usize>> = vec![None; n];
let mut counts = vec![0usize; n + 1];
for (i, slot) in parent_of.iter_mut().enumerate() {
if !self.admit[i] || !self.is_survivor(i) {
continue;
}
if let Some(p) = self.parent[i].and_then(|p| self.resolve(p)) {
*slot = Some(p);
counts[p] += 1;
}
}
let mut off = vec![0usize; n + 1];
let mut acc = 0usize;
for i in 0..n {
off[i] = acc;
acc += counts[i];
}
off[n] = acc;
let mut cursor = off.clone();
let mut at = vec![0usize; acc];
for (i, p) in parent_of.iter().enumerate() {
if let Some(p) = *p {
at[cursor[p]] = i;
cursor[p] += 1;
}
}
self.child_off = off;
self.child_at = at;
}
pub(crate) fn children_of(&self, i: usize) -> &[usize] {
match (self.child_off.get(i), self.child_off.get(i + 1)) {
(Some(&a), Some(&b)) => &self.child_at[a..b],
_ => &[],
}
}
pub(crate) fn len(&self) -> usize {
self.recs.len()
}
pub(crate) fn resolve(&self, uuid: &str) -> Option<usize> {
let slot = self.map.get(uuid)?;
(!self.removed[slot.last]).then_some(slot.last)
}
pub(crate) fn is_survivor(&self, i: usize) -> bool {
self.admit[i] && !self.replay_of.contains_key(&i)
}
pub(crate) fn uuid(&self, i: usize) -> Option<&'a str> {
self.recs[i].uuid()
}
pub(crate) fn is_conv(&self, i: usize) -> bool {
matches!(self.recs[i].kind(), Some("user" | "assistant"))
}
pub(crate) fn is_boundary(&self, i: usize) -> bool {
self.recs[i].is_compact_boundary()
}
pub(crate) fn is_sidechain(&self, i: usize) -> bool {
self.recs[i].is_sidechain() == Some(true)
}
}