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 otp_verification_required(
request_id: &str,
tool_id: &str,
action_description: &str,
available_channels: &[&str],
auth_level: &str,
) -> Value {
json!({
"type": "otp_verification_required",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": {
"toolId": tool_id,
"actionDescription": action_description,
"availableChannels": available_channels,
"authLevel": auth_level,
},
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn interaction_required(
request_id: &str,
interaction_id: &str,
kind: &str,
spec: &Value,
reason: &str,
) -> Value {
json!({
"type": "interaction_required",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": {
"interactionId": interaction_id,
"kind": kind,
"spec": spec,
"reason": reason,
},
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn interaction_invalid(
request_id: &str,
interaction_id: &str,
kind: &str,
errors: &[smooth_operator::InteractionFieldError],
message: &str,
) -> Value {
json!({
"type": "interaction_invalid",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": {
"interactionId": interaction_id,
"kind": kind,
"errors": errors,
"message": message,
},
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn otp_sent(request_id: &str, channel: &str, masked_destination: &str) -> Value {
json!({
"type": "otp_sent",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": {
"channel": channel,
"maskedDestination": masked_destination,
},
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn otp_verified(request_id: &str, message: &str) -> Value {
json!({
"type": "otp_verified",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": { "message": message },
},
"timestamp": now_ms(),
})
}
#[must_use]
pub fn otp_invalid(
request_id: &str,
error: Option<&str>,
attempts_remaining: u32,
message: &str,
) -> Value {
let mut inner = json!({
"attemptsRemaining": attempts_remaining,
"message": message,
});
if let Some(err) = error {
inner["error"] = json!(err);
}
json!({
"type": "otp_invalid",
"requestId": request_id,
"data": {
"requestId": request_id,
"data": inner,
},
"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 otp_verification_required_matches_spec_shape() {
let ev = otp_verification_required(
"r1",
"pay_invoice",
"Verify your identity to use pay_invoice.",
&["email"],
"end_user",
);
assert_eq!(ev["type"], "otp_verification_required");
assert_eq!(ev["requestId"], "r1");
assert_eq!(ev["data"]["requestId"], "r1");
let inner = &ev["data"]["data"];
assert_eq!(inner["toolId"], "pay_invoice");
assert_eq!(inner["authLevel"], "end_user");
assert_eq!(inner["availableChannels"][0], "email");
assert!(inner["actionDescription"]
.as_str()
.unwrap()
.contains("pay_invoice"));
assert!(ev["timestamp"].is_i64());
}
#[test]
fn interaction_required_matches_spec_shape() {
let spec = json!({
"fields": [
{ "key": "email", "required": true, "label": "Work email" },
{ "key": "phone", "required": false },
],
});
let ev = interaction_required(
"r1",
"int-1",
"identity_intake",
&spec,
"to send you the quote",
);
assert_eq!(ev["type"], "interaction_required");
assert_eq!(ev["requestId"], "r1");
assert_eq!(ev["data"]["requestId"], "r1");
let inner = &ev["data"]["data"];
assert_eq!(inner["interactionId"], "int-1");
assert_eq!(inner["kind"], "identity_intake");
assert_eq!(inner["reason"], "to send you the quote");
assert_eq!(inner["spec"]["fields"][0]["key"], "email");
assert!(ev["timestamp"].is_i64());
}
#[test]
fn interaction_invalid_matches_spec_shape() {
let errors = vec![smooth_operator::InteractionFieldError {
field: "email".into(),
message: "must be a valid email address".into(),
}];
let ev = interaction_invalid(
"r1",
"int-1",
"identity_intake",
&errors,
"Some fields need attention.",
);
assert_eq!(ev["type"], "interaction_invalid");
assert_eq!(ev["data"]["requestId"], "r1");
let inner = &ev["data"]["data"];
assert_eq!(inner["interactionId"], "int-1");
assert_eq!(inner["kind"], "identity_intake");
assert_eq!(inner["message"], "Some fields need attention.");
assert_eq!(inner["errors"][0]["field"], "email");
assert!(inner["errors"][0]["message"]
.as_str()
.unwrap()
.contains("valid email"));
}
#[test]
fn otp_sent_matches_spec_shape() {
let ev = otp_sent("r1", "email", "j***@example.com");
assert_eq!(ev["type"], "otp_sent");
assert_eq!(ev["requestId"], "r1");
assert_eq!(ev["data"]["data"]["channel"], "email");
assert_eq!(ev["data"]["data"]["maskedDestination"], "j***@example.com");
}
#[test]
fn otp_verified_matches_spec_shape() {
let ev = otp_verified("r1", "Identity verified successfully.");
assert_eq!(ev["type"], "otp_verified");
assert_eq!(ev["data"]["requestId"], "r1");
assert_eq!(
ev["data"]["data"]["message"],
"Identity verified successfully."
);
}
#[test]
fn otp_invalid_carries_error_and_attempts() {
let ev = otp_invalid(
"r1",
Some("INVALID_CODE"),
2,
"Invalid code. 2 attempt(s) remaining.",
);
assert_eq!(ev["type"], "otp_invalid");
let inner = &ev["data"]["data"];
assert_eq!(inner["error"], "INVALID_CODE");
assert_eq!(inner["attemptsRemaining"], 2);
assert!(inner["message"].as_str().unwrap().contains("remaining"));
}
#[test]
fn otp_invalid_omits_error_when_none() {
let ev = otp_invalid("r1", None, 0, "Verification failed.");
assert!(
ev["data"]["data"].get("error").is_none(),
"error must be absent when None"
);
assert_eq!(ev["data"]["data"]["attemptsRemaining"], 0);
}
#[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");
}
}