#![cfg_attr(
any(test, feature = "test-utils"),
expect(
clippy::panic,
reason = "harness-only sequence assertions; log-only outside rig's own test builds"
)
)]
use crate::streaming::RawStreamingChoice;
fn is_boundary_content<R>(choice: &RawStreamingChoice<R>) -> bool {
matches!(
choice,
RawStreamingChoice::Message(_)
| RawStreamingChoice::TextStart { .. }
| RawStreamingChoice::ToolCall(_)
| RawStreamingChoice::ToolCallDelta { .. }
)
}
#[derive(Default)]
pub(crate) struct SequenceLaws {
open_minted_reasoning: std::collections::HashSet<crate::streaming::StreamPartId>,
}
impl SequenceLaws {
pub(crate) fn check_batch<R>(
&mut self,
batch: &[Result<RawStreamingChoice<R>, crate::completion::CompletionError>],
) {
for item in batch {
let Ok(choice) = item else { continue };
if !self.open_minted_reasoning.is_empty() && is_boundary_content(choice) {
violation(
"boundary",
variant_name(choice),
"emitted while a minted-key reasoning part is open — a \
boundary-less wire's adapter must synthesize ReasoningEnd \
before any other content class",
);
}
match choice {
RawStreamingChoice::ReasoningStart { id, .. }
| RawStreamingChoice::ReasoningDelta { id, .. } => {
if id.is_minted() {
self.open_minted_reasoning.insert(id.clone());
}
}
RawStreamingChoice::ReasoningEnd { id, .. } => {
self.open_minted_reasoning.remove(id);
}
RawStreamingChoice::Reasoning { id, .. } => {
self.open_minted_reasoning.remove(id);
}
_ => {}
}
}
}
}
fn violation(law: &'static str, variant: &'static str, message: &'static str) {
tracing::error!(
target: "rig::sequence_law",
law,
variant,
"sequence-law violation: {message}"
);
#[cfg(any(test, feature = "test-utils"))]
panic!("sequence-law violation ({law}): {variant} {message}");
}
fn variant_name<R>(choice: &RawStreamingChoice<R>) -> &'static str {
match choice {
RawStreamingChoice::Message(_) => "Message",
RawStreamingChoice::TextStart { .. } => "TextStart",
RawStreamingChoice::TextEnd { .. } => "TextEnd",
RawStreamingChoice::TextAdditionalParams(_) => "TextAdditionalParams",
RawStreamingChoice::ToolCall(_) => "ToolCall",
RawStreamingChoice::ToolCallDelta { .. } => "ToolCallDelta",
RawStreamingChoice::ToolInputEnd(_) => "ToolInputEnd",
RawStreamingChoice::Reasoning { .. } => "Reasoning",
RawStreamingChoice::ReasoningStart { .. } => "ReasoningStart",
RawStreamingChoice::ReasoningDelta { .. } => "ReasoningDelta",
RawStreamingChoice::ReasoningEnd { .. } => "ReasoningEnd",
RawStreamingChoice::FinalResponse(_) => "FinalResponse",
RawStreamingChoice::MessageId(_) => "MessageId",
RawStreamingChoice::Unknown(_) => "Unknown",
}
}
#[cfg(test)]
mod tests {
use super::super::adapter::{AdapterOutput, WireAdapter, run_wire_buffered};
use super::super::wire::WireEvent;
use crate::streaming::{MintKind, RawStreamingChoice, StreamPartId};
struct Scripted {
batches: Vec<Vec<RawStreamingChoice<()>>>,
}
impl WireAdapter for Scripted {
type Frame = usize;
type Event = usize;
type Response = ();
fn classify(&self, frame: usize) -> WireEvent<usize> {
WireEvent::Known(frame)
}
fn interpret(&mut self, event: usize, out: &mut AdapterOutput<()>) {
if let Some(batch) = self.batches.get_mut(event) {
out.extend(std::mem::take(batch).into_iter().map(Ok));
}
}
fn finish(&mut self, _out: &mut AdapterOutput<()>) {}
}
fn drive(batches: Vec<Vec<RawStreamingChoice<()>>>) {
let frames = 0..batches.len();
run_wire_buffered(frames, Scripted { batches }).expect("no data errors");
}
fn minted_delta() -> RawStreamingChoice<()> {
RawStreamingChoice::ReasoningDelta {
id: StreamPartId::minted(MintKind::Reasoning, 0),
provider_id: None,
reasoning: "thinking".to_owned(),
}
}
fn minted_end() -> RawStreamingChoice<()> {
RawStreamingChoice::ReasoningEnd {
id: StreamPartId::minted(MintKind::Reasoning, 0),
reasoning: None,
signature: None,
wire_sent: false,
}
}
#[test]
#[should_panic(expected = "sequence-law violation (boundary): Message")]
fn text_while_a_minted_reasoning_part_is_open_panics() {
drive(vec![
vec![minted_delta()],
vec![RawStreamingChoice::Message("visible".to_owned())],
]);
}
#[test]
#[should_panic(expected = "sequence-law violation (boundary): ToolCallDelta")]
fn tool_content_while_a_minted_reasoning_part_is_open_panics() {
drive(vec![
vec![minted_delta()],
vec![RawStreamingChoice::ToolCallDelta {
id: StreamPartId::minted(MintKind::Tool, 0),
content: crate::streaming::ToolCallDeltaContent::Name("probe".to_owned()),
}],
]);
}
#[test]
fn a_synthesized_end_before_text_satisfies_the_boundary_law() {
drive(vec![
vec![minted_delta()],
vec![
minted_end(),
RawStreamingChoice::Message("visible".to_owned()),
],
]);
}
#[test]
fn a_wire_keyed_part_may_stay_open_across_interleaving() {
drive(vec![
vec![RawStreamingChoice::ReasoningDelta {
id: StreamPartId::wire("rs_1"),
provider_id: crate::streaming::WireId::new("rs_1"),
reasoning: "thinking".to_owned(),
}],
vec![RawStreamingChoice::Message("visible".to_owned())],
]);
}
#[test]
fn wire_order_within_a_batch_is_not_a_violation() {
drive(vec![vec![
RawStreamingChoice::Message("visible".to_owned()),
RawStreamingChoice::Reasoning {
id: StreamPartId::minted(MintKind::EncryptedReasoning, 0),
provider_id: None,
content: crate::message::ReasoningContent::Text {
text: "late".to_owned(),
signature: None,
},
},
]]);
}
#[test]
fn a_same_key_whole_block_closes_the_open_reasoning_part() {
let key = || StreamPartId::minted(MintKind::Block, 0);
drive(vec![
vec![RawStreamingChoice::ReasoningDelta {
id: key(),
provider_id: None,
reasoning: "thinking".to_owned(),
}],
vec![RawStreamingChoice::Reasoning {
id: key(),
provider_id: None,
content: crate::message::ReasoningContent::Text {
text: "thinking, complete".to_owned(),
signature: None,
},
}],
vec![RawStreamingChoice::Message("visible".to_owned())],
]);
}
#[test]
fn lifecycle_bookkeeping_is_not_boundary_content() {
drive(vec![vec![
RawStreamingChoice::Message("visible".to_owned()),
RawStreamingChoice::ToolInputEnd(crate::streaming::ToolInputEnd {
id: StreamPartId::minted(MintKind::Tool, 0),
tool_id: None,
call_id: None,
name: None,
arguments: None,
signature: None,
additional_params: None,
on_unparseable: crate::streaming::UnparseableToolInput::Drop,
}),
]]);
}
}