use crate::generative_model::{ToolResult, ToolSpec, ToolUse};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Request {
Hello,
ToolCall {
id: String,
agent_id: uuid::Uuid,
tool_use: ToolUse,
},
AgentFinished { agent_id: uuid::Uuid },
}
impl Request {
pub fn encode(&self) -> Result<Vec<u8>, String> {
let mut line = serde_json::to_string(self).map_err(|e| e.to_string())?;
line.push('\n');
Ok(line.into_bytes())
}
pub fn decode(line: &str) -> Result<Self, String> {
serde_json::from_str(line.trim()).map_err(|e| format!("parse request {line:?}: {e}"))
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Response {
HelloOk {
name: String,
version: String,
tools: Vec<ToolSpec>,
},
ToolResult {
id: String,
result: ToolResult,
},
AgentFinishedOk {
agent_id: uuid::Uuid,
},
Error {
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
message: String,
},
}
impl Response {
pub fn encode(&self) -> Result<Vec<u8>, String> {
let mut line = serde_json::to_string(self).map_err(|e| e.to_string())?;
line.push('\n');
Ok(line.into_bytes())
}
pub fn decode(line: &str) -> Result<Self, String> {
serde_json::from_str(line.trim()).map_err(|e| format!("parse response {line:?}: {e}"))
}
pub async fn write_to<W>(&self, writer: &mut W) -> Result<(), String>
where
W: tokio::io::AsyncWriteExt + Unpin,
{
let bytes = self.encode()?;
writer
.write_all(&bytes)
.await
.map_err(|e| format!("write: {e}"))?;
writer.flush().await.map_err(|e| format!("flush: {e}"))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generative_model::{Content, ToolResult, ToolUse};
use serde_json::json;
#[test]
fn request_tool_call_roundtrip() {
let msg = Request::ToolCall {
id: "1".into(),
agent_id: uuid::Uuid::nil(),
tool_use: ToolUse {
id: "toolu_1".into(),
name: "bash".into(),
input: json!({"command": "echo hi"}),
},
};
let line = String::from_utf8(msg.encode().unwrap()).unwrap();
assert!(line.contains(r#""type":"tool_call""#));
let back = Request::decode(&line).unwrap();
match back {
Request::ToolCall { id, tool_use, .. } => {
assert_eq!(id, "1");
assert_eq!(tool_use.name, "bash");
}
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn response_tool_result_roundtrip() {
let msg = Response::ToolResult {
id: "1".into(),
result: ToolResult::text("hi").with_id("toolu_1"),
};
let line = String::from_utf8(msg.encode().unwrap()).unwrap();
let back = Response::decode(&line).unwrap();
match back {
Response::ToolResult { id, result } => {
assert_eq!(id, "1");
assert!(!result.is_error);
assert!(matches!(&result.content[0], Content::Text { text } if text == "hi"));
}
other => panic!("unexpected {other:?}"),
}
}
}