use serde::{Deserialize, Serialize};
use serde_json::Value;
fn is_false(b: &bool) -> bool {
!b
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
Thinking {
thinking: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
Image {
data: String,
mime_type: String,
},
ToolUse {
id: String,
name: String,
arguments: String,
},
ToolResult {
tool_call_id: String,
tool_name: String,
content: String,
#[serde(default, skip_serializing_if = "is_false")]
is_error: bool,
},
}
impl<T: Into<String>> From<T> for ContentBlock {
fn from(value: T) -> Self {
ContentBlock::Text { text: value.into() }
}
}
#[derive(Debug, Clone)]
pub struct ToolUseBlock {
pub id: String,
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone)]
pub struct ToolResultBlock {
pub tool_call_id: String,
pub tool_name: String,
pub content: String,
pub is_error: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Message {
pub role: Role,
pub content: Vec<ContentBlock>,
}
impl Message {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
content: vec![ContentBlock::from(text)],
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: vec![ContentBlock::from(text)],
}
}
pub fn tool_result(
tool_call_id: impl Into<String>,
tool_name: impl Into<String>,
content: impl Into<String>,
is_error: bool,
) -> Self {
Self {
role: Role::Tool,
content: vec![ContentBlock::ToolResult {
tool_call_id: tool_call_id.into(),
tool_name: tool_name.into(),
content: content.into(),
is_error,
}],
}
}
pub fn text(&self) -> Option<String> {
let joined: String = self
.content
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect();
if joined.is_empty() {
None
} else {
Some(joined)
}
}
pub fn tool_calls(&self) -> Vec<ToolUseBlock> {
self.content
.iter()
.filter_map(|b| match b {
ContentBlock::ToolUse {
id,
name,
arguments,
} => Some(ToolUseBlock {
id: id.clone(),
name: name.clone(),
arguments: arguments.clone(),
}),
_ => None,
})
.collect()
}
pub fn tool_result_block(&self) -> Option<ToolResultBlock> {
self.content.iter().find_map(|b| match b {
ContentBlock::ToolResult {
tool_call_id,
tool_name,
content,
is_error,
} => Some(ToolResultBlock {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
content: content.clone(),
is_error: *is_error,
}),
_ => None,
})
}
}
#[derive(Debug, Serialize, Clone)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDefinition,
}
#[derive(Debug, Serialize, Clone)]
pub struct FunctionDefinition {
pub name: String,
pub description: String,
pub parameters: Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
Stop,
ToolCalls,
MaxTokens,
Other(String),
}
#[derive(Debug, Deserialize)]
pub struct Choice {
pub message: Message,
pub finish_reason: Option<FinishReason>,
}
#[derive(Debug, Serialize, Clone)]
pub struct AgentStep {
pub iteration: usize,
pub message: String,
pub tool_calls: Option<Vec<ToolExecutionResult>>,
}
#[derive(Debug, Serialize, Clone)]
pub struct ToolExecutionResult {
pub tool_name: String,
pub arguments: String,
pub result: String,
}
#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxIterations,
Cancelled,
NoContent,
}
#[derive(Debug, Serialize)]
pub struct AgentResult {
pub final_response: String,
pub steps: Vec<AgentStep>,
pub iterations_used: usize,
pub stop_reason: StopReason,
}
#[derive(Debug, Serialize, Clone)]
#[serde(tag = "event_type")]
pub enum StreamEvent {
#[serde(rename = "iteration_start")]
IterationStart { iteration: usize },
#[serde(rename = "tool_call")]
ToolCall {
id: String,
tool_name: String,
arguments: String,
},
#[serde(rename = "tool_result")]
ToolResult {
id: String,
tool_name: String,
result: String,
is_error: bool,
},
#[serde(rename = "llm_response")]
LlmResponse { content: String },
#[serde(rename = "thinking_content")]
ThinkingContent { content: String },
#[serde(rename = "finished")]
Finished {
final_response: String,
iterations: usize,
},
#[serde(rename = "message_appended")]
MessageAppended { message: Message },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_user_sets_correct_fields() {
let msg = Message::user("hello");
assert_eq!(msg.role, Role::User);
assert_eq!(msg.text().as_deref(), Some("hello"));
assert!(msg.tool_calls().is_empty());
assert!(msg.tool_result_block().is_none());
}
#[test]
fn message_assistant_sets_correct_fields() {
let msg = Message::assistant("response");
assert_eq!(msg.role, Role::Assistant);
assert_eq!(msg.text().as_deref(), Some("response"));
assert!(msg.tool_calls().is_empty());
}
#[test]
fn message_tool_result_sets_correct_fields() {
let msg = Message::tool_result("call_1", "read_file", "content", false);
assert_eq!(msg.role, Role::Tool);
assert!(msg.tool_calls().is_empty());
let tr = msg.tool_result_block().unwrap();
assert_eq!(tr.tool_call_id, "call_1");
assert_eq!(tr.tool_name, "read_file");
assert_eq!(tr.content, "content");
assert!(!tr.is_error);
}
#[test]
fn message_text_joins_multiple_text_blocks() {
let msg = Message {
role: Role::Assistant,
content: vec![
ContentBlock::Text {
text: "hello ".into(),
},
ContentBlock::ToolUse {
id: "call_1".into(),
name: "read_file".into(),
arguments: "{}".into(),
},
ContentBlock::Text {
text: "world".into(),
},
],
};
assert_eq!(msg.text().as_deref(), Some("hello world"));
let calls = msg.tool_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "read_file");
}
#[test]
fn content_block_serializes_with_type_tag() {
let block = ContentBlock::ToolUse {
id: "call_1".into(),
name: "read_file".into(),
arguments: "{}".into(),
};
let json: Value = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "tool_use");
assert_eq!(json["name"], "read_file");
let block = ContentBlock::Thinking {
thinking: "hmm".into(),
signature: Some("sig".into()),
};
let json: Value = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "thinking");
assert_eq!(json["signature"], "sig");
}
#[test]
fn role_serializes_to_lowercase() {
assert_eq!(serde_json::to_string(&Role::System).unwrap(), "\"system\"");
assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
assert_eq!(
serde_json::to_string(&Role::Assistant).unwrap(),
"\"assistant\""
);
assert_eq!(serde_json::to_string(&Role::Tool).unwrap(), "\"tool\"");
}
#[test]
fn role_deserializes_from_lowercase() {
assert_eq!(
serde_json::from_str::<Role>("\"system\"").unwrap(),
Role::System
);
assert_eq!(
serde_json::from_str::<Role>("\"user\"").unwrap(),
Role::User
);
assert_eq!(
serde_json::from_str::<Role>("\"assistant\"").unwrap(),
Role::Assistant
);
assert_eq!(
serde_json::from_str::<Role>("\"tool\"").unwrap(),
Role::Tool
);
}
#[test]
fn stream_event_serializes_with_event_type_tag() {
let event = StreamEvent::IterationStart { iteration: 1 };
let json: Value = serde_json::to_value(&event).unwrap();
assert_eq!(json["event_type"], "iteration_start");
assert_eq!(json["iteration"], 1);
let event = StreamEvent::Finished {
final_response: "done".into(),
iterations: 3,
};
let json: Value = serde_json::to_value(&event).unwrap();
assert_eq!(json["event_type"], "finished");
assert_eq!(json["final_response"], "done");
assert_eq!(json["iterations"], 3);
}
#[test]
fn stream_event_tool_call_serializes() {
let event = StreamEvent::ToolCall {
id: "call_1".into(),
tool_name: "read_file".into(),
arguments: r#"{"path":"a.txt"}"#.into(),
};
let json: Value = serde_json::to_value(&event).unwrap();
assert_eq!(json["event_type"], "tool_call");
assert_eq!(json["tool_name"], "read_file");
}
}