1use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
8
9use derive_more::{Display, From};
10use schemars::{JsonSchema, Schema};
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14use super::{ContentBlock, Meta};
15use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
16
17#[serde_as]
31#[skip_serializing_none]
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "camelCase")]
34#[non_exhaustive]
35pub struct ToolCallUpdate {
36 pub tool_call_id: ToolCallId,
38 #[serde_as(deserialize_as = "DefaultOnError")]
40 #[schemars(extend("x-deserialize-default-on-error" = true))]
41 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
42 pub title: MaybeUndefined<String>,
43 #[serde_as(deserialize_as = "DefaultOnError")]
46 #[schemars(extend("x-deserialize-default-on-error" = true))]
47 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
48 pub kind: MaybeUndefined<ToolKind>,
49 #[serde_as(deserialize_as = "DefaultOnError")]
51 #[schemars(extend("x-deserialize-default-on-error" = true))]
52 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
53 pub status: MaybeUndefined<ToolCallStatus>,
54 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
56 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
57 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
58 pub content: MaybeUndefined<Vec<ToolCallContent>>,
59 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
62 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
63 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
64 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
65 #[serde_as(deserialize_as = "DefaultOnError")]
67 #[schemars(extend("x-deserialize-default-on-error" = true))]
68 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
69 pub raw_input: MaybeUndefined<serde_json::Value>,
70 #[serde_as(deserialize_as = "DefaultOnError")]
72 #[schemars(extend("x-deserialize-default-on-error" = true))]
73 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
74 pub raw_output: MaybeUndefined<serde_json::Value>,
75 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
81 #[schemars(extend("x-deserialize-default-on-error" = true))]
82 #[serde(
83 rename = "_meta",
84 default,
85 skip_serializing_if = "MaybeUndefined::is_undefined"
86 )]
87 pub meta: MaybeUndefined<Meta>,
88}
89
90impl ToolCallUpdate {
91 #[must_use]
93 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
94 Self {
95 tool_call_id: tool_call_id.into(),
96 title: MaybeUndefined::Undefined,
97 kind: MaybeUndefined::Undefined,
98 status: MaybeUndefined::Undefined,
99 content: MaybeUndefined::Undefined,
100 locations: MaybeUndefined::Undefined,
101 raw_input: MaybeUndefined::Undefined,
102 raw_output: MaybeUndefined::Undefined,
103 meta: MaybeUndefined::Undefined,
104 }
105 }
106
107 #[must_use]
109 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
110 self.title = title.into_maybe_undefined();
111 self
112 }
113
114 #[must_use]
117 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
118 self.kind = kind.into_maybe_undefined();
119 self
120 }
121
122 #[must_use]
124 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
125 self.status = status.into_maybe_undefined();
126 self
127 }
128
129 #[must_use]
131 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
132 self.content = content.into_maybe_undefined();
133 self
134 }
135
136 #[must_use]
139 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
140 self.locations = locations.into_maybe_undefined();
141 self
142 }
143
144 #[must_use]
146 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
147 self.raw_input = raw_input.into_maybe_undefined();
148 self
149 }
150
151 #[must_use]
153 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
154 self.raw_output = raw_output.into_maybe_undefined();
155 self
156 }
157
158 #[must_use]
164 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
165 self.meta = meta.into_maybe_undefined();
166 self
167 }
168
169 pub fn apply_update(&mut self, update: ToolCallUpdate) {
174 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
175 if !update.title.is_undefined() {
176 self.title = update.title;
177 }
178 if !update.kind.is_undefined() {
179 self.kind = update.kind;
180 }
181 if !update.status.is_undefined() {
182 self.status = update.status;
183 }
184 if !update.content.is_undefined() {
185 self.content = update.content;
186 }
187 if !update.locations.is_undefined() {
188 self.locations = update.locations;
189 }
190 if !update.raw_input.is_undefined() {
191 self.raw_input = update.raw_input;
192 }
193 if !update.raw_output.is_undefined() {
194 self.raw_output = update.raw_output;
195 }
196 if !update.meta.is_undefined() {
197 self.meta = update.meta;
198 }
199 }
200}
201
202#[serde_as]
209#[skip_serializing_none]
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
211#[serde(rename_all = "camelCase")]
212#[non_exhaustive]
213pub struct ToolCallContentChunk {
214 pub tool_call_id: ToolCallId,
216 pub content: ToolCallContent,
218 #[serde_as(deserialize_as = "DefaultOnError")]
224 #[schemars(extend("x-deserialize-default-on-error" = true))]
225 #[serde(default)]
226 #[serde(rename = "_meta")]
227 pub meta: Option<Meta>,
228}
229
230impl ToolCallContentChunk {
231 #[must_use]
233 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
234 Self {
235 tool_call_id: tool_call_id.into(),
236 content: content.into(),
237 meta: None,
238 }
239 }
240
241 #[must_use]
247 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
248 self.meta = meta.into_option();
249 self
250 }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
255#[serde(transparent)]
256#[from(Arc<str>, String, &'static str)]
257#[non_exhaustive]
258pub struct ToolCallId(pub Arc<str>);
259
260impl ToolCallId {
261 #[must_use]
263 pub fn new(id: impl Into<Arc<str>>) -> Self {
264 Self(id.into())
265 }
266}
267
268impl IntoOption<ToolCallId> for &str {
269 fn into_option(self) -> Option<ToolCallId> {
270 Some(ToolCallId::new(self))
271 }
272}
273
274#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
281#[serde(rename_all = "snake_case")]
282#[non_exhaustive]
283pub enum ToolKind {
284 Read,
286 Edit,
288 Delete,
290 Move,
292 Search,
294 Execute,
296 Think,
298 Fetch,
300 SwitchMode,
302 #[default]
304 Other,
305 #[serde(untagged)]
311 Unknown(String),
312}
313
314#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
320#[serde(rename_all = "snake_case")]
321#[non_exhaustive]
322pub enum ToolCallStatus {
323 #[default]
326 Pending,
327 InProgress,
329 Completed,
331 Failed,
333 #[serde(untagged)]
339 Other(String),
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
349#[serde(tag = "type", rename_all = "snake_case")]
350#[schemars(extend("discriminator" = {"propertyName": "type"}))]
351#[non_exhaustive]
352pub enum ToolCallContent {
353 Content(Box<Content>),
355 Diff(Diff),
357 #[serde(untagged)]
367 Other(OtherToolCallContent),
368}
369
370#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
372#[schemars(inline)]
373#[schemars(transform = other_tool_call_content_schema)]
374#[serde(rename_all = "camelCase")]
375#[non_exhaustive]
376pub struct OtherToolCallContent {
377 #[serde(rename = "type")]
383 pub type_: String,
384 #[serde(flatten)]
386 pub fields: BTreeMap<String, serde_json::Value>,
387}
388
389impl OtherToolCallContent {
390 #[must_use]
392 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
393 fields.remove("type");
394 Self {
395 type_: type_.into(),
396 fields,
397 }
398 }
399}
400
401impl<'de> Deserialize<'de> for OtherToolCallContent {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: serde::Deserializer<'de>,
405 {
406 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
407 let type_ = fields
408 .remove("type")
409 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
410 let serde_json::Value::String(type_) = type_ else {
411 return Err(serde::de::Error::custom("`type` must be a string"));
412 };
413
414 if is_known_tool_call_content_type(&type_) {
415 return Err(serde::de::Error::custom(format!(
416 "known tool call content `{type_}` did not match its schema"
417 )));
418 }
419
420 Ok(Self { type_, fields })
421 }
422}
423
424fn is_known_tool_call_content_type(type_: &str) -> bool {
425 matches!(type_, "content" | "diff")
426}
427
428fn other_tool_call_content_schema(schema: &mut Schema) {
429 super::schema_util::reject_known_string_discriminators(schema, "type", &["content", "diff"]);
430}
431
432impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
433 fn from(content: T) -> Self {
434 ToolCallContent::Content(Box::new(Content::new(content)))
435 }
436}
437
438impl From<Diff> for ToolCallContent {
439 fn from(diff: Diff) -> Self {
440 ToolCallContent::Diff(diff)
441 }
442}
443
444#[serde_as]
446#[skip_serializing_none]
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
448#[serde(rename_all = "camelCase")]
449#[non_exhaustive]
450pub struct Content {
451 pub content: ContentBlock,
453 #[serde_as(deserialize_as = "DefaultOnError")]
459 #[schemars(extend("x-deserialize-default-on-error" = true))]
460 #[serde(default)]
461 #[serde(rename = "_meta")]
462 pub meta: Option<Meta>,
463}
464
465impl Content {
466 #[must_use]
468 pub fn new(content: impl Into<ContentBlock>) -> Self {
469 Self {
470 content: content.into(),
471 meta: None,
472 }
473 }
474
475 #[must_use]
481 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
482 self.meta = meta.into_option();
483 self
484 }
485}
486
487#[serde_as]
496#[skip_serializing_none]
497#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
498#[serde(rename_all = "camelCase")]
499#[non_exhaustive]
500pub struct Diff {
501 #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
505 #[schemars(extend("x-deserialize-skip-invalid-items" = true))]
506 pub changes: Vec<DiffChange>,
507 #[serde_as(deserialize_as = "DefaultOnError")]
512 #[schemars(extend("x-deserialize-default-on-error" = true))]
513 #[serde(default)]
514 pub patch: Option<DiffPatch>,
515 #[serde_as(deserialize_as = "DefaultOnError")]
521 #[schemars(extend("x-deserialize-default-on-error" = true))]
522 #[serde(default)]
523 #[serde(rename = "_meta")]
524 pub meta: Option<Meta>,
525}
526
527impl Diff {
528 #[must_use]
530 pub fn new(changes: Vec<DiffChange>) -> Self {
531 Self {
532 changes,
533 patch: None,
534 meta: None,
535 }
536 }
537
538 #[must_use]
540 pub fn patch(diff: impl Into<String>, changes: Vec<DiffChange>) -> Self {
541 Self::new(changes).with_patch(DiffPatch::new(diff))
542 }
543
544 #[must_use]
546 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
547 self.patch = patch.into_option();
548 self
549 }
550
551 #[must_use]
557 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
558 self.meta = meta.into_option();
559 self
560 }
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
565#[serde(rename_all = "camelCase")]
566#[non_exhaustive]
567pub struct DiffPatch {
568 pub format: DiffPatchFormat,
570 pub diff: String,
572}
573
574impl DiffPatch {
575 #[must_use]
577 pub fn new(diff: impl Into<String>) -> Self {
578 Self {
579 format: DiffPatchFormat::GitPatch,
580 diff: diff.into(),
581 }
582 }
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
587#[serde(rename_all = "snake_case")]
588#[non_exhaustive]
589pub enum DiffPatchFormat {
590 GitPatch,
592 #[serde(untagged)]
598 Other(String),
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
603#[serde(rename_all = "snake_case")]
604#[non_exhaustive]
605pub enum DiffFileType {
606 Text,
608 Binary,
610 Directory,
612 Symlink,
614 #[serde(untagged)]
620 Other(String),
621}
622
623#[serde_as]
628#[skip_serializing_none]
629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
630#[serde(rename_all = "camelCase")]
631#[non_exhaustive]
632pub struct DiffChange {
633 #[serde_as(deserialize_as = "DefaultOnError")]
637 #[schemars(extend("x-deserialize-default-on-error" = true))]
638 #[serde(default)]
639 pub file_type: Option<DiffFileType>,
640 #[serde_as(deserialize_as = "DefaultOnError")]
644 #[schemars(extend("x-deserialize-default-on-error" = true))]
645 #[serde(default)]
646 pub mime_type: Option<String>,
647 #[serde(flatten)]
649 pub operation: DiffChangeOperation,
650 #[serde_as(deserialize_as = "DefaultOnError")]
656 #[schemars(extend("x-deserialize-default-on-error" = true))]
657 #[serde(default)]
658 #[serde(rename = "_meta")]
659 pub meta: Option<Meta>,
660}
661
662impl DiffChange {
663 #[must_use]
665 pub fn new(operation: DiffChangeOperation) -> Self {
666 Self {
667 file_type: None,
668 mime_type: None,
669 operation,
670 meta: None,
671 }
672 }
673
674 #[must_use]
676 pub fn add(path: impl Into<PathBuf>) -> Self {
677 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
678 }
679
680 #[must_use]
682 pub fn delete(path: impl Into<PathBuf>) -> Self {
683 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
684 }
685
686 #[must_use]
688 pub fn modify(path: impl Into<PathBuf>) -> Self {
689 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
690 }
691
692 #[must_use]
694 pub fn move_file(old_path: impl Into<PathBuf>, path: impl Into<PathBuf>) -> Self {
695 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
696 old_path, path,
697 )))
698 }
699
700 #[must_use]
702 pub fn copy(old_path: impl Into<PathBuf>, path: impl Into<PathBuf>) -> Self {
703 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
704 old_path, path,
705 )))
706 }
707
708 #[must_use]
712 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
713 self.file_type = file_type.into_option();
714 self
715 }
716
717 #[must_use]
721 pub fn mime_type(mut self, mime_type: impl IntoOption<String>) -> Self {
722 self.mime_type = mime_type.into_option();
723 self
724 }
725
726 #[must_use]
732 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
733 self.meta = meta.into_option();
734 self
735 }
736}
737
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
740#[serde(tag = "operation", rename_all = "snake_case")]
741#[schemars(extend("discriminator" = {"propertyName": "operation"}))]
742#[non_exhaustive]
743pub enum DiffChangeOperation {
744 Add(DiffPathChange),
746 Delete(DiffPathChange),
748 Modify(DiffPathChange),
750 Move(DiffPathPairChange),
752 Copy(DiffPathPairChange),
754 #[serde(untagged)]
760 Other(OtherDiffChange),
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
765#[serde(rename_all = "camelCase")]
766#[non_exhaustive]
767pub struct DiffPathChange {
768 pub path: PathBuf,
770}
771
772impl DiffPathChange {
773 #[must_use]
775 pub fn new(path: impl Into<PathBuf>) -> Self {
776 Self { path: path.into() }
777 }
778}
779
780#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
782#[serde(rename_all = "camelCase")]
783#[non_exhaustive]
784pub struct DiffPathPairChange {
785 pub old_path: PathBuf,
787 pub path: PathBuf,
789}
790
791impl DiffPathPairChange {
792 #[must_use]
794 pub fn new(old_path: impl Into<PathBuf>, path: impl Into<PathBuf>) -> Self {
795 Self {
796 old_path: old_path.into(),
797 path: path.into(),
798 }
799 }
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
804#[schemars(inline)]
805#[schemars(transform = other_diff_change_schema)]
806#[serde(rename_all = "camelCase")]
807#[non_exhaustive]
808pub struct OtherDiffChange {
809 pub operation: String,
815 #[serde(flatten)]
817 pub fields: BTreeMap<String, serde_json::Value>,
818}
819
820impl OtherDiffChange {
821 #[must_use]
823 pub fn new(
824 operation: impl Into<String>,
825 mut fields: BTreeMap<String, serde_json::Value>,
826 ) -> Self {
827 fields.remove("operation");
828 fields.remove("fileType");
829 fields.remove("mimeType");
830 fields.remove("_meta");
831 Self {
832 operation: operation.into(),
833 fields,
834 }
835 }
836}
837
838impl<'de> Deserialize<'de> for OtherDiffChange {
839 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
840 where
841 D: serde::Deserializer<'de>,
842 {
843 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
844 let operation = fields
845 .remove("operation")
846 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
847 let serde_json::Value::String(operation) = operation else {
848 return Err(serde::de::Error::custom("`operation` must be a string"));
849 };
850
851 if is_known_diff_change_operation(&operation) {
852 return Err(serde::de::Error::custom(format!(
853 "known diff change operation `{operation}` did not match its schema"
854 )));
855 }
856 fields.remove("fileType");
857 fields.remove("mimeType");
858 fields.remove("_meta");
859
860 Ok(Self { operation, fields })
861 }
862}
863
864fn is_known_diff_change_operation(operation: &str) -> bool {
865 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
866}
867
868fn other_diff_change_schema(schema: &mut Schema) {
869 super::schema_util::reject_known_string_discriminators(
870 schema,
871 "operation",
872 &["add", "delete", "modify", "move", "copy"],
873 );
874}
875
876#[serde_as]
883#[skip_serializing_none]
884#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
885#[serde(rename_all = "camelCase")]
886#[non_exhaustive]
887pub struct ToolCallLocation {
888 pub path: PathBuf,
890 #[serde_as(deserialize_as = "DefaultOnError")]
892 #[schemars(extend("x-deserialize-default-on-error" = true))]
893 #[serde(default)]
894 pub line: Option<u32>,
895 #[serde_as(deserialize_as = "DefaultOnError")]
901 #[schemars(extend("x-deserialize-default-on-error" = true))]
902 #[serde(default)]
903 #[serde(rename = "_meta")]
904 pub meta: Option<Meta>,
905}
906
907impl ToolCallLocation {
908 #[must_use]
910 pub fn new(path: impl Into<PathBuf>) -> Self {
911 Self {
912 path: path.into(),
913 line: None,
914 meta: None,
915 }
916 }
917
918 #[must_use]
920 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
921 self.line = line.into_option();
922 self
923 }
924
925 #[must_use]
931 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
932 self.meta = meta.into_option();
933 self
934 }
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940 use crate::MaybeUndefined;
941
942 #[test]
943 fn tool_call_serializes_as_upsert() {
944 let tool_call = ToolCallUpdate::new("tc_1")
945 .title("Reading configuration")
946 .status(ToolCallStatus::InProgress)
947 .raw_input(serde_json::json!({"path": "settings.json"}));
948
949 assert_eq!(
950 serde_json::to_value(tool_call).unwrap(),
951 serde_json::json!({
952 "toolCallId": "tc_1",
953 "title": "Reading configuration",
954 "status": "in_progress",
955 "rawInput": {
956 "path": "settings.json"
957 }
958 })
959 );
960 }
961
962 #[test]
963 fn tool_call_update_distinguishes_omitted_null_and_value() {
964 let tool_call = ToolCallUpdate::new("tc_1")
965 .status(ToolCallStatus::Completed)
966 .content(None::<Vec<ToolCallContent>>);
967
968 assert_eq!(
969 serde_json::to_value(tool_call).unwrap(),
970 serde_json::json!({
971 "toolCallId": "tc_1",
972 "status": "completed",
973 "content": null
974 })
975 );
976
977 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
978 "toolCallId": "tc_1",
979 "status": null,
980 "locations": []
981 }))
982 .unwrap();
983 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
984 assert_eq!(deserialized.status, MaybeUndefined::Null);
985 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
986 }
987
988 #[test]
989 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
990 let mut meta = Meta::new();
991 meta.insert("source".to_string(), serde_json::json!("tool-call"));
992
993 assert_eq!(
994 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
995 serde_json::json!({
996 "toolCallId": "tc_1",
997 "_meta": {
998 "source": "tool-call"
999 }
1000 })
1001 );
1002
1003 assert_eq!(
1004 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1005 serde_json::json!({
1006 "toolCallId": "tc_1",
1007 "_meta": null
1008 })
1009 );
1010
1011 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1012 "toolCallId": "tc_1",
1013 "_meta": null
1014 }))
1015 .unwrap();
1016 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1017
1018 let patch = ToolCallUpdate::new("tc_1");
1019 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1020
1021 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1022 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1023 assert_eq!(stored.meta, MaybeUndefined::Null);
1024 }
1025
1026 #[test]
1027 fn tool_call_update_skips_malformed_list_items() {
1028 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1029 "toolCallId": "tc_1",
1030 "content": [
1031 {
1032 "type": "content",
1033 "content": {
1034 "type": "text",
1035 "text": "ok"
1036 }
1037 },
1038 {
1039 "type": "diff",
1040 "path": "/bad"
1041 }
1042 ],
1043 "locations": [
1044 {
1045 "path": "/ok",
1046 "line": 3
1047 },
1048 {
1049 "line": 4
1050 }
1051 ]
1052 }))
1053 .unwrap();
1054
1055 let MaybeUndefined::Value(content) = deserialized.content else {
1056 panic!("content should deserialize to a value");
1057 };
1058 assert_eq!(content.len(), 1);
1059
1060 let MaybeUndefined::Value(locations) = deserialized.locations else {
1061 panic!("locations should deserialize to a value");
1062 };
1063 assert_eq!(locations.len(), 1);
1064 }
1065
1066 #[test]
1067 fn tool_call_content_chunk_serializes_single_content_item() {
1068 let chunk = ToolCallContentChunk::new(
1069 "tc_1",
1070 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1071 );
1072
1073 assert_eq!(
1074 serde_json::to_value(chunk).unwrap(),
1075 serde_json::json!({
1076 "toolCallId": "tc_1",
1077 "content": {
1078 "type": "content",
1079 "content": {
1080 "type": "text",
1081 "text": "partial output"
1082 }
1083 }
1084 })
1085 );
1086 }
1087
1088 #[test]
1089 fn diff_patch_serializes_git_patch_with_structured_changes() {
1090 let diff = ToolCallContent::Diff(Diff::patch(
1091 "diff --git /repo/config.json /repo/config.json\n",
1092 vec![
1093 DiffChange::modify("/repo/config.json")
1094 .file_type(DiffFileType::Text)
1095 .mime_type("application/json"),
1096 ],
1097 ));
1098
1099 assert_eq!(
1100 serde_json::to_value(diff).unwrap(),
1101 serde_json::json!({
1102 "type": "diff",
1103 "changes": [
1104 {
1105 "operation": "modify",
1106 "path": "/repo/config.json",
1107 "fileType": "text",
1108 "mimeType": "application/json"
1109 }
1110 ],
1111 "patch": {
1112 "format": "git_patch",
1113 "diff": "diff --git /repo/config.json /repo/config.json\n"
1114 }
1115 })
1116 );
1117 }
1118
1119 #[test]
1120 fn diff_serializes_binary_modify_without_patch_text() {
1121 let diff = ToolCallContent::Diff(Diff::new(vec![
1122 DiffChange::modify("/repo/assets/logo.png")
1123 .file_type(DiffFileType::Binary)
1124 .mime_type("image/png"),
1125 ]));
1126
1127 assert_eq!(
1128 serde_json::to_value(diff).unwrap(),
1129 serde_json::json!({
1130 "type": "diff",
1131 "changes": [
1132 {
1133 "operation": "modify",
1134 "path": "/repo/assets/logo.png",
1135 "fileType": "binary",
1136 "mimeType": "image/png"
1137 }
1138 ]
1139 })
1140 );
1141 }
1142
1143 #[test]
1144 fn diff_move_serializes_shared_fields_with_operation_payload() {
1145 let diff = ToolCallContent::Diff(Diff::new(vec![
1146 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1147 .file_type(DiffFileType::Text)
1148 .mime_type("text/rust"),
1149 ]));
1150
1151 assert_eq!(
1152 serde_json::to_value(diff).unwrap(),
1153 serde_json::json!({
1154 "type": "diff",
1155 "changes": [
1156 {
1157 "operation": "move",
1158 "oldPath": "/repo/src/old.rs",
1159 "path": "/repo/src/new.rs",
1160 "fileType": "text",
1161 "mimeType": "text/rust"
1162 }
1163 ]
1164 })
1165 );
1166 }
1167
1168 #[test]
1169 fn diff_changes_skip_malformed_list_items() {
1170 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1171 "type": "diff",
1172 "changes": [
1173 {
1174 "operation": "modify"
1175 },
1176 {
1177 "operation": "delete",
1178 "path": "/ok"
1179 }
1180 ],
1181 "patch": {
1182 "format": "git_patch",
1183 "diff": "diff --git /ok /ok\n"
1184 }
1185 }))
1186 .unwrap();
1187
1188 let ToolCallContent::Diff(diff) = content else {
1189 panic!("expected diff content");
1190 };
1191 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1192 assert_eq!(diff.patch, Some(DiffPatch::new("diff --git /ok /ok\n")));
1193 }
1194
1195 #[test]
1196 fn tool_kind_preserves_unknown_variant() {
1197 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1198 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1199 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1200 }
1201
1202 #[test]
1203 fn tool_call_status_preserves_unknown_variant() {
1204 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1205 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1206 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1207 }
1208
1209 #[test]
1210 fn tool_call_content_preserves_unknown_variant() {
1211 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1212 "type": "_chart",
1213 "title": "Tests",
1214 "data": [1, 2, 3]
1215 }))
1216 .unwrap();
1217
1218 let ToolCallContent::Other(unknown) = content else {
1219 panic!("expected unknown tool call content");
1220 };
1221
1222 assert_eq!(unknown.type_, "_chart");
1223 assert_eq!(
1224 unknown.fields.get("title"),
1225 Some(&serde_json::json!("Tests"))
1226 );
1227 assert_eq!(
1228 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1229 serde_json::json!({
1230 "type": "_chart",
1231 "title": "Tests",
1232 "data": [1, 2, 3]
1233 })
1234 );
1235 }
1236
1237 #[test]
1238 fn tool_call_content_does_not_hide_malformed_known_variant() {
1239 assert!(
1240 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1241 "type": "diff"
1242 }))
1243 .is_err()
1244 );
1245 }
1246}