Skip to main content

switchyard_translation/
compaction.rs

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