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 #[cfg(feature = "unstable_tool_call_name")]
43 #[serde_as(deserialize_as = "DefaultOnError")]
44 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
45 #[serde(default)]
46 pub name: Option<String>,
47 #[serde_as(deserialize_as = "DefaultOnError")]
50 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
51 #[serde(default, skip_serializing_if = "ToolKind::is_default")]
52 pub kind: ToolKind,
53 #[serde_as(deserialize_as = "DefaultOnError")]
55 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
56 #[serde(default, skip_serializing_if = "ToolCallStatus::is_default")]
57 pub status: ToolCallStatus,
58 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
60 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub content: Vec<ToolCallContent>,
63 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
66 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub locations: Vec<ToolCallLocation>,
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_input: Option<serde_json::Value>,
74 #[serde_as(deserialize_as = "DefaultOnError")]
76 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
77 #[serde(default)]
78 pub raw_output: Option<serde_json::Value>,
79 #[serde_as(deserialize_as = "DefaultOnError")]
85 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
86 #[serde(default)]
87 #[serde(rename = "_meta")]
88 pub meta: Option<Meta>,
89}
90
91impl ToolCall {
92 #[must_use]
94 pub fn new(tool_call_id: impl Into<ToolCallId>, title: impl Into<String>) -> Self {
95 Self {
96 tool_call_id: tool_call_id.into(),
97 title: title.into(),
98 #[cfg(feature = "unstable_tool_call_name")]
99 name: None,
100 kind: ToolKind::default(),
101 status: ToolCallStatus::default(),
102 content: Vec::default(),
103 locations: Vec::default(),
104 raw_input: None,
105 raw_output: None,
106 meta: None,
107 }
108 }
109
110 #[cfg(feature = "unstable_tool_call_name")]
116 #[must_use]
117 pub fn name(mut self, name: impl IntoOption<String>) -> Self {
118 self.name = name.into_option();
119 self
120 }
121
122 #[must_use]
125 pub fn kind(mut self, kind: ToolKind) -> Self {
126 self.kind = kind;
127 self
128 }
129
130 #[must_use]
132 pub fn status(mut self, status: ToolCallStatus) -> Self {
133 self.status = status;
134 self
135 }
136
137 #[must_use]
139 pub fn content(mut self, content: Vec<ToolCallContent>) -> Self {
140 self.content = content;
141 self
142 }
143
144 #[must_use]
147 pub fn locations(mut self, locations: Vec<ToolCallLocation>) -> Self {
148 self.locations = locations;
149 self
150 }
151
152 #[must_use]
154 pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
155 self.raw_input = raw_input.into_option();
156 self
157 }
158
159 #[must_use]
161 pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
162 self.raw_output = raw_output.into_option();
163 self
164 }
165
166 #[must_use]
172 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
173 self.meta = meta.into_option();
174 self
175 }
176
177 pub fn update(&mut self, fields: ToolCallUpdateFields) {
180 if let Some(title) = fields.title {
181 self.title = title;
182 }
183 #[cfg(feature = "unstable_tool_call_name")]
184 if let Some(name) = fields.name {
185 self.name = Some(name);
186 }
187 if let Some(kind) = fields.kind {
188 self.kind = kind;
189 }
190 if let Some(status) = fields.status {
191 self.status = status;
192 }
193 if let Some(content) = fields.content {
194 self.content = content;
195 }
196 if let Some(locations) = fields.locations {
197 self.locations = locations;
198 }
199 if let Some(raw_input) = fields.raw_input {
200 self.raw_input = Some(raw_input);
201 }
202 if let Some(raw_output) = fields.raw_output {
203 self.raw_output = Some(raw_output);
204 }
205 }
206}
207
208#[serde_as]
215#[skip_serializing_none]
216#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219#[non_exhaustive]
220pub struct ToolCallUpdate {
221 pub tool_call_id: ToolCallId,
223 #[serde(flatten)]
225 pub fields: ToolCallUpdateFields,
226 #[serde_as(deserialize_as = "DefaultOnError")]
232 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
233 #[serde(default)]
234 #[serde(rename = "_meta")]
235 pub meta: Option<Meta>,
236}
237
238impl ToolCallUpdate {
239 #[must_use]
241 pub fn new(tool_call_id: impl Into<ToolCallId>, fields: ToolCallUpdateFields) -> Self {
242 Self {
243 tool_call_id: tool_call_id.into(),
244 fields,
245 meta: None,
246 }
247 }
248
249 #[must_use]
255 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
256 self.meta = meta.into_option();
257 self
258 }
259}
260
261#[serde_as]
268#[skip_serializing_none]
269#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
270#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272#[non_exhaustive]
273pub struct ToolCallUpdateFields {
274 #[serde_as(deserialize_as = "DefaultOnError")]
276 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
277 #[serde(default)]
278 pub kind: Option<ToolKind>,
279 #[serde_as(deserialize_as = "DefaultOnError")]
281 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
282 #[serde(default)]
283 pub status: Option<ToolCallStatus>,
284 #[serde_as(deserialize_as = "DefaultOnError")]
286 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
287 #[serde(default)]
288 pub title: Option<String>,
289 #[cfg(feature = "unstable_tool_call_name")]
298 #[serde_as(deserialize_as = "DefaultOnError")]
299 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
300 #[serde(default)]
301 pub name: Option<String>,
302 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
304 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
305 #[serde(default)]
306 pub content: Option<Vec<ToolCallContent>>,
307 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
309 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
310 #[serde(default)]
311 pub locations: Option<Vec<ToolCallLocation>>,
312 #[serde_as(deserialize_as = "DefaultOnError")]
314 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
315 #[serde(default)]
316 pub raw_input: Option<serde_json::Value>,
317 #[serde_as(deserialize_as = "DefaultOnError")]
319 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
320 #[serde(default)]
321 pub raw_output: Option<serde_json::Value>,
322}
323
324impl ToolCallUpdateFields {
325 #[must_use]
327 pub fn new() -> Self {
328 Self::default()
329 }
330
331 #[must_use]
333 pub fn kind(mut self, kind: impl IntoOption<ToolKind>) -> Self {
334 self.kind = kind.into_option();
335 self
336 }
337
338 #[must_use]
340 pub fn status(mut self, status: impl IntoOption<ToolCallStatus>) -> Self {
341 self.status = status.into_option();
342 self
343 }
344
345 #[must_use]
347 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
348 self.title = title.into_option();
349 self
350 }
351
352 #[cfg(feature = "unstable_tool_call_name")]
358 #[must_use]
359 pub fn name(mut self, name: impl IntoOption<String>) -> Self {
360 self.name = name.into_option();
361 self
362 }
363
364 #[must_use]
366 pub fn content(mut self, content: impl IntoOption<Vec<ToolCallContent>>) -> Self {
367 self.content = content.into_option();
368 self
369 }
370
371 #[must_use]
373 pub fn locations(mut self, locations: impl IntoOption<Vec<ToolCallLocation>>) -> Self {
374 self.locations = locations.into_option();
375 self
376 }
377
378 #[must_use]
380 pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
381 self.raw_input = raw_input.into_option();
382 self
383 }
384
385 #[must_use]
387 pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
388 self.raw_output = raw_output.into_option();
389 self
390 }
391}
392
393impl TryFrom<ToolCallUpdate> for ToolCall {
396 type Error = Error;
397
398 fn try_from(update: ToolCallUpdate) -> Result<Self, Self::Error> {
399 let ToolCallUpdate {
400 tool_call_id,
401 fields:
402 ToolCallUpdateFields {
403 kind,
404 status,
405 title,
406 #[cfg(feature = "unstable_tool_call_name")]
407 name,
408 content,
409 locations,
410 raw_input,
411 raw_output,
412 },
413 meta,
414 } = update;
415
416 Ok(Self {
417 tool_call_id,
418 title: title.ok_or_else(|| {
419 Error::invalid_params().data(serde_json::json!("title is required for a tool call"))
420 })?,
421 #[cfg(feature = "unstable_tool_call_name")]
422 name,
423 kind: kind.unwrap_or_default(),
424 status: status.unwrap_or_default(),
425 content: content.unwrap_or_default(),
426 locations: locations.unwrap_or_default(),
427 raw_input,
428 raw_output,
429 meta,
430 })
431 }
432}
433
434impl From<ToolCall> for ToolCallUpdate {
435 fn from(value: ToolCall) -> Self {
436 let ToolCall {
437 tool_call_id,
438 title,
439 #[cfg(feature = "unstable_tool_call_name")]
440 name,
441 kind,
442 status,
443 content,
444 locations,
445 raw_input,
446 raw_output,
447 meta,
448 } = value;
449 Self {
450 tool_call_id,
451 fields: ToolCallUpdateFields {
452 kind: Some(kind),
453 status: Some(status),
454 title: Some(title),
455 #[cfg(feature = "unstable_tool_call_name")]
456 name,
457 content: Some(content),
458 locations: Some(locations),
459 raw_input,
460 raw_output,
461 },
462 meta,
463 }
464 }
465}
466
467#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
470#[serde(transparent)]
471#[from(Arc<str>, String, &'static str)]
472#[non_exhaustive]
473pub struct ToolCallId(pub Arc<str>);
474
475impl ToolCallId {
476 #[must_use]
478 pub fn new(id: impl Into<Arc<str>>) -> Self {
479 Self(id.into())
480 }
481}
482
483impl IntoOption<ToolCallId> for &str {
484 fn into_option(self) -> Option<ToolCallId> {
485 Some(ToolCallId::new(self))
486 }
487}
488
489#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
496#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
497#[serde(rename_all = "snake_case")]
498#[non_exhaustive]
499pub enum ToolKind {
500 Read,
502 Edit,
504 Delete,
506 Move,
508 Search,
510 Execute,
512 Think,
514 Fetch,
516 SwitchMode,
518 #[default]
520 #[serde(other)]
521 Other,
522}
523
524impl ToolKind {
525 #[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
526 fn is_default(&self) -> bool {
527 matches!(self, ToolKind::Other)
528 }
529}
530
531#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
537#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
538#[serde(rename_all = "snake_case")]
539#[non_exhaustive]
540pub enum ToolCallStatus {
541 #[default]
544 Pending,
545 InProgress,
547 Completed,
549 Failed,
551}
552
553impl ToolCallStatus {
554 #[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
555 fn is_default(&self) -> bool {
556 matches!(self, ToolCallStatus::Pending)
557 }
558}
559
560#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568#[serde(tag = "type", rename_all = "snake_case")]
569#[cfg_attr(feature = "schemars", schemars(extend("discriminator" = {"propertyName": "type"})))]
570#[non_exhaustive]
571#[expect(clippy::large_enum_variant)]
572pub enum ToolCallContent {
573 Content(Content),
575 Diff(Diff),
577 Terminal(Terminal),
583}
584
585impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
586 fn from(content: T) -> Self {
587 ToolCallContent::Content(Content::new(content))
588 }
589}
590
591impl From<Diff> for ToolCallContent {
592 fn from(diff: Diff) -> Self {
593 ToolCallContent::Diff(diff)
594 }
595}
596
597#[serde_as]
599#[skip_serializing_none]
600#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
601#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
602#[serde(rename_all = "camelCase")]
603#[non_exhaustive]
604pub struct Content {
605 pub content: ContentBlock,
607 #[serde_as(deserialize_as = "DefaultOnError")]
613 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
614 #[serde(default)]
615 #[serde(rename = "_meta")]
616 pub meta: Option<Meta>,
617}
618
619impl Content {
620 #[must_use]
622 pub fn new(content: impl Into<ContentBlock>) -> Self {
623 Self {
624 content: content.into(),
625 meta: None,
626 }
627 }
628
629 #[must_use]
635 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
636 self.meta = meta.into_option();
637 self
638 }
639}
640
641#[serde_as]
647#[skip_serializing_none]
648#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650#[serde(rename_all = "camelCase")]
651#[non_exhaustive]
652pub struct Terminal {
653 pub terminal_id: TerminalId,
655 #[serde_as(deserialize_as = "DefaultOnError")]
661 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
662 #[serde(default)]
663 #[serde(rename = "_meta")]
664 pub meta: Option<Meta>,
665}
666
667impl Terminal {
668 #[must_use]
670 pub fn new(terminal_id: impl Into<TerminalId>) -> Self {
671 Self {
672 terminal_id: terminal_id.into(),
673 meta: None,
674 }
675 }
676
677 #[must_use]
683 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
684 self.meta = meta.into_option();
685 self
686 }
687}
688
689#[serde_as]
695#[skip_serializing_none]
696#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "camelCase")]
699#[non_exhaustive]
700pub struct Diff {
701 pub path: PathBuf,
703 #[serde_as(deserialize_as = "DefaultOnError")]
705 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
706 #[serde(default)]
707 pub old_text: Option<String>,
708 pub new_text: String,
710 #[serde_as(deserialize_as = "DefaultOnError")]
716 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
717 #[serde(default)]
718 #[serde(rename = "_meta")]
719 pub meta: Option<Meta>,
720}
721
722impl Diff {
723 #[must_use]
725 pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
726 Self {
727 path: path.into(),
728 old_text: None,
729 new_text: new_text.into(),
730 meta: None,
731 }
732 }
733
734 #[must_use]
736 pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
737 self.old_text = old_text.into_option();
738 self
739 }
740
741 #[must_use]
747 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
748 self.meta = meta.into_option();
749 self
750 }
751}
752
753#[serde_as]
760#[skip_serializing_none]
761#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
762#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
763#[serde(rename_all = "camelCase")]
764#[non_exhaustive]
765pub struct ToolCallLocation {
766 pub path: PathBuf,
768 #[serde_as(deserialize_as = "DefaultOnError")]
770 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
771 #[serde(default)]
772 pub line: Option<u32>,
773 #[serde_as(deserialize_as = "DefaultOnError")]
779 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
780 #[serde(default)]
781 #[serde(rename = "_meta")]
782 pub meta: Option<Meta>,
783}
784
785impl ToolCallLocation {
786 #[must_use]
788 pub fn new(path: impl Into<PathBuf>) -> Self {
789 Self {
790 path: path.into(),
791 line: None,
792 meta: None,
793 }
794 }
795
796 #[must_use]
798 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
799 self.line = line.into_option();
800 self
801 }
802
803 #[must_use]
809 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
810 self.meta = meta.into_option();
811 self
812 }
813}
814
815#[cfg(all(test, feature = "unstable_tool_call_name"))]
816mod tests {
817 use super::*;
818
819 #[test]
820 fn tool_call_name_is_optional_and_null_is_equivalent_to_omission() {
821 let named = ToolCall::new("tc_1", "Reading configuration").name("read_file");
822 assert_eq!(
823 serde_json::to_value(named).unwrap(),
824 serde_json::json!({
825 "toolCallId": "tc_1",
826 "title": "Reading configuration",
827 "name": "read_file"
828 })
829 );
830
831 let unnamed = ToolCall::new("tc_1", "Reading configuration");
832 assert_eq!(unnamed.name, None);
833 assert_eq!(
834 serde_json::to_value(unnamed).unwrap(),
835 serde_json::json!({
836 "toolCallId": "tc_1",
837 "title": "Reading configuration"
838 })
839 );
840
841 let from_null: ToolCall = serde_json::from_value(serde_json::json!({
842 "toolCallId": "tc_1",
843 "title": "Reading configuration",
844 "name": null
845 }))
846 .unwrap();
847 assert_eq!(from_null.name, None);
848 }
849
850 #[test]
851 fn tool_call_name_update_replaces_a_name_but_cannot_clear_it() {
852 let mut stored = ToolCall::new("tc_1", "Reading configuration").name("read_file");
853
854 stored.update(ToolCallUpdateFields::new());
855 assert_eq!(stored.name.as_deref(), Some("read_file"));
856
857 let null_update: ToolCallUpdateFields =
858 serde_json::from_value(serde_json::json!({"name": null})).unwrap();
859 stored.update(null_update);
860 assert_eq!(stored.name.as_deref(), Some("read_file"));
861
862 stored.update(ToolCallUpdateFields::new().name("read_many_files"));
863 assert_eq!(stored.name.as_deref(), Some("read_many_files"));
864 }
865
866 #[test]
867 fn tool_call_name_survives_v1_upsert_conversion() {
868 let tool_call = ToolCall::new("tc_1", "Reading configuration").name("read_file");
869
870 let update = ToolCallUpdate::from(tool_call.clone());
871 assert_eq!(update.fields.name.as_deref(), Some("read_file"));
872
873 let rebuilt = ToolCall::try_from(update).unwrap();
874 assert_eq!(rebuilt, tool_call);
875 }
876}