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/// Normalizes current and historical compaction signals, preserving explicit IR 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    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| {
30                !matches!(message.role, Role::System | Role::Developer)
31                    && !is_environment_update(message)
32            })
33            .is_some_and(|message| message.role == Role::User && is_compaction_message(message));
34}
35
36fn is_environment_update(message: &Message) -> bool {
37    message.role == Role::User
38        && !message.content.is_empty()
39        && message.content.iter().all(|block| {
40            matches!(block, ContentBlock::Text { text } if text.trim()
41            .strip_prefix("<environment_context>")
42            .and_then(|context| context.split_once("</environment_context>"))
43            .is_some_and(|(_, rest)| rest.trim().is_empty()))
44        })
45}
46
47fn is_compaction_message(message: &Message) -> bool {
48    let mut has_summary = false;
49    for block in &message.content {
50        match block {
51            ContentBlock::Image { .. } => {}
52            _ if is_compaction_block(block) => has_summary = true,
53            _ => return false,
54        }
55    }
56    has_summary
57}
58
59fn is_compaction_block(block: &ContentBlock) -> bool {
60    match block {
61        ContentBlock::Unknown { raw, .. } => matches!(
62            raw.get("type").and_then(serde_json::Value::as_str),
63            Some("compaction" | "context_compaction" | "compaction_trigger")
64        ),
65        ContentBlock::Text { text } => {
66            let text = text.trim().to_ascii_lowercase();
67            text.starts_with("another language model started to solve this problem and produced a summary of its thinking process.")
68                || text.starts_with("this session is being continued from a previous conversation that ran out of context.")
69                || (text.starts_with("the conversation history before this point was compacted into the following summary:")
70                    && text.ends_with("</summary>"))
71                || (text.starts_with("prior model work/tool state available.")
72                    && text.ends_with("</summary>"))
73                || (text.starts_with("<conversation-checkpoint>")
74                    && text.ends_with("</conversation-checkpoint>"))
75                || is_hermes_summary(&text)
76        }
77        _ => false,
78    }
79}
80
81const HERMES_REPLAY: &str = "[still in progress \u{2014} this is the active request, restated after the compaction boundary";
82
83fn is_hermes_summary(text: &str) -> bool {
84    const END: &str = "--- end of context summary \u{2014} respond to the message below, not the summary above ---";
85    if let Some((_, tail)) = text.rsplit_once(END)
86        && !tail.trim().is_empty()
87        && !tail.trim_start().starts_with(HERMES_REPLAY)
88    {
89        return false;
90    }
91    has_hermes_marker(text)
92        || text
93            .lines()
94            .any(|line| line.trim_start().starts_with("[context summary]:"))
95}
96
97fn has_hermes_marker(text: &str) -> bool {
98    text.lines().any(|line| {
99        let line = line.trim_start();
100        line.starts_with("[context compaction \u{2014} reference only]")
101            || line.starts_with(HERMES_REPLAY)
102    })
103}
104
105#[cfg(test)]
106mod tests {
107    use crate::{WireFormat, decode_request};
108    use serde_json::json;
109
110    #[test]
111    fn summary_continues_but_fresh_user_input_does_not() {
112        let mut body = json!({"messages": [
113            {"role": "user", "content": [
114                {"type": "text", "text": "prior context\n[CONTEXT COMPACTION \u{2014} REFERENCE ONLY] summary"},
115                {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}
116            ]},
117            {"role": "user", "content": "<environment_context>updated</environment_context>"}
118        ]});
119        let request = decode_request(WireFormat::OpenAiChat, &body).unwrap();
120        assert!(request.is_compaction && request.has_compaction);
121        for role in [super::Role::System, super::Role::Developer] {
122            let mut with_instruction = request.clone();
123            with_instruction
124                .messages
125                .push(super::Message::text(role, "Follow repository instructions"));
126            with_instruction.is_compaction = false;
127            super::normalize_compaction(&mut with_instruction, None);
128            assert!(with_instruction.is_compaction);
129        }
130        body["messages"]
131            .as_array_mut()
132            .unwrap()
133            .push(json!({"role": "user", "content": "Now fix the parser"}));
134        let followup = decode_request(WireFormat::OpenAiChat, &body).unwrap();
135        assert!(!followup.is_compaction && followup.has_compaction);
136    }
137}