use serde_json::{json, Value};
#[must_use]
pub fn now_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
#[must_use]
pub fn pong(request_id: Option<&str>) -> Value {
let ts = now_ms();
let mut ev = json!({
"type": "pong",
"timestamp": ts,
"data": { "timestamp": ts },
});
set_request_id(&mut ev, request_id);
ev
}
#[must_use]
pub fn immediate_response(
request_id: Option<&str>,
status: i64,
message: &str,
data: Value,
) -> Value {
let mut ev = json!({
"type": "immediate_response",
"status": status,
"message": message,
"data": data,
"timestamp": now_ms(),
});
set_request_id(&mut ev, request_id);
ev
}
#[must_use]
pub fn stream_token(request_id: &str, token: &str) -> Value {
json!({
"type": "stream_token",
"requestId": request_id,
"token": token,
"data": { "requestId": request_id, "token": token },
"timestamp": now_ms(),
})
}
#[must_use]
pub fn stream_reasoning(request_id: &str, token: &str) -> Value {
json!({
"type": "stream_reasoning",
"requestId": request_id,
"token": token,
"data": { "requestId": request_id, "token": token },
"timestamp": now_ms(),
})
}
#[must_use]
pub fn stream_chunk(request_id: &str, node: &str, state: Value) -> Value {
json!({
"type": "stream_chunk",
"requestId": request_id,
"node": node,
"data": { "requestId": request_id, "node": node, "state": state },
"timestamp": now_ms(),
})
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct TurnUsage {
pub cost_usd: f64,
pub prompt_tokens: u64,
pub completion_tokens: u64,
}
#[must_use]
pub fn eventual_response(
request_id: &str,
status: i64,
message_id: &str,
response: Value,
needs_escalation: bool,
citations: &[smooth_operator::domain::Citation],
usage: Option<TurnUsage>,
) -> Value {
let mut inner = json!({
"messageId": message_id,
"response": response,
"needsEscalation": needs_escalation,
});
if !citations.is_empty() {
inner["citations"] = serde_json::to_value(citations).unwrap_or(Value::Null);
}
if let Some(usage) = usage {
inner["usage"] = json!({
"costUsd": usage.cost_usd,
"promptTokens": usage.prompt_tokens,
"completionTokens": usage.completion_tokens,
});
}
json!({
"type": "eventual_response",
"requestId": request_id,
"status": status,
"data": {
"requestId": request_id,
"status": status,
"data": inner,
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn write_confirmation_required(
request_id: &str,
tool_id: &str,
action_description: &str,
) -> Value {
json!({
"type": "write_confirmation_required",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": {
"toolId": tool_id,
"actionDescription": action_description,
},
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn error(request_id: Option<&str>, code: &str, message: &str) -> Value {
let err = json!({ "code": code, "message": message });
let mut data = json!({ "error": err });
if let Some(rid) = request_id {
data["requestId"] = json!(rid);
}
let mut ev = json!({
"type": "error",
"error": err,
"data": data,
"timestamp": now_ms(),
});
set_request_id(&mut ev, request_id);
ev
}
fn set_request_id(ev: &mut Value, request_id: Option<&str>) {
if let Some(rid) = request_id {
ev["requestId"] = json!(rid);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pong_carries_timestamp_both_places() {
let ev = pong(Some("r1"));
assert_eq!(ev["type"], "pong");
assert_eq!(ev["requestId"], "r1");
assert!(ev["timestamp"].is_i64());
assert_eq!(ev["timestamp"], ev["data"]["timestamp"]);
}
#[test]
fn stream_token_mirrors_token() {
let ev = stream_token("r1", "Hel");
assert_eq!(ev["type"], "stream_token");
assert_eq!(ev["token"], "Hel");
assert_eq!(ev["data"]["token"], "Hel");
assert_eq!(ev["data"]["requestId"], "r1");
}
#[test]
fn stream_reasoning_is_distinct_type_but_mirrors_token() {
let ev = stream_reasoning("r1", "let me think");
assert_eq!(ev["type"], "stream_reasoning");
assert_eq!(ev["token"], "let me think");
assert_eq!(ev["data"]["token"], "let me think");
assert_eq!(ev["data"]["requestId"], "r1");
}
#[test]
fn stream_chunk_mirrors_node() {
let ev = stream_chunk("r1", "knowledge_search", json!({ "rawResponse": "x" }));
assert_eq!(ev["type"], "stream_chunk");
assert_eq!(ev["node"], "knowledge_search");
assert_eq!(ev["data"]["node"], "knowledge_search");
assert_eq!(ev["data"]["state"]["rawResponse"], "x");
}
#[test]
fn eventual_response_double_nests_payload() {
let ev = eventual_response(
"r1",
200,
"m1",
json!({"responseParts": ["hi"]}),
false,
&[],
None,
);
assert_eq!(ev["type"], "eventual_response");
assert_eq!(ev["status"], 200);
assert_eq!(ev["data"]["data"]["messageId"], "m1");
assert_eq!(ev["data"]["data"]["needsEscalation"], false);
assert_eq!(ev["data"]["data"]["response"]["responseParts"][0], "hi");
}
#[test]
fn eventual_response_omits_citations_when_empty() {
let ev = eventual_response(
"r1",
200,
"m1",
json!({"responseParts": ["hi"]}),
false,
&[],
None,
);
assert!(
ev["data"]["data"].get("citations").is_none(),
"citations must be absent when empty for back-compat"
);
}
#[test]
fn eventual_response_omits_usage_when_none() {
let ev = eventual_response(
"r1",
200,
"m1",
json!({"responseParts": ["hi"]}),
false,
&[],
None,
);
assert!(
ev["data"]["data"].get("usage").is_none(),
"usage must be absent when None for back-compat"
);
}
#[test]
fn eventual_response_attaches_usage_when_present() {
let usage = TurnUsage {
cost_usd: 0.0123,
prompt_tokens: 1500,
completion_tokens: 42,
};
let ev = eventual_response(
"r1",
200,
"m1",
json!({"responseParts": ["hi"]}),
false,
&[],
Some(usage),
);
let u = &ev["data"]["data"]["usage"];
assert!(
u.is_object(),
"usage should be a sibling object under data.data"
);
let cost = u["costUsd"].as_f64().expect("costUsd is a number");
assert!((cost - 0.0123).abs() < 1e-9, "costUsd should round-trip");
assert_eq!(u["promptTokens"], 1500);
assert_eq!(u["completionTokens"], 42);
}
#[test]
fn eventual_response_attaches_citations_when_present() {
let citations = vec![
smooth_operator::domain::Citation {
id: "doc-1".into(),
title: "acme/handbook@main#wildlife/quokka.md".into(),
url: Some("https://github.com/acme/handbook/blob/main/wildlife/quokka.md".into()),
snippet: "Quokkas are the friendliest marsupial.".into(),
score: 0.91,
},
smooth_operator::domain::Citation {
id: "doc-2".into(),
title: "policies/shipping.md".into(),
url: None,
snippet: "Standard shipping takes 5 to 7 business days.".into(),
score: 0.42,
},
];
let ev = eventual_response(
"r1",
200,
"m1",
json!({"responseParts": ["hi"]}),
false,
&citations,
None,
);
let cites = &ev["data"]["data"]["citations"];
assert!(cites.is_array(), "citations should be an array");
assert_eq!(cites.as_array().unwrap().len(), 2);
assert_eq!(cites[0]["id"], "doc-1");
assert_eq!(
cites[0]["url"],
"https://github.com/acme/handbook/blob/main/wildlife/quokka.md"
);
assert_eq!(
cites[0]["snippet"],
"Quokkas are the friendliest marsupial."
);
let score = cites[0]["score"].as_f64().expect("score is a number");
assert!(
(score - 0.91).abs() < 1e-4,
"score should round-trip ~0.91, got {score}"
);
assert!(
cites[1].get("url").is_none(),
"a urless citation should omit `url`, not emit null"
);
assert_eq!(cites[1]["id"], "doc-2");
}
#[test]
fn write_confirmation_required_matches_spec_shape() {
let ev = write_confirmation_required(
"r1",
"delete_record",
"Tool 'delete_record' requires confirmation. Allow?",
);
assert_eq!(ev["type"], "write_confirmation_required");
assert_eq!(ev["requestId"], "r1");
assert_eq!(ev["data"]["requestId"], "r1");
let inner = &ev["data"]["data"];
assert_eq!(inner["toolId"], "delete_record");
assert!(inner["actionDescription"]
.as_str()
.unwrap()
.contains("delete_record"));
assert!(ev["timestamp"].is_i64());
}
#[test]
fn error_duplicates_descriptor() {
let ev = error(Some("r1"), "VALIDATION_ERROR", "bad");
assert_eq!(ev["type"], "error");
assert_eq!(ev["error"]["code"], "VALIDATION_ERROR");
assert_eq!(ev["data"]["error"]["message"], "bad");
assert_eq!(ev["data"]["requestId"], "r1");
}
#[test]
fn immediate_response_carries_data() {
let ev = immediate_response(Some("r1"), 200, "ok", json!({"sessionId": "s1"}));
assert_eq!(ev["type"], "immediate_response");
assert_eq!(ev["status"], 200);
assert_eq!(ev["data"]["sessionId"], "s1");
}
}