use runtime_extension_api::ContextItem;
use runtime_kernel_api::KernelFailure;
use runtime_ports::{ConversationMessage, ModelMessage, ModelRole};
pub struct ContextManager {
max_messages: usize,
}
impl ContextManager {
pub fn new(max_messages: usize) -> Result<Self, KernelFailure> {
if max_messages < 2 {
return Err(KernelFailure::new(
"CONTEXT_LIMIT_INVALID",
"context requires at least 2 messages",
));
}
Ok(Self { max_messages })
}
pub fn assemble(
&self,
system_prompt: &str,
mut context_items: Vec<ContextItem>,
history: Vec<ConversationMessage>,
request_messages: &[ConversationMessage],
) -> Result<Vec<ModelMessage>, KernelFailure> {
let system = self.render_system(system_prompt, &mut context_items)?;
if request_messages.is_empty() || request_messages.len() >= self.max_messages {
return Err(KernelFailure::new(
"REQUEST_TRANSCRIPT_INVALID",
"request messages must fit within the configured context message limit",
));
}
let mut selected = Vec::new();
let history_slots = self.max_messages.saturating_sub(1 + request_messages.len());
for message in history.iter().rev().take(history_slots) {
selected.push(message.clone());
}
selected.reverse();
let dropped = history.len().saturating_sub(selected.len());
if dropped > 0 && history_slots > 0 {
let checkpoint = structured_checkpoint(dropped, &history);
if selected.len() >= history_slots && !selected.is_empty() {
selected.remove(0);
}
selected.insert(
0,
ConversationMessage {
role: ModelRole::System,
text: checkpoint,
},
);
}
let mut messages = Vec::with_capacity(selected.len() + request_messages.len() + 1);
messages.push(ModelMessage::text(ModelRole::System, system));
messages.extend(
selected
.into_iter()
.map(|message| ModelMessage::text(message.role, message.text)),
);
messages.extend(
request_messages
.iter()
.map(|message| ModelMessage::text(message.role, message.text.clone())),
);
Ok(messages)
}
pub fn refresh_system(
&self,
messages: &mut [ModelMessage],
system_prompt: &str,
mut context_items: Vec<ContextItem>,
) -> Result<(), KernelFailure> {
let system = self.render_system(system_prompt, &mut context_items)?;
let first = messages.first_mut().ok_or_else(|| {
KernelFailure::new("CONTEXT_TRANSCRIPT_INVALID", "model transcript is empty")
})?;
if first.role != ModelRole::System {
return Err(KernelFailure::new(
"CONTEXT_TRANSCRIPT_INVALID",
"model transcript must begin with system context",
));
}
*first = ModelMessage::text(ModelRole::System, system);
Ok(())
}
fn render_system(
&self,
system_prompt: &str,
context_items: &mut [ContextItem],
) -> Result<String, KernelFailure> {
context_items.sort_by(|left, right| {
right
.required
.cmp(&left.required)
.then_with(|| right.priority.cmp(&left.priority))
.then_with(|| left.source.cmp(&right.source))
});
let mut system = system_prompt.trim().to_string();
for item in context_items {
if !item.content.trim().is_empty() {
system.push_str("\n\n<context source=\"");
system.push_str(&item.source);
system.push_str("\">\n");
system.push_str(item.content.trim());
system.push_str("\n</context>");
}
}
Ok(system)
}
}
fn structured_checkpoint(dropped: usize, history: &[ConversationMessage]) -> String {
let goal = history
.iter()
.find(|message| message.role == ModelRole::User)
.map(|message| truncate(&message.text, 512))
.unwrap_or_else(|| "unknown".to_string());
format!(
"<checkpoint>\ngoal: {goal}\ndone: {dropped} earlier conversation messages compacted\nstate: later messages remain authoritative\nwatch: verify assumptions before destructive actions\nnext: continue from the most recent user request\n</checkpoint>"
)
}
fn truncate(value: &str, max: usize) -> String {
if value.len() <= max {
return value.to_string();
}
let mut boundary = max;
while !value.is_char_boundary(boundary) {
boundary -= 1;
}
format!("{}…", &value[..boundary])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_required_edges_and_compacts_old_history() {
let manager = ContextManager::new(4).unwrap();
let history = (0..10)
.map(|index| ConversationMessage {
role: if index % 2 == 0 {
ModelRole::User
} else {
ModelRole::Assistant
},
text: format!("message-{index}"),
})
.collect();
let messages = manager
.assemble(
"system",
Vec::new(),
history,
&[ConversationMessage {
role: ModelRole::User,
text: "current".into(),
}],
)
.unwrap();
assert_eq!(messages.first().unwrap().role, ModelRole::System);
assert_eq!(messages.last().unwrap().role, ModelRole::User);
assert!(messages.len() <= 5);
assert!(format!("{:?}", messages).contains("checkpoint"));
}
}