use serde_json::Value;
#[derive(Debug, PartialEq, Eq)]
pub enum Chunk {
Text(String),
Reasoning(String),
ToolCall { id: String, name: String, input: Value },
ToolResult { id: String, name: String, result: Value, is_error: bool },
}
fn str_field(v: &Value, k: &str) -> String {
v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string()
}
pub fn convert_event(event: &Value) -> Vec<Chunk> {
let t = event.get("type").and_then(|v| v.as_str()).unwrap_or("");
match t {
"tool_execution_start" => vec![Chunk::ToolCall {
id: str_field(event, "toolCallId"),
name: str_field(event, "toolName"),
input: event.get("args").cloned().unwrap_or(Value::Null),
}],
"tool_execution_end" => {
let is_error = event.get("isError").and_then(|v| v.as_bool()).unwrap_or(false);
vec![Chunk::ToolResult {
id: str_field(event, "toolCallId"),
name: str_field(event, "toolName"),
result: event.get("result").cloned().unwrap_or(Value::Null),
is_error,
}]
}
_ => vec![],
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_execution_start_yields_tool_call() {
let chunks = convert_event(&json!({ "type": "tool_execution_start", "toolCallId": "call-1", "toolName": "read", "args": { "path": "a.rs" } }));
assert_eq!(chunks, vec![Chunk::ToolCall { id: "call-1".into(), name: "read".into(), input: json!({ "path": "a.rs" }) }]);
}
#[test]
fn tool_execution_end_yields_result() {
let chunks = convert_event(&json!({
"type": "tool_execution_end",
"toolCallId": "call-1",
"toolName": "read",
"result": { "ok": true }
}));
assert_eq!(
chunks,
vec![Chunk::ToolResult {
id: "call-1".into(),
name: "read".into(),
result: json!({ "ok": true }),
is_error: false,
}]
);
}
#[test]
fn tool_execution_end_flags_error() {
let chunks = convert_event(&json!({
"type": "tool_execution_end",
"toolCallId": "call-2",
"toolName": "read",
"isError": true,
"result": "boom"
}));
assert_eq!(
chunks,
vec![Chunk::ToolResult {
id: "call-2".into(),
name: "read".into(),
result: json!("boom"),
is_error: true,
}]
);
}
#[test]
fn missing_fields_default_to_empty() {
let chunks = convert_event(&json!({ "type": "tool_execution_end" }));
assert_eq!(
chunks,
vec![Chunk::ToolResult {
id: "".into(),
name: "".into(),
result: Value::Null,
is_error: false,
}]
);
}
#[test]
fn message_events_are_skipped() {
assert!(convert_event(&json!({
"type": "message_update",
"assistantMessageEvent": { "type": "text_delta", "delta": "hi" }
}))
.is_empty());
}
#[test]
fn unknown_events_are_skipped() {
assert!(convert_event(&json!({ "type": "ping" })).is_empty());
assert!(convert_event(&json!({})).is_empty());
}
}