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 #[serde_as(deserialize_as = "DefaultOnError")]
47 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
48 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
49 pub name: MaybeUndefined<String>,
50 #[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 title: MaybeUndefined<String>,
55 #[serde_as(deserialize_as = "DefaultOnError")]
58 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
59 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
60 pub kind: MaybeUndefined<ToolKind>,
61 #[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 status: MaybeUndefined<ToolCallStatus>,
66 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
68 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
69 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
70 pub content: MaybeUndefined<Vec<ToolCallContent>>,
71 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
74 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
75 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
76 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
77 #[serde_as(deserialize_as = "DefaultOnError")]
79 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
80 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
81 pub raw_input: MaybeUndefined<serde_json::Value>,
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_output: MaybeUndefined<serde_json::Value>,
87 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
93 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
94 #[serde(
95 rename = "_meta",
96 default,
97 skip_serializing_if = "MaybeUndefined::is_undefined"
98 )]
99 pub meta: MaybeUndefined<Meta>,
100}
101
102impl ToolCallUpdate {
103 #[must_use]
105 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
106 Self {
107 tool_call_id: tool_call_id.into(),
108 name: MaybeUndefined::Undefined,
109 title: MaybeUndefined::Undefined,
110 kind: MaybeUndefined::Undefined,
111 status: MaybeUndefined::Undefined,
112 content: MaybeUndefined::Undefined,
113 locations: MaybeUndefined::Undefined,
114 raw_input: MaybeUndefined::Undefined,
115 raw_output: MaybeUndefined::Undefined,
116 meta: MaybeUndefined::Undefined,
117 }
118 }
119
120 #[must_use]
122 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
123 self.name = name.into_maybe_undefined();
124 self
125 }
126
127 #[must_use]
129 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
130 self.title = title.into_maybe_undefined();
131 self
132 }
133
134 #[must_use]
137 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
138 self.kind = kind.into_maybe_undefined();
139 self
140 }
141
142 #[must_use]
144 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
145 self.status = status.into_maybe_undefined();
146 self
147 }
148
149 #[must_use]
151 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
152 self.content = content.into_maybe_undefined();
153 self
154 }
155
156 #[must_use]
159 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
160 self.locations = locations.into_maybe_undefined();
161 self
162 }
163
164 #[must_use]
166 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
167 self.raw_input = raw_input.into_maybe_undefined();
168 self
169 }
170
171 #[must_use]
173 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
174 self.raw_output = raw_output.into_maybe_undefined();
175 self
176 }
177
178 #[must_use]
184 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
185 self.meta = meta.into_maybe_undefined();
186 self
187 }
188
189 pub fn apply_update(&mut self, update: ToolCallUpdate) {
194 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
195 if !update.name.is_undefined() {
196 self.name = update.name;
197 }
198 if !update.title.is_undefined() {
199 self.title = update.title;
200 }
201 if !update.kind.is_undefined() {
202 self.kind = update.kind;
203 }
204 if !update.status.is_undefined() {
205 self.status = update.status;
206 }
207 if !update.content.is_undefined() {
208 self.content = update.content;
209 }
210 if !update.locations.is_undefined() {
211 self.locations = update.locations;
212 }
213 if !update.raw_input.is_undefined() {
214 self.raw_input = update.raw_input;
215 }
216 if !update.raw_output.is_undefined() {
217 self.raw_output = update.raw_output;
218 }
219 if !update.meta.is_undefined() {
220 self.meta = update.meta;
221 }
222 }
223}
224
225#[serde_as]
232#[skip_serializing_none]
233#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236#[non_exhaustive]
237pub struct ToolCallContentChunk {
238 pub tool_call_id: ToolCallId,
240 pub content: ToolCallContent,
242 #[serde_as(deserialize_as = "DefaultOnError")]
248 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
249 #[serde(default)]
250 #[serde(rename = "_meta")]
251 pub meta: Option<Meta>,
252}
253
254impl ToolCallContentChunk {
255 #[must_use]
257 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
258 Self {
259 tool_call_id: tool_call_id.into(),
260 content: content.into(),
261 meta: None,
262 }
263 }
264
265 #[must_use]
271 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
272 self.meta = meta.into_option();
273 self
274 }
275}
276
277#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
280#[serde(transparent)]
281#[from(forward)]
282#[non_exhaustive]
283pub struct ToolCallId(pub Arc<str>);
284
285impl ToolCallId {
286 #[must_use]
288 pub fn new(id: impl Into<Self>) -> Self {
289 id.into()
290 }
291}
292
293impl IntoOption<ToolCallId> for &str {
294 fn into_option(self) -> Option<ToolCallId> {
295 Some(ToolCallId::new(self))
296 }
297}
298
299#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
306#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
307#[serde(rename_all = "snake_case")]
308#[non_exhaustive]
309pub enum ToolKind {
310 Read,
312 Edit,
314 Delete,
316 Move,
318 Search,
320 Execute,
322 Think,
324 Fetch,
326 SwitchMode,
328 #[default]
330 Other,
331 #[serde(untagged)]
337 Unknown(String),
338}
339
340#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
346#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
347#[serde(rename_all = "snake_case")]
348#[non_exhaustive]
349pub enum ToolCallStatus {
350 #[default]
353 Pending,
354 InProgress,
356 Completed,
358 Failed,
360 Cancelled,
362 #[serde(untagged)]
368 Other(String),
369}
370
371#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379#[serde(tag = "type", rename_all = "snake_case")]
380#[non_exhaustive]
381pub enum ToolCallContent {
382 Content(Box<Content>),
384 Diff(Diff),
386 Terminal(Terminal),
388 #[serde(untagged)]
398 Other(OtherToolCallContent),
399}
400
401#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
403#[derive(Debug, Clone, PartialEq, Serialize)]
404#[cfg_attr(feature = "schemars", schemars(inline))]
405#[cfg_attr(feature = "schemars", schemars(transform = other_tool_call_content_schema))]
406#[serde(rename_all = "camelCase")]
407#[non_exhaustive]
408pub struct OtherToolCallContent {
409 #[serde(rename = "type")]
415 pub type_: String,
416 #[serde(flatten)]
418 pub fields: BTreeMap<String, serde_json::Value>,
419}
420
421impl OtherToolCallContent {
422 #[must_use]
424 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
425 fields.remove("type");
426 Self {
427 type_: type_.into(),
428 fields,
429 }
430 }
431}
432
433impl<'de> Deserialize<'de> for OtherToolCallContent {
434 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435 where
436 D: serde::Deserializer<'de>,
437 {
438 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
439 let type_ = fields
440 .remove("type")
441 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
442 let serde_json::Value::String(type_) = type_ else {
443 return Err(serde::de::Error::custom("`type` must be a string"));
444 };
445
446 if is_known_tool_call_content_type(&type_) {
447 return Err(serde::de::Error::custom(format!(
448 "known tool call content `{type_}` did not match its schema"
449 )));
450 }
451
452 Ok(Self { type_, fields })
453 }
454}
455
456fn is_known_tool_call_content_type(type_: &str) -> bool {
457 matches!(type_, "content" | "diff" | "terminal")
458}
459
460#[cfg(feature = "schemars")]
461fn other_tool_call_content_schema(schema: &mut Schema) {
462 super::schema_util::reject_known_string_discriminators(
463 schema,
464 "type",
465 &["content", "diff", "terminal"],
466 );
467}
468
469impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
470 fn from(content: T) -> Self {
471 ToolCallContent::Content(Box::new(Content::new(content)))
472 }
473}
474
475impl From<Diff> for ToolCallContent {
476 fn from(diff: Diff) -> Self {
477 ToolCallContent::Diff(diff)
478 }
479}
480
481impl From<Terminal> for ToolCallContent {
482 fn from(terminal: Terminal) -> Self {
483 ToolCallContent::Terminal(terminal)
484 }
485}
486
487#[serde_as]
489#[skip_serializing_none]
490#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493#[non_exhaustive]
494pub struct Content {
495 pub content: ContentBlock,
497 #[serde_as(deserialize_as = "DefaultOnError")]
503 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
504 #[serde(default)]
505 #[serde(rename = "_meta")]
506 pub meta: Option<Meta>,
507}
508
509impl Content {
510 #[must_use]
512 pub fn new(content: impl Into<ContentBlock>) -> Self {
513 Self {
514 content: content.into(),
515 meta: None,
516 }
517 }
518
519 #[must_use]
525 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
526 self.meta = meta.into_option();
527 self
528 }
529}
530
531#[serde_as]
540#[skip_serializing_none]
541#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543#[serde(rename_all = "camelCase")]
544#[non_exhaustive]
545pub struct Diff {
546 #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
550 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
551 pub changes: Vec<DiffChange>,
552 #[serde_as(deserialize_as = "DefaultOnError")]
557 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
558 #[serde(default)]
559 pub patch: Option<DiffPatch>,
560 #[serde_as(deserialize_as = "DefaultOnError")]
566 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
567 #[serde(default)]
568 #[serde(rename = "_meta")]
569 pub meta: Option<Meta>,
570}
571
572impl Diff {
573 #[must_use]
575 pub fn new(changes: Vec<DiffChange>) -> Self {
576 Self {
577 changes,
578 patch: None,
579 meta: None,
580 }
581 }
582
583 #[must_use]
585 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
586 Self::new(changes).with_patch(DiffPatch::new(text))
587 }
588
589 #[must_use]
591 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
592 self.patch = patch.into_option();
593 self
594 }
595
596 #[must_use]
602 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
603 self.meta = meta.into_option();
604 self
605 }
606}
607
608#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611#[serde(rename_all = "camelCase")]
612#[non_exhaustive]
613pub struct DiffPatch {
614 pub format: DiffPatchFormat,
616 pub text: String,
618}
619
620impl DiffPatch {
621 #[must_use]
623 pub fn new(text: impl Into<String>) -> Self {
624 Self {
625 format: DiffPatchFormat::GitPatch,
626 text: text.into(),
627 }
628 }
629}
630
631#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "snake_case")]
635#[non_exhaustive]
636pub enum DiffPatchFormat {
637 GitPatch,
642 #[serde(untagged)]
648 Other(String),
649}
650
651#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(rename_all = "snake_case")]
655#[non_exhaustive]
656pub enum DiffFileType {
657 Text,
659 Binary,
661 Directory,
663 Symlink,
665 #[serde(untagged)]
671 Other(String),
672}
673
674#[serde_as]
679#[skip_serializing_none]
680#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682#[serde(rename_all = "camelCase")]
683#[non_exhaustive]
684pub struct DiffChange {
685 #[serde_as(deserialize_as = "DefaultOnError")]
689 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
690 #[serde(default)]
691 pub file_type: Option<DiffFileType>,
692 #[serde_as(deserialize_as = "DefaultOnError")]
696 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
697 #[serde(default)]
698 pub mime_type: Option<MediaType>,
699 #[serde(flatten)]
701 pub operation: DiffChangeOperation,
702 #[serde_as(deserialize_as = "DefaultOnError")]
708 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
709 #[serde(default)]
710 #[serde(rename = "_meta")]
711 pub meta: Option<Meta>,
712}
713
714impl DiffChange {
715 #[must_use]
717 pub fn new(operation: DiffChangeOperation) -> Self {
718 Self {
719 file_type: None,
720 mime_type: None,
721 operation,
722 meta: None,
723 }
724 }
725
726 #[must_use]
728 pub fn add(path: impl Into<AbsolutePath>) -> Self {
729 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
730 }
731
732 #[must_use]
734 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
735 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
736 }
737
738 #[must_use]
740 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
741 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
742 }
743
744 #[must_use]
746 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
747 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
748 old_path, path,
749 )))
750 }
751
752 #[must_use]
754 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
755 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
756 old_path, path,
757 )))
758 }
759
760 #[must_use]
764 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
765 self.file_type = file_type.into_option();
766 self
767 }
768
769 #[must_use]
773 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
774 self.mime_type = mime_type.into_option();
775 self
776 }
777
778 #[must_use]
784 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
785 self.meta = meta.into_option();
786 self
787 }
788}
789
790#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(tag = "operation", rename_all = "snake_case")]
794#[non_exhaustive]
795pub enum DiffChangeOperation {
796 Add(DiffPathChange),
798 Delete(DiffPathChange),
800 Modify(DiffPathChange),
802 Move(DiffPathPairChange),
804 Copy(DiffPathPairChange),
806 #[serde(untagged)]
812 Other(OtherDiffChange),
813}
814
815#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818#[serde(rename_all = "camelCase")]
819#[non_exhaustive]
820pub struct DiffPathChange {
821 pub path: AbsolutePath,
823}
824
825impl DiffPathChange {
826 #[must_use]
828 pub fn new(path: impl Into<AbsolutePath>) -> Self {
829 Self { path: path.into() }
830 }
831}
832
833#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
835#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
836#[serde(rename_all = "camelCase")]
837#[non_exhaustive]
838pub struct DiffPathPairChange {
839 pub old_path: AbsolutePath,
841 pub path: AbsolutePath,
843}
844
845impl DiffPathPairChange {
846 #[must_use]
848 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
849 Self {
850 old_path: old_path.into(),
851 path: path.into(),
852 }
853 }
854}
855
856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
858#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
859#[cfg_attr(feature = "schemars", schemars(inline))]
860#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
861#[serde(rename_all = "camelCase")]
862#[non_exhaustive]
863pub struct OtherDiffChange {
864 pub operation: String,
870 #[serde(flatten)]
872 pub fields: BTreeMap<String, serde_json::Value>,
873}
874
875impl OtherDiffChange {
876 #[must_use]
878 pub fn new(
879 operation: impl Into<String>,
880 mut fields: BTreeMap<String, serde_json::Value>,
881 ) -> Self {
882 fields.remove("operation");
883 fields.remove("fileType");
884 fields.remove("mimeType");
885 fields.remove("_meta");
886 Self {
887 operation: operation.into(),
888 fields,
889 }
890 }
891}
892
893impl<'de> Deserialize<'de> for OtherDiffChange {
894 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
895 where
896 D: serde::Deserializer<'de>,
897 {
898 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
899 let operation = fields
900 .remove("operation")
901 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
902 let serde_json::Value::String(operation) = operation else {
903 return Err(serde::de::Error::custom("`operation` must be a string"));
904 };
905
906 if is_known_diff_change_operation(&operation) {
907 return Err(serde::de::Error::custom(format!(
908 "known diff change operation `{operation}` did not match its schema"
909 )));
910 }
911 fields.remove("fileType");
912 fields.remove("mimeType");
913 fields.remove("_meta");
914
915 Ok(Self { operation, fields })
916 }
917}
918
919fn is_known_diff_change_operation(operation: &str) -> bool {
920 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
921}
922
923#[cfg(feature = "schemars")]
924fn other_diff_change_schema(schema: &mut Schema) {
925 super::schema_util::reject_known_string_discriminators(
926 schema,
927 "operation",
928 &["add", "delete", "modify", "move", "copy"],
929 );
930}
931
932#[serde_as]
939#[skip_serializing_none]
940#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
941#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
942#[serde(rename_all = "camelCase")]
943#[non_exhaustive]
944pub struct ToolCallLocation {
945 pub path: AbsolutePath,
947 #[serde_as(deserialize_as = "DefaultOnError")]
949 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
950 #[serde(default)]
951 pub line: Option<u32>,
952 #[serde_as(deserialize_as = "DefaultOnError")]
958 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
959 #[serde(default)]
960 #[serde(rename = "_meta")]
961 pub meta: Option<Meta>,
962}
963
964impl ToolCallLocation {
965 #[must_use]
967 pub fn new(path: impl Into<AbsolutePath>) -> Self {
968 Self {
969 path: path.into(),
970 line: None,
971 meta: None,
972 }
973 }
974
975 #[must_use]
977 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
978 self.line = line.into_option();
979 self
980 }
981
982 #[must_use]
988 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
989 self.meta = meta.into_option();
990 self
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997 use crate::MaybeUndefined;
998
999 #[test]
1000 fn tool_call_serializes_as_upsert() {
1001 let tool_call = ToolCallUpdate::new("tc_1")
1002 .title("Reading configuration")
1003 .status(ToolCallStatus::InProgress)
1004 .raw_input(serde_json::json!({"path": "settings.json"}));
1005
1006 assert_eq!(
1007 serde_json::to_value(tool_call).unwrap(),
1008 serde_json::json!({
1009 "toolCallId": "tc_1",
1010 "title": "Reading configuration",
1011 "status": "in_progress",
1012 "rawInput": {
1013 "path": "settings.json"
1014 }
1015 })
1016 );
1017 }
1018
1019 #[test]
1020 fn tool_call_update_distinguishes_omitted_null_and_value() {
1021 let tool_call = ToolCallUpdate::new("tc_1")
1022 .status(ToolCallStatus::Completed)
1023 .content(None::<Vec<ToolCallContent>>);
1024
1025 assert_eq!(
1026 serde_json::to_value(tool_call).unwrap(),
1027 serde_json::json!({
1028 "toolCallId": "tc_1",
1029 "status": "completed",
1030 "content": null
1031 })
1032 );
1033
1034 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1035 "toolCallId": "tc_1",
1036 "status": null,
1037 "locations": []
1038 }))
1039 .unwrap();
1040 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1041 assert_eq!(deserialized.status, MaybeUndefined::Null);
1042 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1043 }
1044
1045 #[test]
1046 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1047 let named = ToolCallUpdate::new("tc_1").name("read_file");
1048 assert_eq!(
1049 serde_json::to_value(named).unwrap(),
1050 serde_json::json!({
1051 "toolCallId": "tc_1",
1052 "name": "read_file"
1053 })
1054 );
1055
1056 let omitted = ToolCallUpdate::new("tc_1");
1057 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1058
1059 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1060 "toolCallId": "tc_1",
1061 "name": null
1062 }))
1063 .unwrap();
1064 assert_eq!(from_null.name, MaybeUndefined::Null);
1065
1066 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1067 stored.apply_update(ToolCallUpdate::new("tc_1"));
1068 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1069
1070 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1071 assert_eq!(stored.name, MaybeUndefined::Null);
1072
1073 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1074 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1075 }
1076
1077 #[test]
1078 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1079 let mut meta = Meta::new();
1080 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1081
1082 assert_eq!(
1083 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1084 serde_json::json!({
1085 "toolCallId": "tc_1",
1086 "_meta": {
1087 "source": "tool-call"
1088 }
1089 })
1090 );
1091
1092 assert_eq!(
1093 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1094 serde_json::json!({
1095 "toolCallId": "tc_1",
1096 "_meta": null
1097 })
1098 );
1099
1100 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1101 "toolCallId": "tc_1",
1102 "_meta": null
1103 }))
1104 .unwrap();
1105 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1106
1107 let patch = ToolCallUpdate::new("tc_1");
1108 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1109
1110 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1111 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1112 assert_eq!(stored.meta, MaybeUndefined::Null);
1113 }
1114
1115 #[test]
1116 fn tool_call_update_skips_malformed_list_items() {
1117 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1118 "toolCallId": "tc_1",
1119 "content": [
1120 {
1121 "type": "content",
1122 "content": {
1123 "type": "text",
1124 "text": "ok"
1125 }
1126 },
1127 {
1128 "type": "diff",
1129 "path": "/bad"
1130 }
1131 ],
1132 "locations": [
1133 {
1134 "path": "/ok",
1135 "line": 3
1136 },
1137 {
1138 "line": 4
1139 }
1140 ]
1141 }))
1142 .unwrap();
1143
1144 let MaybeUndefined::Value(content) = deserialized.content else {
1145 panic!("content should deserialize to a value");
1146 };
1147 assert_eq!(content.len(), 1);
1148
1149 let MaybeUndefined::Value(locations) = deserialized.locations else {
1150 panic!("locations should deserialize to a value");
1151 };
1152 assert_eq!(locations.len(), 1);
1153 }
1154
1155 #[test]
1156 fn tool_call_content_chunk_serializes_single_content_item() {
1157 let chunk = ToolCallContentChunk::new(
1158 "tc_1",
1159 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1160 );
1161
1162 assert_eq!(
1163 serde_json::to_value(chunk).unwrap(),
1164 serde_json::json!({
1165 "toolCallId": "tc_1",
1166 "content": {
1167 "type": "content",
1168 "content": {
1169 "type": "text",
1170 "text": "partial output"
1171 }
1172 }
1173 })
1174 );
1175 }
1176
1177 #[test]
1178 fn terminal_content_serializes_as_display_reference() {
1179 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1180
1181 assert_eq!(
1182 serde_json::to_value(terminal).unwrap(),
1183 serde_json::json!({
1184 "type": "terminal",
1185 "terminalId": "term_1"
1186 })
1187 );
1188 }
1189
1190 #[test]
1191 fn diff_patch_serializes_git_patch_with_structured_changes() {
1192 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";
1193 let diff = ToolCallContent::Diff(Diff::patch(
1194 patch_text,
1195 vec![
1196 DiffChange::modify("/repo/config.json")
1197 .file_type(DiffFileType::Text)
1198 .mime_type("application/json"),
1199 ],
1200 ));
1201
1202 assert_eq!(
1203 serde_json::to_value(diff).unwrap(),
1204 serde_json::json!({
1205 "type": "diff",
1206 "changes": [
1207 {
1208 "operation": "modify",
1209 "path": "/repo/config.json",
1210 "fileType": "text",
1211 "mimeType": "application/json"
1212 }
1213 ],
1214 "patch": {
1215 "format": "git_patch",
1216 "text": patch_text
1217 }
1218 })
1219 );
1220 }
1221
1222 #[test]
1223 fn diff_patch_requires_text() {
1224 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1225 "format": "git_patch",
1226 "diff": "diff --git /repo/config.json /repo/config.json\n"
1227 }));
1228
1229 assert!(result.is_err());
1230 }
1231
1232 #[test]
1233 fn diff_serializes_binary_modify_without_patch_text() {
1234 let diff = ToolCallContent::Diff(Diff::new(vec![
1235 DiffChange::modify("/repo/assets/logo.png")
1236 .file_type(DiffFileType::Binary)
1237 .mime_type("image/png"),
1238 ]));
1239
1240 assert_eq!(
1241 serde_json::to_value(diff).unwrap(),
1242 serde_json::json!({
1243 "type": "diff",
1244 "changes": [
1245 {
1246 "operation": "modify",
1247 "path": "/repo/assets/logo.png",
1248 "fileType": "binary",
1249 "mimeType": "image/png"
1250 }
1251 ]
1252 })
1253 );
1254 }
1255
1256 #[test]
1257 fn diff_move_serializes_shared_fields_with_operation_payload() {
1258 let diff = ToolCallContent::Diff(Diff::new(vec![
1259 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1260 .file_type(DiffFileType::Text)
1261 .mime_type("text/rust"),
1262 ]));
1263
1264 assert_eq!(
1265 serde_json::to_value(diff).unwrap(),
1266 serde_json::json!({
1267 "type": "diff",
1268 "changes": [
1269 {
1270 "operation": "move",
1271 "oldPath": "/repo/src/old.rs",
1272 "path": "/repo/src/new.rs",
1273 "fileType": "text",
1274 "mimeType": "text/rust"
1275 }
1276 ]
1277 })
1278 );
1279 }
1280
1281 #[test]
1282 fn diff_changes_skip_malformed_list_items() {
1283 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1284 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1285 "type": "diff",
1286 "changes": [
1287 {
1288 "operation": "modify"
1289 },
1290 {
1291 "operation": "delete",
1292 "path": "/ok"
1293 }
1294 ],
1295 "patch": {
1296 "format": "git_patch",
1297 "text": patch_text
1298 }
1299 }))
1300 .unwrap();
1301
1302 let ToolCallContent::Diff(diff) = content else {
1303 panic!("expected diff content");
1304 };
1305 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1306 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1307 }
1308
1309 #[test]
1310 fn tool_kind_preserves_unknown_variant() {
1311 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1312 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1313 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1314 }
1315
1316 #[test]
1317 fn tool_call_status_preserves_unknown_variant() {
1318 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1319 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1320 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1321 }
1322
1323 #[test]
1324 fn tool_call_status_recognizes_cancelled_variant() {
1325 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1326 assert_eq!(status, ToolCallStatus::Cancelled);
1327 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1328 }
1329
1330 #[test]
1331 fn tool_call_content_preserves_unknown_variant() {
1332 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1333 "type": "_chart",
1334 "title": "Tests",
1335 "data": [1, 2, 3]
1336 }))
1337 .unwrap();
1338
1339 let ToolCallContent::Other(unknown) = content else {
1340 panic!("expected unknown tool call content");
1341 };
1342
1343 assert_eq!(unknown.type_, "_chart");
1344 assert_eq!(
1345 unknown.fields.get("title"),
1346 Some(&serde_json::json!("Tests"))
1347 );
1348 assert_eq!(
1349 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1350 serde_json::json!({
1351 "type": "_chart",
1352 "title": "Tests",
1353 "data": [1, 2, 3]
1354 })
1355 );
1356 }
1357
1358 #[test]
1359 fn tool_call_content_does_not_hide_malformed_known_variant() {
1360 assert!(
1361 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1362 "type": "diff"
1363 }))
1364 .is_err()
1365 );
1366 assert!(
1367 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1368 "type": "terminal"
1369 }))
1370 .is_err()
1371 );
1372 }
1373}