use crate::serve::storage::Failure;
use serde::Deserialize;
use serde_json::{Value, json};
use std::io::{self, BufRead, Write};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Greeting {
pub hello: Hello,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Hello {
pub agent: String,
pub pass: String,
pub store: String,
pub resumable: bool,
pub harness: Harness,
}
#[derive(Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Harness {
pub id: String,
pub version: String,
pub protocol: u64,
}
pub(super) fn read(reader: &mut impl BufRead, bound: usize) -> Result<Option<Value>, Failure> {
let mut bytes = Vec::new();
loop {
let available = reader.fill_buf().map_err(|_| {
Failure::new(
"assistant_socket_read_failed",
"the seat frame could not be read",
)
})?;
if available.is_empty() {
return if bytes.is_empty() {
Ok(None)
} else {
Err(Failure::new(
"assistant_frame_invalid",
"frame ended without a newline",
))
};
}
let count = available
.iter()
.position(|byte| *byte == b'\n')
.map_or(available.len(), |index| index + 1);
if count > bound.saturating_sub(bytes.len()) {
return Err(super::assistant_state::bound(
"frame_bytes",
bound,
bytes.len().saturating_add(count),
));
}
let ended = available[count - 1] == b'\n';
bytes.extend_from_slice(&available[..count]);
reader.consume(count);
if ended {
break;
}
}
let value: Value = serde_json::from_slice(&bytes)
.map_err(|_| Failure::new("assistant_frame_invalid", "frame is not a JSON object"))?;
if !value.is_object() {
return Err(Failure::new(
"assistant_frame_invalid",
"frame is not a JSON object",
));
}
Ok(Some(value))
}
pub(super) fn write(writer: &mut impl Write, value: &Value) -> io::Result<()> {
serde_json::to_writer(&mut *writer, value)?;
writer.write_all(b"\n")?;
writer.flush()
}
pub(super) fn refuse(writer: &mut impl Write, error: &Failure) -> io::Result<()> {
write(
writer,
&json!({"refused":{"code":error.code,"message":error.message}}),
)
}
pub(super) fn identifier(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 256
&& !value
.chars()
.any(|c| c.is_control() || matches!(c, '/' | '\\'))
}
pub(super) fn item(value: &Value) -> Result<(), Failure> {
let kind = value["kind"].as_str().unwrap_or_default();
if !value["conversation"].as_str().is_some_and(identifier)
|| value["seq"].as_u64().is_none_or(|seq| seq == 0)
|| !value["durable"].is_boolean()
|| !matches!(
kind,
"request"
| "prompt_accepted"
| "prompt_queued"
| "text"
| "thought"
| "tool_call"
| "plan"
| "available_commands"
| "config_options"
| "config_failed"
| "permission_ask"
| "permission_answered"
| "state"
| "turn_ended"
| "turn_failed"
| "sessions"
| "session_opening"
| "session_opened"
| "session_failed"
| "omitted"
| "presence"
| "ended"
)
{
return Err(Failure::new(
"assistant_item_invalid",
"item needs a conversation, positive sequence, durable flag and declared kind",
));
}
Ok(())
}