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