1use std::{path::PathBuf, sync::Arc};
8
9use derive_more::{Display, From};
10use serde::{Deserialize, Serialize};
11use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
12
13use crate::{IntoOption, SkipListener};
14
15use super::{ContentBlock, Error, Meta, TerminalId};
16
17#[serde_as]
24#[skip_serializing_none]
25#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28#[non_exhaustive]
29pub struct ToolCall {
30 pub tool_call_id: ToolCallId,
32 pub title: String,
34 #[serde_as(deserialize_as = "DefaultOnError")]
39 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
40 #[serde(default)]
41 pub name: Option<String>,
42 #[serde_as(deserialize_as = "DefaultOnError")]
45 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
46 #[serde(default, skip_serializing_if = "ToolKind::is_default")]
47 pub kind: ToolKind,
48 #[serde_as(deserialize_as = "DefaultOnError")]
50 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
51 #[serde(default, skip_serializing_if = "ToolCallStatus::is_default")]
52 pub status: ToolCallStatus,
53 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
55 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub content: Vec<ToolCallContent>,
58 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
61 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub locations: Vec<ToolCallLocation>,
64 #[serde_as(deserialize_as = "DefaultOnError")]
66 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
67 #[serde(default)]
68 pub raw_input: Option<serde_json::Value>,
69 #[serde_as(deserialize_as = "DefaultOnError")]
71 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
72 #[serde(default)]
73 pub raw_output: Option<serde_json::Value>,
74 #[serde_as(deserialize_as = "DefaultOnError")]
80 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
81 #[serde(default)]
82 #[serde(rename = "_meta")]
83 pub meta: Option<Meta>,
84}
85
86impl ToolCall {
87 #[must_use]
89 pub fn new(tool_call_id: impl Into<ToolCallId>, title: impl Into<String>) -> Self {
90 Self {
91 tool_call_id: tool_call_id.into(),
92 title: title.into(),
93 name: None,
94 kind: ToolKind::default(),
95 status: ToolCallStatus::default(),
96 content: Vec::default(),
97 locations: Vec::default(),
98 raw_input: None,
99 raw_output: None,
100 meta: None,
101 }
102 }
103
104 #[must_use]
106 pub fn name(mut self, name: impl IntoOption<String>) -> Self {
107 self.name = name.into_option();
108 self
109 }
110
111 #[must_use]
114 pub fn kind(mut self, kind: ToolKind) -> Self {
115 self.kind = kind;
116 self
117 }
118
119 #[must_use]
121 pub fn status(mut self, status: ToolCallStatus) -> Self {
122 self.status = status;
123 self
124 }
125
126 #[must_use]
128 pub fn content(mut self, content: Vec<ToolCallContent>) -> Self {
129 self.content = content;
130 self
131 }
132
133 #[must_use]
136 pub fn locations(mut self, locations: Vec<ToolCallLocation>) -> Self {
137 self.locations = locations;
138 self
139 }
140
141 #[must_use]
143 pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
144 self.raw_input = raw_input.into_option();
145 self
146 }
147
148 #[must_use]
150 pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
151 self.raw_output = raw_output.into_option();
152 self
153 }
154
155 #[must_use]
161 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
162 self.meta = meta.into_option();
163 self
164 }
165
166 pub fn update(&mut self, fields: ToolCallUpdateFields) {
169 if let Some(title) = fields.title {
170 self.title = title;
171 }
172 if let Some(name) = fields.name {
173 self.name = Some(name);
174 }
175 if let Some(kind) = fields.kind {
176 self.kind = kind;
177 }
178 if let Some(status) = fields.status {
179 self.status = status;
180 }
181 if let Some(content) = fields.content {
182 self.content = content;
183 }
184 if let Some(locations) = fields.locations {
185 self.locations = locations;
186 }
187 if let Some(raw_input) = fields.raw_input {
188 self.raw_input = Some(raw_input);
189 }
190 if let Some(raw_output) = fields.raw_output {
191 self.raw_output = Some(raw_output);
192 }
193 }
194}
195
196#[serde_as]
203#[skip_serializing_none]
204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207#[non_exhaustive]
208pub struct ToolCallUpdate {
209 pub tool_call_id: ToolCallId,
211 #[serde(flatten)]
213 pub fields: ToolCallUpdateFields,
214 #[serde_as(deserialize_as = "DefaultOnError")]
220 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
221 #[serde(default)]
222 #[serde(rename = "_meta")]
223 pub meta: Option<Meta>,
224}
225
226impl ToolCallUpdate {
227 #[must_use]
229 pub fn new(tool_call_id: impl Into<ToolCallId>, fields: ToolCallUpdateFields) -> Self {
230 Self {
231 tool_call_id: tool_call_id.into(),
232 fields,
233 meta: None,
234 }
235 }
236
237 #[must_use]
243 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
244 self.meta = meta.into_option();
245 self
246 }
247}
248
249#[serde_as]
256#[skip_serializing_none]
257#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
258#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260#[non_exhaustive]
261pub struct ToolCallUpdateFields {
262 #[serde_as(deserialize_as = "DefaultOnError")]
264 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
265 #[serde(default)]
266 pub kind: Option<ToolKind>,
267 #[serde_as(deserialize_as = "DefaultOnError")]
269 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
270 #[serde(default)]
271 pub status: Option<ToolCallStatus>,
272 #[serde_as(deserialize_as = "DefaultOnError")]
274 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
275 #[serde(default)]
276 pub title: Option<String>,
277 #[serde_as(deserialize_as = "DefaultOnError")]
282 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
283 #[serde(default)]
284 pub name: Option<String>,
285 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
287 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
288 #[serde(default)]
289 pub content: Option<Vec<ToolCallContent>>,
290 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
292 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
293 #[serde(default)]
294 pub locations: Option<Vec<ToolCallLocation>>,
295 #[serde_as(deserialize_as = "DefaultOnError")]
297 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
298 #[serde(default)]
299 pub raw_input: Option<serde_json::Value>,
300 #[serde_as(deserialize_as = "DefaultOnError")]
302 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
303 #[serde(default)]
304 pub raw_output: Option<serde_json::Value>,
305}
306
307impl ToolCallUpdateFields {
308 #[must_use]
310 pub fn new() -> Self {
311 Self::default()
312 }
313
314 #[must_use]
316 pub fn kind(mut self, kind: impl IntoOption<ToolKind>) -> Self {
317 self.kind = kind.into_option();
318 self
319 }
320
321 #[must_use]
323 pub fn status(mut self, status: impl IntoOption<ToolCallStatus>) -> Self {
324 self.status = status.into_option();
325 self
326 }
327
328 #[must_use]
330 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
331 self.title = title.into_option();
332 self
333 }
334
335 #[must_use]
337 pub fn name(mut self, name: impl IntoOption<String>) -> Self {
338 self.name = name.into_option();
339 self
340 }
341
342 #[must_use]
344 pub fn content(mut self, content: impl IntoOption<Vec<ToolCallContent>>) -> Self {
345 self.content = content.into_option();
346 self
347 }
348
349 #[must_use]
351 pub fn locations(mut self, locations: impl IntoOption<Vec<ToolCallLocation>>) -> Self {
352 self.locations = locations.into_option();
353 self
354 }
355
356 #[must_use]
358 pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
359 self.raw_input = raw_input.into_option();
360 self
361 }
362
363 #[must_use]
365 pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
366 self.raw_output = raw_output.into_option();
367 self
368 }
369}
370
371impl TryFrom<ToolCallUpdate> for ToolCall {
374 type Error = Error;
375
376 fn try_from(update: ToolCallUpdate) -> Result<Self, Self::Error> {
377 let ToolCallUpdate {
378 tool_call_id,
379 fields:
380 ToolCallUpdateFields {
381 kind,
382 status,
383 title,
384 name,
385 content,
386 locations,
387 raw_input,
388 raw_output,
389 },
390 meta,
391 } = update;
392
393 Ok(Self {
394 tool_call_id,
395 title: title.ok_or_else(|| {
396 Error::invalid_params().data(serde_json::json!("title is required for a tool call"))
397 })?,
398 name,
399 kind: kind.unwrap_or_default(),
400 status: status.unwrap_or_default(),
401 content: content.unwrap_or_default(),
402 locations: locations.unwrap_or_default(),
403 raw_input,
404 raw_output,
405 meta,
406 })
407 }
408}
409
410impl From<ToolCall> for ToolCallUpdate {
411 fn from(value: ToolCall) -> Self {
412 let ToolCall {
413 tool_call_id,
414 title,
415 name,
416 kind,
417 status,
418 content,
419 locations,
420 raw_input,
421 raw_output,
422 meta,
423 } = value;
424 Self {
425 tool_call_id,
426 fields: ToolCallUpdateFields {
427 kind: Some(kind),
428 status: Some(status),
429 title: Some(title),
430 name,
431 content: Some(content),
432 locations: Some(locations),
433 raw_input,
434 raw_output,
435 },
436 meta,
437 }
438 }
439}
440
441#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
444#[serde(transparent)]
445#[from(Arc<str>, String, &'static str)]
446#[non_exhaustive]
447pub struct ToolCallId(pub Arc<str>);
448
449impl ToolCallId {
450 #[must_use]
452 pub fn new(id: impl Into<Arc<str>>) -> Self {
453 Self(id.into())
454 }
455}
456
457impl IntoOption<ToolCallId> for &str {
458 fn into_option(self) -> Option<ToolCallId> {
459 Some(ToolCallId::new(self))
460 }
461}
462
463#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
470#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
471#[serde(rename_all = "snake_case")]
472#[non_exhaustive]
473pub enum ToolKind {
474 Read,
476 Edit,
478 Delete,
480 Move,
482 Search,
484 Execute,
486 Think,
488 Fetch,
490 SwitchMode,
492 #[default]
494 #[serde(other)]
495 Other,
496}
497
498impl ToolKind {
499 #[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
500 fn is_default(&self) -> bool {
501 matches!(self, ToolKind::Other)
502 }
503}
504
505#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
511#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
512#[serde(rename_all = "snake_case")]
513#[non_exhaustive]
514pub enum ToolCallStatus {
515 #[default]
518 Pending,
519 InProgress,
521 Completed,
523 Failed,
525}
526
527impl ToolCallStatus {
528 #[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
529 fn is_default(&self) -> bool {
530 matches!(self, ToolCallStatus::Pending)
531 }
532}
533
534#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
542#[serde(tag = "type", rename_all = "snake_case")]
543#[cfg_attr(feature = "schemars", schemars(extend("discriminator" = {"propertyName": "type"})))]
544#[non_exhaustive]
545#[expect(clippy::large_enum_variant)]
546pub enum ToolCallContent {
547 Content(Content),
549 Diff(Diff),
551 Terminal(Terminal),
557}
558
559impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
560 fn from(content: T) -> Self {
561 ToolCallContent::Content(Content::new(content))
562 }
563}
564
565impl From<Diff> for ToolCallContent {
566 fn from(diff: Diff) -> Self {
567 ToolCallContent::Diff(diff)
568 }
569}
570
571#[serde_as]
573#[skip_serializing_none]
574#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
575#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
576#[serde(rename_all = "camelCase")]
577#[non_exhaustive]
578pub struct Content {
579 pub content: ContentBlock,
581 #[serde_as(deserialize_as = "DefaultOnError")]
587 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
588 #[serde(default)]
589 #[serde(rename = "_meta")]
590 pub meta: Option<Meta>,
591}
592
593impl Content {
594 #[must_use]
596 pub fn new(content: impl Into<ContentBlock>) -> Self {
597 Self {
598 content: content.into(),
599 meta: None,
600 }
601 }
602
603 #[must_use]
609 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
610 self.meta = meta.into_option();
611 self
612 }
613}
614
615#[serde_as]
621#[skip_serializing_none]
622#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625#[non_exhaustive]
626pub struct Terminal {
627 pub terminal_id: TerminalId,
629 #[serde_as(deserialize_as = "DefaultOnError")]
635 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
636 #[serde(default)]
637 #[serde(rename = "_meta")]
638 pub meta: Option<Meta>,
639}
640
641impl Terminal {
642 #[must_use]
644 pub fn new(terminal_id: impl Into<TerminalId>) -> Self {
645 Self {
646 terminal_id: terminal_id.into(),
647 meta: None,
648 }
649 }
650
651 #[must_use]
657 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
658 self.meta = meta.into_option();
659 self
660 }
661}
662
663#[serde_as]
669#[skip_serializing_none]
670#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
672#[serde(rename_all = "camelCase")]
673#[non_exhaustive]
674pub struct Diff {
675 pub path: PathBuf,
677 #[serde_as(deserialize_as = "DefaultOnError")]
679 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
680 #[serde(default)]
681 pub old_text: Option<String>,
682 pub new_text: String,
684 #[serde_as(deserialize_as = "DefaultOnError")]
690 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
691 #[serde(default)]
692 #[serde(rename = "_meta")]
693 pub meta: Option<Meta>,
694}
695
696impl Diff {
697 #[must_use]
699 pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
700 Self {
701 path: path.into(),
702 old_text: None,
703 new_text: new_text.into(),
704 meta: None,
705 }
706 }
707
708 #[must_use]
710 pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
711 self.old_text = old_text.into_option();
712 self
713 }
714
715 #[must_use]
721 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
722 self.meta = meta.into_option();
723 self
724 }
725}
726
727#[serde_as]
734#[skip_serializing_none]
735#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
736#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
737#[serde(rename_all = "camelCase")]
738#[non_exhaustive]
739pub struct ToolCallLocation {
740 pub path: PathBuf,
742 #[serde_as(deserialize_as = "DefaultOnError")]
744 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
745 #[serde(default)]
746 pub line: Option<u32>,
747 #[serde_as(deserialize_as = "DefaultOnError")]
753 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
754 #[serde(default)]
755 #[serde(rename = "_meta")]
756 pub meta: Option<Meta>,
757}
758
759impl ToolCallLocation {
760 #[must_use]
762 pub fn new(path: impl Into<PathBuf>) -> Self {
763 Self {
764 path: path.into(),
765 line: None,
766 meta: None,
767 }
768 }
769
770 #[must_use]
772 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
773 self.line = line.into_option();
774 self
775 }
776
777 #[must_use]
783 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
784 self.meta = meta.into_option();
785 self
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792
793 #[test]
794 fn tool_call_name_is_optional_and_null_is_equivalent_to_omission() {
795 let named = ToolCall::new("tc_1", "Reading configuration").name("read_file");
796 assert_eq!(
797 serde_json::to_value(named).unwrap(),
798 serde_json::json!({
799 "toolCallId": "tc_1",
800 "title": "Reading configuration",
801 "name": "read_file"
802 })
803 );
804
805 let unnamed = ToolCall::new("tc_1", "Reading configuration");
806 assert_eq!(unnamed.name, None);
807 assert_eq!(
808 serde_json::to_value(unnamed).unwrap(),
809 serde_json::json!({
810 "toolCallId": "tc_1",
811 "title": "Reading configuration"
812 })
813 );
814
815 let from_null: ToolCall = serde_json::from_value(serde_json::json!({
816 "toolCallId": "tc_1",
817 "title": "Reading configuration",
818 "name": null
819 }))
820 .unwrap();
821 assert_eq!(from_null.name, None);
822 }
823
824 #[test]
825 fn tool_call_name_update_replaces_a_name_but_cannot_clear_it() {
826 let mut stored = ToolCall::new("tc_1", "Reading configuration").name("read_file");
827
828 stored.update(ToolCallUpdateFields::new());
829 assert_eq!(stored.name.as_deref(), Some("read_file"));
830
831 let null_update: ToolCallUpdateFields =
832 serde_json::from_value(serde_json::json!({"name": null})).unwrap();
833 stored.update(null_update);
834 assert_eq!(stored.name.as_deref(), Some("read_file"));
835
836 stored.update(ToolCallUpdateFields::new().name("read_many_files"));
837 assert_eq!(stored.name.as_deref(), Some("read_many_files"));
838 }
839
840 #[test]
841 fn tool_call_name_survives_v1_upsert_conversion() {
842 let tool_call = ToolCall::new("tc_1", "Reading configuration").name("read_file");
843
844 let update = ToolCallUpdate::from(tool_call.clone());
845 assert_eq!(update.fields.name.as_deref(), Some("read_file"));
846
847 let rebuilt = ToolCall::try_from(update).unwrap();
848 assert_eq!(rebuilt, tool_call);
849 }
850}