use std::io;
use swink_agent::{
AgentMessage, CustomMessageRegistry, LlmMessage, restore_single_custom,
serialize_custom_message,
};
use crate::entry::SessionEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MessageKind {
Llm,
Custom,
}
impl MessageKind {
#[allow(dead_code)]
pub const fn as_str(self) -> &'static str {
match self {
Self::Llm => "llm",
Self::Custom => "custom",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"llm" => Some(Self::Llm),
"custom" => Some(Self::Custom),
_ => None,
}
}
}
#[allow(dead_code)]
fn encode(msg: &AgentMessage, session_id: &str) -> Option<(MessageKind, String)> {
match msg {
AgentMessage::Llm(llm) => serde_json::to_string(llm)
.ok()
.map(|json| (MessageKind::Llm, json)),
AgentMessage::Custom(custom) => {
let Some(envelope) = serialize_custom_message(custom.as_ref()) else {
tracing::warn!(
session_id,
type_name = custom.type_name().unwrap_or("<unknown>"),
"skipping non-serializable CustomMessage"
);
return None;
};
serde_json::to_string(&envelope)
.ok()
.map(|json| (MessageKind::Custom, json))
}
_ => {
tracing::warn!(
session_id,
"skipping AgentMessage variant not recognised by this build"
);
None
}
}
}
fn decode(
kind: MessageKind,
data: &str,
registry: Option<&CustomMessageRegistry>,
) -> io::Result<Option<AgentMessage>> {
match kind {
MessageKind::Llm => serde_json::from_str::<LlmMessage>(data)
.map(|m| Some(AgentMessage::Llm(m)))
.map_err(io::Error::other),
MessageKind::Custom => {
let envelope: serde_json::Value =
serde_json::from_str(data).map_err(io::Error::other)?;
restore_single_custom(registry, &envelope)
.map(|opt| opt.map(AgentMessage::Custom))
.map_err(io::Error::other)
}
}
}
pub fn encode_jsonl_message_line(msg: &AgentMessage, session_id: &str) -> Option<String> {
match msg {
AgentMessage::Llm(llm) => serde_json::to_string(llm).ok(),
AgentMessage::Custom(custom) => {
let Some(envelope) = serialize_custom_message(custom.as_ref()) else {
tracing::warn!(
session_id,
type_name = custom.type_name().unwrap_or("<unknown>"),
"skipping non-serializable CustomMessage"
);
return None;
};
let mut envelope = envelope;
envelope
.as_object_mut()
.expect("custom message envelope must be an object")
.insert("_custom".to_string(), serde_json::Value::Bool(true));
serde_json::to_string(&envelope).ok()
}
_ => {
tracing::warn!(
session_id,
"skipping AgentMessage variant not recognised by this build"
);
None
}
}
}
pub fn decode_jsonl_message_line(
line: &str,
registry: Option<&CustomMessageRegistry>,
) -> io::Result<Option<AgentMessage>> {
let value: serde_json::Value = serde_json::from_str(line).map_err(io::Error::other)?;
if value.get("_state").and_then(serde_json::Value::as_bool) == Some(true) {
return Ok(None);
}
if value.get("_custom").and_then(serde_json::Value::as_bool) == Some(true) {
return decode(MessageKind::Custom, line, registry);
}
if let Ok(message) = serde_json::from_value::<LlmMessage>(value) {
return Ok(Some(AgentMessage::Llm(message)));
}
match SessionEntry::parse(line).map_err(io::Error::other)? {
SessionEntry::Message(message) => Ok(Some(AgentMessage::Llm(message))),
_ => Ok(None),
}
}
#[allow(dead_code)]
fn encode_session_entry(entry: &SessionEntry) -> io::Result<(String, String)> {
let kind = entry.entry_type_name().to_string();
let data = serde_json::to_string(entry).map_err(io::Error::other)?;
Ok((kind, data))
}
#[allow(dead_code)]
fn decode_session_entry(kind: &str, data: &str) -> io::Result<Option<SessionEntry>> {
match MessageKind::parse(kind) {
Some(MessageKind::Llm) => serde_json::from_str::<LlmMessage>(data)
.map(SessionEntry::Message)
.map(Some)
.map_err(io::Error::other),
Some(MessageKind::Custom) => serde_json::from_str::<SessionEntry>(data)
.map_or_else(|_| Ok(None), |entry| Ok(Some(entry))),
None => serde_json::from_str::<SessionEntry>(data)
.map(Some)
.map_err(io::Error::other),
}
}
#[cfg(test)]
#[path = "codec_tests.rs"]
mod tests;