use serde_json::Value;
pub fn key_present_non_null(obj: &serde_json::Map<String, Value>, key: &str) -> bool {
obj.get(key).is_some_and(|v| !v.is_null())
}
pub fn classify_sse_control_outcome(v: &Value) -> &'static str {
let Some(obj) = v.as_object() else {
return "plain";
};
if key_present_non_null(obj, "error") {
return "stop";
}
const NON_NULL_KEYS: &[&str] = &[
"command_approval_request",
"clarification_questionnaire",
"assistant_answer_phase",
"turn_segment_start",
"turn_segment_end",
"tool_call",
"tool_output_chunk",
"tool_result",
"timeline_log",
"thinking_trace",
"conversation_saved",
"sse_capabilities",
"stream_draining",
];
for key in NON_NULL_KEYS {
if key_present_non_null(obj, key) {
return "handled";
}
}
const BOOL_TRUE_KEYS: &[&str] = &[
"turn_tool_phase_end",
"tool_running",
"parsing_tool_calls",
"workspace_changed",
];
for key in BOOL_TRUE_KEYS {
if v.get(key) == Some(&Value::Bool(true)) {
return "handled";
}
}
if v.get("tool_running") == Some(&Value::Bool(false))
|| v.get("parsing_tool_calls") == Some(&Value::Bool(false))
{
return "handled";
}
if v.get("chat_ui_separator")
.and_then(|x| x.as_bool())
.is_some()
{
return "handled";
}
if v.get("v").and_then(|x| x.as_u64()).is_some() {
return "handled";
}
"plain"
}
#[cfg(test)]
mod tests {
use super::classify_sse_control_outcome;
use serde_json::Value;
use std::path::PathBuf;
#[test]
fn golden_sse_control() {
let path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/sse_control_golden.jsonl");
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
for (line_no, line) in raw.lines().enumerate() {
let t = line.trim();
if t.is_empty() || t.starts_with('#') {
continue;
}
let parts: Vec<&str> = t.splitn(3, '\t').collect();
assert_eq!(
parts.len(),
3,
"{}:{}: expected 3 tab columns",
path.display(),
line_no + 1
);
let json_line = parts[1].trim();
let want = parts[2].trim();
let v: Value = serde_json::from_str(json_line).unwrap_or_else(|e| {
panic!(
"{}:{}: invalid JSON ({e}): {json_line}",
path.display(),
line_no + 1
)
});
let got = classify_sse_control_outcome(&v);
assert_eq!(
got,
want,
"{}:{}: classify mismatch\n desc: {}\n json: {json_line}\n want: {want}\n got: {got}",
path.display(),
line_no + 1,
parts[0],
);
}
}
}