use bcode_session_models::{
ProjectionSourceRange, ProjectionWindow, ProjectionWindowAnchor, ProjectionWindowDirection,
ProjectionWindowRequest, SessionEvent, SessionEventKind, SessionProjectionKind,
TranscriptProjectionItem, TranscriptProjectionItemKind,
};
use std::collections::BTreeMap;
#[cfg(test)]
pub(crate) fn projection_window_from_db_transcript_items(
items: &[crate::db::TranscriptItem],
first_event_sequence: Option<u64>,
last_event_sequence: Option<u64>,
request: &ProjectionWindowRequest,
) -> Option<ProjectionWindow> {
let mut spans = items
.iter()
.map(|item| {
let content_bytes = item.content.as_ref().map_or(0, String::len);
TranscriptProjectionItem {
kind: transcript_projection_kind_from_db_item(item),
source_range: ProjectionSourceRange {
start_sequence: item.event_seq_start,
end_sequence: item.event_seq_end,
},
estimated_rows: estimate_rows(content_bytes, request.target.width_columns),
content_bytes,
}
})
.collect::<Vec<_>>();
projection_window_from_transcript_projection_items(
&mut spans,
first_event_sequence,
last_event_sequence,
request,
)
}
#[cfg(test)]
fn projection_window_from_transcript_projection_items(
spans: &mut [TranscriptProjectionItem],
first_event_sequence: Option<u64>,
last_event_sequence: Option<u64>,
request: &ProjectionWindowRequest,
) -> Option<ProjectionWindow> {
if request.projection != SessionProjectionKind::Transcript {
return None;
}
spans.sort_by_key(|item| {
(
item.source_range.start_sequence,
item.source_range.end_sequence,
)
});
let selected_items = match (request.anchor, request.direction) {
(ProjectionWindowAnchor::Latest, ProjectionWindowDirection::Backward) => {
select_latest_items(spans, request)
}
(ProjectionWindowAnchor::BeforeSequence(sequence), ProjectionWindowDirection::Backward) => {
let end = spans.partition_point(|item| item.source_range.end_sequence < sequence);
select_latest_items(&spans[..end], request)
}
(ProjectionWindowAnchor::AfterSequence(sequence), ProjectionWindowDirection::Forward) => {
let start = spans.partition_point(|item| item.source_range.start_sequence <= sequence);
select_earliest_items(&spans[start..], request)
}
(ProjectionWindowAnchor::AroundSequence(sequence), _) => {
select_around_sequence_items(spans, sequence, request)
}
_ => return None,
};
let source_range = source_range_for_items(&selected_items);
let scanned_events = source_range.map_or(0, |range| {
usize::try_from(range.end_sequence - range.start_sequence + 1).unwrap_or(usize::MAX)
});
let has_older = source_range.is_some_and(|range| {
first_event_sequence.is_some_and(|first| first < range.start_sequence)
});
let has_newer = source_range
.is_some_and(|range| last_event_sequence.is_some_and(|last| last > range.end_sequence));
Some(ProjectionWindow {
projection: request.projection,
transcript_items: selected_items,
source_range,
has_older,
has_newer,
scanned_events,
})
}
#[cfg(test)]
fn transcript_projection_kind_from_db_item(
item: &crate::db::TranscriptItem,
) -> TranscriptProjectionItemKind {
match item.kind.as_str() {
"message" if item.role == "user" => TranscriptProjectionItemKind::UserMessage,
"message" if item.role == "assistant" => TranscriptProjectionItemKind::AssistantMessage,
"invocation" | "result" => TranscriptProjectionItemKind::ToolInvocation,
_ => TranscriptProjectionItemKind::Other,
}
}
#[must_use]
pub fn projection_window_from_events(
events: &[SessionEvent],
request: &ProjectionWindowRequest,
) -> Option<ProjectionWindow> {
projection_window_from_events_with_source_bounds(
events,
events.first().map(|event| event.sequence),
events.last().map(|event| event.sequence),
request,
)
}
pub(crate) fn projection_window_from_events_with_source_bounds(
events: &[SessionEvent],
first_event_sequence: Option<u64>,
last_event_sequence: Option<u64>,
request: &ProjectionWindowRequest,
) -> Option<ProjectionWindow> {
if request.projection != SessionProjectionKind::Transcript {
return None;
}
let (mut items, scanned_events, scanned_source_range) = match request.anchor {
ProjectionWindowAnchor::Latest => {
if request.direction != ProjectionWindowDirection::Backward {
return None;
}
let scan_start = events
.len()
.saturating_sub(request.limits.max_events_scanned);
let scanned = &events[scan_start..];
(
build_transcript_projection(scanned, request.target.width_columns),
events.len().saturating_sub(scan_start),
scanned
.first()
.zip(scanned.last())
.map(|(first, last)| ProjectionSourceRange {
start_sequence: first.sequence,
end_sequence: last.sequence,
}),
)
}
ProjectionWindowAnchor::BeforeSequence(_)
| ProjectionWindowAnchor::AfterSequence(_)
| ProjectionWindowAnchor::AroundSequence(_) => (
build_transcript_projection(events, request.target.width_columns),
events.len(),
events
.first()
.zip(events.last())
.map(|(first, last)| ProjectionSourceRange {
start_sequence: first.sequence,
end_sequence: last.sequence,
}),
),
};
items.sort_by_key(|item| {
(
item.source_range.start_sequence,
item.source_range.end_sequence,
)
});
let selected_items = match (request.anchor, request.direction) {
(ProjectionWindowAnchor::Latest, ProjectionWindowDirection::Backward) => {
select_latest_items(&items, request)
}
(ProjectionWindowAnchor::BeforeSequence(sequence), ProjectionWindowDirection::Backward) => {
let end = items.partition_point(|item| item.source_range.end_sequence < sequence);
select_latest_items(&items[..end], request)
}
(ProjectionWindowAnchor::AfterSequence(sequence), ProjectionWindowDirection::Forward) => {
let start = items.partition_point(|item| item.source_range.start_sequence <= sequence);
select_earliest_items(&items[start..], request)
}
(ProjectionWindowAnchor::AroundSequence(sequence), _) => {
select_around_sequence_items(&items, sequence, request)
}
_ => return None,
};
let source_range =
projection_page_source_range(&items, &selected_items, scanned_source_range, request);
let has_older = source_range.is_some_and(|range| {
first_event_sequence.is_some_and(|first| first < range.start_sequence)
});
let has_newer = source_range
.is_some_and(|range| last_event_sequence.is_some_and(|last| last > range.end_sequence));
Some(ProjectionWindow {
projection: request.projection,
transcript_items: selected_items,
source_range,
has_older,
has_newer,
scanned_events,
})
}
fn projection_page_source_range(
items: &[TranscriptProjectionItem],
selected_items: &[TranscriptProjectionItem],
scanned_source_range: Option<ProjectionSourceRange>,
request: &ProjectionWindowRequest,
) -> Option<ProjectionSourceRange> {
let mut range = source_range_for_items(selected_items).or(scanned_source_range)?;
let scanned = scanned_source_range?;
match (request.anchor, request.direction) {
(
ProjectionWindowAnchor::BeforeSequence(_) | ProjectionWindowAnchor::Latest,
ProjectionWindowDirection::Backward,
) => {
let next_item_start = items
.iter()
.filter(|item| item.source_range.start_sequence > range.end_sequence)
.map(|item| item.source_range.start_sequence)
.min();
range.end_sequence =
next_item_start.map_or(scanned.end_sequence, |start| start.saturating_sub(1));
}
(ProjectionWindowAnchor::AfterSequence(_), ProjectionWindowDirection::Forward) => {
let previous_item_end = items
.iter()
.filter(|item| item.source_range.end_sequence < range.start_sequence)
.map(|item| item.source_range.end_sequence)
.max();
range.start_sequence =
previous_item_end.map_or(scanned.start_sequence, |end| end.saturating_add(1));
}
_ => {}
}
Some(range)
}
#[must_use]
pub fn build_transcript_projection(
events: &[SessionEvent],
width_columns: Option<u16>,
) -> Vec<TranscriptProjectionItem> {
let mut builder = TranscriptProjectionBuilder::new(width_columns);
for event in events {
builder.apply(event);
}
builder.finish()
}
fn select_latest_items(
items: &[TranscriptProjectionItem],
request: &ProjectionWindowRequest,
) -> Vec<TranscriptProjectionItem> {
let mut selected = Vec::new();
let mut selected_rows = 0usize;
let mut selected_bytes = 0usize;
for item in items.iter().rev() {
if selected.len() >= request.limits.max_items {
break;
}
if selected_bytes.saturating_add(item.content_bytes) > request.limits.max_bytes
&& !selected.is_empty()
{
break;
}
selected_rows = selected_rows.saturating_add(item.estimated_rows.unwrap_or(1));
selected_bytes = selected_bytes.saturating_add(item.content_bytes);
selected.push(item.clone());
if target_satisfied(
selected.len(),
selected_rows,
selected_bytes,
request.target.min_items,
request.target.min_estimated_rows,
request.target.min_bytes,
) {
break;
}
}
selected.reverse();
selected
}
fn select_earliest_items(
items: &[TranscriptProjectionItem],
request: &ProjectionWindowRequest,
) -> Vec<TranscriptProjectionItem> {
let mut selected = Vec::new();
let mut selected_rows = 0usize;
let mut selected_bytes = 0usize;
for item in items {
if selected.len() >= request.limits.max_items {
break;
}
if selected_bytes.saturating_add(item.content_bytes) > request.limits.max_bytes
&& !selected.is_empty()
{
break;
}
selected_rows = selected_rows.saturating_add(item.estimated_rows.unwrap_or(1));
selected_bytes = selected_bytes.saturating_add(item.content_bytes);
selected.push(item.clone());
if target_satisfied(
selected.len(),
selected_rows,
selected_bytes,
request.target.min_items,
request.target.min_estimated_rows,
request.target.min_bytes,
) {
break;
}
}
selected
}
fn select_around_sequence_items(
items: &[TranscriptProjectionItem],
sequence: u64,
request: &ProjectionWindowRequest,
) -> Vec<TranscriptProjectionItem> {
let Some(anchor_index) = items.iter().position(|item| {
item.source_range.start_sequence <= sequence && sequence <= item.source_range.end_sequence
}) else {
return Vec::new();
};
let mut start = anchor_index;
let mut end = anchor_index + 1;
let mut selected_rows = items[anchor_index].estimated_rows.unwrap_or(1);
let mut selected_bytes = items[anchor_index].content_bytes;
while end.saturating_sub(start) < request.limits.max_items
&& selected_bytes <= request.limits.max_bytes
&& !target_satisfied(
end.saturating_sub(start),
selected_rows,
selected_bytes,
request.target.min_items,
request.target.min_estimated_rows,
request.target.min_bytes,
)
{
let can_prepend = start > 0;
let can_append = end < items.len();
if !can_prepend && !can_append {
break;
}
let use_prepend = can_prepend
&& (!can_append
|| anchor_index.saturating_sub(start) <= end.saturating_sub(anchor_index + 1));
let candidate = if use_prepend {
&items[start - 1]
} else {
&items[end]
};
if selected_bytes.saturating_add(candidate.content_bytes) > request.limits.max_bytes {
break;
}
selected_rows = selected_rows.saturating_add(candidate.estimated_rows.unwrap_or(1));
selected_bytes = selected_bytes.saturating_add(candidate.content_bytes);
if use_prepend {
start -= 1;
} else {
end += 1;
}
}
items[start..end].to_vec()
}
fn target_satisfied(
selected_items: usize,
selected_rows: usize,
selected_bytes: usize,
min_items: Option<usize>,
min_estimated_rows: Option<usize>,
min_bytes: Option<usize>,
) -> bool {
min_items.is_none_or(|minimum| selected_items >= minimum)
&& min_estimated_rows.is_none_or(|minimum| selected_rows >= minimum)
&& min_bytes.is_none_or(|minimum| selected_bytes >= minimum)
}
fn source_range_for_items(items: &[TranscriptProjectionItem]) -> Option<ProjectionSourceRange> {
let first = items.first()?;
let last = items.last()?;
Some(ProjectionSourceRange {
start_sequence: first.source_range.start_sequence,
end_sequence: last.source_range.end_sequence,
})
}
struct TranscriptProjectionBuilder {
items: Vec<TranscriptProjectionItem>,
width_columns: Option<u16>,
assistant_stream: Option<PendingStream>,
reasoning_stream: Option<PendingStream>,
tool_invocations: BTreeMap<String, PendingToolInvocation>,
}
impl TranscriptProjectionBuilder {
const fn new(width_columns: Option<u16>) -> Self {
Self {
items: Vec::new(),
width_columns,
assistant_stream: None,
reasoning_stream: None,
tool_invocations: BTreeMap::new(),
}
}
fn apply(&mut self, event: &SessionEvent) {
match &event.kind {
SessionEventKind::AssistantDelta { text } => {
self.push_stream_delta(TranscriptProjectionItemKind::AssistantMessage, event, text);
}
SessionEventKind::AssistantMessage { text }
| SessionEventKind::AssistantResponseSegment { text, .. }
| SessionEventKind::PositionedAssistantResponseSegment { text, .. } => {
self.finish_stream(TranscriptProjectionItemKind::AssistantMessage, event, text);
}
SessionEventKind::AssistantReasoningDelta { text } => {
self.push_stream_delta(TranscriptProjectionItemKind::Reasoning, event, text);
}
SessionEventKind::AssistantReasoningMessage { text } => {
self.finish_stream(TranscriptProjectionItemKind::Reasoning, event, text);
}
SessionEventKind::AssistantReasoningActivity { activity, .. }
| SessionEventKind::PositionedAssistantReasoningActivity { activity, .. } => {
self.flush_streams();
let content_bytes = activity
.parts
.iter()
.map(|part| part.text.len())
.sum::<usize>();
self.push_item(
TranscriptProjectionItemKind::Reasoning,
event.sequence,
event.sequence,
content_bytes,
);
}
SessionEventKind::ToolCallRequested {
tool_call_id,
arguments_json,
..
}
| SessionEventKind::PositionedToolCallRequested {
tool_call_id,
arguments_json,
..
} => {
self.flush_streams();
self.start_tool_invocation(tool_call_id, event.sequence, arguments_json.len());
}
SessionEventKind::ToolInvocationResultRecorded { record } => {
self.flush_streams();
self.finish_tool_invocation(
&record.invocation_id,
event.sequence,
record.model_output.len(),
);
}
_ => {
self.flush_streams();
if let Some((kind, bytes)) = non_streaming_item(event) {
self.push_item(kind, event.sequence, event.sequence, bytes);
}
}
}
}
fn finish(mut self) -> Vec<TranscriptProjectionItem> {
self.flush_streams();
self.flush_tool_invocations();
self.items.sort_by_key(|item| {
(
item.source_range.start_sequence,
item.source_range.end_sequence,
)
});
self.items
}
fn push_stream_delta(
&mut self,
kind: TranscriptProjectionItemKind,
event: &SessionEvent,
text: &str,
) {
let stream = self.pending_stream_mut(kind);
if let Some(stream) = stream {
stream.end_sequence = event.sequence;
stream.content_bytes += text.len();
} else {
*self.pending_stream_slot(kind) = Some(PendingStream {
kind,
start_sequence: event.sequence,
end_sequence: event.sequence,
content_bytes: text.len(),
});
}
}
fn finish_stream(
&mut self,
kind: TranscriptProjectionItemKind,
event: &SessionEvent,
text: &str,
) {
let start_sequence = self
.pending_stream_slot(kind)
.take()
.map_or(event.sequence, |stream| stream.start_sequence);
self.push_item(kind, start_sequence, event.sequence, text.len());
}
fn flush_streams(&mut self) {
let assistant_stream = self.assistant_stream.take();
let reasoning_stream = self.reasoning_stream.take();
if let Some(stream) = assistant_stream {
self.push_stream_item(stream);
}
if let Some(stream) = reasoning_stream {
self.push_stream_item(stream);
}
}
fn push_stream_item(&mut self, stream: PendingStream) {
self.push_item(
stream.kind,
stream.start_sequence,
stream.end_sequence,
stream.content_bytes,
);
}
fn start_tool_invocation(&mut self, tool_call_id: &str, sequence: u64, content_bytes: usize) {
self.tool_invocations.insert(
tool_call_id.to_owned(),
PendingToolInvocation {
start_sequence: sequence,
end_sequence: sequence,
content_bytes,
saw_stream_output: false,
},
);
}
fn finish_tool_invocation(&mut self, tool_call_id: &str, sequence: u64, result_bytes: usize) {
if let Some(mut invocation) = self.tool_invocations.remove(tool_call_id) {
invocation.end_sequence = sequence;
if !invocation.saw_stream_output {
invocation.content_bytes = invocation.content_bytes.saturating_add(result_bytes);
}
self.push_tool_invocation_item(invocation);
return;
}
self.push_item(
TranscriptProjectionItemKind::ToolInvocation,
sequence,
sequence,
result_bytes,
);
}
fn flush_tool_invocations(&mut self) {
let mut invocations = std::mem::take(&mut self.tool_invocations)
.into_values()
.collect::<Vec<_>>();
invocations.sort_by_key(|invocation| (invocation.start_sequence, invocation.end_sequence));
for invocation in invocations {
self.push_tool_invocation_item(invocation);
}
}
fn push_tool_invocation_item(&mut self, invocation: PendingToolInvocation) {
self.push_item(
TranscriptProjectionItemKind::ToolInvocation,
invocation.start_sequence,
invocation.end_sequence,
invocation.content_bytes,
);
}
fn push_item(
&mut self,
kind: TranscriptProjectionItemKind,
start_sequence: u64,
end_sequence: u64,
content_bytes: usize,
) {
self.items.push(TranscriptProjectionItem {
kind,
source_range: ProjectionSourceRange {
start_sequence,
end_sequence,
},
estimated_rows: estimate_rows(content_bytes, self.width_columns),
content_bytes,
});
}
fn pending_stream_mut(
&mut self,
kind: TranscriptProjectionItemKind,
) -> Option<&mut PendingStream> {
self.pending_stream_slot(kind).as_mut()
}
fn pending_stream_slot(
&mut self,
kind: TranscriptProjectionItemKind,
) -> &mut Option<PendingStream> {
match kind {
TranscriptProjectionItemKind::AssistantMessage => &mut self.assistant_stream,
TranscriptProjectionItemKind::Reasoning => &mut self.reasoning_stream,
_ => unreachable!("only streaming transcript item kinds have pending slots"),
}
}
}
#[derive(Debug, Clone, Copy)]
struct PendingStream {
kind: TranscriptProjectionItemKind,
start_sequence: u64,
end_sequence: u64,
content_bytes: usize,
}
#[derive(Debug, Clone, Copy)]
struct PendingToolInvocation {
start_sequence: u64,
end_sequence: u64,
content_bytes: usize,
saw_stream_output: bool,
}
fn non_streaming_item(event: &SessionEvent) -> Option<(TranscriptProjectionItemKind, usize)> {
match &event.kind {
SessionEventKind::UserMessage { text, .. }
| SessionEventKind::SubmissionAccepted {
submission: bcode_session_models::PromptSubmission { text, .. },
}
| SessionEventKind::SystemMessage { text } => {
Some((TranscriptProjectionItemKind::UserMessage, text.len()))
}
SessionEventKind::PluginStatusNote { text, .. } => {
Some((TranscriptProjectionItemKind::Other, text.len()))
}
SessionEventKind::PermissionRequested { arguments_json, .. } => Some((
TranscriptProjectionItemKind::Permission,
arguments_json.len(),
)),
SessionEventKind::PermissionResolved { .. } => {
Some((TranscriptProjectionItemKind::Permission, 0))
}
SessionEventKind::ToolContribution { event } => Some((
TranscriptProjectionItemKind::Other,
serde_json::to_vec(event).map_or(0, |encoded| encoded.len()),
)),
SessionEventKind::ToolExchangeRequested { request } => Some((
TranscriptProjectionItemKind::Other,
serde_json::to_vec(request).map_or(0, |encoded| encoded.len()),
)),
SessionEventKind::ToolExchangeResolved { event } => Some((
TranscriptProjectionItemKind::Other,
serde_json::to_vec(event).map_or(0, |encoded| encoded.len()),
)),
SessionEventKind::ContextCompacted { summary, .. } => Some((
TranscriptProjectionItemKind::ContextCompaction,
summary.len(),
)),
SessionEventKind::WorkingDirectoryChanged {
old_working_directory,
new_working_directory,
} => Some((
TranscriptProjectionItemKind::WorkingDirectoryChange,
old_working_directory.as_os_str().len() + new_working_directory.as_os_str().len(),
)),
SessionEventKind::SkillInvoked { arguments, .. } => {
Some((TranscriptProjectionItemKind::Other, arguments.len()))
}
SessionEventKind::SkillInvocationFailed { error, .. } => {
Some((TranscriptProjectionItemKind::Other, error.len()))
}
SessionEventKind::ModelUsage { .. } => Some((TranscriptProjectionItemKind::Other, 0)),
_ => None,
}
}
fn estimate_rows(content_bytes: usize, width_columns: Option<u16>) -> Option<usize> {
let width = usize::from(width_columns?);
if width == 0 {
return Some(1);
}
Some((content_bytes / width).saturating_add(1))
}
#[cfg(test)]
mod tests {
use super::*;
use bcode_session_models::{
CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, ProjectionWindowLimits,
ProjectionWindowRequest, ProjectionWindowTarget, SessionId, ToolInvocationProjectionStatus,
ToolInvocationResult, build_tool_invocation_projections,
};
fn tool_invocation_projection_replay_events(session_id: SessionId) -> Vec<SessionEvent> {
vec![
event(
session_id,
1,
SessionEventKind::ToolCallRequested {
tool_call_id: "call-1".to_owned(),
producer_plugin_id: Some("plugin.shell".to_owned()),
tool_name: "shell.run".to_owned(),
arguments_json: r#"{"command":"echo hi"}"#.to_owned(),
working_directory: None,
},
),
event(
session_id,
2,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "call-1".to_owned(),
model_output: "final text".to_owned(),
is_error: false,
presentation: None,
result: Some(ToolInvocationResult::Text {
text: "semantic text".to_owned(),
}),
content: Vec::new(),
},
},
),
]
}
#[test]
fn tool_invocation_projection_preserves_raw_replayed_facts() {
let session_id = SessionId::new();
let events = tool_invocation_projection_replay_events(session_id);
let projections = build_tool_invocation_projections(&events);
assert_eq!(projections.len(), 1);
let projection = &projections[0];
assert_eq!(projection.tool_call_id, "call-1");
assert_eq!(
projection.producer_plugin_id.as_deref(),
Some("plugin.shell")
);
assert_eq!(projection.tool_name.as_deref(), Some("shell.run"));
assert_eq!(
projection.arguments_json.as_deref(),
Some(r#"{"command":"echo hi"}"#)
);
assert_eq!(projection.status, ToolInvocationProjectionStatus::Finished);
assert_eq!(projection.result_text.as_deref(), Some("final text"));
assert_eq!(projection.is_error, Some(false));
assert_eq!(
projection.raw_result,
Some(ToolInvocationResult::Text {
text: "semantic text".to_owned()
})
);
}
#[test]
fn tool_invocation_projection_does_not_store_generic_fallback_output() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::ToolCallRequested {
tool_call_id: "call-1".to_owned(),
producer_plugin_id: None,
tool_name: "unknown.tool".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
),
event(
session_id,
2,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "call-1".to_owned(),
model_output: "raw result only".to_owned(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
),
];
let projection = build_tool_invocation_projections(&events)
.into_iter()
.next()
.expect("tool projection");
assert_eq!(projection.result_text.as_deref(), Some("raw result only"));
assert!(projection.raw_result.is_none());
}
#[test]
fn latest_projection_window_from_db_transcript_items_satisfies_targets() {
let entries = vec![
db_transcript_item(1, TranscriptProjectionItemKind::UserMessage, 5),
db_transcript_item(2, TranscriptProjectionItemKind::AssistantMessage, 5),
db_transcript_item(3, TranscriptProjectionItemKind::UserMessage, 5),
db_transcript_item(4, TranscriptProjectionItemKind::AssistantMessage, 5),
];
let window = projection_window_from_db_transcript_items(
&entries,
Some(1),
Some(4),
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(3),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 4096,
},
},
)
.expect("DB-backed latest transcript windows are supported");
assert_eq!(window.transcript_items.len(), 3);
assert_eq!(window.source_range.expect("source range").start_sequence, 2);
assert!(window.has_older);
assert!(!window.has_newer);
}
#[test]
fn latest_projection_window_from_db_transcript_items_sorts_unsorted_rows() {
let entries = vec![
db_transcript_item(10, TranscriptProjectionItemKind::ToolInvocation, 5),
db_transcript_item(2, TranscriptProjectionItemKind::UserMessage, 5),
db_transcript_item(20, TranscriptProjectionItemKind::AssistantMessage, 5),
];
let window = projection_window_from_db_transcript_items(
&entries,
Some(2),
Some(20),
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(2),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 4096,
},
},
)
.expect("DB-backed latest transcript windows are supported");
assert_eq!(window.transcript_items.len(), 2);
assert_eq!(
window.source_range.expect("source range").start_sequence,
10
);
assert_eq!(window.source_range.expect("source range").end_sequence, 20);
}
#[test]
fn latest_projection_window_from_db_transcript_items_estimates_rows_from_request_width() {
let entries = vec![db_transcript_item(
1,
TranscriptProjectionItemKind::AssistantMessage,
100,
)];
let narrow = projection_window_from_db_transcript_items(
&entries,
Some(1),
Some(1),
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(1),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(20),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 4096,
},
},
)
.expect("DB-backed latest transcript windows are supported");
let wide = projection_window_from_db_transcript_items(
&entries,
Some(1),
Some(1),
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(1),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(100),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 4096,
},
},
)
.expect("DB-backed latest transcript windows are supported");
assert_eq!(narrow.transcript_items[0].estimated_rows, Some(6));
assert_eq!(wide.transcript_items[0].estimated_rows, Some(2));
}
#[test]
fn latest_projection_window_from_db_transcript_items_respects_byte_cap() {
let entries = vec![
db_transcript_item(1, TranscriptProjectionItemKind::UserMessage, 5),
db_transcript_item(2, TranscriptProjectionItemKind::AssistantMessage, 5),
];
let window = projection_window_from_db_transcript_items(
&entries,
Some(1),
Some(2),
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(2),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 5,
},
},
)
.expect("DB-backed latest transcript windows are supported");
assert_eq!(window.transcript_items.len(), 1);
assert_eq!(window.source_range.expect("source range").start_sequence, 2);
}
#[test]
fn positioned_semantics_are_transcript_items_and_lifecycle_is_not() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::PositionedToolCallRequested {
turn_id: "turn-1".to_owned(),
output_position: bcode_session_models::TurnOutputPosition::new(0),
tool_call_id: "call-1".to_owned(),
tool_name: "filesystem.read".to_owned(),
arguments_json: "{}".to_owned(),
producer_plugin_id: None,
working_directory: None,
},
),
event(
session_id,
2,
SessionEventKind::ToolInvocationLifecycle {
event: bcode_session_models::ToolInvocationLifecycleEvent {
invocation_id: "call-1".to_owned(),
sequence: 1,
stage: bcode_session_models::ToolInvocationLifecycleStage::Started,
message: Some("running".to_owned()),
metadata: serde_json::Value::Object(serde_json::Map::new()),
},
},
),
event(
session_id,
3,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "call-1".to_owned(),
model_output: "done".to_owned(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
),
event(
session_id,
4,
SessionEventKind::PositionedAssistantResponseSegment {
turn_id: "turn-1".to_owned(),
output_position: bcode_session_models::TurnOutputPosition::new(1),
segment_id: "segment-0".to_owned(),
segment_order: 0,
text: "answer".to_owned(),
},
),
];
let items = build_transcript_projection(&events, Some(80));
assert_eq!(items.len(), 2);
assert_eq!(items[0].kind, TranscriptProjectionItemKind::ToolInvocation);
assert_eq!(items[0].source_range.start_sequence, 1);
assert_eq!(items[0].source_range.end_sequence, 3);
assert_eq!(
items[1].kind,
TranscriptProjectionItemKind::AssistantMessage
);
assert_eq!(items[1].source_range.start_sequence, 4);
}
#[test]
fn assistant_deltas_finish_as_one_projection_item() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::AssistantDelta {
text: "hel".to_owned(),
},
),
event(
session_id,
2,
SessionEventKind::AssistantDelta {
text: "lo".to_owned(),
},
),
event(
session_id,
3,
SessionEventKind::AssistantMessage {
text: "hello".to_owned(),
},
),
];
let items = build_transcript_projection(&events, Some(80));
assert_eq!(items.len(), 1);
assert_eq!(
items[0].kind,
TranscriptProjectionItemKind::AssistantMessage
);
assert_eq!(items[0].source_range.start_sequence, 1);
assert_eq!(items[0].source_range.end_sequence, 3);
assert_eq!(items[0].content_bytes, 5);
}
#[test]
fn unterminated_assistant_deltas_are_retained() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::AssistantDelta {
text: "hel".to_owned(),
},
),
event(
session_id,
2,
SessionEventKind::AssistantDelta {
text: "lo".to_owned(),
},
),
];
let items = build_transcript_projection(&events, Some(80));
assert_eq!(items.len(), 1);
assert_eq!(items[0].source_range.start_sequence, 1);
assert_eq!(items[0].source_range.end_sequence, 2);
assert_eq!(items[0].content_bytes, 5);
}
#[test]
fn non_streamed_tool_invocation_groups_request_and_result() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::ToolCallRequested {
tool_call_id: "tool".to_owned(),
producer_plugin_id: None,
tool_name: "read".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
),
event(
session_id,
2,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "tool".to_owned(),
model_output: "result".to_owned(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
),
];
let items = build_transcript_projection(&events, Some(80));
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind, TranscriptProjectionItemKind::ToolInvocation);
assert_eq!(items[0].source_range.start_sequence, 1);
assert_eq!(items[0].source_range.end_sequence, 2);
assert_eq!(items[0].content_bytes, 8);
}
#[test]
fn generic_result_record_closes_bounded_tool_projection() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::ToolCallRequested {
tool_call_id: "call-1".to_owned(),
producer_plugin_id: Some("example.plugin".to_owned()),
tool_name: "example.tool".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
),
event(
session_id,
2,
SessionEventKind::ToolInvocationResultRecorded {
record: bcode_session_models::ToolInvocationResultRecord {
invocation_id: "call-1".to_owned(),
model_output: "done".to_owned(),
is_error: false,
presentation: None,
result: None,
content: Vec::new(),
},
},
),
];
let items = build_transcript_projection(&events, Some(80));
assert_eq!(items.len(), 1);
assert_eq!(items[0].kind, TranscriptProjectionItemKind::ToolInvocation);
assert_eq!(items[0].source_range.start_sequence, 1);
assert_eq!(items[0].source_range.end_sequence, 2);
assert_eq!(items[0].content_bytes, 6);
}
#[test]
fn latest_projection_window_satisfies_item_target_after_compaction() {
let session_id = SessionId::new();
let mut events = vec![
event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "older".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "reply".to_owned(),
},
),
event(
session_id,
3,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "newer".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
];
events.extend((4..20).map(|sequence| {
event(
session_id,
sequence,
SessionEventKind::AssistantDelta {
text: "x".to_owned(),
},
)
}));
events.push(event(
session_id,
20,
SessionEventKind::AssistantMessage {
text: "streamed final".to_owned(),
},
));
let window = projection_window_from_events(
&events,
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(4),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 4096,
},
},
)
.expect("latest transcript windows are supported");
assert_eq!(window.transcript_items.len(), 4);
assert_eq!(
window
.transcript_items
.iter()
.map(|item| item.kind)
.collect::<Vec<_>>(),
vec![
TranscriptProjectionItemKind::UserMessage,
TranscriptProjectionItemKind::AssistantMessage,
TranscriptProjectionItemKind::UserMessage,
TranscriptProjectionItemKind::AssistantMessage,
]
);
assert_eq!(window.source_range.expect("source range").start_sequence, 1);
}
#[test]
fn min_estimated_rows_pulls_short_older_messages() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "a".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "b".to_owned(),
},
),
event(
session_id,
3,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "c".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
event(
session_id,
4,
SessionEventKind::AssistantMessage {
text: "d".to_owned(),
},
),
];
let window = projection_window_from_events(
&events,
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: None,
min_estimated_rows: Some(3),
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 4096,
},
},
)
.expect("latest transcript windows are supported");
assert_eq!(window.transcript_items.len(), 3);
assert_eq!(window.source_range.expect("source range").start_sequence, 2);
}
#[test]
fn latest_projection_window_respects_byte_cap() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "older".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "newer".to_owned(),
},
),
];
let window = projection_window_from_events(
&events,
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(2),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 64,
max_bytes: 5,
},
},
)
.expect("latest transcript windows are supported");
assert_eq!(window.transcript_items.len(), 1);
assert_eq!(window.source_range.expect("source range").start_sequence, 2);
}
#[test]
fn latest_projection_window_respects_scan_cap() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::UserMessage {
client_id: ClientId::new(),
text: "older".to_owned(),
admission: bcode_session_models::TurnAdmissionMetadata::default(),
},
),
event(
session_id,
2,
SessionEventKind::AssistantMessage {
text: "newer".to_owned(),
},
),
];
let window = projection_window_from_events(
&events,
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: ProjectionWindowTarget {
min_items: Some(2),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 8,
max_events_scanned: 1,
max_bytes: 4096,
},
},
)
.expect("latest transcript windows are supported");
assert_eq!(window.scanned_events, 1);
assert_eq!(window.transcript_items.len(), 1);
assert!(window.has_older);
}
#[test]
fn capped_projection_pages_leave_unselected_items_for_next_page() {
use bcode_session_models::{CURRENT_SESSION_EVENT_SCHEMA_VERSION, SessionId};
let session_id = SessionId::new();
let events = (1..=5)
.map(|sequence| SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::AssistantMessage {
text: format!("visible {sequence}"),
},
})
.collect::<Vec<_>>();
let request = |anchor, direction| ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor,
direction,
target: bcode_session_models::ProjectionWindowTarget {
min_items: Some(1),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: bcode_session_models::ProjectionWindowLimits {
max_items: 1,
max_events_scanned: 5,
max_bytes: 4096,
},
};
for (anchor, direction, expected) in [
(
ProjectionWindowAnchor::BeforeSequence(6),
ProjectionWindowDirection::Backward,
ProjectionSourceRange {
start_sequence: 5,
end_sequence: 5,
},
),
(
ProjectionWindowAnchor::AfterSequence(0),
ProjectionWindowDirection::Forward,
ProjectionSourceRange {
start_sequence: 1,
end_sequence: 1,
},
),
] {
let page = projection_window_from_events_with_source_bounds(
&events,
Some(1),
Some(5),
&request(anchor, direction),
)
.expect("projection page");
assert_eq!(page.source_range, Some(expected));
assert_eq!(page.transcript_items.len(), 1);
}
}
#[test]
fn forward_non_presenting_pages_advance_to_visible_content() {
use bcode_session_models::{CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, SessionId};
let session_id = SessionId::new();
let events = (1..=12)
.map(|sequence| SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind: if sequence == 11 {
SessionEventKind::AssistantMessage {
text: "visible after gap".into(),
}
} else {
SessionEventKind::ClientAttached {
client_id: ClientId::new(),
}
},
})
.collect::<Vec<_>>();
let mut cursor = 0;
let mut scanned = 0;
loop {
let start = usize::try_from(cursor).expect("bounded start");
let end = start.saturating_add(3).min(events.len());
let page = projection_window_from_events_with_source_bounds(
&events[start..end],
Some(1),
Some(12),
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::AfterSequence(cursor),
direction: ProjectionWindowDirection::Forward,
target: bcode_session_models::ProjectionWindowTarget {
min_items: Some(1),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: bcode_session_models::ProjectionWindowLimits {
max_items: 1,
max_events_scanned: 3,
max_bytes: 4096,
},
},
)
.expect("bounded page");
assert!(page.scanned_events <= 3);
let range = page
.source_range
.expect("even empty pages advance the cursor");
assert_eq!(range.start_sequence, cursor + 1);
assert!(range.end_sequence > cursor);
scanned += page.scanned_events;
cursor = range.end_sequence;
if cursor >= 11 {
assert_eq!(page.transcript_items.len(), 1);
assert!(page.has_newer);
break;
}
assert!(page.transcript_items.is_empty());
assert!(page.has_newer);
}
assert!(scanned > 3);
}
#[test]
fn non_presenting_page_advances_across_multiple_bounded_scans() {
use bcode_session_models::{CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, SessionId};
let session_id = SessionId::new();
let events = (1..=12)
.map(|sequence| SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind: if sequence == 2 {
SessionEventKind::AssistantMessage {
text: "visible before gap".into(),
}
} else {
SessionEventKind::ClientAttached {
client_id: ClientId::new(),
}
},
})
.collect::<Vec<_>>();
let mut cursor = 13;
let mut scanned = 0;
while cursor > 1 {
let end = usize::try_from(cursor - 1).expect("bounded end");
let start = end.saturating_sub(3);
let request = ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::BeforeSequence(cursor),
direction: ProjectionWindowDirection::Backward,
target: bcode_session_models::ProjectionWindowTarget {
min_items: Some(1),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: bcode_session_models::ProjectionWindowLimits {
max_items: 1,
max_events_scanned: 3,
max_bytes: 4096,
},
};
let page = projection_window_from_events_with_source_bounds(
&events[start..end],
Some(1),
Some(12),
&request,
)
.expect("bounded page");
assert!(page.scanned_events <= 3);
let range = page
.source_range
.expect("even empty pages advance the cursor");
assert_eq!(range.end_sequence + 1, cursor);
assert!(range.start_sequence < cursor);
scanned += page.scanned_events;
cursor = range.start_sequence;
if cursor <= 2 {
assert_eq!(page.transcript_items.len(), 1);
break;
}
assert!(page.transcript_items.is_empty());
assert!(page.has_older);
}
assert!(scanned > 3);
}
#[test]
fn latest_bounded_projection_does_not_claim_unscanned_source_events() {
use bcode_session_models::{CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, SessionId};
let session_id = SessionId::new();
let events = (1..=5)
.map(|sequence| SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::ClientAttached {
client_id: ClientId::new(),
},
})
.collect::<Vec<_>>();
let window = projection_window_from_events(
&events,
&ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor: ProjectionWindowAnchor::Latest,
direction: ProjectionWindowDirection::Backward,
target: bcode_session_models::ProjectionWindowTarget::default(),
limits: bcode_session_models::ProjectionWindowLimits {
max_items: 1,
max_events_scanned: 2,
max_bytes: 4096,
},
},
)
.expect("bounded latest projection");
assert!(window.transcript_items.is_empty());
assert_eq!(window.scanned_events, 2);
assert_eq!(
window.source_range,
Some(ProjectionSourceRange {
start_sequence: 4,
end_sequence: 5,
})
);
assert!(window.has_older);
assert!(!window.has_newer);
}
#[test]
fn projection_page_includes_non_presenting_events_between_item_and_cursor() {
use bcode_session_models::{CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, SessionId};
let session_id = SessionId::new();
let events = (11..=15)
.map(|sequence| SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind: if sequence == 13 {
SessionEventKind::AssistantMessage {
text: "visible".into(),
}
} else {
SessionEventKind::ClientAttached {
client_id: ClientId::new(),
}
},
})
.collect::<Vec<_>>();
let request = |anchor, direction| ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor,
direction,
target: bcode_session_models::ProjectionWindowTarget {
min_items: Some(1),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: bcode_session_models::ProjectionWindowLimits {
max_items: 1,
max_events_scanned: 5,
max_bytes: 4096,
},
};
for (anchor, direction, expected_range) in [
(
ProjectionWindowAnchor::BeforeSequence(16),
ProjectionWindowDirection::Backward,
ProjectionSourceRange {
start_sequence: 13,
end_sequence: 15,
},
),
(
ProjectionWindowAnchor::AfterSequence(10),
ProjectionWindowDirection::Forward,
ProjectionSourceRange {
start_sequence: 11,
end_sequence: 13,
},
),
] {
let window = projection_window_from_events_with_source_bounds(
&events,
Some(1),
Some(20),
&request(anchor, direction),
)
.expect("supported projection");
assert_eq!(window.transcript_items.len(), 1);
assert_eq!(window.source_range, Some(expected_range));
assert!(window.has_older && window.has_newer);
}
}
#[test]
fn non_presenting_projection_scan_advances_without_claiming_exhaustion() {
use bcode_session_models::{CURRENT_SESSION_EVENT_SCHEMA_VERSION, ClientId, SessionId};
let session_id = SessionId::new();
let events = (11..=13)
.map(|sequence| SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind: SessionEventKind::ClientAttached {
client_id: ClientId::new(),
},
})
.collect::<Vec<_>>();
let request = |anchor, direction| ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor,
direction,
target: bcode_session_models::ProjectionWindowTarget {
min_items: Some(2),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: bcode_session_models::ProjectionWindowLimits {
max_items: 2,
max_events_scanned: 3,
max_bytes: 4096,
},
};
for (anchor, direction) in [
(
ProjectionWindowAnchor::BeforeSequence(14),
ProjectionWindowDirection::Backward,
),
(
ProjectionWindowAnchor::AfterSequence(10),
ProjectionWindowDirection::Forward,
),
] {
let window = projection_window_from_events_with_source_bounds(
&events,
Some(1),
Some(20),
&request(anchor, direction),
)
.expect("supported projection");
assert!(window.transcript_items.is_empty());
assert_eq!(
window.source_range,
Some(ProjectionSourceRange {
start_sequence: 11,
end_sequence: 13,
})
);
assert!(window.has_older);
assert!(window.has_newer);
assert_eq!(window.scanned_events, 3);
}
}
#[test]
fn projection_windows_page_strictly_before_and_after_source_anchors() {
let entries = (1..=6)
.map(|sequence| {
db_transcript_item(sequence, TranscriptProjectionItemKind::UserMessage, 5)
})
.collect::<Vec<_>>();
let request = |anchor, direction| ProjectionWindowRequest {
projection: SessionProjectionKind::Transcript,
anchor,
direction,
target: ProjectionWindowTarget {
min_items: Some(2),
min_estimated_rows: None,
min_bytes: None,
width_columns: Some(80),
},
limits: ProjectionWindowLimits {
max_items: 2,
max_events_scanned: 8,
max_bytes: 4096,
},
};
let older = projection_window_from_db_transcript_items(
&entries,
Some(1),
Some(6),
&request(
ProjectionWindowAnchor::BeforeSequence(5),
ProjectionWindowDirection::Backward,
),
)
.expect("older transcript windows are supported");
assert_eq!(
older.source_range,
Some(ProjectionSourceRange {
start_sequence: 3,
end_sequence: 4,
})
);
assert!(older.has_older);
assert!(older.has_newer);
let newer = projection_window_from_db_transcript_items(
&entries,
Some(1),
Some(6),
&request(
ProjectionWindowAnchor::AfterSequence(2),
ProjectionWindowDirection::Forward,
),
)
.expect("newer transcript windows are supported");
assert_eq!(
newer.source_range,
Some(ProjectionSourceRange {
start_sequence: 3,
end_sequence: 4,
})
);
assert!(newer.has_older);
assert!(newer.has_newer);
}
#[test]
fn transcript_projection_sorts_unfinished_tool_invocations_by_source_sequence() {
let session_id = SessionId::new();
let events = vec![
event(
session_id,
1,
SessionEventKind::ToolCallRequested {
tool_call_id: "z".to_owned(),
producer_plugin_id: None,
tool_name: "late".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
),
event(
session_id,
2,
SessionEventKind::ToolCallRequested {
tool_call_id: "a".to_owned(),
producer_plugin_id: None,
tool_name: "early".to_owned(),
arguments_json: "{}".to_owned(),
working_directory: None,
},
),
event(
session_id,
3,
SessionEventKind::AssistantMessage {
text: "done".to_owned(),
},
),
];
let items = build_transcript_projection(&events, None);
let ranges = items
.iter()
.map(|item| item.source_range.start_sequence)
.collect::<Vec<_>>();
assert_eq!(ranges, vec![1, 2, 3]);
}
fn db_transcript_item(
sequence: u64,
kind: TranscriptProjectionItemKind,
content_bytes: usize,
) -> crate::db::TranscriptItem {
let (role, db_kind) = match kind {
TranscriptProjectionItemKind::UserMessage => ("user", "message"),
TranscriptProjectionItemKind::AssistantMessage => ("assistant", "message"),
TranscriptProjectionItemKind::ToolInvocation => ("tool", "invocation"),
_ => ("system", "other"),
};
crate::db::TranscriptItem {
transcript_seq: sequence,
event_seq_start: sequence,
event_seq_end: sequence,
role: role.to_string(),
kind: db_kind.to_string(),
status: "complete".to_string(),
content: Some("x".repeat(content_bytes)),
}
}
fn event(session_id: SessionId, sequence: u64, kind: SessionEventKind) -> SessionEvent {
SessionEvent {
schema_version: CURRENT_SESSION_EVENT_SCHEMA_VERSION,
sequence,
timestamp_ms: 1,
session_id,
provenance: None,
kind,
}
}
}