1use std::{collections::BTreeMap, 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::{AbsolutePath, ContentBlock, MediaType, Meta, Terminal};
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(forward)]
257#[non_exhaustive]
258pub struct ToolCallId(pub Arc<str>);
259
260impl ToolCallId {
261 #[must_use]
263 pub fn new(id: impl Into<Self>) -> Self {
264 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#[non_exhaustive]
351pub enum ToolCallContent {
352 Content(Box<Content>),
354 Diff(Diff),
356 Terminal(Terminal),
358 #[serde(untagged)]
368 Other(OtherToolCallContent),
369}
370
371#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
373#[schemars(inline)]
374#[schemars(transform = other_tool_call_content_schema)]
375#[serde(rename_all = "camelCase")]
376#[non_exhaustive]
377pub struct OtherToolCallContent {
378 #[serde(rename = "type")]
384 pub type_: String,
385 #[serde(flatten)]
387 pub fields: BTreeMap<String, serde_json::Value>,
388}
389
390impl OtherToolCallContent {
391 #[must_use]
393 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
394 fields.remove("type");
395 Self {
396 type_: type_.into(),
397 fields,
398 }
399 }
400}
401
402impl<'de> Deserialize<'de> for OtherToolCallContent {
403 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
404 where
405 D: serde::Deserializer<'de>,
406 {
407 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
408 let type_ = fields
409 .remove("type")
410 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
411 let serde_json::Value::String(type_) = type_ else {
412 return Err(serde::de::Error::custom("`type` must be a string"));
413 };
414
415 if is_known_tool_call_content_type(&type_) {
416 return Err(serde::de::Error::custom(format!(
417 "known tool call content `{type_}` did not match its schema"
418 )));
419 }
420
421 Ok(Self { type_, fields })
422 }
423}
424
425fn is_known_tool_call_content_type(type_: &str) -> bool {
426 matches!(type_, "content" | "diff" | "terminal")
427}
428
429fn other_tool_call_content_schema(schema: &mut Schema) {
430 super::schema_util::reject_known_string_discriminators(
431 schema,
432 "type",
433 &["content", "diff", "terminal"],
434 );
435}
436
437impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
438 fn from(content: T) -> Self {
439 ToolCallContent::Content(Box::new(Content::new(content)))
440 }
441}
442
443impl From<Diff> for ToolCallContent {
444 fn from(diff: Diff) -> Self {
445 ToolCallContent::Diff(diff)
446 }
447}
448
449impl From<Terminal> for ToolCallContent {
450 fn from(terminal: Terminal) -> Self {
451 ToolCallContent::Terminal(terminal)
452 }
453}
454
455#[serde_as]
457#[skip_serializing_none]
458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
459#[serde(rename_all = "camelCase")]
460#[non_exhaustive]
461pub struct Content {
462 pub content: ContentBlock,
464 #[serde_as(deserialize_as = "DefaultOnError")]
470 #[schemars(extend("x-deserialize-default-on-error" = true))]
471 #[serde(default)]
472 #[serde(rename = "_meta")]
473 pub meta: Option<Meta>,
474}
475
476impl Content {
477 #[must_use]
479 pub fn new(content: impl Into<ContentBlock>) -> Self {
480 Self {
481 content: content.into(),
482 meta: None,
483 }
484 }
485
486 #[must_use]
492 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
493 self.meta = meta.into_option();
494 self
495 }
496}
497
498#[serde_as]
507#[skip_serializing_none]
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
509#[serde(rename_all = "camelCase")]
510#[non_exhaustive]
511pub struct Diff {
512 #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
516 #[schemars(extend("x-deserialize-skip-invalid-items" = true))]
517 pub changes: Vec<DiffChange>,
518 #[serde_as(deserialize_as = "DefaultOnError")]
523 #[schemars(extend("x-deserialize-default-on-error" = true))]
524 #[serde(default)]
525 pub patch: Option<DiffPatch>,
526 #[serde_as(deserialize_as = "DefaultOnError")]
532 #[schemars(extend("x-deserialize-default-on-error" = true))]
533 #[serde(default)]
534 #[serde(rename = "_meta")]
535 pub meta: Option<Meta>,
536}
537
538impl Diff {
539 #[must_use]
541 pub fn new(changes: Vec<DiffChange>) -> Self {
542 Self {
543 changes,
544 patch: None,
545 meta: None,
546 }
547 }
548
549 #[must_use]
551 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
552 Self::new(changes).with_patch(DiffPatch::new(text))
553 }
554
555 #[must_use]
557 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
558 self.patch = patch.into_option();
559 self
560 }
561
562 #[must_use]
568 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
569 self.meta = meta.into_option();
570 self
571 }
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
576#[serde(rename_all = "camelCase")]
577#[non_exhaustive]
578pub struct DiffPatch {
579 pub format: DiffPatchFormat,
581 pub text: String,
583}
584
585impl DiffPatch {
586 #[must_use]
588 pub fn new(text: impl Into<String>) -> Self {
589 Self {
590 format: DiffPatchFormat::GitPatch,
591 text: text.into(),
592 }
593 }
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
598#[serde(rename_all = "snake_case")]
599#[non_exhaustive]
600pub enum DiffPatchFormat {
601 GitPatch,
606 #[serde(untagged)]
612 Other(String),
613}
614
615#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
617#[serde(rename_all = "snake_case")]
618#[non_exhaustive]
619pub enum DiffFileType {
620 Text,
622 Binary,
624 Directory,
626 Symlink,
628 #[serde(untagged)]
634 Other(String),
635}
636
637#[serde_as]
642#[skip_serializing_none]
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
644#[serde(rename_all = "camelCase")]
645#[non_exhaustive]
646pub struct DiffChange {
647 #[serde_as(deserialize_as = "DefaultOnError")]
651 #[schemars(extend("x-deserialize-default-on-error" = true))]
652 #[serde(default)]
653 pub file_type: Option<DiffFileType>,
654 #[serde_as(deserialize_as = "DefaultOnError")]
658 #[schemars(extend("x-deserialize-default-on-error" = true))]
659 #[serde(default)]
660 pub mime_type: Option<MediaType>,
661 #[serde(flatten)]
663 pub operation: DiffChangeOperation,
664 #[serde_as(deserialize_as = "DefaultOnError")]
670 #[schemars(extend("x-deserialize-default-on-error" = true))]
671 #[serde(default)]
672 #[serde(rename = "_meta")]
673 pub meta: Option<Meta>,
674}
675
676impl DiffChange {
677 #[must_use]
679 pub fn new(operation: DiffChangeOperation) -> Self {
680 Self {
681 file_type: None,
682 mime_type: None,
683 operation,
684 meta: None,
685 }
686 }
687
688 #[must_use]
690 pub fn add(path: impl Into<AbsolutePath>) -> Self {
691 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
692 }
693
694 #[must_use]
696 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
697 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
698 }
699
700 #[must_use]
702 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
703 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
704 }
705
706 #[must_use]
708 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
709 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
710 old_path, path,
711 )))
712 }
713
714 #[must_use]
716 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
717 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
718 old_path, path,
719 )))
720 }
721
722 #[must_use]
726 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
727 self.file_type = file_type.into_option();
728 self
729 }
730
731 #[must_use]
735 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
736 self.mime_type = mime_type.into_option();
737 self
738 }
739
740 #[must_use]
746 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
747 self.meta = meta.into_option();
748 self
749 }
750}
751
752#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
754#[serde(tag = "operation", rename_all = "snake_case")]
755#[non_exhaustive]
756pub enum DiffChangeOperation {
757 Add(DiffPathChange),
759 Delete(DiffPathChange),
761 Modify(DiffPathChange),
763 Move(DiffPathPairChange),
765 Copy(DiffPathPairChange),
767 #[serde(untagged)]
773 Other(OtherDiffChange),
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
778#[serde(rename_all = "camelCase")]
779#[non_exhaustive]
780pub struct DiffPathChange {
781 pub path: AbsolutePath,
783}
784
785impl DiffPathChange {
786 #[must_use]
788 pub fn new(path: impl Into<AbsolutePath>) -> Self {
789 Self { path: path.into() }
790 }
791}
792
793#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
795#[serde(rename_all = "camelCase")]
796#[non_exhaustive]
797pub struct DiffPathPairChange {
798 pub old_path: AbsolutePath,
800 pub path: AbsolutePath,
802}
803
804impl DiffPathPairChange {
805 #[must_use]
807 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
808 Self {
809 old_path: old_path.into(),
810 path: path.into(),
811 }
812 }
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
817#[schemars(inline)]
818#[schemars(transform = other_diff_change_schema)]
819#[serde(rename_all = "camelCase")]
820#[non_exhaustive]
821pub struct OtherDiffChange {
822 pub operation: String,
828 #[serde(flatten)]
830 pub fields: BTreeMap<String, serde_json::Value>,
831}
832
833impl OtherDiffChange {
834 #[must_use]
836 pub fn new(
837 operation: impl Into<String>,
838 mut fields: BTreeMap<String, serde_json::Value>,
839 ) -> Self {
840 fields.remove("operation");
841 fields.remove("fileType");
842 fields.remove("mimeType");
843 fields.remove("_meta");
844 Self {
845 operation: operation.into(),
846 fields,
847 }
848 }
849}
850
851impl<'de> Deserialize<'de> for OtherDiffChange {
852 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
853 where
854 D: serde::Deserializer<'de>,
855 {
856 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
857 let operation = fields
858 .remove("operation")
859 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
860 let serde_json::Value::String(operation) = operation else {
861 return Err(serde::de::Error::custom("`operation` must be a string"));
862 };
863
864 if is_known_diff_change_operation(&operation) {
865 return Err(serde::de::Error::custom(format!(
866 "known diff change operation `{operation}` did not match its schema"
867 )));
868 }
869 fields.remove("fileType");
870 fields.remove("mimeType");
871 fields.remove("_meta");
872
873 Ok(Self { operation, fields })
874 }
875}
876
877fn is_known_diff_change_operation(operation: &str) -> bool {
878 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
879}
880
881fn other_diff_change_schema(schema: &mut Schema) {
882 super::schema_util::reject_known_string_discriminators(
883 schema,
884 "operation",
885 &["add", "delete", "modify", "move", "copy"],
886 );
887}
888
889#[serde_as]
896#[skip_serializing_none]
897#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
898#[serde(rename_all = "camelCase")]
899#[non_exhaustive]
900pub struct ToolCallLocation {
901 pub path: AbsolutePath,
903 #[serde_as(deserialize_as = "DefaultOnError")]
905 #[schemars(extend("x-deserialize-default-on-error" = true))]
906 #[serde(default)]
907 pub line: Option<u32>,
908 #[serde_as(deserialize_as = "DefaultOnError")]
914 #[schemars(extend("x-deserialize-default-on-error" = true))]
915 #[serde(default)]
916 #[serde(rename = "_meta")]
917 pub meta: Option<Meta>,
918}
919
920impl ToolCallLocation {
921 #[must_use]
923 pub fn new(path: impl Into<AbsolutePath>) -> Self {
924 Self {
925 path: path.into(),
926 line: None,
927 meta: None,
928 }
929 }
930
931 #[must_use]
933 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
934 self.line = line.into_option();
935 self
936 }
937
938 #[must_use]
944 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
945 self.meta = meta.into_option();
946 self
947 }
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953 use crate::MaybeUndefined;
954
955 #[test]
956 fn tool_call_serializes_as_upsert() {
957 let tool_call = ToolCallUpdate::new("tc_1")
958 .title("Reading configuration")
959 .status(ToolCallStatus::InProgress)
960 .raw_input(serde_json::json!({"path": "settings.json"}));
961
962 assert_eq!(
963 serde_json::to_value(tool_call).unwrap(),
964 serde_json::json!({
965 "toolCallId": "tc_1",
966 "title": "Reading configuration",
967 "status": "in_progress",
968 "rawInput": {
969 "path": "settings.json"
970 }
971 })
972 );
973 }
974
975 #[test]
976 fn tool_call_update_distinguishes_omitted_null_and_value() {
977 let tool_call = ToolCallUpdate::new("tc_1")
978 .status(ToolCallStatus::Completed)
979 .content(None::<Vec<ToolCallContent>>);
980
981 assert_eq!(
982 serde_json::to_value(tool_call).unwrap(),
983 serde_json::json!({
984 "toolCallId": "tc_1",
985 "status": "completed",
986 "content": null
987 })
988 );
989
990 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
991 "toolCallId": "tc_1",
992 "status": null,
993 "locations": []
994 }))
995 .unwrap();
996 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
997 assert_eq!(deserialized.status, MaybeUndefined::Null);
998 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
999 }
1000
1001 #[test]
1002 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1003 let mut meta = Meta::new();
1004 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1005
1006 assert_eq!(
1007 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1008 serde_json::json!({
1009 "toolCallId": "tc_1",
1010 "_meta": {
1011 "source": "tool-call"
1012 }
1013 })
1014 );
1015
1016 assert_eq!(
1017 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1018 serde_json::json!({
1019 "toolCallId": "tc_1",
1020 "_meta": null
1021 })
1022 );
1023
1024 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1025 "toolCallId": "tc_1",
1026 "_meta": null
1027 }))
1028 .unwrap();
1029 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1030
1031 let patch = ToolCallUpdate::new("tc_1");
1032 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1033
1034 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1035 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1036 assert_eq!(stored.meta, MaybeUndefined::Null);
1037 }
1038
1039 #[test]
1040 fn tool_call_update_skips_malformed_list_items() {
1041 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1042 "toolCallId": "tc_1",
1043 "content": [
1044 {
1045 "type": "content",
1046 "content": {
1047 "type": "text",
1048 "text": "ok"
1049 }
1050 },
1051 {
1052 "type": "diff",
1053 "path": "/bad"
1054 }
1055 ],
1056 "locations": [
1057 {
1058 "path": "/ok",
1059 "line": 3
1060 },
1061 {
1062 "line": 4
1063 }
1064 ]
1065 }))
1066 .unwrap();
1067
1068 let MaybeUndefined::Value(content) = deserialized.content else {
1069 panic!("content should deserialize to a value");
1070 };
1071 assert_eq!(content.len(), 1);
1072
1073 let MaybeUndefined::Value(locations) = deserialized.locations else {
1074 panic!("locations should deserialize to a value");
1075 };
1076 assert_eq!(locations.len(), 1);
1077 }
1078
1079 #[test]
1080 fn tool_call_content_chunk_serializes_single_content_item() {
1081 let chunk = ToolCallContentChunk::new(
1082 "tc_1",
1083 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1084 );
1085
1086 assert_eq!(
1087 serde_json::to_value(chunk).unwrap(),
1088 serde_json::json!({
1089 "toolCallId": "tc_1",
1090 "content": {
1091 "type": "content",
1092 "content": {
1093 "type": "text",
1094 "text": "partial output"
1095 }
1096 }
1097 })
1098 );
1099 }
1100
1101 #[test]
1102 fn terminal_content_serializes_as_display_reference() {
1103 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1104
1105 assert_eq!(
1106 serde_json::to_value(terminal).unwrap(),
1107 serde_json::json!({
1108 "type": "terminal",
1109 "terminalId": "term_1"
1110 })
1111 );
1112 }
1113
1114 #[test]
1115 fn diff_patch_serializes_git_patch_with_structured_changes() {
1116 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";
1117 let diff = ToolCallContent::Diff(Diff::patch(
1118 patch_text,
1119 vec![
1120 DiffChange::modify("/repo/config.json")
1121 .file_type(DiffFileType::Text)
1122 .mime_type("application/json"),
1123 ],
1124 ));
1125
1126 assert_eq!(
1127 serde_json::to_value(diff).unwrap(),
1128 serde_json::json!({
1129 "type": "diff",
1130 "changes": [
1131 {
1132 "operation": "modify",
1133 "path": "/repo/config.json",
1134 "fileType": "text",
1135 "mimeType": "application/json"
1136 }
1137 ],
1138 "patch": {
1139 "format": "git_patch",
1140 "text": patch_text
1141 }
1142 })
1143 );
1144 }
1145
1146 #[test]
1147 fn diff_patch_requires_text() {
1148 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1149 "format": "git_patch",
1150 "diff": "diff --git /repo/config.json /repo/config.json\n"
1151 }));
1152
1153 assert!(result.is_err());
1154 }
1155
1156 #[test]
1157 fn diff_serializes_binary_modify_without_patch_text() {
1158 let diff = ToolCallContent::Diff(Diff::new(vec![
1159 DiffChange::modify("/repo/assets/logo.png")
1160 .file_type(DiffFileType::Binary)
1161 .mime_type("image/png"),
1162 ]));
1163
1164 assert_eq!(
1165 serde_json::to_value(diff).unwrap(),
1166 serde_json::json!({
1167 "type": "diff",
1168 "changes": [
1169 {
1170 "operation": "modify",
1171 "path": "/repo/assets/logo.png",
1172 "fileType": "binary",
1173 "mimeType": "image/png"
1174 }
1175 ]
1176 })
1177 );
1178 }
1179
1180 #[test]
1181 fn diff_move_serializes_shared_fields_with_operation_payload() {
1182 let diff = ToolCallContent::Diff(Diff::new(vec![
1183 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1184 .file_type(DiffFileType::Text)
1185 .mime_type("text/rust"),
1186 ]));
1187
1188 assert_eq!(
1189 serde_json::to_value(diff).unwrap(),
1190 serde_json::json!({
1191 "type": "diff",
1192 "changes": [
1193 {
1194 "operation": "move",
1195 "oldPath": "/repo/src/old.rs",
1196 "path": "/repo/src/new.rs",
1197 "fileType": "text",
1198 "mimeType": "text/rust"
1199 }
1200 ]
1201 })
1202 );
1203 }
1204
1205 #[test]
1206 fn diff_changes_skip_malformed_list_items() {
1207 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1208 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1209 "type": "diff",
1210 "changes": [
1211 {
1212 "operation": "modify"
1213 },
1214 {
1215 "operation": "delete",
1216 "path": "/ok"
1217 }
1218 ],
1219 "patch": {
1220 "format": "git_patch",
1221 "text": patch_text
1222 }
1223 }))
1224 .unwrap();
1225
1226 let ToolCallContent::Diff(diff) = content else {
1227 panic!("expected diff content");
1228 };
1229 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1230 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1231 }
1232
1233 #[test]
1234 fn tool_kind_preserves_unknown_variant() {
1235 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1236 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1237 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1238 }
1239
1240 #[test]
1241 fn tool_call_status_preserves_unknown_variant() {
1242 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1243 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1244 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1245 }
1246
1247 #[test]
1248 fn tool_call_content_preserves_unknown_variant() {
1249 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1250 "type": "_chart",
1251 "title": "Tests",
1252 "data": [1, 2, 3]
1253 }))
1254 .unwrap();
1255
1256 let ToolCallContent::Other(unknown) = content else {
1257 panic!("expected unknown tool call content");
1258 };
1259
1260 assert_eq!(unknown.type_, "_chart");
1261 assert_eq!(
1262 unknown.fields.get("title"),
1263 Some(&serde_json::json!("Tests"))
1264 );
1265 assert_eq!(
1266 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1267 serde_json::json!({
1268 "type": "_chart",
1269 "title": "Tests",
1270 "data": [1, 2, 3]
1271 })
1272 );
1273 }
1274
1275 #[test]
1276 fn tool_call_content_does_not_hide_malformed_known_variant() {
1277 assert!(
1278 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1279 "type": "diff"
1280 }))
1281 .is_err()
1282 );
1283 assert!(
1284 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1285 "type": "terminal"
1286 }))
1287 .is_err()
1288 );
1289 }
1290}