use serde::{Deserialize, Serialize};
use crate::provider::{ContentPart, Message, Role};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PageKind {
Bootstrap,
Constraint,
Plan,
Preference,
Evidence,
Conversation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResidencyLevel {
Full,
Compressed,
Structured,
Pointer,
}
impl PageKind {
pub fn min_fidelity(self) -> ResidencyLevel {
match self {
PageKind::Bootstrap => ResidencyLevel::Structured,
PageKind::Constraint => ResidencyLevel::Structured,
PageKind::Plan => ResidencyLevel::Pointer,
PageKind::Preference => ResidencyLevel::Pointer,
PageKind::Evidence => ResidencyLevel::Pointer,
PageKind::Conversation => ResidencyLevel::Pointer,
}
}
pub fn degradation_path(self) -> &'static [ResidencyLevel] {
match self {
PageKind::Bootstrap | PageKind::Constraint => {
&[ResidencyLevel::Full, ResidencyLevel::Structured]
}
PageKind::Plan => &[
ResidencyLevel::Full,
ResidencyLevel::Structured,
ResidencyLevel::Pointer,
],
PageKind::Preference | PageKind::Evidence | PageKind::Conversation => &[
ResidencyLevel::Full,
ResidencyLevel::Compressed,
ResidencyLevel::Structured,
ResidencyLevel::Pointer,
],
}
}
}
pub fn classify(msg: &Message) -> PageKind {
if matches!(msg.role, Role::System) {
return PageKind::Bootstrap;
}
if matches!(msg.role, Role::Tool) {
return PageKind::Evidence;
}
for part in &msg.content {
if matches!(part, ContentPart::ToolResult { .. }) {
return PageKind::Evidence;
}
}
PageKind::Conversation
}
pub fn classify_all(messages: &[Message]) -> Vec<PageKind> {
messages.iter().map(classify).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn user(s: &str) -> Message {
Message {
role: Role::User,
content: vec![ContentPart::Text {
text: s.to_string(),
}],
}
}
fn assistant(s: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentPart::Text {
text: s.to_string(),
}],
}
}
fn tool_result(id: &str, body: &str) -> Message {
Message {
role: Role::Tool,
content: vec![ContentPart::ToolResult {
tool_call_id: id.to_string(),
content: body.to_string(),
}],
}
}
#[test]
fn classify_routes_system_to_bootstrap() {
let msg = Message {
role: Role::System,
content: vec![ContentPart::Text {
text: "you are…".to_string(),
}],
};
assert_eq!(classify(&msg), PageKind::Bootstrap);
}
#[test]
fn classify_routes_tool_results_to_evidence() {
assert_eq!(
classify(&tool_result("call-1", "output")),
PageKind::Evidence
);
}
#[test]
fn classify_defaults_user_and_assistant_to_conversation() {
assert_eq!(classify(&user("hi")), PageKind::Conversation);
assert_eq!(classify(&assistant("reply")), PageKind::Conversation);
}
#[test]
fn min_fidelity_never_drops_below_structured_for_invariant_pages() {
assert_eq!(
PageKind::Bootstrap.min_fidelity(),
ResidencyLevel::Structured
);
assert_eq!(
PageKind::Constraint.min_fidelity(),
ResidencyLevel::Structured
);
}
#[test]
fn degradation_paths_start_at_full() {
for kind in [
PageKind::Bootstrap,
PageKind::Constraint,
PageKind::Plan,
PageKind::Preference,
PageKind::Evidence,
PageKind::Conversation,
] {
assert_eq!(kind.degradation_path()[0], ResidencyLevel::Full);
}
}
#[test]
fn classify_all_is_parallel() {
let msgs = vec![user("a"), assistant("b"), tool_result("c", "out")];
let pages = classify_all(&msgs);
assert_eq!(pages.len(), 3);
assert_eq!(pages[0], PageKind::Conversation);
assert_eq!(pages[1], PageKind::Conversation);
assert_eq!(pages[2], PageKind::Evidence);
}
}