Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions crates/libsy/src/algorithms/llm_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ impl ClassifierInput for TaskInput {
Some(window) => trim_messages(&request.llm_request.messages, window),
None => task_messages(&request.llm_request.messages),
};
// Reasoning is provider-private and not required to classify the task. Some
// upstreams also reject an unsigned reasoning item replayed without the
// opaque state it was issued with, so it cannot travel through a windowed
// classifier request as ordinary history.
for message in &mut messages {
message
.content
.retain(|block| !matches!(block, ContentBlock::Reasoning { .. }));
Comment thread
ayushag-nv marked this conversation as resolved.
}
messages.retain(|message| !message.content.is_empty());
// Only the windowed path carries assistant turns and tool traffic for the judge
// to be distracted by. The default path is user task messages only — the anchor
// and the latest follow-up — so there is nothing there to outrank.
Expand Down Expand Up @@ -1467,6 +1477,74 @@ mod tests {
Ok(())
}

/// Reasoning is provider-private and some upstreams reject it replayed without the
/// opaque state it was issued with, so it must not reach a windowed classifier
/// request. Visible assistant text and complete tool pairs still do, and a turn
/// whose only content was reasoning is dropped rather than left behind empty.
#[test]
fn a_window_drops_reasoning_but_keeps_visible_text_and_tool_pairs() {
let messages = vec![
Message::text(Role::System, "client instructions"),
Message::text(Role::User, "initial task"),
Message {
role: Role::Assistant,
content: vec![
ContentBlock::Reasoning {
text: "private chain of thought".to_string(),
signature: None,
details: Vec::new(),
},
ContentBlock::Text {
text: "visible answer".to_string(),
},
],
},
tool_call("call-1"),
tool_result("call-1"),
Message {
role: Role::Assistant,
content: vec![ContentBlock::Reasoning {
text: "reasoning-only turn".to_string(),
signature: None,
details: Vec::new(),
}],
},
Message::text(Role::User, "follow-up"),
];
let request = Request {
llm_request: LlmRequest {
messages,
..LlmRequest::default()
},
raw_request: None,
metadata: None,
};

let built = TaskInput {
recent_turn_window: Some(10),
}
.build_messages(&State::default(), &request);

assert!(
!built
.iter()
.flat_map(|message| &message.content)
.any(|block| matches!(block, ContentBlock::Reasoning { .. })),
"{built:?}"
);
assert!(
built
.iter()
.any(|message| message.text_content("\n").as_deref() == Some("visible answer"))
);
assert!(built.contains(&tool_call("call-1")));
assert!(built.contains(&tool_result("call-1")));
// Six of the seven fixtures survive — the reasoning-only turn is gone entirely
// rather than left behind empty — plus the trailing routing instruction.
assert_eq!(built.len(), 7);
assert!(built.iter().all(|message| !message.content.is_empty()));
}

#[test]
fn the_default_path_is_left_unchanged() -> Result<()> {
// No window means no conversation to be distracted by, so the default
Expand Down