fix(sampler): Messages streams keep every thinking block and join split signatures

A turn with interleaved thinking lost all but its last signed block on
replay, and a signature split across deltas kept only its last part.
This commit is contained in:
雷电芽衣 committed 2026-09-29 13:32:03 -04:00
1 parent cb4c2eaad9
commit b840fff80c
2 files changed
+99 -10

No files matched your search

@@ -208,6 +208,91 @@ async fn thinking_block_emits_reasoning_channel_and_preserved_in_response() {
}
}
fn thinking_block(
index: u32,
start_signature: &str,
thinking: &str,
signature_deltas: &[&str],
) -> Vec<Result<MessageStreamEvent, SamplingError>> {
let mut events = vec![
Ok(MessageStreamEvent::ContentBlockStart {
index,
content_block: ContentBlock::Thinking {
thinking: String::new(),
signature: start_signature.into(),
},
}),
Ok(MessageStreamEvent::ContentBlockDelta {
index,
delta: StreamDelta::ThinkingDelta {
thinking: thinking.into(),
},
}),
];
for signature in signature_deltas {
events.push(Ok(MessageStreamEvent::ContentBlockDelta {
index,
delta: StreamDelta::SignatureDelta {
signature: (*signature).into(),
},
}));
}
events.push(Ok(block_stop(index)));
events
}
async fn reasoning_signatures(
blocks: Vec<Vec<Result<MessageStreamEvent, SamplingError>>>,
) -> Vec<(String, Option<String>)> {
let mut events = vec![Ok(message_start())];
events.extend(blocks.into_iter().flatten());
events.push(Ok(MessageStreamEvent::MessageStop));
let raw = stream::iter(events).boxed();
let evs = collect(stream_messages(raw, None, rid(), Duration::from_secs(60))).await;
match evs.last().unwrap() {
SamplingEvent::Completed { response, .. } => response
.reasoning_items()
.map(|r| {
let text = r
.summary
.iter()
.map(|rs::SummaryPart::SummaryText(t)| t.text.as_str())
.collect::<String>();
(text, r.encrypted_content.clone())
})
.collect(),
other => panic!("expected Completed, got {other:?}"),
}
}
#[tokio::test]
async fn every_thinking_block_is_kept_in_order() {
let items = reasoning_signatures(vec![
thinking_block(0, "", "first", &["sig-a"]),
thinking_block(1, "", "second", &["sig-b"]),
])
.await;
assert_eq!(
items,
vec![
("first".to_string(), Some("sig-a".to_string())),
("second".to_string(), Some("sig-b".to_string())),
]
);
}
#[tokio::test]
async fn split_signature_deltas_join_into_one_signature() {
let items = reasoning_signatures(vec![thinking_block(0, "", "t", &["abc", "def"])]).await;
assert_eq!(items, vec![("t".to_string(), Some("abcdef".to_string()))]);
}
#[tokio::test]
async fn first_signature_delta_replaces_a_start_seeded_signature() {
let items = reasoning_signatures(vec![thinking_block(0, "sig", "t", &["sig"])]).await;
assert_eq!(items, vec![("t".to_string(), Some("sig".to_string()))]);
}
#[tokio::test]
async fn tool_use_block_assembles_into_tool_call() {
let tool_start = MessageStreamEvent::ContentBlockStart {