agent_runtime_kernel/
context.rs1use runtime_extension_api::ContextItem;
2use runtime_kernel_api::KernelFailure;
3use runtime_ports::{ConversationMessage, ModelMessage, ModelRole};
4
5pub struct ContextManager {
9 max_messages: usize,
10}
11
12impl ContextManager {
13 pub fn new(max_messages: usize) -> Result<Self, KernelFailure> {
14 if max_messages < 2 {
15 return Err(KernelFailure::new(
16 "CONTEXT_LIMIT_INVALID",
17 "context requires at least 2 messages",
18 ));
19 }
20 Ok(Self { max_messages })
21 }
22
23 pub fn assemble(
24 &self,
25 system_prompt: &str,
26 mut context_items: Vec<ContextItem>,
27 history: Vec<ConversationMessage>,
28 user_prompt: &str,
29 ) -> Result<Vec<ModelMessage>, KernelFailure> {
30 let system = self.render_system(system_prompt, &mut context_items)?;
31
32 let mut selected = Vec::new();
33 let history_slots = self.max_messages.saturating_sub(2);
34 for message in history.iter().rev().take(history_slots) {
35 selected.push(message.clone());
36 }
37 selected.reverse();
38
39 let dropped = history.len().saturating_sub(selected.len());
40 if dropped > 0 && history_slots > 0 {
41 let checkpoint = structured_checkpoint(dropped, &history);
42 if selected.len() >= history_slots && !selected.is_empty() {
43 selected.remove(0);
44 }
45 selected.insert(
46 0,
47 ConversationMessage {
48 role: ModelRole::System,
49 text: checkpoint,
50 },
51 );
52 }
53
54 let mut messages = Vec::with_capacity(selected.len() + 2);
55 messages.push(ModelMessage::text(ModelRole::System, system));
56 messages.extend(
57 selected
58 .into_iter()
59 .map(|message| ModelMessage::text(message.role, message.text)),
60 );
61 messages.push(ModelMessage::text(ModelRole::User, user_prompt));
62 Ok(messages)
63 }
64
65 pub fn refresh_system(
66 &self,
67 messages: &mut [ModelMessage],
68 system_prompt: &str,
69 mut context_items: Vec<ContextItem>,
70 ) -> Result<(), KernelFailure> {
71 let system = self.render_system(system_prompt, &mut context_items)?;
72 let first = messages.first_mut().ok_or_else(|| {
73 KernelFailure::new("CONTEXT_TRANSCRIPT_INVALID", "model transcript is empty")
74 })?;
75 if first.role != ModelRole::System {
76 return Err(KernelFailure::new(
77 "CONTEXT_TRANSCRIPT_INVALID",
78 "model transcript must begin with system context",
79 ));
80 }
81 *first = ModelMessage::text(ModelRole::System, system);
82 Ok(())
83 }
84
85 fn render_system(
86 &self,
87 system_prompt: &str,
88 context_items: &mut [ContextItem],
89 ) -> Result<String, KernelFailure> {
90 context_items.sort_by(|left, right| {
91 right
92 .required
93 .cmp(&left.required)
94 .then_with(|| right.priority.cmp(&left.priority))
95 .then_with(|| left.source.cmp(&right.source))
96 });
97
98 let mut system = system_prompt.trim().to_string();
99 for item in context_items {
100 if !item.content.trim().is_empty() {
101 system.push_str("\n\n<context source=\"");
102 system.push_str(&item.source);
103 system.push_str("\">\n");
104 system.push_str(item.content.trim());
105 system.push_str("\n</context>");
106 }
107 }
108
109 Ok(system)
110 }
111}
112
113fn structured_checkpoint(dropped: usize, history: &[ConversationMessage]) -> String {
114 let goal = history
115 .iter()
116 .find(|message| message.role == ModelRole::User)
117 .map(|message| truncate(&message.text, 512))
118 .unwrap_or_else(|| "unknown".to_string());
119 format!(
120 "<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>"
121 )
122}
123
124fn truncate(value: &str, max: usize) -> String {
125 if value.len() <= max {
126 return value.to_string();
127 }
128 let mut boundary = max;
129 while !value.is_char_boundary(boundary) {
130 boundary -= 1;
131 }
132 format!("{}…", &value[..boundary])
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn keeps_required_edges_and_compacts_old_history() {
141 let manager = ContextManager::new(4).unwrap();
142 let history = (0..10)
143 .map(|index| ConversationMessage {
144 role: if index % 2 == 0 {
145 ModelRole::User
146 } else {
147 ModelRole::Assistant
148 },
149 text: format!("message-{index}"),
150 })
151 .collect();
152 let messages = manager
153 .assemble("system", Vec::new(), history, "current")
154 .unwrap();
155 assert_eq!(messages.first().unwrap().role, ModelRole::System);
156 assert_eq!(messages.last().unwrap().role, ModelRole::User);
157 assert!(messages.len() <= 5);
158 assert!(format!("{:?}", messages).contains("checkpoint"));
159 }
160}