use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
pub use self::replay::events as replay_events;
pub(crate) use self::replay::{
ATTACHMENTS_FIELD, CONTEXT_COMPACTED_MARKER, INTERNAL_MESSAGE_FIELD, REPLAY_REASONING_FIELD,
TOOL_ERROR_FIELD, internal_message_kind, is_internal_message, strip_attachment_references,
};
mod replay;
pub const MAX_USER_INPUT_BYTES: usize = 1024 * 1024;
pub const MAX_CAPABILITY_INPUT_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionFileReference {
pub id: String,
pub name: String,
pub size: u64,
pub media_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Submission {
pub id: String,
pub op: Op,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin_label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Op {
UserInput {
text: String,
attachments: Vec<SessionFileReference>,
},
ActiveInput {
operation: String,
turn_id: String,
text: String,
},
Interrupt { turn_id: String },
ExecApproval {
id: String,
decision: ReviewDecision,
},
CapabilityCommand {
capability: String,
command: String,
arguments: String,
#[serde(deserialize_with = "required_option")]
input: Option<String>,
#[serde(deserialize_with = "required_option")]
target: Option<MessageTarget>,
},
SetModel { route: String },
ResumeSession { session_id: String },
}
fn required_option<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Option::deserialize(deserializer)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Event {
#[serde(skip_serializing_if = "Option::is_none")]
pub submission_id: Option<String>,
pub msg: EventMsg,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum EventMsg {
Error(ErrorEvent),
Warning(WarningEvent),
SessionConfigured(SessionConfiguredEvent),
#[serde(rename = "task_started")]
TurnStarted(TurnStartedEvent),
#[serde(rename = "task_complete")]
TurnComplete(TurnCompleteEvent),
TurnAborted(TurnAbortedEvent),
UserMessage(UserMessageEvent),
AgentMessage(AgentMessageEvent),
AgentMessageContentDelta(AgentMessageContentDeltaEvent),
AgentReasoningContentDelta(AgentReasoningContentDeltaEvent),
SessionHistory(SessionHistoryEvent),
ModelChanged(ModelChangedEvent),
SessionResumeRequested(SessionResumeRequestedEvent),
ToolCallBegin(ToolCallBeginEvent),
ToolCallEnd(ToolCallEndEvent),
ExecApprovalRequest(ExecApprovalRequestEvent),
TokenCount(TokenCountEvent),
ContextCompacted,
WebSearchBegin(WebSearchBeginEvent),
WebSearchEnd(WebSearchEndEvent),
Frontend(FrontendEvent),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelEvent {
TextDelta(String),
CommentaryDelta(String),
ReasoningDelta(String),
WebSearchStarted {
call_id: String,
},
WebSearchCompleted {
call_id: String,
action: WebSearchAction,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebSearchAction {
Search {
query: Option<String>,
},
OpenPage {
url: Option<String>,
},
FindInPage {
url: Option<String>,
pattern: Option<String>,
},
Other,
}
impl ModelEvent {
#[must_use]
pub fn into_event(self, thread_id: &str, turn_id: &str, item_id: &str) -> EventMsg {
match self {
Self::TextDelta(delta) => {
EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent {
thread_id: thread_id.into(),
turn_id: turn_id.into(),
item_id: item_id.into(),
delta,
phase: Some(AgentMessagePhase::FinalAnswer),
})
}
Self::CommentaryDelta(delta) => {
EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent {
thread_id: thread_id.into(),
turn_id: turn_id.into(),
item_id: item_id.into(),
delta,
phase: Some(AgentMessagePhase::Commentary),
})
}
Self::ReasoningDelta(delta) => {
EventMsg::AgentReasoningContentDelta(AgentReasoningContentDeltaEvent {
thread_id: thread_id.into(),
turn_id: turn_id.into(),
item_id: item_id.into(),
delta,
})
}
Self::WebSearchStarted { call_id } => {
EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id })
}
Self::WebSearchCompleted { call_id, action } => {
let (action, query) = match action {
WebSearchAction::Search { query } => ("search", query),
WebSearchAction::OpenPage { url } => ("open_page", url),
WebSearchAction::FindInPage { url, pattern } => {
("find_in_page", pattern.or(url))
}
WebSearchAction::Other => ("other", None),
};
EventMsg::WebSearchEnd(WebSearchEndEvent {
call_id,
query,
action: action.into(),
})
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendCommand {
pub name: String,
pub arguments: String,
pub description: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendContribution {
pub capability: String,
pub accepts_file_attachments: bool,
pub count: Option<usize>,
pub commands: Vec<FrontendCommand>,
pub widgets: Vec<FrontendWidget>,
pub references: Vec<FrontendReference>,
pub active_input: Option<FrontendActiveInput>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MiddlewareFeature {
pub id: String,
pub label: String,
pub description: String,
pub required: bool,
pub settings: Vec<FrontendSetting>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendSetting {
pub id: String,
pub label: String,
pub description: String,
#[serde(flatten)]
pub kind: FrontendSettingKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum FrontendSettingKind {
Integer {
min: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<i64>,
step: i64,
},
Select {
options: Vec<FrontendSettingOption>,
#[serde(default, skip_serializing_if = "Option::is_none")]
unset_label: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendSettingOption {
pub value: String,
pub label: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FrontendSettingValue {
Integer(i64),
String(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendActiveInput {
pub operation: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendReference {
pub trigger: char,
pub value: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendWidget {
pub id: String,
pub slot: FrontendSlot,
pub text: String,
pub tone: FrontendTone,
pub symbol: Option<FrontendSymbol>,
pub icon_only: bool,
pub progress: Option<FrontendProgress>,
pub content: Option<FrontendWidgetContent>,
pub action: Option<Op>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendProgress {
pub completed: usize,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FrontendWidgetContent {
Blocks {
title: String,
blocks: Vec<FrontendBlock>,
},
Picker {
title: String,
options: Vec<FrontendPickerOption>,
},
ActionList {
title: String,
items: Vec<FrontendActionListItem>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendSlot {
Header,
ComposerHeader,
ComposerFooter,
MessageActions,
TranscriptTail,
Navigation,
ChatMenu,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendBlock {
pub id: Option<String>,
pub group: Option<String>,
pub append: bool,
pub pending: bool,
pub text: String,
pub files: Vec<SessionFileReference>,
pub format: FrontendBlockFormat,
pub tone: FrontendTone,
}
impl FrontendBlock {
#[must_use]
pub fn namespaced(mut self, capability: &str) -> Self {
if let Some(id) = self.id.take() {
self.id = Some(format!("{capability}/{id}"));
}
if let Some(group) = self.group.take() {
self.group = Some(format!("{capability}/{group}"));
}
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendBlockFormat {
PlainText,
UnifiedDiff,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendPickerOption {
pub label: String,
pub description: String,
pub detail: String,
pub op: Op,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendActionListItem {
pub id: String,
pub text: String,
pub state: FrontendListItemState,
pub actions: Vec<FrontendAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendListItemState {
Plain,
Pending,
InProgress,
Completed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendAction {
pub id: String,
pub label: String,
pub symbol: FrontendSymbol,
pub tone: FrontendTone,
pub op: Op,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "frontend_type", rename_all = "snake_case")]
pub enum FrontendEvent {
Render {
capability: String,
block: FrontendBlock,
},
Widget {
capability: String,
item: FrontendWidget,
},
RemoveWidget {
capability: String,
id: String,
},
Picker {
title: String,
options: Vec<FrontendPickerOption>,
},
Preview {
title: String,
events: Vec<EventMsg>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendTone {
Neutral,
Success,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrontendSymbol {
Agent,
Brain,
Branch,
Chat,
ChatGpt,
Claude,
Deepseek,
Delete,
Edit,
Kimi,
Moon,
Promote,
Route,
Search,
Sparkle,
Storage,
Task,
Custom(String),
}
impl FrontendSymbol {
pub fn as_str(&self) -> &str {
match self {
Self::Agent => "agent",
Self::Brain => "brain",
Self::Branch => "branch",
Self::Chat => "chat",
Self::ChatGpt => "chat_gpt",
Self::Claude => "claude",
Self::Deepseek => "deepseek",
Self::Delete => "delete",
Self::Edit => "edit",
Self::Kimi => "kimi",
Self::Moon => "moon",
Self::Promote => "promote",
Self::Route => "route",
Self::Search => "search",
Self::Sparkle => "sparkle",
Self::Storage => "storage",
Self::Task => "task",
Self::Custom(name) => name,
}
}
fn from_wire(name: &str) -> Self {
match name {
"agent" => Self::Agent,
"brain" => Self::Brain,
"branch" => Self::Branch,
"chat" => Self::Chat,
"chat_gpt" => Self::ChatGpt,
"claude" => Self::Claude,
"deepseek" => Self::Deepseek,
"delete" => Self::Delete,
"edit" => Self::Edit,
"kimi" => Self::Kimi,
"moon" => Self::Moon,
"promote" => Self::Promote,
"route" => Self::Route,
"search" => Self::Search,
"sparkle" => Self::Sparkle,
"storage" => Self::Storage,
"task" => Self::Task,
other => Self::Custom(other.to_owned()),
}
}
}
impl std::fmt::Display for FrontendSymbol {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for FrontendSymbol {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for FrontendSymbol {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer).map(|name| Self::from_wire(&name))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorEvent {
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WarningEvent {
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionConfiguredEvent {
pub session_id: String,
pub context: SessionContext,
pub model: ModelChangedEvent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnStartedEvent {
pub turn_id: String,
pub model_context_window: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnCompleteEvent {
pub turn_id: String,
pub last_agent_message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnAbortedEvent {
pub turn_id: String,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageTarget {
pub checkpoint_sequence: u64,
#[serde(deserialize_with = "positive_usize")]
pub batch_item_count: usize,
}
fn positive_usize<'de, D>(deserializer: D) -> std::result::Result<usize, D::Error>
where
D: Deserializer<'de>,
{
let value = usize::deserialize(deserializer)?;
if value == 0 {
return Err(serde::de::Error::custom(
"message target item count must be positive",
));
}
Ok(value)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserMessageEvent {
pub message: String,
pub attachments: Vec<SessionFileReference>,
#[serde(deserialize_with = "required_option")]
pub message_target: Option<MessageTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentMessageEvent {
pub message: String,
pub phase: Option<AgentMessagePhase>,
#[serde(deserialize_with = "required_option")]
pub message_target: Option<MessageTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentMessageContentDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
pub phase: Option<AgentMessagePhase>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentReasoningContentDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionHistoryEvent {
pub events: Vec<EventMsg>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelChangedEvent {
pub route: String,
pub model: String,
pub reasoning_effort: Option<String>,
pub model_context_window: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionResumeRequestedEvent {
pub session_id: String,
pub context: SessionContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentMessagePhase {
Commentary,
FinalAnswer,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCallBeginEvent {
pub turn_id: String,
pub call_id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCallEndEvent {
pub turn_id: String,
pub call_id: String,
pub name: String,
pub output: String,
pub is_error: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecApprovalRequestEvent {
pub id: String,
pub turn_id: String,
pub calls: Vec<ApprovalCall>,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApprovalCall {
pub call_id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewDecision {
Approved,
ApprovedForSession,
Denied { rejection: String },
Abort,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenUsage {
pub input_tokens: i64,
pub cached_input_tokens: i64,
pub cache_write_input_tokens: i64,
pub output_tokens: i64,
pub reasoning_output_tokens: i64,
pub total_tokens: i64,
}
impl TokenUsage {
pub fn checked_add(&mut self, other: &Self) -> Option<()> {
let input_tokens = self.input_tokens.checked_add(other.input_tokens)?;
let cached_input_tokens = self
.cached_input_tokens
.checked_add(other.cached_input_tokens)?;
let cache_write_input_tokens = self
.cache_write_input_tokens
.checked_add(other.cache_write_input_tokens)?;
let output_tokens = self.output_tokens.checked_add(other.output_tokens)?;
let reasoning_output_tokens = self
.reasoning_output_tokens
.checked_add(other.reasoning_output_tokens)?;
let total_tokens = self.total_tokens.checked_add(other.total_tokens)?;
*self = Self {
input_tokens,
cached_input_tokens,
cache_write_input_tokens,
output_tokens,
reasoning_output_tokens,
total_tokens,
};
Some(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenUsageInfo {
pub total_token_usage: TokenUsage,
pub last_token_usage: TokenUsage,
pub model_context_window: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenCountEvent {
pub info: Option<TokenUsageInfo>,
pub rate_limits: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebSearchBeginEvent {
pub call_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebSearchEndEvent {
pub call_id: String,
pub query: Option<String>,
pub action: String,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn middleware_settings_have_a_generic_wire_shape() {
let feature = MiddlewareFeature {
id: "example".into(),
label: "Example".into(),
description: "Example capability".into(),
required: false,
settings: vec![FrontendSetting {
id: "limit".into(),
label: "Limit".into(),
description: "Example limit".into(),
kind: FrontendSettingKind::Integer {
min: 1,
max: None,
step: 10,
},
}],
};
assert_eq!(
serde_json::to_value(feature).expect("serialize middleware setting"),
json!({
"id": "example",
"label": "Example",
"description": "Example capability",
"required": false,
"settings": [{
"id": "limit",
"label": "Limit",
"description": "Example limit",
"type": "integer",
"min": 1,
"step": 10
}]
})
);
}
#[test]
fn session_configured_has_a_stable_wire_shape() {
let event = EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: "session-1".into(),
context: SessionContext {
tenant_id: Some("tenant-1".into()),
user_id: Some("user-1".into()),
user_name: Some("Ada".into()),
workspace_id: Some("workspace-1".into()),
workspace_label: Some("Project One".into()),
origin_label: Some("cron".into()),
},
model: ModelChangedEvent {
route: "default".into(),
model: "test-model".into(),
reasoning_effort: Some("high".into()),
model_context_window: Some(128_000),
},
});
assert_eq!(
serde_json::to_value(event).expect("serialize session event"),
json!({
"type": "session_configured",
"session_id": "session-1",
"context": {
"tenant_id": "tenant-1",
"user_id": "user-1",
"user_name": "Ada",
"workspace_id": "workspace-1",
"workspace_label": "Project One",
"origin_label": "cron"
},
"model": {
"route": "default",
"model": "test-model",
"reasoning_effort": "high",
"model_context_window": 128_000
}
})
);
}
#[test]
fn session_resume_request_carries_the_target_context() {
let event = EventMsg::SessionResumeRequested(SessionResumeRequestedEvent {
session_id: "session-2".into(),
context: SessionContext {
workspace_label: Some("Project Two".into()),
origin_label: Some("cron".into()),
..SessionContext::default()
},
});
assert_eq!(
serde_json::to_value(event).expect("serialize resume event"),
json!({
"type": "session_resume_requested",
"session_id": "session-2",
"context": {
"workspace_label": "Project Two",
"origin_label": "cron"
}
})
);
}
#[test]
fn frontend_event_has_a_distinct_nested_discriminator() {
let event = EventMsg::Frontend(FrontendEvent::Widget {
capability: "subagents".into(),
item: FrontendWidget {
id: "status".into(),
slot: FrontendSlot::ComposerHeader,
text: "2 agents".into(),
tone: FrontendTone::Neutral,
symbol: Some(FrontendSymbol::Agent),
icon_only: true,
progress: None,
content: None,
action: None,
},
});
let value = serde_json::to_value(&event).expect("serialize frontend event");
assert_eq!(value["type"], "frontend");
assert_eq!(value["frontend_type"], "widget");
assert_eq!(
serde_json::from_value::<EventMsg>(value).expect("deserialize frontend event"),
event
);
}
#[test]
fn capability_surface_slots_have_stable_wire_names() {
assert_eq!(
serde_json::to_value(FrontendSlot::Navigation).expect("navigation slot"),
json!("navigation")
);
assert_eq!(
serde_json::to_value(FrontendSlot::ChatMenu).expect("chat menu slot"),
json!("chat_menu")
);
assert_eq!(
serde_json::to_value(FrontendSlot::TranscriptTail).expect("transcript tail slot"),
json!("transcript_tail")
);
}
#[test]
fn interrupt_has_a_targeted_wire_shape() {
let submission = Submission {
id: "cancel-1".into(),
op: Op::Interrupt {
turn_id: "turn-1".into(),
},
};
assert_eq!(
serde_json::to_value(submission).expect("serialize interrupt"),
json!({
"id": "cancel-1",
"op": {
"type": "interrupt",
"turn_id": "turn-1"
}
})
);
}
#[test]
fn user_input_has_one_text_payload() {
let submission = Submission {
id: "input-1".into(),
op: Op::UserInput {
text: "hello".into(),
attachments: Vec::new(),
},
};
assert_eq!(
serde_json::to_value(submission).expect("serialize input"),
json!({
"id": "input-1",
"op": {
"type": "user_input",
"text": "hello",
"attachments": []
}
})
);
}
#[test]
fn system_event_omits_submission_correlation() {
let event = Event {
submission_id: None,
msg: EventMsg::Warning(WarningEvent {
message: "system notice".into(),
}),
};
assert_eq!(
serde_json::to_value(event).expect("serialize system event"),
json!({
"msg": {
"type": "warning",
"message": "system notice"
}
})
);
}
#[test]
fn context_compacted_is_a_unit_event() {
assert_eq!(
serde_json::to_value(EventMsg::ContextCompacted).expect("serialize compaction"),
json!({"type": "context_compacted"})
);
}
#[test]
fn token_usage_overflow_does_not_partially_update_the_total() {
let mut total = TokenUsage {
input_tokens: 7,
total_tokens: i64::MAX,
..TokenUsage::default()
};
let original = total.clone();
assert!(
total
.checked_add(&TokenUsage {
input_tokens: 1,
total_tokens: 1,
..TokenUsage::default()
})
.is_none()
);
assert_eq!(total, original);
}
#[test]
fn symbols_round_trip_and_keep_unknown_names() {
for symbol in [
FrontendSymbol::Agent,
FrontendSymbol::Brain,
FrontendSymbol::Branch,
FrontendSymbol::Chat,
FrontendSymbol::ChatGpt,
FrontendSymbol::Claude,
FrontendSymbol::Deepseek,
FrontendSymbol::Delete,
FrontendSymbol::Edit,
FrontendSymbol::Kimi,
FrontendSymbol::Moon,
FrontendSymbol::Promote,
FrontendSymbol::Route,
FrontendSymbol::Search,
FrontendSymbol::Sparkle,
FrontendSymbol::Storage,
FrontendSymbol::Task,
] {
let json = serde_json::to_string(&symbol).expect("symbol serializes");
assert_eq!(json, format!("\"{}\"", symbol.as_str()));
let decoded: FrontendSymbol = serde_json::from_str(&json).expect("symbol deserializes");
assert_eq!(decoded, symbol);
}
let custom: FrontendSymbol =
serde_json::from_str("\"telescope\"").expect("unknown symbol deserializes");
assert_eq!(custom, FrontendSymbol::Custom("telescope".into()));
assert_eq!(custom.as_str(), "telescope");
let normalized: FrontendSymbol = serde_json::from_str(
&serde_json::to_string(&FrontendSymbol::Custom("edit".into()))
.expect("custom serializes"),
)
.expect("custom deserializes");
assert_eq!(normalized, FrontendSymbol::Edit);
}
}