use std::{path::PathBuf, sync::Arc};
use derive_more::{Display, From};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{ContentBlock, Error, IntoOption, Meta, TerminalId};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ToolCall {
pub tool_call_id: ToolCallId,
pub title: String,
#[serde(default, skip_serializing_if = "ToolKind::is_default")]
pub kind: ToolKind,
#[serde(default, skip_serializing_if = "ToolCallStatus::is_default")]
pub status: ToolCallStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content: Vec<ToolCallContent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub locations: Vec<ToolCallLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_input: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_output: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl ToolCall {
#[must_use]
pub fn new(tool_call_id: impl Into<ToolCallId>, title: impl Into<String>) -> Self {
Self {
tool_call_id: tool_call_id.into(),
title: title.into(),
kind: ToolKind::default(),
status: ToolCallStatus::default(),
content: Vec::default(),
locations: Vec::default(),
raw_input: None,
raw_output: None,
meta: None,
}
}
#[must_use]
pub fn kind(mut self, kind: ToolKind) -> Self {
self.kind = kind;
self
}
#[must_use]
pub fn status(mut self, status: ToolCallStatus) -> Self {
self.status = status;
self
}
#[must_use]
pub fn content(mut self, content: Vec<ToolCallContent>) -> Self {
self.content = content;
self
}
#[must_use]
pub fn locations(mut self, locations: Vec<ToolCallLocation>) -> Self {
self.locations = locations;
self
}
#[must_use]
pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
self.raw_input = raw_input.into_option();
self
}
#[must_use]
pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
self.raw_output = raw_output.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
pub fn update(&mut self, fields: ToolCallUpdateFields) {
if let Some(title) = fields.title {
self.title = title;
}
if let Some(kind) = fields.kind {
self.kind = kind;
}
if let Some(status) = fields.status {
self.status = status;
}
if let Some(content) = fields.content {
self.content = content;
}
if let Some(locations) = fields.locations {
self.locations = locations;
}
if let Some(raw_input) = fields.raw_input {
self.raw_input = Some(raw_input);
}
if let Some(raw_output) = fields.raw_output {
self.raw_output = Some(raw_output);
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ToolCallUpdate {
pub tool_call_id: ToolCallId,
#[serde(flatten)]
pub fields: ToolCallUpdateFields,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl ToolCallUpdate {
#[must_use]
pub fn new(tool_call_id: impl Into<ToolCallId>, fields: ToolCallUpdateFields) -> Self {
Self {
tool_call_id: tool_call_id.into(),
fields,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ToolCallUpdateFields {
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<ToolKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<ToolCallStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<ToolCallContent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<ToolCallLocation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_input: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_output: Option<serde_json::Value>,
}
impl ToolCallUpdateFields {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn kind(mut self, kind: impl IntoOption<ToolKind>) -> Self {
self.kind = kind.into_option();
self
}
#[must_use]
pub fn status(mut self, status: impl IntoOption<ToolCallStatus>) -> Self {
self.status = status.into_option();
self
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn content(mut self, content: impl IntoOption<Vec<ToolCallContent>>) -> Self {
self.content = content.into_option();
self
}
#[must_use]
pub fn locations(mut self, locations: impl IntoOption<Vec<ToolCallLocation>>) -> Self {
self.locations = locations.into_option();
self
}
#[must_use]
pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
self.raw_input = raw_input.into_option();
self
}
#[must_use]
pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
self.raw_output = raw_output.into_option();
self
}
}
impl TryFrom<ToolCallUpdate> for ToolCall {
type Error = Error;
fn try_from(update: ToolCallUpdate) -> Result<Self, Self::Error> {
let ToolCallUpdate {
tool_call_id,
fields:
ToolCallUpdateFields {
kind,
status,
title,
content,
locations,
raw_input,
raw_output,
},
meta,
} = update;
Ok(Self {
tool_call_id,
title: title.ok_or_else(|| {
Error::invalid_params().data(serde_json::json!("title is required for a tool call"))
})?,
kind: kind.unwrap_or_default(),
status: status.unwrap_or_default(),
content: content.unwrap_or_default(),
locations: locations.unwrap_or_default(),
raw_input,
raw_output,
meta,
})
}
}
impl From<ToolCall> for ToolCallUpdate {
fn from(value: ToolCall) -> Self {
let ToolCall {
tool_call_id,
title,
kind,
status,
content,
locations,
raw_input,
raw_output,
meta,
} = value;
Self {
tool_call_id,
fields: ToolCallUpdateFields {
kind: Some(kind),
status: Some(status),
title: Some(title),
content: Some(content),
locations: Some(locations),
raw_input,
raw_output,
},
meta,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct ToolCallId(pub Arc<str>);
impl ToolCallId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ToolKind {
Read,
Edit,
Delete,
Move,
Search,
Execute,
Think,
Fetch,
SwitchMode,
#[default]
#[serde(other)]
Other,
}
impl ToolKind {
#[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
fn is_default(&self) -> bool {
matches!(self, ToolKind::Other)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ToolCallStatus {
#[default]
Pending,
InProgress,
Completed,
Failed,
}
impl ToolCallStatus {
#[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
fn is_default(&self) -> bool {
matches!(self, ToolCallStatus::Pending)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "type"}))]
#[non_exhaustive]
pub enum ToolCallContent {
Content(Content),
Diff(Diff),
Terminal(Terminal),
}
impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
fn from(content: T) -> Self {
ToolCallContent::Content(Content::new(content))
}
}
impl From<Diff> for ToolCallContent {
fn from(diff: Diff) -> Self {
ToolCallContent::Diff(diff)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Content {
pub content: ContentBlock,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl Content {
#[must_use]
pub fn new(content: impl Into<ContentBlock>) -> Self {
Self {
content: content.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Terminal {
pub terminal_id: TerminalId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl Terminal {
#[must_use]
pub fn new(terminal_id: impl Into<TerminalId>) -> Self {
Self {
terminal_id: terminal_id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Diff {
pub path: PathBuf,
pub old_text: Option<String>,
pub new_text: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl Diff {
#[must_use]
pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
Self {
path: path.into(),
old_text: None,
new_text: new_text.into(),
meta: None,
}
}
#[must_use]
pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
self.old_text = old_text.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ToolCallLocation {
pub path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl ToolCallLocation {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
line: None,
meta: None,
}
}
#[must_use]
pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
self.line = line.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}