use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use toolkit_macros::domain_model;
use uuid::Uuid;
use crate::domain::message::StreamingEvent;
#[domain_model]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeltaOp {
Add,
Append,
Patch,
Remove,
Stop,
}
#[domain_model]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WireStreamEvent {
#[serde(rename = "message.start")]
Start { message_id: Uuid, seq: u64 },
#[serde(rename = "message.part.add")]
PartAdd {
message_id: Uuid,
seq: u64,
#[serde(rename = "o")]
op: DeltaOp,
#[serde(rename = "p")]
path: String,
#[serde(rename = "v")]
value: JsonValue,
},
#[serde(rename = "message.text.delta")]
TextDelta {
message_id: Uuid,
seq: u64,
#[serde(rename = "o")]
op: DeltaOp,
#[serde(rename = "p")]
path: String,
#[serde(rename = "v")]
value: JsonValue,
},
#[serde(rename = "message.file_citation.add")]
FileCitationAdd {
message_id: Uuid,
seq: u64,
#[serde(rename = "o")]
op: DeltaOp,
#[serde(rename = "p")]
path: String,
#[serde(rename = "v")]
value: JsonValue,
},
#[serde(rename = "message.link_citation.add")]
LinkCitationAdd {
message_id: Uuid,
seq: u64,
#[serde(rename = "o")]
op: DeltaOp,
#[serde(rename = "p")]
path: String,
#[serde(rename = "v")]
value: JsonValue,
},
#[serde(rename = "message.reference.add")]
ReferenceAdd {
message_id: Uuid,
seq: u64,
#[serde(rename = "o")]
op: DeltaOp,
#[serde(rename = "p")]
path: String,
#[serde(rename = "v")]
value: JsonValue,
},
#[serde(rename = "message.status.changed")]
StatusChanged {
message_id: Uuid,
seq: u64,
code: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
#[serde(rename = "message.state.changed")]
StateChanged {
message_id: Uuid,
seq: u64,
state: JsonValue,
},
#[serde(rename = "session.meta.updated")]
SessionMetaUpdated {
message_id: Uuid,
seq: u64,
patch: JsonValue,
},
#[serde(rename = "message.tool")]
Tool {
message_id: Uuid,
seq: u64,
tool: String,
payload: JsonValue,
},
#[serde(rename = "message.complete")]
Complete {
message_id: Uuid,
seq: u64,
#[serde(rename = "o")]
op: DeltaOp,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<JsonValue>,
},
#[serde(rename = "message.error")]
Error {
message_id: Uuid,
seq: u64,
error: String,
},
}
impl WireStreamEvent {
#[must_use]
pub fn seq(&self) -> u64 {
match self {
WireStreamEvent::Start { seq, .. }
| WireStreamEvent::PartAdd { seq, .. }
| WireStreamEvent::TextDelta { seq, .. }
| WireStreamEvent::FileCitationAdd { seq, .. }
| WireStreamEvent::LinkCitationAdd { seq, .. }
| WireStreamEvent::ReferenceAdd { seq, .. }
| WireStreamEvent::StatusChanged { seq, .. }
| WireStreamEvent::StateChanged { seq, .. }
| WireStreamEvent::SessionMetaUpdated { seq, .. }
| WireStreamEvent::Tool { seq, .. }
| WireStreamEvent::Complete { seq, .. }
| WireStreamEvent::Error { seq, .. } => *seq,
}
}
#[must_use]
pub fn event_name(&self) -> &'static str {
match self {
WireStreamEvent::Start { .. } => "message.start",
WireStreamEvent::PartAdd { .. } => "message.part.add",
WireStreamEvent::TextDelta { .. } => "message.text.delta",
WireStreamEvent::FileCitationAdd { .. } => "message.file_citation.add",
WireStreamEvent::LinkCitationAdd { .. } => "message.link_citation.add",
WireStreamEvent::ReferenceAdd { .. } => "message.reference.add",
WireStreamEvent::StatusChanged { .. } => "message.status.changed",
WireStreamEvent::StateChanged { .. } => "message.state.changed",
WireStreamEvent::SessionMetaUpdated { .. } => "session.meta.updated",
WireStreamEvent::Tool { .. } => "message.tool",
WireStreamEvent::Complete { .. } => "message.complete",
WireStreamEvent::Error { .. } => "message.error",
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(
self,
WireStreamEvent::Complete { .. } | WireStreamEvent::Error { .. }
)
}
}
#[domain_model]
pub struct DeltaProjector {
message_id: Uuid,
next_seq: u64,
text_part: Option<usize>,
next_part: usize,
}
impl Default for DeltaProjector {
fn default() -> Self {
Self::new()
}
}
impl DeltaProjector {
#[must_use]
pub fn new() -> Self {
Self {
message_id: Uuid::nil(),
next_seq: 0,
text_part: None,
next_part: 0,
}
}
fn take_seq(&mut self) -> u64 {
let s = self.next_seq;
self.next_seq += 1;
s
}
pub fn project(&mut self, event: StreamingEvent) -> Vec<WireStreamEvent> {
match event {
StreamingEvent::Start(s) => {
self.message_id = s.message_id;
vec![WireStreamEvent::Start {
message_id: self.message_id,
seq: self.take_seq(),
}]
}
StreamingEvent::Chunk(c) => {
let mut out = Vec::new();
let idx = match self.text_part {
Some(i) => i,
None => {
let i = self.next_part;
self.next_part += 1;
self.text_part = Some(i);
out.push(WireStreamEvent::PartAdd {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Add,
path: format!("parts/{i}"),
value: json!({ "type": "text", "content": { "text": "" }, "number": i }),
});
i
}
};
out.push(WireStreamEvent::TextDelta {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Append,
path: format!("parts/{idx}/content/text"),
value: JsonValue::String(c.chunk),
});
out
}
StreamingEvent::Status(s) => {
vec![WireStreamEvent::StatusChanged {
message_id: self.message_id,
seq: self.take_seq(),
code: s.code,
detail: s.detail,
}]
}
StreamingEvent::Part(p) => {
let idx = self.next_part;
self.next_part += 1;
let mut value = serde_json::to_value(&p.part).unwrap_or(JsonValue::Null);
if let Some(obj) = value.as_object_mut() {
obj.insert("number".to_owned(), JsonValue::from(idx));
}
vec![WireStreamEvent::PartAdd {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Add,
path: format!("parts/{idx}"),
value,
}]
}
StreamingEvent::Citation(c) => self.citation_deltas(
c.part_number,
&c.file_citations,
&c.link_citations,
&c.references,
),
StreamingEvent::State(s) => {
vec![WireStreamEvent::StateChanged {
message_id: self.message_id,
seq: self.take_seq(),
state: s.state,
}]
}
StreamingEvent::SessionMeta(s) => {
vec![WireStreamEvent::SessionMetaUpdated {
message_id: self.message_id,
seq: self.take_seq(),
patch: s.patch,
}]
}
StreamingEvent::Tool(t) => {
vec![WireStreamEvent::Tool {
message_id: self.message_id,
seq: self.take_seq(),
tool: t.tool,
payload: t.payload,
}]
}
StreamingEvent::Complete(c) => {
let tp = i32::try_from(self.text_part.unwrap_or(0)).unwrap_or(0);
let mut out =
self.citation_deltas(tp, &c.file_citations, &c.link_citations, &c.references);
out.push(WireStreamEvent::Complete {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Stop,
metadata: c.metadata,
});
out
}
StreamingEvent::Error(e) => {
vec![WireStreamEvent::Error {
message_id: self.message_id,
seq: self.take_seq(),
error: e.error,
}]
}
}
}
fn citation_deltas(
&mut self,
part_number: i32,
file_citations: &[chat_engine_sdk::models::FileCitation],
link_citations: &[chat_engine_sdk::models::LinkCitation],
references: &[chat_engine_sdk::models::LinkReference],
) -> Vec<WireStreamEvent> {
let mut out = Vec::new();
if !file_citations.is_empty() {
let value = serde_json::to_value(file_citations).unwrap_or(JsonValue::Null);
out.push(WireStreamEvent::FileCitationAdd {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Append,
path: format!("parts/{part_number}/file_citations"),
value,
});
}
if !link_citations.is_empty() {
let value = serde_json::to_value(link_citations).unwrap_or(JsonValue::Null);
out.push(WireStreamEvent::LinkCitationAdd {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Append,
path: format!("parts/{part_number}/link_citations"),
value,
});
}
if !references.is_empty() {
let value = serde_json::to_value(references).unwrap_or(JsonValue::Null);
out.push(WireStreamEvent::ReferenceAdd {
message_id: self.message_id,
seq: self.take_seq(),
op: DeltaOp::Append,
path: format!("parts/{part_number}/references"),
value,
});
}
out
}
}
#[cfg(test)]
#[path = "stream_delta_tests.rs"]
mod stream_delta_tests;