switchyard_translation/
compaction.rs1use http::HeaderMap;
5use switchyard_protocol::{ContentBlock, LlmRequest, Message, Role};
6
7pub fn normalize_compaction(request: &mut LlmRequest, headers: Option<&HeaderMap>) {
9 let is_compaction = headers
10 .and_then(|headers| headers.get("x-codex-turn-metadata"))
11 .and_then(|value| serde_json::from_slice::<serde_json::Value>(value.as_bytes()).ok())
12 .is_some_and(|metadata| metadata["request_kind"] == "compaction");
13 request.has_compaction |= request
14 .messages
15 .iter()
16 .flat_map(|message| &message.content)
17 .any(|block| match block {
18 ContentBlock::Text { text } => {
19 let text = text.to_lowercase();
20 text.contains("session is being continued") || has_hermes_marker(&text)
21 }
22 _ => false,
23 });
24 request.is_compaction |= is_compaction
25 || request
26 .messages
27 .iter()
28 .rev()
29 .find(|message| !is_environment_update(message))
30 .is_some_and(|message| message.role == Role::User && is_compaction_message(message));
31}
32
33fn is_environment_update(message: &Message) -> bool {
34 message.role == Role::User
35 && !message.content.is_empty()
36 && message.content.iter().all(|block| {
37 matches!(block, ContentBlock::Text { text } if text.trim()
38 .strip_prefix("<environment_context>")
39 .and_then(|context| context.split_once("</environment_context>"))
40 .is_some_and(|(_, rest)| rest.trim().is_empty()))
41 })
42}
43
44fn is_compaction_message(message: &Message) -> bool {
45 let mut has_summary = false;
46 for block in &message.content {
47 match block {
48 ContentBlock::Image { .. } => {}
49 _ if is_compaction_block(block) => has_summary = true,
50 _ => return false,
51 }
52 }
53 has_summary
54}
55
56fn is_compaction_block(block: &ContentBlock) -> bool {
57 match block {
58 ContentBlock::Unknown { raw, .. } => matches!(
59 raw.get("type").and_then(serde_json::Value::as_str),
60 Some("compaction" | "context_compaction" | "compaction_trigger")
61 ),
62 ContentBlock::Text { text } => {
63 let text = text.trim().to_ascii_lowercase();
64 text.starts_with("another language model started to solve this problem and produced a summary of its thinking process.")
65 || text.starts_with("this session is being continued from a previous conversation that ran out of context.")
66 || (text.starts_with("the conversation history before this point was compacted into the following summary:")
67 && text.ends_with("</summary>"))
68 || (text.starts_with("prior model work/tool state available.")
69 && text.ends_with("</summary>"))
70 || (text.starts_with("<conversation-checkpoint>")
71 && text.ends_with("</conversation-checkpoint>"))
72 || is_hermes_summary(&text)
73 }
74 _ => false,
75 }
76}
77
78const HERMES_REPLAY: &str = "[still in progress \u{2014} this is the active request, restated after the compaction boundary";
79
80fn is_hermes_summary(text: &str) -> bool {
81 const END: &str = "--- end of context summary \u{2014} respond to the message below, not the summary above ---";
82 if let Some((_, tail)) = text.rsplit_once(END)
83 && !tail.trim().is_empty()
84 && !tail.trim_start().starts_with(HERMES_REPLAY)
85 {
86 return false;
87 }
88 has_hermes_marker(text)
89 || text
90 .lines()
91 .any(|line| line.trim_start().starts_with("[context summary]:"))
92}
93
94fn has_hermes_marker(text: &str) -> bool {
95 text.lines().any(|line| {
96 let line = line.trim_start();
97 line.starts_with("[context compaction \u{2014} reference only]")
98 || line.starts_with(HERMES_REPLAY)
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use crate::{WireFormat, decode_request};
105 use serde_json::json;
106
107 #[test]
108 fn summary_continues_but_fresh_user_input_does_not() {
109 let mut body = json!({"messages": [
110 {"role": "user", "content": [
111 {"type": "text", "text": "prior context\n[CONTEXT COMPACTION \u{2014} REFERENCE ONLY] summary"},
112 {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}
113 ]},
114 {"role": "user", "content": "<environment_context>updated</environment_context>"}
115 ]});
116 let request = decode_request(WireFormat::OpenAiChat, &body).unwrap();
117 assert!(request.is_compaction && request.has_compaction);
118 body["messages"]
119 .as_array_mut()
120 .unwrap()
121 .push(json!({"role": "user", "content": "Now fix the parser"}));
122 let followup = decode_request(WireFormat::OpenAiChat, &body).unwrap();
123 assert!(!followup.is_compaction && followup.has_compaction);
124 }
125}