use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::content::ContentBlock;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ToolCallId(pub String);
impl ToolCallId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
impl core::fmt::Display for ToolCallId {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(&self.0, formatter)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TerminalId(pub String);
impl TerminalId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
impl core::fmt::Display for TerminalId {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(&self.0, formatter)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolKind {
Read,
Edit,
Delete,
Move,
Search,
Execute,
Think,
Fetch,
SwitchMode,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
Pending,
InProgress,
Completed,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCall {
pub tool_call_id: ToolCallId,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<ToolKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<ToolCallStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<ToolCallContent>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<ToolCallLocation>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_input: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_output: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallUpdate {
pub tool_call_id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<Option<ToolKind>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<Option<ToolCallStatus>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<ToolCallContent>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<ToolCallLocation>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_input: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_output: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolCallContent {
Content(ContentBlockWrapper),
Diff(Diff),
Terminal(TerminalRef),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentBlockWrapper {
pub content: ContentBlock,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Diff {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub old_text: Option<String>,
pub new_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminalRef {
pub terminal_id: TerminalId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallLocation {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(default, rename = "_meta", skip_serializing_if = "BTreeMap::is_empty")]
pub meta: BTreeMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_kind_string_round_trips() -> Result<(), serde_json::Error> {
for (variant, wire) in [
(ToolKind::Read, "read"),
(ToolKind::Edit, "edit"),
(ToolKind::Delete, "delete"),
(ToolKind::Move, "move"),
(ToolKind::Search, "search"),
(ToolKind::Execute, "execute"),
(ToolKind::Think, "think"),
(ToolKind::Fetch, "fetch"),
(ToolKind::SwitchMode, "switch_mode"),
(ToolKind::Other, "other"),
] {
let value = serde_json::to_value(variant)?;
assert_eq!(value, json!(wire));
let back: ToolKind = serde_json::from_value(value)?;
assert_eq!(back, variant);
}
Ok(())
}
#[test]
fn tool_call_status_string_round_trips() -> Result<(), serde_json::Error> {
for (variant, wire) in [
(ToolCallStatus::Pending, "pending"),
(ToolCallStatus::InProgress, "in_progress"),
(ToolCallStatus::Completed, "completed"),
(ToolCallStatus::Failed, "failed"),
] {
let value = serde_json::to_value(variant)?;
assert_eq!(value, json!(wire));
let back: ToolCallStatus = serde_json::from_value(value)?;
assert_eq!(back, variant);
}
Ok(())
}
#[test]
fn a_tool_call_round_trips() -> Result<(), serde_json::Error> {
let call = ToolCall {
tool_call_id: ToolCallId::new("call-1"),
title: "Reading config".to_owned(),
name: Some("read_file".to_owned()),
kind: Some(ToolKind::Read),
status: Some(ToolCallStatus::InProgress),
content: None,
locations: Some(vec![ToolCallLocation {
path: "/etc/hosts".to_owned(),
line: Some(1),
meta: BTreeMap::new(),
}]),
raw_input: None,
raw_output: None,
};
let value = serde_json::to_value(&call)?;
assert_eq!(value.get("toolCallId"), Some(&json!("call-1")));
assert_eq!(value.get("title"), Some(&json!("Reading config")));
assert_eq!(value.get("kind"), Some(&json!("read")));
let back: ToolCall = serde_json::from_value(value)?;
assert_eq!(back, call);
Ok(())
}
#[test]
fn a_tool_call_update_round_trips() -> Result<(), serde_json::Error> {
let update = ToolCallUpdate {
tool_call_id: ToolCallId::new("call-1"),
kind: Some(Some(ToolKind::Execute)),
status: Some(Some(ToolCallStatus::Completed)),
title: None,
name: None,
content: None,
locations: None,
raw_input: None,
raw_output: None,
};
let value = serde_json::to_value(&update)?;
assert_eq!(value.get("toolCallId"), Some(&json!("call-1")));
assert_eq!(value.get("kind"), Some(&json!("execute")));
assert_eq!(value.get("status"), Some(&json!("completed")));
Ok(())
}
#[test]
fn a_tool_call_content_diff_round_trips() -> Result<(), serde_json::Error> {
let content = ToolCallContent::Diff(Diff {
path: "/tmp/example.txt".to_owned(),
old_text: Some("old".to_owned()),
new_text: "new".to_owned(),
});
let value = serde_json::to_value(&content)?;
assert_eq!(value.get("type"), Some(&json!("diff")));
assert_eq!(value.get("path"), Some(&json!("/tmp/example.txt")));
assert_eq!(value.get("oldText"), Some(&json!("old")));
assert_eq!(value.get("newText"), Some(&json!("new")));
Ok(())
}
#[test]
fn a_tool_call_content_terminal_round_trips() -> Result<(), serde_json::Error> {
let content = ToolCallContent::Terminal(TerminalRef {
terminal_id: TerminalId::new("term-1"),
});
let value = serde_json::to_value(&content)?;
assert_eq!(value.get("type"), Some(&json!("terminal")));
assert_eq!(value.get("terminalId"), Some(&json!("term-1")));
Ok(())
}
}