1use std::{collections::BTreeMap, sync::Arc};
8
9use derive_more::{Display, From};
10#[cfg(feature = "schemars")]
11use schemars::Schema;
12use serde::{Deserialize, Serialize};
13use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
14
15use super::{AbsolutePath, ContentBlock, MediaType, Meta, Terminal};
16use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
17
18#[serde_as]
32#[skip_serializing_none]
33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36#[non_exhaustive]
37pub struct ToolCallUpdate {
38 pub tool_call_id: ToolCallId,
40 #[cfg(feature = "unstable_tool_call_name")]
51 #[serde_as(deserialize_as = "DefaultOnError")]
52 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
53 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
54 pub name: MaybeUndefined<String>,
55 #[serde_as(deserialize_as = "DefaultOnError")]
57 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
58 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
59 pub title: MaybeUndefined<String>,
60 #[serde_as(deserialize_as = "DefaultOnError")]
63 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
64 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
65 pub kind: MaybeUndefined<ToolKind>,
66 #[serde_as(deserialize_as = "DefaultOnError")]
68 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
69 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
70 pub status: MaybeUndefined<ToolCallStatus>,
71 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
73 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
74 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
75 pub content: MaybeUndefined<Vec<ToolCallContent>>,
76 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
79 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
80 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
81 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
82 #[serde_as(deserialize_as = "DefaultOnError")]
84 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
85 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
86 pub raw_input: MaybeUndefined<serde_json::Value>,
87 #[serde_as(deserialize_as = "DefaultOnError")]
89 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
90 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
91 pub raw_output: MaybeUndefined<serde_json::Value>,
92 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
98 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
99 #[serde(
100 rename = "_meta",
101 default,
102 skip_serializing_if = "MaybeUndefined::is_undefined"
103 )]
104 pub meta: MaybeUndefined<Meta>,
105}
106
107impl ToolCallUpdate {
108 #[must_use]
110 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
111 Self {
112 tool_call_id: tool_call_id.into(),
113 #[cfg(feature = "unstable_tool_call_name")]
114 name: MaybeUndefined::Undefined,
115 title: MaybeUndefined::Undefined,
116 kind: MaybeUndefined::Undefined,
117 status: MaybeUndefined::Undefined,
118 content: MaybeUndefined::Undefined,
119 locations: MaybeUndefined::Undefined,
120 raw_input: MaybeUndefined::Undefined,
121 raw_output: MaybeUndefined::Undefined,
122 meta: MaybeUndefined::Undefined,
123 }
124 }
125
126 #[cfg(feature = "unstable_tool_call_name")]
132 #[must_use]
133 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
134 self.name = name.into_maybe_undefined();
135 self
136 }
137
138 #[must_use]
140 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
141 self.title = title.into_maybe_undefined();
142 self
143 }
144
145 #[must_use]
148 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
149 self.kind = kind.into_maybe_undefined();
150 self
151 }
152
153 #[must_use]
155 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
156 self.status = status.into_maybe_undefined();
157 self
158 }
159
160 #[must_use]
162 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
163 self.content = content.into_maybe_undefined();
164 self
165 }
166
167 #[must_use]
170 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
171 self.locations = locations.into_maybe_undefined();
172 self
173 }
174
175 #[must_use]
177 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
178 self.raw_input = raw_input.into_maybe_undefined();
179 self
180 }
181
182 #[must_use]
184 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
185 self.raw_output = raw_output.into_maybe_undefined();
186 self
187 }
188
189 #[must_use]
195 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
196 self.meta = meta.into_maybe_undefined();
197 self
198 }
199
200 pub fn apply_update(&mut self, update: ToolCallUpdate) {
205 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
206 #[cfg(feature = "unstable_tool_call_name")]
207 if !update.name.is_undefined() {
208 self.name = update.name;
209 }
210 if !update.title.is_undefined() {
211 self.title = update.title;
212 }
213 if !update.kind.is_undefined() {
214 self.kind = update.kind;
215 }
216 if !update.status.is_undefined() {
217 self.status = update.status;
218 }
219 if !update.content.is_undefined() {
220 self.content = update.content;
221 }
222 if !update.locations.is_undefined() {
223 self.locations = update.locations;
224 }
225 if !update.raw_input.is_undefined() {
226 self.raw_input = update.raw_input;
227 }
228 if !update.raw_output.is_undefined() {
229 self.raw_output = update.raw_output;
230 }
231 if !update.meta.is_undefined() {
232 self.meta = update.meta;
233 }
234 }
235}
236
237#[serde_as]
244#[skip_serializing_none]
245#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248#[non_exhaustive]
249pub struct ToolCallContentChunk {
250 pub tool_call_id: ToolCallId,
252 pub content: ToolCallContent,
254 #[serde_as(deserialize_as = "DefaultOnError")]
260 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
261 #[serde(default)]
262 #[serde(rename = "_meta")]
263 pub meta: Option<Meta>,
264}
265
266impl ToolCallContentChunk {
267 #[must_use]
269 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
270 Self {
271 tool_call_id: tool_call_id.into(),
272 content: content.into(),
273 meta: None,
274 }
275 }
276
277 #[must_use]
283 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
284 self.meta = meta.into_option();
285 self
286 }
287}
288
289#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
292#[serde(transparent)]
293#[from(forward)]
294#[non_exhaustive]
295pub struct ToolCallId(pub Arc<str>);
296
297impl ToolCallId {
298 #[must_use]
300 pub fn new(id: impl Into<Self>) -> Self {
301 id.into()
302 }
303}
304
305impl IntoOption<ToolCallId> for &str {
306 fn into_option(self) -> Option<ToolCallId> {
307 Some(ToolCallId::new(self))
308 }
309}
310
311#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
318#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
319#[serde(rename_all = "snake_case")]
320#[non_exhaustive]
321pub enum ToolKind {
322 Read,
324 Edit,
326 Delete,
328 Move,
330 Search,
332 Execute,
334 Think,
336 Fetch,
338 SwitchMode,
340 #[default]
342 Other,
343 #[serde(untagged)]
349 Unknown(String),
350}
351
352#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
358#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
359#[serde(rename_all = "snake_case")]
360#[non_exhaustive]
361pub enum ToolCallStatus {
362 #[default]
365 Pending,
366 InProgress,
368 Completed,
370 Failed,
372 Cancelled,
374 #[serde(untagged)]
380 Other(String),
381}
382
383#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
391#[serde(tag = "type", rename_all = "snake_case")]
392#[non_exhaustive]
393pub enum ToolCallContent {
394 Content(Box<Content>),
396 Diff(Diff),
398 Terminal(Terminal),
400 #[serde(untagged)]
410 Other(OtherToolCallContent),
411}
412
413#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
415#[derive(Debug, Clone, PartialEq, Serialize)]
416#[cfg_attr(feature = "schemars", schemars(inline))]
417#[cfg_attr(feature = "schemars", schemars(transform = other_tool_call_content_schema))]
418#[serde(rename_all = "camelCase")]
419#[non_exhaustive]
420pub struct OtherToolCallContent {
421 #[serde(rename = "type")]
427 pub type_: String,
428 #[serde(flatten)]
430 pub fields: BTreeMap<String, serde_json::Value>,
431}
432
433impl OtherToolCallContent {
434 #[must_use]
436 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
437 fields.remove("type");
438 Self {
439 type_: type_.into(),
440 fields,
441 }
442 }
443}
444
445impl<'de> Deserialize<'de> for OtherToolCallContent {
446 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
447 where
448 D: serde::Deserializer<'de>,
449 {
450 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
451 let type_ = fields
452 .remove("type")
453 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
454 let serde_json::Value::String(type_) = type_ else {
455 return Err(serde::de::Error::custom("`type` must be a string"));
456 };
457
458 if is_known_tool_call_content_type(&type_) {
459 return Err(serde::de::Error::custom(format!(
460 "known tool call content `{type_}` did not match its schema"
461 )));
462 }
463
464 Ok(Self { type_, fields })
465 }
466}
467
468fn is_known_tool_call_content_type(type_: &str) -> bool {
469 matches!(type_, "content" | "diff" | "terminal")
470}
471
472#[cfg(feature = "schemars")]
473fn other_tool_call_content_schema(schema: &mut Schema) {
474 super::schema_util::reject_known_string_discriminators(
475 schema,
476 "type",
477 &["content", "diff", "terminal"],
478 );
479}
480
481impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
482 fn from(content: T) -> Self {
483 ToolCallContent::Content(Box::new(Content::new(content)))
484 }
485}
486
487impl From<Diff> for ToolCallContent {
488 fn from(diff: Diff) -> Self {
489 ToolCallContent::Diff(diff)
490 }
491}
492
493impl From<Terminal> for ToolCallContent {
494 fn from(terminal: Terminal) -> Self {
495 ToolCallContent::Terminal(terminal)
496 }
497}
498
499#[serde_as]
501#[skip_serializing_none]
502#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
503#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505#[non_exhaustive]
506pub struct Content {
507 pub content: ContentBlock,
509 #[serde_as(deserialize_as = "DefaultOnError")]
515 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
516 #[serde(default)]
517 #[serde(rename = "_meta")]
518 pub meta: Option<Meta>,
519}
520
521impl Content {
522 #[must_use]
524 pub fn new(content: impl Into<ContentBlock>) -> Self {
525 Self {
526 content: content.into(),
527 meta: None,
528 }
529 }
530
531 #[must_use]
537 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
538 self.meta = meta.into_option();
539 self
540 }
541}
542
543#[serde_as]
552#[skip_serializing_none]
553#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
555#[serde(rename_all = "camelCase")]
556#[non_exhaustive]
557pub struct Diff {
558 #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
562 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
563 pub changes: Vec<DiffChange>,
564 #[serde_as(deserialize_as = "DefaultOnError")]
569 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
570 #[serde(default)]
571 pub patch: Option<DiffPatch>,
572 #[serde_as(deserialize_as = "DefaultOnError")]
578 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
579 #[serde(default)]
580 #[serde(rename = "_meta")]
581 pub meta: Option<Meta>,
582}
583
584impl Diff {
585 #[must_use]
587 pub fn new(changes: Vec<DiffChange>) -> Self {
588 Self {
589 changes,
590 patch: None,
591 meta: None,
592 }
593 }
594
595 #[must_use]
597 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
598 Self::new(changes).with_patch(DiffPatch::new(text))
599 }
600
601 #[must_use]
603 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
604 self.patch = patch.into_option();
605 self
606 }
607
608 #[must_use]
614 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
615 self.meta = meta.into_option();
616 self
617 }
618}
619
620#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
623#[serde(rename_all = "camelCase")]
624#[non_exhaustive]
625pub struct DiffPatch {
626 pub format: DiffPatchFormat,
628 pub text: String,
630}
631
632impl DiffPatch {
633 #[must_use]
635 pub fn new(text: impl Into<String>) -> Self {
636 Self {
637 format: DiffPatchFormat::GitPatch,
638 text: text.into(),
639 }
640 }
641}
642
643#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
646#[serde(rename_all = "snake_case")]
647#[non_exhaustive]
648pub enum DiffPatchFormat {
649 GitPatch,
654 #[serde(untagged)]
660 Other(String),
661}
662
663#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
665#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
666#[serde(rename_all = "snake_case")]
667#[non_exhaustive]
668pub enum DiffFileType {
669 Text,
671 Binary,
673 Directory,
675 Symlink,
677 #[serde(untagged)]
683 Other(String),
684}
685
686#[serde_as]
691#[skip_serializing_none]
692#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
693#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694#[serde(rename_all = "camelCase")]
695#[non_exhaustive]
696pub struct DiffChange {
697 #[serde_as(deserialize_as = "DefaultOnError")]
701 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
702 #[serde(default)]
703 pub file_type: Option<DiffFileType>,
704 #[serde_as(deserialize_as = "DefaultOnError")]
708 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
709 #[serde(default)]
710 pub mime_type: Option<MediaType>,
711 #[serde(flatten)]
713 pub operation: DiffChangeOperation,
714 #[serde_as(deserialize_as = "DefaultOnError")]
720 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
721 #[serde(default)]
722 #[serde(rename = "_meta")]
723 pub meta: Option<Meta>,
724}
725
726impl DiffChange {
727 #[must_use]
729 pub fn new(operation: DiffChangeOperation) -> Self {
730 Self {
731 file_type: None,
732 mime_type: None,
733 operation,
734 meta: None,
735 }
736 }
737
738 #[must_use]
740 pub fn add(path: impl Into<AbsolutePath>) -> Self {
741 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
742 }
743
744 #[must_use]
746 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
747 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
748 }
749
750 #[must_use]
752 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
753 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
754 }
755
756 #[must_use]
758 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
759 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
760 old_path, path,
761 )))
762 }
763
764 #[must_use]
766 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
767 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
768 old_path, path,
769 )))
770 }
771
772 #[must_use]
776 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
777 self.file_type = file_type.into_option();
778 self
779 }
780
781 #[must_use]
785 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
786 self.mime_type = mime_type.into_option();
787 self
788 }
789
790 #[must_use]
796 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
797 self.meta = meta.into_option();
798 self
799 }
800}
801
802#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(tag = "operation", rename_all = "snake_case")]
806#[non_exhaustive]
807pub enum DiffChangeOperation {
808 Add(DiffPathChange),
810 Delete(DiffPathChange),
812 Modify(DiffPathChange),
814 Move(DiffPathPairChange),
816 Copy(DiffPathPairChange),
818 #[serde(untagged)]
824 Other(OtherDiffChange),
825}
826
827#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(rename_all = "camelCase")]
831#[non_exhaustive]
832pub struct DiffPathChange {
833 pub path: AbsolutePath,
835}
836
837impl DiffPathChange {
838 #[must_use]
840 pub fn new(path: impl Into<AbsolutePath>) -> Self {
841 Self { path: path.into() }
842 }
843}
844
845#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
848#[serde(rename_all = "camelCase")]
849#[non_exhaustive]
850pub struct DiffPathPairChange {
851 pub old_path: AbsolutePath,
853 pub path: AbsolutePath,
855}
856
857impl DiffPathPairChange {
858 #[must_use]
860 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
861 Self {
862 old_path: old_path.into(),
863 path: path.into(),
864 }
865 }
866}
867
868#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
870#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
871#[cfg_attr(feature = "schemars", schemars(inline))]
872#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
873#[serde(rename_all = "camelCase")]
874#[non_exhaustive]
875pub struct OtherDiffChange {
876 pub operation: String,
882 #[serde(flatten)]
884 pub fields: BTreeMap<String, serde_json::Value>,
885}
886
887impl OtherDiffChange {
888 #[must_use]
890 pub fn new(
891 operation: impl Into<String>,
892 mut fields: BTreeMap<String, serde_json::Value>,
893 ) -> Self {
894 fields.remove("operation");
895 fields.remove("fileType");
896 fields.remove("mimeType");
897 fields.remove("_meta");
898 Self {
899 operation: operation.into(),
900 fields,
901 }
902 }
903}
904
905impl<'de> Deserialize<'de> for OtherDiffChange {
906 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
907 where
908 D: serde::Deserializer<'de>,
909 {
910 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
911 let operation = fields
912 .remove("operation")
913 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
914 let serde_json::Value::String(operation) = operation else {
915 return Err(serde::de::Error::custom("`operation` must be a string"));
916 };
917
918 if is_known_diff_change_operation(&operation) {
919 return Err(serde::de::Error::custom(format!(
920 "known diff change operation `{operation}` did not match its schema"
921 )));
922 }
923 fields.remove("fileType");
924 fields.remove("mimeType");
925 fields.remove("_meta");
926
927 Ok(Self { operation, fields })
928 }
929}
930
931fn is_known_diff_change_operation(operation: &str) -> bool {
932 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
933}
934
935#[cfg(feature = "schemars")]
936fn other_diff_change_schema(schema: &mut Schema) {
937 super::schema_util::reject_known_string_discriminators(
938 schema,
939 "operation",
940 &["add", "delete", "modify", "move", "copy"],
941 );
942}
943
944#[serde_as]
951#[skip_serializing_none]
952#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
953#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
954#[serde(rename_all = "camelCase")]
955#[non_exhaustive]
956pub struct ToolCallLocation {
957 pub path: AbsolutePath,
959 #[serde_as(deserialize_as = "DefaultOnError")]
961 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
962 #[serde(default)]
963 pub line: Option<u32>,
964 #[serde_as(deserialize_as = "DefaultOnError")]
970 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
971 #[serde(default)]
972 #[serde(rename = "_meta")]
973 pub meta: Option<Meta>,
974}
975
976impl ToolCallLocation {
977 #[must_use]
979 pub fn new(path: impl Into<AbsolutePath>) -> Self {
980 Self {
981 path: path.into(),
982 line: None,
983 meta: None,
984 }
985 }
986
987 #[must_use]
989 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
990 self.line = line.into_option();
991 self
992 }
993
994 #[must_use]
1000 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1001 self.meta = meta.into_option();
1002 self
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use crate::MaybeUndefined;
1010
1011 #[test]
1012 fn tool_call_serializes_as_upsert() {
1013 let tool_call = ToolCallUpdate::new("tc_1")
1014 .title("Reading configuration")
1015 .status(ToolCallStatus::InProgress)
1016 .raw_input(serde_json::json!({"path": "settings.json"}));
1017
1018 assert_eq!(
1019 serde_json::to_value(tool_call).unwrap(),
1020 serde_json::json!({
1021 "toolCallId": "tc_1",
1022 "title": "Reading configuration",
1023 "status": "in_progress",
1024 "rawInput": {
1025 "path": "settings.json"
1026 }
1027 })
1028 );
1029 }
1030
1031 #[test]
1032 fn tool_call_update_distinguishes_omitted_null_and_value() {
1033 let tool_call = ToolCallUpdate::new("tc_1")
1034 .status(ToolCallStatus::Completed)
1035 .content(None::<Vec<ToolCallContent>>);
1036
1037 assert_eq!(
1038 serde_json::to_value(tool_call).unwrap(),
1039 serde_json::json!({
1040 "toolCallId": "tc_1",
1041 "status": "completed",
1042 "content": null
1043 })
1044 );
1045
1046 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1047 "toolCallId": "tc_1",
1048 "status": null,
1049 "locations": []
1050 }))
1051 .unwrap();
1052 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1053 assert_eq!(deserialized.status, MaybeUndefined::Null);
1054 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1055 }
1056
1057 #[cfg(feature = "unstable_tool_call_name")]
1058 #[test]
1059 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1060 let named = ToolCallUpdate::new("tc_1").name("read_file");
1061 assert_eq!(
1062 serde_json::to_value(named).unwrap(),
1063 serde_json::json!({
1064 "toolCallId": "tc_1",
1065 "name": "read_file"
1066 })
1067 );
1068
1069 let omitted = ToolCallUpdate::new("tc_1");
1070 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1071
1072 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1073 "toolCallId": "tc_1",
1074 "name": null
1075 }))
1076 .unwrap();
1077 assert_eq!(from_null.name, MaybeUndefined::Null);
1078
1079 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1080 stored.apply_update(ToolCallUpdate::new("tc_1"));
1081 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1082
1083 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1084 assert_eq!(stored.name, MaybeUndefined::Null);
1085
1086 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1087 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1088 }
1089
1090 #[test]
1091 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1092 let mut meta = Meta::new();
1093 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1094
1095 assert_eq!(
1096 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1097 serde_json::json!({
1098 "toolCallId": "tc_1",
1099 "_meta": {
1100 "source": "tool-call"
1101 }
1102 })
1103 );
1104
1105 assert_eq!(
1106 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1107 serde_json::json!({
1108 "toolCallId": "tc_1",
1109 "_meta": null
1110 })
1111 );
1112
1113 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1114 "toolCallId": "tc_1",
1115 "_meta": null
1116 }))
1117 .unwrap();
1118 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1119
1120 let patch = ToolCallUpdate::new("tc_1");
1121 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1122
1123 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1124 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1125 assert_eq!(stored.meta, MaybeUndefined::Null);
1126 }
1127
1128 #[test]
1129 fn tool_call_update_skips_malformed_list_items() {
1130 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1131 "toolCallId": "tc_1",
1132 "content": [
1133 {
1134 "type": "content",
1135 "content": {
1136 "type": "text",
1137 "text": "ok"
1138 }
1139 },
1140 {
1141 "type": "diff",
1142 "path": "/bad"
1143 }
1144 ],
1145 "locations": [
1146 {
1147 "path": "/ok",
1148 "line": 3
1149 },
1150 {
1151 "line": 4
1152 }
1153 ]
1154 }))
1155 .unwrap();
1156
1157 let MaybeUndefined::Value(content) = deserialized.content else {
1158 panic!("content should deserialize to a value");
1159 };
1160 assert_eq!(content.len(), 1);
1161
1162 let MaybeUndefined::Value(locations) = deserialized.locations else {
1163 panic!("locations should deserialize to a value");
1164 };
1165 assert_eq!(locations.len(), 1);
1166 }
1167
1168 #[test]
1169 fn tool_call_content_chunk_serializes_single_content_item() {
1170 let chunk = ToolCallContentChunk::new(
1171 "tc_1",
1172 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1173 );
1174
1175 assert_eq!(
1176 serde_json::to_value(chunk).unwrap(),
1177 serde_json::json!({
1178 "toolCallId": "tc_1",
1179 "content": {
1180 "type": "content",
1181 "content": {
1182 "type": "text",
1183 "text": "partial output"
1184 }
1185 }
1186 })
1187 );
1188 }
1189
1190 #[test]
1191 fn terminal_content_serializes_as_display_reference() {
1192 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1193
1194 assert_eq!(
1195 serde_json::to_value(terminal).unwrap(),
1196 serde_json::json!({
1197 "type": "terminal",
1198 "terminalId": "term_1"
1199 })
1200 );
1201 }
1202
1203 #[test]
1204 fn diff_patch_serializes_git_patch_with_structured_changes() {
1205 let patch_text = "diff --git /repo/config.json /repo/config.json\n--- /repo/config.json\n+++ /repo/config.json\n@@ -1 +1 @@\n-old\n+new\n";
1206 let diff = ToolCallContent::Diff(Diff::patch(
1207 patch_text,
1208 vec![
1209 DiffChange::modify("/repo/config.json")
1210 .file_type(DiffFileType::Text)
1211 .mime_type("application/json"),
1212 ],
1213 ));
1214
1215 assert_eq!(
1216 serde_json::to_value(diff).unwrap(),
1217 serde_json::json!({
1218 "type": "diff",
1219 "changes": [
1220 {
1221 "operation": "modify",
1222 "path": "/repo/config.json",
1223 "fileType": "text",
1224 "mimeType": "application/json"
1225 }
1226 ],
1227 "patch": {
1228 "format": "git_patch",
1229 "text": patch_text
1230 }
1231 })
1232 );
1233 }
1234
1235 #[test]
1236 fn diff_patch_requires_text() {
1237 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1238 "format": "git_patch",
1239 "diff": "diff --git /repo/config.json /repo/config.json\n"
1240 }));
1241
1242 assert!(result.is_err());
1243 }
1244
1245 #[test]
1246 fn diff_serializes_binary_modify_without_patch_text() {
1247 let diff = ToolCallContent::Diff(Diff::new(vec![
1248 DiffChange::modify("/repo/assets/logo.png")
1249 .file_type(DiffFileType::Binary)
1250 .mime_type("image/png"),
1251 ]));
1252
1253 assert_eq!(
1254 serde_json::to_value(diff).unwrap(),
1255 serde_json::json!({
1256 "type": "diff",
1257 "changes": [
1258 {
1259 "operation": "modify",
1260 "path": "/repo/assets/logo.png",
1261 "fileType": "binary",
1262 "mimeType": "image/png"
1263 }
1264 ]
1265 })
1266 );
1267 }
1268
1269 #[test]
1270 fn diff_move_serializes_shared_fields_with_operation_payload() {
1271 let diff = ToolCallContent::Diff(Diff::new(vec![
1272 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1273 .file_type(DiffFileType::Text)
1274 .mime_type("text/rust"),
1275 ]));
1276
1277 assert_eq!(
1278 serde_json::to_value(diff).unwrap(),
1279 serde_json::json!({
1280 "type": "diff",
1281 "changes": [
1282 {
1283 "operation": "move",
1284 "oldPath": "/repo/src/old.rs",
1285 "path": "/repo/src/new.rs",
1286 "fileType": "text",
1287 "mimeType": "text/rust"
1288 }
1289 ]
1290 })
1291 );
1292 }
1293
1294 #[test]
1295 fn diff_changes_skip_malformed_list_items() {
1296 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1297 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1298 "type": "diff",
1299 "changes": [
1300 {
1301 "operation": "modify"
1302 },
1303 {
1304 "operation": "delete",
1305 "path": "/ok"
1306 }
1307 ],
1308 "patch": {
1309 "format": "git_patch",
1310 "text": patch_text
1311 }
1312 }))
1313 .unwrap();
1314
1315 let ToolCallContent::Diff(diff) = content else {
1316 panic!("expected diff content");
1317 };
1318 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1319 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1320 }
1321
1322 #[test]
1323 fn tool_kind_preserves_unknown_variant() {
1324 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1325 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1326 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1327 }
1328
1329 #[test]
1330 fn tool_call_status_preserves_unknown_variant() {
1331 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1332 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1333 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1334 }
1335
1336 #[test]
1337 fn tool_call_status_recognizes_cancelled_variant() {
1338 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1339 assert_eq!(status, ToolCallStatus::Cancelled);
1340 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1341 }
1342
1343 #[test]
1344 fn tool_call_content_preserves_unknown_variant() {
1345 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1346 "type": "_chart",
1347 "title": "Tests",
1348 "data": [1, 2, 3]
1349 }))
1350 .unwrap();
1351
1352 let ToolCallContent::Other(unknown) = content else {
1353 panic!("expected unknown tool call content");
1354 };
1355
1356 assert_eq!(unknown.type_, "_chart");
1357 assert_eq!(
1358 unknown.fields.get("title"),
1359 Some(&serde_json::json!("Tests"))
1360 );
1361 assert_eq!(
1362 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1363 serde_json::json!({
1364 "type": "_chart",
1365 "title": "Tests",
1366 "data": [1, 2, 3]
1367 })
1368 );
1369 }
1370
1371 #[test]
1372 fn tool_call_content_does_not_hide_malformed_known_variant() {
1373 assert!(
1374 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1375 "type": "diff"
1376 }))
1377 .is_err()
1378 );
1379 assert!(
1380 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1381 "type": "terminal"
1382 }))
1383 .is_err()
1384 );
1385 }
1386}