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 #[cfg(feature = "unstable_tool_call_name")]
49 #[serde_as(deserialize_as = "DefaultOnError")]
50 #[schemars(extend("x-deserialize-default-on-error" = true))]
51 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
52 pub name: MaybeUndefined<String>,
53 #[serde_as(deserialize_as = "DefaultOnError")]
55 #[schemars(extend("x-deserialize-default-on-error" = true))]
56 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
57 pub title: MaybeUndefined<String>,
58 #[serde_as(deserialize_as = "DefaultOnError")]
61 #[schemars(extend("x-deserialize-default-on-error" = true))]
62 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
63 pub kind: MaybeUndefined<ToolKind>,
64 #[serde_as(deserialize_as = "DefaultOnError")]
66 #[schemars(extend("x-deserialize-default-on-error" = true))]
67 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
68 pub status: MaybeUndefined<ToolCallStatus>,
69 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
71 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
72 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
73 pub content: MaybeUndefined<Vec<ToolCallContent>>,
74 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
77 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
78 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
79 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
80 #[serde_as(deserialize_as = "DefaultOnError")]
82 #[schemars(extend("x-deserialize-default-on-error" = true))]
83 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
84 pub raw_input: MaybeUndefined<serde_json::Value>,
85 #[serde_as(deserialize_as = "DefaultOnError")]
87 #[schemars(extend("x-deserialize-default-on-error" = true))]
88 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
89 pub raw_output: MaybeUndefined<serde_json::Value>,
90 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
96 #[schemars(extend("x-deserialize-default-on-error" = true))]
97 #[serde(
98 rename = "_meta",
99 default,
100 skip_serializing_if = "MaybeUndefined::is_undefined"
101 )]
102 pub meta: MaybeUndefined<Meta>,
103}
104
105impl ToolCallUpdate {
106 #[must_use]
108 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
109 Self {
110 tool_call_id: tool_call_id.into(),
111 #[cfg(feature = "unstable_tool_call_name")]
112 name: MaybeUndefined::Undefined,
113 title: MaybeUndefined::Undefined,
114 kind: MaybeUndefined::Undefined,
115 status: MaybeUndefined::Undefined,
116 content: MaybeUndefined::Undefined,
117 locations: MaybeUndefined::Undefined,
118 raw_input: MaybeUndefined::Undefined,
119 raw_output: MaybeUndefined::Undefined,
120 meta: MaybeUndefined::Undefined,
121 }
122 }
123
124 #[cfg(feature = "unstable_tool_call_name")]
130 #[must_use]
131 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
132 self.name = name.into_maybe_undefined();
133 self
134 }
135
136 #[must_use]
138 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
139 self.title = title.into_maybe_undefined();
140 self
141 }
142
143 #[must_use]
146 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
147 self.kind = kind.into_maybe_undefined();
148 self
149 }
150
151 #[must_use]
153 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
154 self.status = status.into_maybe_undefined();
155 self
156 }
157
158 #[must_use]
160 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
161 self.content = content.into_maybe_undefined();
162 self
163 }
164
165 #[must_use]
168 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
169 self.locations = locations.into_maybe_undefined();
170 self
171 }
172
173 #[must_use]
175 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
176 self.raw_input = raw_input.into_maybe_undefined();
177 self
178 }
179
180 #[must_use]
182 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
183 self.raw_output = raw_output.into_maybe_undefined();
184 self
185 }
186
187 #[must_use]
193 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
194 self.meta = meta.into_maybe_undefined();
195 self
196 }
197
198 pub fn apply_update(&mut self, update: ToolCallUpdate) {
203 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
204 #[cfg(feature = "unstable_tool_call_name")]
205 if !update.name.is_undefined() {
206 self.name = update.name;
207 }
208 if !update.title.is_undefined() {
209 self.title = update.title;
210 }
211 if !update.kind.is_undefined() {
212 self.kind = update.kind;
213 }
214 if !update.status.is_undefined() {
215 self.status = update.status;
216 }
217 if !update.content.is_undefined() {
218 self.content = update.content;
219 }
220 if !update.locations.is_undefined() {
221 self.locations = update.locations;
222 }
223 if !update.raw_input.is_undefined() {
224 self.raw_input = update.raw_input;
225 }
226 if !update.raw_output.is_undefined() {
227 self.raw_output = update.raw_output;
228 }
229 if !update.meta.is_undefined() {
230 self.meta = update.meta;
231 }
232 }
233}
234
235#[serde_as]
242#[skip_serializing_none]
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
244#[serde(rename_all = "camelCase")]
245#[non_exhaustive]
246pub struct ToolCallContentChunk {
247 pub tool_call_id: ToolCallId,
249 pub content: ToolCallContent,
251 #[serde_as(deserialize_as = "DefaultOnError")]
257 #[schemars(extend("x-deserialize-default-on-error" = true))]
258 #[serde(default)]
259 #[serde(rename = "_meta")]
260 pub meta: Option<Meta>,
261}
262
263impl ToolCallContentChunk {
264 #[must_use]
266 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
267 Self {
268 tool_call_id: tool_call_id.into(),
269 content: content.into(),
270 meta: None,
271 }
272 }
273
274 #[must_use]
280 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
281 self.meta = meta.into_option();
282 self
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
288#[serde(transparent)]
289#[from(forward)]
290#[non_exhaustive]
291pub struct ToolCallId(pub Arc<str>);
292
293impl ToolCallId {
294 #[must_use]
296 pub fn new(id: impl Into<Self>) -> Self {
297 id.into()
298 }
299}
300
301impl IntoOption<ToolCallId> for &str {
302 fn into_option(self) -> Option<ToolCallId> {
303 Some(ToolCallId::new(self))
304 }
305}
306
307#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
314#[serde(rename_all = "snake_case")]
315#[non_exhaustive]
316pub enum ToolKind {
317 Read,
319 Edit,
321 Delete,
323 Move,
325 Search,
327 Execute,
329 Think,
331 Fetch,
333 SwitchMode,
335 #[default]
337 Other,
338 #[serde(untagged)]
344 Unknown(String),
345}
346
347#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
353#[serde(rename_all = "snake_case")]
354#[non_exhaustive]
355pub enum ToolCallStatus {
356 #[default]
359 Pending,
360 InProgress,
362 Completed,
364 Failed,
366 Cancelled,
368 #[serde(untagged)]
374 Other(String),
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
384#[serde(tag = "type", rename_all = "snake_case")]
385#[non_exhaustive]
386pub enum ToolCallContent {
387 Content(Box<Content>),
389 Diff(Diff),
391 Terminal(Terminal),
393 #[serde(untagged)]
403 Other(OtherToolCallContent),
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
408#[schemars(inline)]
409#[schemars(transform = other_tool_call_content_schema)]
410#[serde(rename_all = "camelCase")]
411#[non_exhaustive]
412pub struct OtherToolCallContent {
413 #[serde(rename = "type")]
419 pub type_: String,
420 #[serde(flatten)]
422 pub fields: BTreeMap<String, serde_json::Value>,
423}
424
425impl OtherToolCallContent {
426 #[must_use]
428 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
429 fields.remove("type");
430 Self {
431 type_: type_.into(),
432 fields,
433 }
434 }
435}
436
437impl<'de> Deserialize<'de> for OtherToolCallContent {
438 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
439 where
440 D: serde::Deserializer<'de>,
441 {
442 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
443 let type_ = fields
444 .remove("type")
445 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
446 let serde_json::Value::String(type_) = type_ else {
447 return Err(serde::de::Error::custom("`type` must be a string"));
448 };
449
450 if is_known_tool_call_content_type(&type_) {
451 return Err(serde::de::Error::custom(format!(
452 "known tool call content `{type_}` did not match its schema"
453 )));
454 }
455
456 Ok(Self { type_, fields })
457 }
458}
459
460fn is_known_tool_call_content_type(type_: &str) -> bool {
461 matches!(type_, "content" | "diff" | "terminal")
462}
463
464fn other_tool_call_content_schema(schema: &mut Schema) {
465 super::schema_util::reject_known_string_discriminators(
466 schema,
467 "type",
468 &["content", "diff", "terminal"],
469 );
470}
471
472impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
473 fn from(content: T) -> Self {
474 ToolCallContent::Content(Box::new(Content::new(content)))
475 }
476}
477
478impl From<Diff> for ToolCallContent {
479 fn from(diff: Diff) -> Self {
480 ToolCallContent::Diff(diff)
481 }
482}
483
484impl From<Terminal> for ToolCallContent {
485 fn from(terminal: Terminal) -> Self {
486 ToolCallContent::Terminal(terminal)
487 }
488}
489
490#[serde_as]
492#[skip_serializing_none]
493#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
494#[serde(rename_all = "camelCase")]
495#[non_exhaustive]
496pub struct Content {
497 pub content: ContentBlock,
499 #[serde_as(deserialize_as = "DefaultOnError")]
505 #[schemars(extend("x-deserialize-default-on-error" = true))]
506 #[serde(default)]
507 #[serde(rename = "_meta")]
508 pub meta: Option<Meta>,
509}
510
511impl Content {
512 #[must_use]
514 pub fn new(content: impl Into<ContentBlock>) -> Self {
515 Self {
516 content: content.into(),
517 meta: None,
518 }
519 }
520
521 #[must_use]
527 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
528 self.meta = meta.into_option();
529 self
530 }
531}
532
533#[serde_as]
542#[skip_serializing_none]
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
544#[serde(rename_all = "camelCase")]
545#[non_exhaustive]
546pub struct Diff {
547 #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
551 #[schemars(extend("x-deserialize-skip-invalid-items" = true))]
552 pub changes: Vec<DiffChange>,
553 #[serde_as(deserialize_as = "DefaultOnError")]
558 #[schemars(extend("x-deserialize-default-on-error" = true))]
559 #[serde(default)]
560 pub patch: Option<DiffPatch>,
561 #[serde_as(deserialize_as = "DefaultOnError")]
567 #[schemars(extend("x-deserialize-default-on-error" = true))]
568 #[serde(default)]
569 #[serde(rename = "_meta")]
570 pub meta: Option<Meta>,
571}
572
573impl Diff {
574 #[must_use]
576 pub fn new(changes: Vec<DiffChange>) -> Self {
577 Self {
578 changes,
579 patch: None,
580 meta: None,
581 }
582 }
583
584 #[must_use]
586 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
587 Self::new(changes).with_patch(DiffPatch::new(text))
588 }
589
590 #[must_use]
592 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
593 self.patch = patch.into_option();
594 self
595 }
596
597 #[must_use]
603 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
604 self.meta = meta.into_option();
605 self
606 }
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
633#[serde(rename_all = "snake_case")]
634#[non_exhaustive]
635pub enum DiffPatchFormat {
636 GitPatch,
641 #[serde(untagged)]
647 Other(String),
648}
649
650#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
652#[serde(rename_all = "snake_case")]
653#[non_exhaustive]
654pub enum DiffFileType {
655 Text,
657 Binary,
659 Directory,
661 Symlink,
663 #[serde(untagged)]
669 Other(String),
670}
671
672#[serde_as]
677#[skip_serializing_none]
678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
679#[serde(rename_all = "camelCase")]
680#[non_exhaustive]
681pub struct DiffChange {
682 #[serde_as(deserialize_as = "DefaultOnError")]
686 #[schemars(extend("x-deserialize-default-on-error" = true))]
687 #[serde(default)]
688 pub file_type: Option<DiffFileType>,
689 #[serde_as(deserialize_as = "DefaultOnError")]
693 #[schemars(extend("x-deserialize-default-on-error" = true))]
694 #[serde(default)]
695 pub mime_type: Option<MediaType>,
696 #[serde(flatten)]
698 pub operation: DiffChangeOperation,
699 #[serde_as(deserialize_as = "DefaultOnError")]
705 #[schemars(extend("x-deserialize-default-on-error" = true))]
706 #[serde(default)]
707 #[serde(rename = "_meta")]
708 pub meta: Option<Meta>,
709}
710
711impl DiffChange {
712 #[must_use]
714 pub fn new(operation: DiffChangeOperation) -> Self {
715 Self {
716 file_type: None,
717 mime_type: None,
718 operation,
719 meta: None,
720 }
721 }
722
723 #[must_use]
725 pub fn add(path: impl Into<AbsolutePath>) -> Self {
726 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
727 }
728
729 #[must_use]
731 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
732 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
733 }
734
735 #[must_use]
737 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
738 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
739 }
740
741 #[must_use]
743 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
744 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
745 old_path, path,
746 )))
747 }
748
749 #[must_use]
751 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
752 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
753 old_path, path,
754 )))
755 }
756
757 #[must_use]
761 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
762 self.file_type = file_type.into_option();
763 self
764 }
765
766 #[must_use]
770 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
771 self.mime_type = mime_type.into_option();
772 self
773 }
774
775 #[must_use]
781 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
782 self.meta = meta.into_option();
783 self
784 }
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
789#[serde(tag = "operation", rename_all = "snake_case")]
790#[non_exhaustive]
791pub enum DiffChangeOperation {
792 Add(DiffPathChange),
794 Delete(DiffPathChange),
796 Modify(DiffPathChange),
798 Move(DiffPathPairChange),
800 Copy(DiffPathPairChange),
802 #[serde(untagged)]
808 Other(OtherDiffChange),
809}
810
811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
813#[serde(rename_all = "camelCase")]
814#[non_exhaustive]
815pub struct DiffPathChange {
816 pub path: AbsolutePath,
818}
819
820impl DiffPathChange {
821 #[must_use]
823 pub fn new(path: impl Into<AbsolutePath>) -> Self {
824 Self { path: path.into() }
825 }
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
830#[serde(rename_all = "camelCase")]
831#[non_exhaustive]
832pub struct DiffPathPairChange {
833 pub old_path: AbsolutePath,
835 pub path: AbsolutePath,
837}
838
839impl DiffPathPairChange {
840 #[must_use]
842 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
843 Self {
844 old_path: old_path.into(),
845 path: path.into(),
846 }
847 }
848}
849
850#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
852#[schemars(inline)]
853#[schemars(transform = other_diff_change_schema)]
854#[serde(rename_all = "camelCase")]
855#[non_exhaustive]
856pub struct OtherDiffChange {
857 pub operation: String,
863 #[serde(flatten)]
865 pub fields: BTreeMap<String, serde_json::Value>,
866}
867
868impl OtherDiffChange {
869 #[must_use]
871 pub fn new(
872 operation: impl Into<String>,
873 mut fields: BTreeMap<String, serde_json::Value>,
874 ) -> Self {
875 fields.remove("operation");
876 fields.remove("fileType");
877 fields.remove("mimeType");
878 fields.remove("_meta");
879 Self {
880 operation: operation.into(),
881 fields,
882 }
883 }
884}
885
886impl<'de> Deserialize<'de> for OtherDiffChange {
887 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
888 where
889 D: serde::Deserializer<'de>,
890 {
891 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
892 let operation = fields
893 .remove("operation")
894 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
895 let serde_json::Value::String(operation) = operation else {
896 return Err(serde::de::Error::custom("`operation` must be a string"));
897 };
898
899 if is_known_diff_change_operation(&operation) {
900 return Err(serde::de::Error::custom(format!(
901 "known diff change operation `{operation}` did not match its schema"
902 )));
903 }
904 fields.remove("fileType");
905 fields.remove("mimeType");
906 fields.remove("_meta");
907
908 Ok(Self { operation, fields })
909 }
910}
911
912fn is_known_diff_change_operation(operation: &str) -> bool {
913 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
914}
915
916fn other_diff_change_schema(schema: &mut Schema) {
917 super::schema_util::reject_known_string_discriminators(
918 schema,
919 "operation",
920 &["add", "delete", "modify", "move", "copy"],
921 );
922}
923
924#[serde_as]
931#[skip_serializing_none]
932#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
933#[serde(rename_all = "camelCase")]
934#[non_exhaustive]
935pub struct ToolCallLocation {
936 pub path: AbsolutePath,
938 #[serde_as(deserialize_as = "DefaultOnError")]
940 #[schemars(extend("x-deserialize-default-on-error" = true))]
941 #[serde(default)]
942 pub line: Option<u32>,
943 #[serde_as(deserialize_as = "DefaultOnError")]
949 #[schemars(extend("x-deserialize-default-on-error" = true))]
950 #[serde(default)]
951 #[serde(rename = "_meta")]
952 pub meta: Option<Meta>,
953}
954
955impl ToolCallLocation {
956 #[must_use]
958 pub fn new(path: impl Into<AbsolutePath>) -> Self {
959 Self {
960 path: path.into(),
961 line: None,
962 meta: None,
963 }
964 }
965
966 #[must_use]
968 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
969 self.line = line.into_option();
970 self
971 }
972
973 #[must_use]
979 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
980 self.meta = meta.into_option();
981 self
982 }
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988 use crate::MaybeUndefined;
989
990 #[test]
991 fn tool_call_serializes_as_upsert() {
992 let tool_call = ToolCallUpdate::new("tc_1")
993 .title("Reading configuration")
994 .status(ToolCallStatus::InProgress)
995 .raw_input(serde_json::json!({"path": "settings.json"}));
996
997 assert_eq!(
998 serde_json::to_value(tool_call).unwrap(),
999 serde_json::json!({
1000 "toolCallId": "tc_1",
1001 "title": "Reading configuration",
1002 "status": "in_progress",
1003 "rawInput": {
1004 "path": "settings.json"
1005 }
1006 })
1007 );
1008 }
1009
1010 #[test]
1011 fn tool_call_update_distinguishes_omitted_null_and_value() {
1012 let tool_call = ToolCallUpdate::new("tc_1")
1013 .status(ToolCallStatus::Completed)
1014 .content(None::<Vec<ToolCallContent>>);
1015
1016 assert_eq!(
1017 serde_json::to_value(tool_call).unwrap(),
1018 serde_json::json!({
1019 "toolCallId": "tc_1",
1020 "status": "completed",
1021 "content": null
1022 })
1023 );
1024
1025 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1026 "toolCallId": "tc_1",
1027 "status": null,
1028 "locations": []
1029 }))
1030 .unwrap();
1031 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1032 assert_eq!(deserialized.status, MaybeUndefined::Null);
1033 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1034 }
1035
1036 #[cfg(feature = "unstable_tool_call_name")]
1037 #[test]
1038 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1039 let named = ToolCallUpdate::new("tc_1").name("read_file");
1040 assert_eq!(
1041 serde_json::to_value(named).unwrap(),
1042 serde_json::json!({
1043 "toolCallId": "tc_1",
1044 "name": "read_file"
1045 })
1046 );
1047
1048 let omitted = ToolCallUpdate::new("tc_1");
1049 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1050
1051 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1052 "toolCallId": "tc_1",
1053 "name": null
1054 }))
1055 .unwrap();
1056 assert_eq!(from_null.name, MaybeUndefined::Null);
1057
1058 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1059 stored.apply_update(ToolCallUpdate::new("tc_1"));
1060 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1061
1062 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1063 assert_eq!(stored.name, MaybeUndefined::Null);
1064
1065 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1066 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1067 }
1068
1069 #[test]
1070 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1071 let mut meta = Meta::new();
1072 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1073
1074 assert_eq!(
1075 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1076 serde_json::json!({
1077 "toolCallId": "tc_1",
1078 "_meta": {
1079 "source": "tool-call"
1080 }
1081 })
1082 );
1083
1084 assert_eq!(
1085 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1086 serde_json::json!({
1087 "toolCallId": "tc_1",
1088 "_meta": null
1089 })
1090 );
1091
1092 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1093 "toolCallId": "tc_1",
1094 "_meta": null
1095 }))
1096 .unwrap();
1097 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1098
1099 let patch = ToolCallUpdate::new("tc_1");
1100 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1101
1102 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1103 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1104 assert_eq!(stored.meta, MaybeUndefined::Null);
1105 }
1106
1107 #[test]
1108 fn tool_call_update_skips_malformed_list_items() {
1109 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1110 "toolCallId": "tc_1",
1111 "content": [
1112 {
1113 "type": "content",
1114 "content": {
1115 "type": "text",
1116 "text": "ok"
1117 }
1118 },
1119 {
1120 "type": "diff",
1121 "path": "/bad"
1122 }
1123 ],
1124 "locations": [
1125 {
1126 "path": "/ok",
1127 "line": 3
1128 },
1129 {
1130 "line": 4
1131 }
1132 ]
1133 }))
1134 .unwrap();
1135
1136 let MaybeUndefined::Value(content) = deserialized.content else {
1137 panic!("content should deserialize to a value");
1138 };
1139 assert_eq!(content.len(), 1);
1140
1141 let MaybeUndefined::Value(locations) = deserialized.locations else {
1142 panic!("locations should deserialize to a value");
1143 };
1144 assert_eq!(locations.len(), 1);
1145 }
1146
1147 #[test]
1148 fn tool_call_content_chunk_serializes_single_content_item() {
1149 let chunk = ToolCallContentChunk::new(
1150 "tc_1",
1151 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1152 );
1153
1154 assert_eq!(
1155 serde_json::to_value(chunk).unwrap(),
1156 serde_json::json!({
1157 "toolCallId": "tc_1",
1158 "content": {
1159 "type": "content",
1160 "content": {
1161 "type": "text",
1162 "text": "partial output"
1163 }
1164 }
1165 })
1166 );
1167 }
1168
1169 #[test]
1170 fn terminal_content_serializes_as_display_reference() {
1171 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1172
1173 assert_eq!(
1174 serde_json::to_value(terminal).unwrap(),
1175 serde_json::json!({
1176 "type": "terminal",
1177 "terminalId": "term_1"
1178 })
1179 );
1180 }
1181
1182 #[test]
1183 fn diff_patch_serializes_git_patch_with_structured_changes() {
1184 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";
1185 let diff = ToolCallContent::Diff(Diff::patch(
1186 patch_text,
1187 vec![
1188 DiffChange::modify("/repo/config.json")
1189 .file_type(DiffFileType::Text)
1190 .mime_type("application/json"),
1191 ],
1192 ));
1193
1194 assert_eq!(
1195 serde_json::to_value(diff).unwrap(),
1196 serde_json::json!({
1197 "type": "diff",
1198 "changes": [
1199 {
1200 "operation": "modify",
1201 "path": "/repo/config.json",
1202 "fileType": "text",
1203 "mimeType": "application/json"
1204 }
1205 ],
1206 "patch": {
1207 "format": "git_patch",
1208 "text": patch_text
1209 }
1210 })
1211 );
1212 }
1213
1214 #[test]
1215 fn diff_patch_requires_text() {
1216 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1217 "format": "git_patch",
1218 "diff": "diff --git /repo/config.json /repo/config.json\n"
1219 }));
1220
1221 assert!(result.is_err());
1222 }
1223
1224 #[test]
1225 fn diff_serializes_binary_modify_without_patch_text() {
1226 let diff = ToolCallContent::Diff(Diff::new(vec![
1227 DiffChange::modify("/repo/assets/logo.png")
1228 .file_type(DiffFileType::Binary)
1229 .mime_type("image/png"),
1230 ]));
1231
1232 assert_eq!(
1233 serde_json::to_value(diff).unwrap(),
1234 serde_json::json!({
1235 "type": "diff",
1236 "changes": [
1237 {
1238 "operation": "modify",
1239 "path": "/repo/assets/logo.png",
1240 "fileType": "binary",
1241 "mimeType": "image/png"
1242 }
1243 ]
1244 })
1245 );
1246 }
1247
1248 #[test]
1249 fn diff_move_serializes_shared_fields_with_operation_payload() {
1250 let diff = ToolCallContent::Diff(Diff::new(vec![
1251 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1252 .file_type(DiffFileType::Text)
1253 .mime_type("text/rust"),
1254 ]));
1255
1256 assert_eq!(
1257 serde_json::to_value(diff).unwrap(),
1258 serde_json::json!({
1259 "type": "diff",
1260 "changes": [
1261 {
1262 "operation": "move",
1263 "oldPath": "/repo/src/old.rs",
1264 "path": "/repo/src/new.rs",
1265 "fileType": "text",
1266 "mimeType": "text/rust"
1267 }
1268 ]
1269 })
1270 );
1271 }
1272
1273 #[test]
1274 fn diff_changes_skip_malformed_list_items() {
1275 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1276 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1277 "type": "diff",
1278 "changes": [
1279 {
1280 "operation": "modify"
1281 },
1282 {
1283 "operation": "delete",
1284 "path": "/ok"
1285 }
1286 ],
1287 "patch": {
1288 "format": "git_patch",
1289 "text": patch_text
1290 }
1291 }))
1292 .unwrap();
1293
1294 let ToolCallContent::Diff(diff) = content else {
1295 panic!("expected diff content");
1296 };
1297 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1298 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1299 }
1300
1301 #[test]
1302 fn tool_kind_preserves_unknown_variant() {
1303 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1304 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1305 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1306 }
1307
1308 #[test]
1309 fn tool_call_status_preserves_unknown_variant() {
1310 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1311 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1312 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1313 }
1314
1315 #[test]
1316 fn tool_call_status_recognizes_cancelled_variant() {
1317 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1318 assert_eq!(status, ToolCallStatus::Cancelled);
1319 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1320 }
1321
1322 #[test]
1323 fn tool_call_content_preserves_unknown_variant() {
1324 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1325 "type": "_chart",
1326 "title": "Tests",
1327 "data": [1, 2, 3]
1328 }))
1329 .unwrap();
1330
1331 let ToolCallContent::Other(unknown) = content else {
1332 panic!("expected unknown tool call content");
1333 };
1334
1335 assert_eq!(unknown.type_, "_chart");
1336 assert_eq!(
1337 unknown.fields.get("title"),
1338 Some(&serde_json::json!("Tests"))
1339 );
1340 assert_eq!(
1341 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1342 serde_json::json!({
1343 "type": "_chart",
1344 "title": "Tests",
1345 "data": [1, 2, 3]
1346 })
1347 );
1348 }
1349
1350 #[test]
1351 fn tool_call_content_does_not_hide_malformed_known_variant() {
1352 assert!(
1353 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1354 "type": "diff"
1355 }))
1356 .is_err()
1357 );
1358 assert!(
1359 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1360 "type": "terminal"
1361 }))
1362 .is_err()
1363 );
1364 }
1365}