From 83b5fc3bba5e3df5f96804ce1622b62d22f198cb Mon Sep 17 00:00:00 2001 From: Sabhatina Selvam Date: Thu, 6 Aug 2026 11:28:19 -0700 Subject: [PATCH] fix(libsy): keep tier prompts and handoff notes on a same-format hop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A codec replays the preserved inbound body verbatim when the target format matches the source, which is what keeps a same-format hop lossless. That body is captured at decode, before a routing algorithm has added anything, so replaying it discarded whatever was added: stage_router's tier system prompts and its handoff notes never reached the model whenever the inbound request and the selected target shared a format. The encode path was already correct — it just was not reached. Dropping the preserved body once something has been added sends the codec down its normal path, which encodes from the request itself. Not OpenAI-specific: all three buffered codecs short-circuit the same way, so Anthropic-to-Anthropic and Responses-to-Responses dropped it too. Cross-format escaped only because the body is keyed by inbound format, so the target codec's lookup missed and fell through to the same normal path. Fixes SWITCH-1224 Signed-off-by: Sabhatina Selvam --- crates/libsy/src/algorithms/util/prompts.rs | 76 +++++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/crates/libsy/src/algorithms/util/prompts.rs b/crates/libsy/src/algorithms/util/prompts.rs index e4270847c..9b82dadc9 100644 --- a/crates/libsy/src/algorithms/util/prompts.rs +++ b/crates/libsy/src/algorithms/util/prompts.rs @@ -14,6 +14,14 @@ //! //! Which text, and when, is the caller's policy; this module only knows how to //! place it so the provider accepts it and the prompt cache survives. +//! +//! **Anything added here must call [`drop_exact_replay`].** Both shapes above +//! mutate the normalized request, and a codec asked to encode for the format the +//! request arrived in replays the body captured at decode instead of reading that +//! request — so an addition that leaves exact replay in place never reaches the +//! model. This is not enforced: a future processor that mutates the request and +//! forgets the call reintroduces SWITCH-1224, silently and without a failing +//! test. use std::collections::BTreeMap; @@ -41,6 +49,25 @@ pub fn append_note(request: &mut Request, note: &str) { .messages .push(Message::text(Role::User, note)), } + drop_exact_replay(request); +} + +/// Gives up exact same-format replay for this turn. +/// +/// A codec replays the preserved inbound body verbatim when the target format +/// matches the source, which is what keeps a same-format hop lossless. That body +/// predates anything added here, so leaving it in place would encode the request +/// as it arrived and silently drop the addition. Dropping it sends the codec down +/// its normal path, which encodes from the request itself. +/// +/// Every stored body goes, not just the one for the inbound format: preservation +/// also carries bodies embedded by earlier hops, and the addition is missing from +/// all of them equally. +/// +/// Call this from any new code that mutates the request. Nothing checks that you +/// have. +fn drop_exact_replay(request: &mut Request) { + request.llm_request.preservation.requests.clear(); } /// System prompts keyed by routing target. A target left unset is routed @@ -104,6 +131,7 @@ impl Processor for SystemPromptProcessor { }], }, ); + drop_exact_replay(request); Ok(()) } } @@ -117,10 +145,13 @@ mod tests { const STRONG_PROMPT: &str = "diagnose before you edit"; const WEAK_PROMPT: &str = "follow the settled plan"; + /// Every test request carries the exact inbound body a codec keeps for + /// same-format replay, so each assertion below also says what happens to it. fn request_with(messages: Vec) -> Request { Request { llm_request: LlmRequest { messages, + preservation: preserved_body(), ..LlmRequest::default() }, raw_request: None, @@ -128,6 +159,19 @@ mod tests { } } + fn preserved_body() -> switchyard_protocol::PreservationMetadata { + let mut preservation = switchyard_protocol::PreservationMetadata::default(); + preservation.requests.insert( + "openai_chat".into(), + serde_json::json!({"model": "weak", "messages": [{"role": "user", "content": "hi"}]}), + ); + preservation + } + + fn replays_exactly(request: &Request) -> bool { + !request.llm_request.preservation.requests.is_empty() + } + #[test] fn a_note_joins_a_trailing_user_turn_after_its_tool_result() { // The shape a coding-agent turn actually arrives in: the tool result @@ -174,7 +218,10 @@ mod tests { #[test] fn a_note_leaves_the_rest_of_the_conversation_untouched() { let mut request = Request { - llm_request: text_request(Some("auto".to_string()), "fix the build"), + llm_request: LlmRequest { + preservation: preserved_body(), + ..text_request(Some("auto".to_string()), "fix the build") + }, raw_request: None, metadata: None, }; @@ -188,6 +235,10 @@ mod tests { .filter_map(|message| message.text_content("|")) .collect(); assert_eq!(trail, vec![format!("fix the build|{NOTE}")]); + assert!( + !replays_exactly(&request), + "a same-format hop would replay the body captured before the note" + ); } /// A decision routed to `target`. @@ -221,7 +272,13 @@ mod tests { /// Runs one outbound request routed to `target` through `processor`. async fn run(processor: &SystemPromptProcessor, target: &'static str) -> Result { - let mut request = Request::default(); + let mut request = Request { + llm_request: LlmRequest { + preservation: preserved_body(), + ..LlmRequest::default() + }, + ..Request::default() + }; processor .process( &mut (), @@ -244,9 +301,11 @@ mod tests { async fn each_target_gets_its_own_prompt() -> Result<()> { let processor = SystemPromptProcessor::new(prompts()); for (target, expected) in [("strong", STRONG_PROMPT), ("weak", WEAK_PROMPT)] { - assert_eq!( - instructions(&run(&processor, target).await?), - vec![expected] + let request = run(&processor, target).await?; + assert_eq!(instructions(&request), vec![expected]); + assert!( + !replays_exactly(&request), + "{target}: a same-format hop would replay the body captured before the prompt" ); } Ok(()) @@ -261,7 +320,12 @@ mod tests { instructions(&run(&processor, "strong").await?), vec![STRONG_PROMPT] ); - assert!(instructions(&run(&processor, "weak").await?).is_empty()); + let untouched = run(&processor, "weak").await?; + assert!(instructions(&untouched).is_empty()); + assert!( + replays_exactly(&untouched), + "an untouched request must keep its lossless same-format replay" + ); Ok(()) }