use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use crate::agent::history::{ConversationRole, HistoryMessage};
use crate::agent::messages::FunctionCallResponseMessage;
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum AgentResponse {
Welcome(WelcomeEvent),
SettingsApplied(SettingsAppliedEvent),
ConversationText(ConversationTextEvent),
UserStartedSpeaking(UserStartedSpeakingEvent),
AgentThinking(AgentThinkingEvent),
FunctionCallRequest(FunctionCallRequestEvent),
FunctionCallCancelled(FunctionCallCancelledEvent),
AgentStartedSpeaking(AgentStartedSpeakingEvent),
AgentAudioDone(AgentAudioDoneEvent),
Error(ErrorEvent),
Warning(WarningEvent),
History(HistoryMessage),
LatencyReport(LatencyReportEvent),
ListenUpdated(ListenUpdatedEvent),
PromptUpdated(PromptUpdatedEvent),
SpeakUpdated(SpeakUpdatedEvent),
ThinkUpdated(ThinkUpdatedEvent),
InjectionRefused(InjectionRefusedEvent),
FunctionCallResponse(FunctionCallResponseMessage),
Unknown(serde_json::Value),
}
fn known_event<T, E>(event_type: &str, value: serde_json::Value) -> Result<T, E>
where
T: serde::de::DeserializeOwned,
E: de::Error,
{
serde_json::from_value(value).map_err(|err| {
E::custom(format_args!(
"malformed `{event_type}` event from the Voice Agent server: {err}"
))
})
}
impl<'de> Deserialize<'de> for AgentResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let Some(event_type) = value.get("type").and_then(serde_json::Value::as_str) else {
return Ok(Self::Unknown(value));
};
let event_type = event_type.to_owned();
match event_type.as_str() {
"Welcome" => known_event(&event_type, value).map(Self::Welcome),
"SettingsApplied" => known_event(&event_type, value).map(Self::SettingsApplied),
"ConversationText" => known_event(&event_type, value).map(Self::ConversationText),
"UserStartedSpeaking" => known_event(&event_type, value).map(Self::UserStartedSpeaking),
"AgentThinking" => known_event(&event_type, value).map(Self::AgentThinking),
"FunctionCallRequest" => known_event(&event_type, value).map(Self::FunctionCallRequest),
"FunctionCallCancelled" => {
known_event(&event_type, value).map(Self::FunctionCallCancelled)
}
"AgentStartedSpeaking" => {
known_event(&event_type, value).map(Self::AgentStartedSpeaking)
}
"AgentAudioDone" => known_event(&event_type, value).map(Self::AgentAudioDone),
"Error" => known_event(&event_type, value).map(Self::Error),
"Warning" => known_event(&event_type, value).map(Self::Warning),
"History" => known_event(&event_type, value).map(Self::History),
"LatencyReport" => known_event(&event_type, value).map(Self::LatencyReport),
"ListenUpdated" => known_event(&event_type, value).map(Self::ListenUpdated),
"PromptUpdated" => known_event(&event_type, value).map(Self::PromptUpdated),
"SpeakUpdated" => known_event(&event_type, value).map(Self::SpeakUpdated),
"ThinkUpdated" => known_event(&event_type, value).map(Self::ThinkUpdated),
"InjectionRefused" => known_event(&event_type, value).map(Self::InjectionRefused),
"FunctionCallResponse" => {
known_event(&event_type, value).map(Self::FunctionCallResponse)
}
_ => Ok(Self::Unknown(value)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum WelcomeType {
#[default]
Welcome,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WelcomeEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: WelcomeType,
pub request_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SettingsAppliedType {
#[default]
SettingsApplied,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SettingsAppliedEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: SettingsAppliedType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ConversationTextType {
#[default]
ConversationText,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ConversationTextEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: ConversationTextType,
pub role: ConversationRole,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub languages_hinted: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UserStartedSpeakingType {
#[default]
UserStartedSpeaking,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UserStartedSpeakingEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: UserStartedSpeakingType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AgentThinkingType {
#[default]
AgentThinking,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AgentThinkingEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: AgentThinkingType,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FunctionCallRequestType {
#[default]
FunctionCallRequest,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FunctionCallRequestEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: FunctionCallRequestType,
pub functions: Vec<AgentFunctionCall>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AgentFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
pub client_side: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FunctionCallCancelledType {
#[default]
FunctionCallCancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FunctionCallCancelledEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: FunctionCallCancelledType,
pub functions: Vec<CancelledFunctionCall>,
}
impl FunctionCallCancelledEvent {
pub fn ids(&self) -> impl Iterator<Item = &str> {
self.functions.iter().map(|f| f.id.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CancelledFunctionCall {
pub id: String,
pub name: String,
}
mod latency_seconds {
use core::fmt;
use serde::de::{self, Deserializer, Visitor};
struct Seconds;
impl Visitor<'_> for Seconds {
type Value = f64;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a latency in seconds, as a JSON number or as a string holding one")
}
fn visit_f64<E: de::Error>(self, value: f64) -> Result<f64, E> {
Ok(value)
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<f64, E> {
Ok(value as f64)
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<f64, E> {
Ok(value as f64)
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<f64, E> {
value
.trim()
.parse()
.map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))
}
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<f64, D::Error> {
de.deserialize_any(Seconds)
}
struct MaybeSeconds;
impl<'de> Visitor<'de> for MaybeSeconds {
type Value = Option<f64>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"an optional latency in seconds, as a JSON number or as a string holding one",
)
}
fn visit_none<E: de::Error>(self) -> Result<Option<f64>, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Option<f64>, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(self, de: D) -> Result<Option<f64>, D::Error> {
de.deserialize_any(Seconds).map(Some)
}
}
pub(super) fn deserialize_option<'de, D: Deserializer<'de>>(
de: D,
) -> Result<Option<f64>, D::Error> {
de.deserialize_option(MaybeSeconds)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AgentStartedSpeakingType {
#[default]
AgentStartedSpeaking,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AgentStartedSpeakingEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: AgentStartedSpeakingType,
#[serde(deserialize_with = "latency_seconds::deserialize")]
pub total_latency: f64,
#[serde(deserialize_with = "latency_seconds::deserialize")]
pub tts_latency: f64,
#[serde(deserialize_with = "latency_seconds::deserialize")]
pub ttt_latency: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AgentAudioDoneType {
#[default]
AgentAudioDone,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AgentAudioDoneEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: AgentAudioDoneType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ErrorType {
#[default]
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ErrorEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: ErrorType,
pub description: String,
pub code: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum WarningType {
#[default]
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WarningEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: WarningType,
pub description: String,
pub code: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LatencyReportType {
#[default]
LatencyReport,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LatencyReportEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: LatencyReportType,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub stt_latency: Option<f64>,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub ttt_token_latency: Option<f64>,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub ttt_text_latency: Option<f64>,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub ttt_tool_latency: Option<f64>,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub ttt_thinking_latency: Option<f64>,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub tts_latency: Option<f64>,
#[serde(
default,
deserialize_with = "latency_seconds::deserialize_option",
skip_serializing_if = "Option::is_none"
)]
pub total_latency: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ListenUpdatedType {
#[default]
ListenUpdated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListenUpdatedEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: ListenUpdatedType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PromptUpdatedType {
#[default]
PromptUpdated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PromptUpdatedEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: PromptUpdatedType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SpeakUpdatedType {
#[default]
SpeakUpdated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SpeakUpdatedEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: SpeakUpdatedType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ThinkUpdatedType {
#[default]
ThinkUpdated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ThinkUpdatedEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: ThinkUpdatedType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum InjectionRefusedType {
#[default]
InjectionRefused,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InjectionRefusedEvent {
#[serde(rename = "type", default)]
#[allow(missing_docs)]
pub message_type: InjectionRefusedType,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn welcome_round_trip() {
let raw = json!({
"type": "Welcome",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::Welcome(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn settings_applied_round_trip() {
let raw = json!({ "type": "SettingsApplied" });
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::SettingsApplied(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn conversation_text_user_basic() {
let raw = json!({
"type": "ConversationText",
"role": "user",
"content": "Hello"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::ConversationText(e) => {
assert_eq!(e.role, ConversationRole::User);
assert_eq!(e.content, "Hello");
assert!(e.languages_hinted.is_empty());
assert!(e.languages.is_empty());
}
_ => panic!("expected ConversationText"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn conversation_text_with_multi_language_fields() {
let raw = json!({
"type": "ConversationText",
"role": "user",
"content": "Hola",
"languages_hinted": ["en", "es"],
"languages": ["es"]
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::ConversationText(e) => {
assert_eq!(e.languages_hinted, vec!["en", "es"]);
assert_eq!(e.languages, vec!["es"]);
}
_ => panic!("expected ConversationText"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn user_started_speaking_round_trip() {
let raw = json!({ "type": "UserStartedSpeaking" });
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::UserStartedSpeaking(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn agent_thinking_round_trip() {
let raw = json!({
"type": "AgentThinking",
"content": "Looking up the weather…"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::AgentThinking(e) => assert_eq!(e.content, "Looking up the weather…"),
_ => panic!("expected AgentThinking"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn function_call_request_round_trip() {
let raw = json!({
"type": "FunctionCallRequest",
"functions": [{
"id": "fc_1",
"name": "get_weather",
"arguments": "{\"city\":\"NYC\"}",
"client_side": true
}]
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::FunctionCallRequest(e) => {
assert_eq!(e.functions.len(), 1);
assert_eq!(e.functions[0].id, "fc_1");
assert!(e.functions[0].client_side);
assert!(e.functions[0].thought_signature.is_none());
}
_ => panic!("expected FunctionCallRequest"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn function_call_request_with_thought_signature() {
let raw = json!({
"type": "FunctionCallRequest",
"functions": [{
"id": "fc_2",
"name": "lookup",
"arguments": "{}",
"client_side": false,
"thought_signature": "sig-abc"
}]
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::FunctionCallRequest(e) => {
assert_eq!(e.functions[0].thought_signature.as_deref(), Some("sig-abc"));
}
_ => panic!("expected FunctionCallRequest"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn function_call_cancelled_round_trip() {
let raw = json!({
"type": "FunctionCallCancelled",
"functions": [
{ "id": "fc_12345678-90ab-cdef-1234-567890abcdef", "name": "book_appointment" },
{ "id": "fc_2", "name": "end_call" }
]
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::FunctionCallCancelled(e) => {
assert_eq!(e.functions.len(), 2);
assert_eq!(e.functions[0].id, "fc_12345678-90ab-cdef-1234-567890abcdef");
assert_eq!(e.functions[0].name, "book_appointment");
assert_eq!(
e.ids().collect::<Vec<_>>(),
vec!["fc_12345678-90ab-cdef-1234-567890abcdef", "fc_2"]
);
}
other => panic!("expected FunctionCallCancelled, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn function_call_cancelled_is_not_confused_with_request() {
let cancelled = json!({
"type": "FunctionCallCancelled",
"functions": [{ "id": "fc_1", "name": "get_weather" }]
});
assert!(matches!(
serde_json::from_value::<AgentResponse>(cancelled).unwrap(),
AgentResponse::FunctionCallCancelled(_)
));
let request = json!({
"type": "FunctionCallRequest",
"functions": [{
"id": "fc_1",
"name": "get_weather",
"arguments": "{}",
"client_side": true
}]
});
assert!(matches!(
serde_json::from_value::<AgentResponse>(request).unwrap(),
AgentResponse::FunctionCallRequest(_)
));
}
#[test]
fn latency_report_full_round_trip() {
let raw = json!({
"type": "LatencyReport",
"stt_latency": 0.21,
"ttt_token_latency": 0.35,
"ttt_text_latency": 0.4,
"ttt_tool_latency": 0.38,
"ttt_thinking_latency": 0.36,
"tts_latency": 0.12,
"total_latency": 0.9
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::LatencyReport(e) => {
assert_eq!(e.stt_latency, Some(0.21));
assert_eq!(e.ttt_token_latency, Some(0.35));
assert_eq!(e.ttt_text_latency, Some(0.4));
assert_eq!(e.ttt_tool_latency, Some(0.38));
assert_eq!(e.ttt_thinking_latency, Some(0.36));
assert_eq!(e.tts_latency, Some(0.12));
assert_eq!(e.total_latency, Some(0.9));
}
other => panic!("expected LatencyReport, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn latency_report_partial_fields_round_trip() {
let raw = json!({
"type": "LatencyReport",
"stt_latency": 0.2,
"ttt_text_latency": 0.5,
"tts_latency": 0.1,
"total_latency": 0.8
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::LatencyReport(e) => {
assert!(e.ttt_tool_latency.is_none());
assert!(e.ttt_thinking_latency.is_none());
assert!(e.ttt_token_latency.is_none());
}
other => panic!("expected LatencyReport, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
assert_eq!(
serde_json::to_string(&AgentResponse::LatencyReport(LatencyReportEvent::default()))
.unwrap(),
r#"{"type":"LatencyReport"}"#
);
}
#[test]
fn listen_updated_round_trip() {
let raw = json!({ "type": "ListenUpdated" });
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::ListenUpdated(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
assert_eq!(
serde_json::to_string(&ListenUpdatedEvent::default()).unwrap(),
r#"{"type":"ListenUpdated"}"#
);
}
#[test]
fn agent_started_speaking_round_trip() {
let raw = json!({
"type": "AgentStartedSpeaking",
"total_latency": 1.23,
"tts_latency": 0.4,
"ttt_latency": 0.83
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::AgentStartedSpeaking(e) => {
assert!((e.total_latency - 1.23).abs() < 1e-9);
assert!((e.tts_latency - 0.4).abs() < 1e-9);
assert!((e.ttt_latency - 0.83).abs() < 1e-9);
}
_ => panic!("expected AgentStartedSpeaking"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn agent_started_speaking_accepts_string_encoded_latencies() {
let raw = json!({
"type": "AgentStartedSpeaking",
"total_latency": "0.83",
"tts_latency": "0.105614436",
"ttt_latency": "2"
});
match serde_json::from_value::<AgentResponse>(raw).unwrap() {
AgentResponse::AgentStartedSpeaking(e) => {
assert!((e.total_latency - 0.83).abs() < 1e-9);
assert!((e.tts_latency - 0.105_614_436).abs() < 1e-9);
assert!((e.ttt_latency - 2.0).abs() < 1e-9);
}
other => panic!("expected AgentStartedSpeaking, got {other:?}"),
}
}
#[test]
fn agent_started_speaking_string_and_number_forms_agree() {
let numbers = json!({
"type": "AgentStartedSpeaking",
"tts_latency": 0.105614436,
"ttt_latency": 0.645568172,
"total_latency": 0.75118354
});
let strings = json!({
"type": "AgentStartedSpeaking",
"tts_latency": "0.105614436",
"ttt_latency": "0.645568172",
"total_latency": "0.75118354"
});
assert_eq!(
serde_json::from_value::<AgentResponse>(numbers).unwrap(),
serde_json::from_value::<AgentResponse>(strings).unwrap()
);
}
#[test]
fn agent_started_speaking_rejects_a_non_numeric_latency_string() {
let raw = json!({
"type": "AgentStartedSpeaking",
"total_latency": "fast",
"tts_latency": 0.4,
"ttt_latency": 0.83
});
assert!(serde_json::from_value::<AgentResponse>(raw).is_err());
}
#[test]
fn latency_report_accepts_string_encoded_latencies() {
let raw = json!({
"type": "LatencyReport",
"stt_latency": "0.21",
"ttt_token_latency": "0.35",
"ttt_text_latency": "0.4",
"ttt_tool_latency": "0.38",
"ttt_thinking_latency": "0.36",
"tts_latency": "0.12",
"total_latency": "0.9"
});
match serde_json::from_value::<AgentResponse>(raw).unwrap() {
AgentResponse::LatencyReport(e) => {
assert_eq!(e.stt_latency, Some(0.21));
assert_eq!(e.ttt_token_latency, Some(0.35));
assert_eq!(e.ttt_text_latency, Some(0.4));
assert_eq!(e.ttt_tool_latency, Some(0.38));
assert_eq!(e.ttt_thinking_latency, Some(0.36));
assert_eq!(e.tts_latency, Some(0.12));
assert_eq!(e.total_latency, Some(0.9));
}
other => panic!("expected LatencyReport, got {other:?}"),
}
}
#[test]
fn latency_report_string_and_number_forms_agree() {
for (numbers, strings) in [
(
json!({ "type": "LatencyReport", "stt_latency": 0.039999961853027344 }),
json!({ "type": "LatencyReport", "stt_latency": "0.039999961853027344" }),
),
(
json!({ "type": "LatencyReport", "total_latency": 0.83 }),
json!({ "type": "LatencyReport", "total_latency": "0.83" }),
),
(
json!({ "type": "LatencyReport", "tts_latency": 0 }),
json!({ "type": "LatencyReport", "tts_latency": "0" }),
),
] {
assert_eq!(
serde_json::from_value::<AgentResponse>(numbers).unwrap(),
serde_json::from_value::<AgentResponse>(strings).unwrap()
);
}
}
#[test]
fn latency_report_null_latency_is_absent() {
let raw = json!({ "type": "LatencyReport", "stt_latency": null });
match serde_json::from_value::<AgentResponse>(raw).unwrap() {
AgentResponse::LatencyReport(e) => assert!(e.stt_latency.is_none()),
other => panic!("expected LatencyReport, got {other:?}"),
}
}
#[test]
fn latency_report_rejects_a_non_numeric_latency_string() {
let raw = json!({ "type": "LatencyReport", "total_latency": "quick" });
assert!(serde_json::from_value::<AgentResponse>(raw).is_err());
}
#[test]
fn agent_audio_done_round_trip() {
let raw = json!({ "type": "AgentAudioDone" });
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::AgentAudioDone(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn error_round_trip() {
let raw = json!({
"type": "Error",
"description": "rate limit exceeded",
"code": "RATE_LIMITED"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::Error(e) => {
assert_eq!(e.code, "RATE_LIMITED");
assert_eq!(e.description, "rate limit exceeded");
}
_ => panic!("expected Error"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn warning_round_trip() {
let raw = json!({
"type": "Warning",
"description": "audio buffer running low",
"code": "BUFFER_LOW"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::Warning(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn history_event_round_trip_conversation() {
let raw = json!({
"type": "History",
"role": "assistant",
"content": "Hello!"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::History(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn history_event_round_trip_function_call() {
let raw = json!({
"type": "History",
"function_calls": [{
"id": "f1",
"name": "fn",
"client_side": false,
"arguments": "{}",
"response": "{}"
}]
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::History(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn prompt_updated_round_trip() {
let raw = json!({ "type": "PromptUpdated" });
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::PromptUpdated(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn speak_and_think_updated_round_trip() {
for (raw, is_speak) in [
(json!({ "type": "SpeakUpdated" }), true),
(json!({ "type": "ThinkUpdated" }), false),
] {
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
if is_speak {
assert!(matches!(event, AgentResponse::SpeakUpdated(_)));
} else {
assert!(matches!(event, AgentResponse::ThinkUpdated(_)));
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
}
#[test]
fn injection_refused_round_trip() {
let raw = json!({
"type": "InjectionRefused",
"message": "agent is mid-turn"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::InjectionRefused(e) => assert_eq!(e.message, "agent is mid-turn"),
_ => panic!("expected InjectionRefused"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn function_call_response_round_trip() {
let raw = json!({
"type": "FunctionCallResponse",
"name": "internal_lookup",
"content": "[{\"id\":1}]"
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::FunctionCallResponse(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn unknown_type_lands_in_unknown() {
let raw = json!({
"type": "FutureEvent",
"some_field": 42,
"data": [1, 2, 3]
});
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::Unknown(value) => {
assert_eq!(value["type"], "FutureEvent");
assert_eq!(value["some_field"], 42);
}
_ => panic!("expected Unknown"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn off_spec_end_of_turn_lands_in_unknown_with_its_payload() {
for trigger in ["model", "timeout"] {
let raw = json!({ "type": "EndOfTurn", "trigger": trigger });
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
match &event {
AgentResponse::Unknown(value) => {
assert_eq!(value["type"], "EndOfTurn");
assert_eq!(value["trigger"], trigger);
}
other => panic!("expected Unknown for EndOfTurn, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
}
#[test]
fn unknown_round_trips_arbitrary_shapes() {
let raw = json!([1, 2, 3]);
let event: AgentResponse = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(event, AgentResponse::Unknown(_)));
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn known_type_with_missing_required_field_is_an_error() {
for (raw, event_type, missing) in [
(
json!({ "type": "Error", "description": "boom" }),
"Error",
"code",
),
(
json!({ "type": "Warning", "code": "BUFFER_LOW" }),
"Warning",
"description",
),
(
json!({ "type": "ConversationText", "role": "user" }),
"ConversationText",
"content",
),
(json!({ "type": "Welcome" }), "Welcome", "request_id"),
(
json!({ "type": "FunctionCallRequest" }),
"FunctionCallRequest",
"functions",
),
] {
let err = serde_json::from_value::<AgentResponse>(raw.clone()).expect_err(&format!(
"{event_type} without {missing} must not deserialize"
));
let text = err.to_string();
assert!(text.contains(event_type), "{event_type}: {text}");
assert!(text.contains(missing), "{event_type}: {text}");
}
}
#[test]
fn known_type_with_wrong_field_type_is_an_error() {
let raw = json!({ "type": "AgentThinking", "content": 42 });
let err =
serde_json::from_value::<AgentResponse>(raw).expect_err("content is not a string");
assert!(err.to_string().contains("AgentThinking"), "{err}");
}
#[test]
fn unrecognized_or_absent_type_still_lands_in_unknown() {
for raw in [
json!({ "type": "SomeFutureError", "description": "d", "code": "c" }),
json!({ "description": "d", "code": "c" }),
json!({ "type": 7, "description": "d" }),
] {
let event: AgentResponse = serde_json::from_value(raw.clone())
.unwrap_or_else(|err| panic!("{raw} must deserialize, got {err}"));
assert!(
matches!(event, AgentResponse::Unknown(_)),
"expected Unknown for {raw}, got {event:?}"
);
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
}
#[test]
fn unmodeled_role_on_a_modeled_event_is_not_an_error() {
let raw = json!({
"type": "ConversationText",
"role": "system",
"content": "You are a helpful agent."
});
let event: AgentResponse = serde_json::from_value(raw.clone())
.expect("an unrecognized role must not end the session");
match &event {
AgentResponse::ConversationText(e) => {
assert_eq!(e.role, ConversationRole::Unknown("system".to_string()));
assert_eq!(e.role.as_str(), "system");
}
other => panic!("expected ConversationText, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), raw);
}
#[test]
fn unmodeled_history_role_and_shape_are_not_errors() {
let unknown_role = json!({
"type": "History",
"role": "system",
"content": "seed"
});
let event: AgentResponse = serde_json::from_value(unknown_role.clone())
.expect("an unrecognized role must not end the session");
match &event {
AgentResponse::History(HistoryMessage::Conversation(c)) => {
assert_eq!(c.role, ConversationRole::Unknown("system".to_string()));
}
other => panic!("expected History/Conversation, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), unknown_role);
let unknown_shape = json!({
"type": "History",
"tool_result": { "id": "t1", "output": "42" }
});
let event: AgentResponse = serde_json::from_value(unknown_shape.clone())
.expect("an unmodeled history shape must not end the session");
match &event {
AgentResponse::History(HistoryMessage::Unknown(value)) => {
assert_eq!(value, &unknown_shape);
}
other => panic!("expected History/Unknown, got {other:?}"),
}
assert_eq!(serde_json::to_value(&event).unwrap(), unknown_shape);
}
#[test]
fn dispatch_does_not_misroute_history_to_other_variants() {
let raw = json!({
"type": "History",
"role": "user",
"content": "Hi"
});
let event: AgentResponse = serde_json::from_value(raw).unwrap();
match event {
AgentResponse::History(HistoryMessage::Conversation(c)) => {
assert_eq!(c.role, ConversationRole::User);
assert_eq!(c.content, "Hi");
}
other => panic!("expected History/Conversation, got {other:?}"),
}
}
}