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, apply_meta_update};
16use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined};
17
18#[serde_as]
33#[skip_serializing_none]
34#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37#[non_exhaustive]
38pub struct ToolCallUpdate {
39 pub tool_call_id: ToolCallId,
41 #[serde_as(deserialize_as = "DefaultOnError")]
48 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
49 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
50 pub name: MaybeUndefined<String>,
51 #[serde_as(deserialize_as = "DefaultOnError")]
56 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
57 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
58 pub title: MaybeUndefined<String>,
59 #[serde_as(deserialize_as = "DefaultOnError")]
65 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
66 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
67 pub kind: MaybeUndefined<ToolKind>,
68 #[serde_as(deserialize_as = "DefaultOnError")]
73 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
74 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
75 pub status: MaybeUndefined<ToolCallStatus>,
76 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
78 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
79 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
80 pub content: MaybeUndefined<Vec<ToolCallContent>>,
81 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
84 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
85 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
86 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
87 #[serde_as(deserialize_as = "DefaultOnError")]
89 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
90 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
91 pub raw_input: MaybeUndefined<serde_json::Value>,
92 #[serde_as(deserialize_as = "DefaultOnError")]
94 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
95 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
96 pub raw_output: MaybeUndefined<serde_json::Value>,
97 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
106 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
107 #[serde(
108 rename = "_meta",
109 default,
110 skip_serializing_if = "MaybeUndefined::is_undefined"
111 )]
112 pub meta: MaybeUndefined<Meta>,
113}
114
115impl ToolCallUpdate {
116 #[must_use]
118 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
119 Self {
120 tool_call_id: tool_call_id.into(),
121 name: MaybeUndefined::Undefined,
122 title: MaybeUndefined::Undefined,
123 kind: MaybeUndefined::Undefined,
124 status: MaybeUndefined::Undefined,
125 content: MaybeUndefined::Undefined,
126 locations: MaybeUndefined::Undefined,
127 raw_input: MaybeUndefined::Undefined,
128 raw_output: MaybeUndefined::Undefined,
129 meta: MaybeUndefined::Undefined,
130 }
131 }
132
133 #[must_use]
135 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
136 self.name = name.into_maybe_undefined();
137 self
138 }
139
140 #[must_use]
142 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
143 self.title = title.into_maybe_undefined();
144 self
145 }
146
147 #[must_use]
150 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
151 self.kind = kind.into_maybe_undefined();
152 self
153 }
154
155 #[must_use]
157 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
158 self.status = status.into_maybe_undefined();
159 self
160 }
161
162 #[must_use]
164 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
165 self.content = content.into_maybe_undefined();
166 self
167 }
168
169 #[must_use]
172 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
173 self.locations = locations.into_maybe_undefined();
174 self
175 }
176
177 #[must_use]
179 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
180 self.raw_input = raw_input.into_maybe_undefined();
181 self
182 }
183
184 #[must_use]
186 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
187 self.raw_output = raw_output.into_maybe_undefined();
188 self
189 }
190
191 #[must_use]
196 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
197 self.meta = meta.into_maybe_undefined();
198 self
199 }
200
201 pub fn apply_update(&mut self, update: ToolCallUpdate) {
207 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
208 if !update.name.is_undefined() {
209 self.name = update.name;
210 }
211 if !update.title.is_undefined() {
212 self.title = update.title;
213 }
214 if !update.kind.is_undefined() {
215 self.kind = update.kind;
216 }
217 if !update.status.is_undefined() {
218 self.status = update.status;
219 }
220 if !update.content.is_undefined() {
221 self.content = update.content;
222 }
223 if !update.locations.is_undefined() {
224 self.locations = update.locations;
225 }
226 if !update.raw_input.is_undefined() {
227 self.raw_input = update.raw_input;
228 }
229 if !update.raw_output.is_undefined() {
230 self.raw_output = update.raw_output;
231 }
232 apply_meta_update(&mut self.meta, update.meta);
233 }
234}
235
236#[serde_as]
243#[skip_serializing_none]
244#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247#[non_exhaustive]
248pub struct ToolCallContentChunk {
249 pub tool_call_id: ToolCallId,
251 pub content: ToolCallContent,
253 #[serde_as(deserialize_as = "DefaultOnError")]
260 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
261 #[serde(default)]
262 #[serde(rename = "_meta")]
263 pub meta: Option<Meta>,
264}
265
266impl ToolCallContentChunk {
267 #[must_use]
269 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
270 Self {
271 tool_call_id: tool_call_id.into(),
272 content: content.into(),
273 meta: None,
274 }
275 }
276
277 #[must_use]
283 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
284 self.meta = meta.into_option();
285 self
286 }
287}
288
289#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
292#[serde(transparent)]
293#[from(forward)]
294#[non_exhaustive]
295pub struct ToolCallId(pub Arc<str>);
296
297impl ToolCallId {
298 #[must_use]
300 pub fn new(id: impl Into<Self>) -> Self {
301 id.into()
302 }
303}
304
305impl IntoOption<ToolCallId> for &str {
306 fn into_option(self) -> Option<ToolCallId> {
307 Some(ToolCallId::new(self))
308 }
309}
310
311#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
318#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
319#[serde(rename_all = "snake_case")]
320#[non_exhaustive]
321pub enum ToolKind {
322 Read,
324 Edit,
326 Delete,
328 Move,
330 Search,
332 Execute,
334 Think,
336 Fetch,
338 SwitchMode,
340 #[default]
342 Other,
343 #[serde(untagged)]
349 Unknown(String),
350}
351
352#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
358#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
359#[serde(rename_all = "snake_case")]
360#[non_exhaustive]
361pub enum ToolCallStatus {
362 #[default]
365 Pending,
366 InProgress,
368 Completed,
370 Failed,
372 Cancelled,
374 #[serde(untagged)]
382 Other(String),
383}
384
385#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
393#[serde(tag = "type", rename_all = "snake_case")]
394#[non_exhaustive]
395pub enum ToolCallContent {
396 Content(Box<Content>),
398 Diff(Diff),
400 Terminal(Terminal),
402 #[serde(untagged)]
412 Other(OtherToolCallContent),
413}
414
415#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
417#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
418#[cfg_attr(feature = "schemars", schemars(inline))]
419#[cfg_attr(feature = "schemars", schemars(transform = other_tool_call_content_schema))]
420#[serde(rename_all = "camelCase")]
421#[non_exhaustive]
422pub struct OtherToolCallContent {
423 #[serde(rename = "type")]
429 pub type_: String,
430 #[serde(flatten)]
432 pub fields: BTreeMap<String, serde_json::Value>,
433}
434
435impl OtherToolCallContent {
436 #[must_use]
438 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
439 fields.remove("type");
440 Self {
441 type_: type_.into(),
442 fields,
443 }
444 }
445}
446
447impl<'de> Deserialize<'de> for OtherToolCallContent {
448 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
449 where
450 D: serde::Deserializer<'de>,
451 {
452 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
453 let type_ = fields
454 .remove("type")
455 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
456 let serde_json::Value::String(type_) = type_ else {
457 return Err(serde::de::Error::custom("`type` must be a string"));
458 };
459
460 if is_known_tool_call_content_type(&type_) {
461 return Err(serde::de::Error::custom(format!(
462 "known tool call content `{type_}` did not match its schema"
463 )));
464 }
465
466 Ok(Self { type_, fields })
467 }
468}
469
470fn is_known_tool_call_content_type(type_: &str) -> bool {
471 matches!(type_, "content" | "diff" | "terminal")
472}
473
474#[cfg(feature = "schemars")]
475fn other_tool_call_content_schema(schema: &mut Schema) {
476 super::schema_util::reject_known_string_discriminators(
477 schema,
478 "type",
479 &["content", "diff", "terminal"],
480 );
481}
482
483impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
484 fn from(content: T) -> Self {
485 ToolCallContent::Content(Box::new(Content::new(content)))
486 }
487}
488
489impl From<Diff> for ToolCallContent {
490 fn from(diff: Diff) -> Self {
491 ToolCallContent::Diff(diff)
492 }
493}
494
495impl From<Terminal> for ToolCallContent {
496 fn from(terminal: Terminal) -> Self {
497 ToolCallContent::Terminal(terminal)
498 }
499}
500
501#[serde_as]
503#[skip_serializing_none]
504#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "camelCase")]
507#[non_exhaustive]
508pub struct Content {
509 pub content: ContentBlock,
511 #[serde_as(deserialize_as = "DefaultOnError")]
517 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
518 #[serde(default)]
519 #[serde(rename = "_meta")]
520 pub meta: Option<Meta>,
521}
522
523impl Content {
524 #[must_use]
526 pub fn new(content: impl Into<ContentBlock>) -> Self {
527 Self {
528 content: content.into(),
529 meta: None,
530 }
531 }
532
533 #[must_use]
539 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
540 self.meta = meta.into_option();
541 self
542 }
543}
544
545#[serde_as]
554#[skip_serializing_none]
555#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
556#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557#[serde(rename_all = "camelCase")]
558#[non_exhaustive]
559pub struct Diff {
560 #[serde_as(deserialize_as = "VecSkipError<_>")]
564 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
565 pub changes: Vec<DiffChange>,
566 #[serde_as(deserialize_as = "DefaultOnError")]
571 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
572 #[serde(default)]
573 pub patch: Option<DiffPatch>,
574 #[serde_as(deserialize_as = "DefaultOnError")]
580 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
581 #[serde(default)]
582 #[serde(rename = "_meta")]
583 pub meta: Option<Meta>,
584}
585
586impl Diff {
587 #[must_use]
589 pub fn new(changes: Vec<DiffChange>) -> Self {
590 Self {
591 changes,
592 patch: None,
593 meta: None,
594 }
595 }
596
597 #[must_use]
599 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
600 Self::new(changes).with_patch(DiffPatch::new(text))
601 }
602
603 #[must_use]
605 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
606 self.patch = patch.into_option();
607 self
608 }
609
610 #[must_use]
616 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
617 self.meta = meta.into_option();
618 self
619 }
620}
621
622#[serde_as]
624#[skip_serializing_none]
625#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase")]
628#[non_exhaustive]
629pub struct DiffPatch {
630 pub format: DiffPatchFormat,
632 pub text: String,
634 #[serde_as(deserialize_as = "DefaultOnError")]
640 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
641 #[serde(default)]
642 #[serde(rename = "_meta")]
643 pub meta: Option<Meta>,
644}
645
646impl DiffPatch {
647 #[must_use]
649 pub fn new(text: impl Into<String>) -> Self {
650 Self {
651 format: DiffPatchFormat::GitPatch,
652 text: text.into(),
653 meta: None,
654 }
655 }
656
657 #[must_use]
663 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
664 self.meta = meta.into_option();
665 self
666 }
667}
668
669#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
672#[serde(rename_all = "snake_case")]
673#[non_exhaustive]
674pub enum DiffPatchFormat {
675 GitPatch,
687 #[serde(untagged)]
693 Other(String),
694}
695
696#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case")]
700#[non_exhaustive]
701pub enum DiffFileType {
702 Text,
704 Binary,
706 Directory,
708 Symlink,
710 #[serde(untagged)]
716 Other(String),
717}
718
719#[serde_as]
724#[skip_serializing_none]
725#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
727#[serde(rename_all = "camelCase")]
728#[non_exhaustive]
729pub struct DiffChange {
730 #[serde_as(deserialize_as = "DefaultOnError")]
734 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
735 #[serde(default)]
736 pub file_type: Option<DiffFileType>,
737 #[serde_as(deserialize_as = "DefaultOnError")]
741 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
742 #[serde(default)]
743 pub mime_type: Option<MediaType>,
744 #[serde(flatten)]
746 pub operation: DiffChangeOperation,
747 #[serde_as(deserialize_as = "DefaultOnError")]
753 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
754 #[serde(default)]
755 #[serde(rename = "_meta")]
756 pub meta: Option<Meta>,
757}
758
759impl DiffChange {
760 #[must_use]
762 pub fn new(operation: DiffChangeOperation) -> Self {
763 Self {
764 file_type: None,
765 mime_type: None,
766 operation,
767 meta: None,
768 }
769 }
770
771 #[must_use]
773 pub fn add(path: impl Into<AbsolutePath>) -> Self {
774 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
775 }
776
777 #[must_use]
779 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
780 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
781 }
782
783 #[must_use]
785 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
786 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
787 }
788
789 #[must_use]
791 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
792 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
793 old_path, path,
794 )))
795 }
796
797 #[must_use]
799 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
800 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
801 old_path, path,
802 )))
803 }
804
805 #[must_use]
809 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
810 self.file_type = file_type.into_option();
811 self
812 }
813
814 #[must_use]
818 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
819 self.mime_type = mime_type.into_option();
820 self
821 }
822
823 #[must_use]
829 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
830 self.meta = meta.into_option();
831 self
832 }
833}
834
835#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
837#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
838#[serde(tag = "operation", rename_all = "snake_case")]
839#[non_exhaustive]
840pub enum DiffChangeOperation {
841 Add(DiffPathChange),
843 Delete(DiffPathChange),
845 Modify(DiffPathChange),
847 Move(DiffPathPairChange),
849 Copy(DiffPathPairChange),
851 #[serde(untagged)]
857 Other(OtherDiffChange),
858}
859
860#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
862#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
863#[serde(rename_all = "camelCase")]
864#[non_exhaustive]
865pub struct DiffPathChange {
866 pub path: AbsolutePath,
868}
869
870impl DiffPathChange {
871 #[must_use]
873 pub fn new(path: impl Into<AbsolutePath>) -> Self {
874 Self { path: path.into() }
875 }
876}
877
878#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
880#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
881#[serde(rename_all = "camelCase")]
882#[non_exhaustive]
883pub struct DiffPathPairChange {
884 pub old_path: AbsolutePath,
886 pub path: AbsolutePath,
888}
889
890impl DiffPathPairChange {
891 #[must_use]
893 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
894 Self {
895 old_path: old_path.into(),
896 path: path.into(),
897 }
898 }
899}
900
901#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
903#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
904#[cfg_attr(feature = "schemars", schemars(inline))]
905#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
906#[serde(rename_all = "camelCase")]
907#[non_exhaustive]
908pub struct OtherDiffChange {
909 pub operation: String,
915 #[serde(flatten)]
917 pub fields: BTreeMap<String, serde_json::Value>,
918}
919
920impl OtherDiffChange {
921 #[must_use]
923 pub fn new(
924 operation: impl Into<String>,
925 mut fields: BTreeMap<String, serde_json::Value>,
926 ) -> Self {
927 fields.remove("operation");
928 fields.remove("fileType");
929 fields.remove("mimeType");
930 fields.remove("_meta");
931 Self {
932 operation: operation.into(),
933 fields,
934 }
935 }
936}
937
938impl<'de> Deserialize<'de> for OtherDiffChange {
939 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
940 where
941 D: serde::Deserializer<'de>,
942 {
943 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
944 let operation = fields
945 .remove("operation")
946 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
947 let serde_json::Value::String(operation) = operation else {
948 return Err(serde::de::Error::custom("`operation` must be a string"));
949 };
950
951 if is_known_diff_change_operation(&operation) {
952 return Err(serde::de::Error::custom(format!(
953 "known diff change operation `{operation}` did not match its schema"
954 )));
955 }
956 fields.remove("fileType");
957 fields.remove("mimeType");
958 fields.remove("_meta");
959
960 Ok(Self { operation, fields })
961 }
962}
963
964fn is_known_diff_change_operation(operation: &str) -> bool {
965 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
966}
967
968#[cfg(feature = "schemars")]
969fn other_diff_change_schema(schema: &mut Schema) {
970 super::schema_util::reject_known_string_discriminators(
971 schema,
972 "operation",
973 &["add", "delete", "modify", "move", "copy"],
974 );
975}
976
977#[serde_as]
984#[skip_serializing_none]
985#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
986#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(rename_all = "camelCase")]
988#[non_exhaustive]
989pub struct ToolCallLocation {
990 pub path: AbsolutePath,
992 #[serde_as(deserialize_as = "DefaultOnError")]
995 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
996 #[serde(default)]
997 pub line: Option<std::num::NonZeroU32>,
998 #[serde_as(deserialize_as = "DefaultOnError")]
1004 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1005 #[serde(default)]
1006 #[serde(rename = "_meta")]
1007 pub meta: Option<Meta>,
1008}
1009
1010impl ToolCallLocation {
1011 #[must_use]
1013 pub fn new(path: impl Into<AbsolutePath>) -> Self {
1014 Self {
1015 path: path.into(),
1016 line: None,
1017 meta: None,
1018 }
1019 }
1020
1021 #[must_use]
1026 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
1027 self.line = line.into_option().and_then(std::num::NonZeroU32::new);
1028 self
1029 }
1030
1031 #[must_use]
1037 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1038 self.meta = meta.into_option();
1039 self
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045
1046 #[test]
1047 fn location_line_is_one_based() {
1048 let location: ToolCallLocation =
1049 serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 0 })).unwrap();
1050 assert_eq!(location.line, None);
1051 let location: ToolCallLocation =
1052 serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 3 })).unwrap();
1053 assert_eq!(location.line.map(std::num::NonZeroU32::get), Some(3));
1054 assert_eq!(ToolCallLocation::new("/a.rs").line(0).line, None);
1055 }
1056
1057 use super::*;
1058 use crate::MaybeUndefined;
1059 use serde_json::{from_value, json, to_value};
1060
1061 #[test]
1062 fn tool_call_serializes_as_upsert() {
1063 let tool_call = ToolCallUpdate::new("tc_1")
1064 .title("Reading configuration")
1065 .status(ToolCallStatus::InProgress)
1066 .raw_input(serde_json::json!({"path": "settings.json"}));
1067
1068 assert_eq!(
1069 serde_json::to_value(tool_call).unwrap(),
1070 serde_json::json!({
1071 "toolCallId": "tc_1",
1072 "title": "Reading configuration",
1073 "status": "in_progress",
1074 "rawInput": {
1075 "path": "settings.json"
1076 }
1077 })
1078 );
1079 }
1080
1081 #[test]
1082 fn tool_call_update_distinguishes_omitted_null_and_value() {
1083 let tool_call = ToolCallUpdate::new("tc_1")
1084 .status(ToolCallStatus::Completed)
1085 .content(None::<Vec<ToolCallContent>>);
1086
1087 assert_eq!(
1088 serde_json::to_value(tool_call).unwrap(),
1089 serde_json::json!({
1090 "toolCallId": "tc_1",
1091 "status": "completed",
1092 "content": null
1093 })
1094 );
1095
1096 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1097 "toolCallId": "tc_1",
1098 "status": null,
1099 "locations": []
1100 }))
1101 .unwrap();
1102 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1103 assert_eq!(deserialized.status, MaybeUndefined::Null);
1104 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1105 }
1106
1107 #[test]
1108 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1109 let named = ToolCallUpdate::new("tc_1").name("read_file");
1110 assert_eq!(
1111 serde_json::to_value(named).unwrap(),
1112 serde_json::json!({
1113 "toolCallId": "tc_1",
1114 "name": "read_file"
1115 })
1116 );
1117
1118 let omitted = ToolCallUpdate::new("tc_1");
1119 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1120
1121 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1122 "toolCallId": "tc_1",
1123 "name": null
1124 }))
1125 .unwrap();
1126 assert_eq!(from_null.name, MaybeUndefined::Null);
1127
1128 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1129 stored.apply_update(ToolCallUpdate::new("tc_1"));
1130 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1131
1132 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1133 assert_eq!(stored.name, MaybeUndefined::Null);
1134
1135 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1136 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1137 }
1138
1139 #[test]
1140 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1141 let mut meta = Meta::new();
1142 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1143
1144 assert_eq!(
1145 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1146 serde_json::json!({
1147 "toolCallId": "tc_1",
1148 "_meta": {
1149 "source": "tool-call"
1150 }
1151 })
1152 );
1153
1154 assert_eq!(
1155 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1156 serde_json::json!({
1157 "toolCallId": "tc_1",
1158 "_meta": null
1159 })
1160 );
1161
1162 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1163 "toolCallId": "tc_1",
1164 "_meta": null
1165 }))
1166 .unwrap();
1167 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1168
1169 let patch = ToolCallUpdate::new("tc_1");
1170 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1171
1172 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1173 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1174 assert_eq!(stored.meta, MaybeUndefined::Null);
1175 }
1176
1177 #[test]
1178 fn tool_call_update_patches_meta_by_top_level_key() {
1179 let mut stored: ToolCallUpdate = from_value(json!({
1180 "toolCallId": "tc_1",
1181 "_meta": {"zed.dev/a": 1, "acme.dev/b": {"x": 1}}
1182 }))
1183 .unwrap();
1184
1185 stored.apply_update(
1187 from_value(json!({"toolCallId": "tc_1", "_meta": {"proxy.dev/c": true}})).unwrap(),
1188 );
1189 stored.apply_update(
1190 from_value(json!({
1191 "toolCallId": "tc_1",
1192 "_meta": {"zed.dev/a": null, "acme.dev/b": {"y": 2}}
1193 }))
1194 .unwrap(),
1195 );
1196
1197 assert_eq!(
1198 to_value(&stored).unwrap(),
1199 json!({
1200 "toolCallId": "tc_1",
1201 "_meta": {"acme.dev/b": {"y": 2}, "proxy.dev/c": true}
1202 })
1203 );
1204 }
1205
1206 #[test]
1207 fn tool_call_wire_patches_preserve_omitted_fields_and_replace_values() {
1208 let initial = json!({
1209 "toolCallId": "tc_1",
1210 "_meta": {"source": "replay", "opaque": {"sequence": 1}}
1211 });
1212 let mut stored: ToolCallUpdate = from_value(initial.clone()).unwrap();
1213 assert_eq!(to_value(&stored).unwrap(), initial);
1215
1216 let populated = json!({
1217 "toolCallId": "tc_1",
1218 "name": "read_file",
1219 "title": "Reading configuration",
1220 "kind": "read",
1221 "status": "in_progress",
1222 "content": [{
1223 "type": "content",
1224 "content": {
1225 "type": "text",
1226 "text": "old",
1227 "_meta": {"source": "tool"}
1228 }
1229 }],
1230 "locations": [{"path": "/workspace/config.json", "line": 3}],
1231 "rawInput": {"path": "/workspace/config.json"},
1232 "rawOutput": {"text": "old"}
1233 });
1234 stored.apply_update(from_value(populated.clone()).unwrap());
1235 let mut expected = populated;
1236 expected["_meta"] = initial["_meta"].clone();
1237 assert_eq!(to_value(&stored).unwrap(), expected);
1238
1239 for (field, empty) in [
1240 ("content", json!([])),
1241 ("locations", json!([])),
1242 ("rawInput", json!({})),
1243 ("rawOutput", json!({})),
1244 ] {
1245 let original = expected[field].clone();
1246 for replacement in [empty, json!(null), original] {
1249 stored.apply_update(
1250 from_value(json!({"toolCallId": "tc_1", (field): replacement})).unwrap(),
1251 );
1252 expected[field] = replacement;
1253 assert_eq!(to_value(&stored).unwrap(), expected, "patching {field}");
1254
1255 stored.apply_update(from_value(json!({"toolCallId": "tc_1"})).unwrap());
1256 assert_eq!(to_value(&stored).unwrap(), expected, "omitting {field}");
1257 }
1258 }
1259 }
1260
1261 #[test]
1262 fn tool_call_wire_patches_preserve_unknown_statuses() {
1263 for status in ["deferred", "_awaiting_review"] {
1264 let mut stored = ToolCallUpdate::new("tc_1").status(ToolCallStatus::InProgress);
1265 stored
1266 .apply_update(from_value(json!({"toolCallId": "tc_1", "status": status})).unwrap());
1267 let expected_status = MaybeUndefined::Value(ToolCallStatus::Other(status.to_owned()));
1268 assert_eq!(stored.status, expected_status);
1269
1270 stored.apply_update(
1273 from_value(json!({"toolCallId": "tc_1", "title": "Still waiting"})).unwrap(),
1274 );
1275 assert_eq!(stored.status, expected_status);
1276 assert_eq!(
1277 to_value(&stored).unwrap(),
1278 json!({"toolCallId": "tc_1", "title": "Still waiting", "status": status})
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn tool_call_update_skips_malformed_list_items() {
1285 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1286 "toolCallId": "tc_1",
1287 "content": [
1288 {
1289 "type": "content",
1290 "content": {
1291 "type": "text",
1292 "text": "ok"
1293 }
1294 },
1295 {
1296 "type": "diff",
1297 "path": "/bad"
1298 }
1299 ],
1300 "locations": [
1301 {
1302 "path": "/ok",
1303 "line": 3
1304 },
1305 {
1306 "line": 4
1307 }
1308 ]
1309 }))
1310 .unwrap();
1311
1312 let MaybeUndefined::Value(content) = deserialized.content else {
1313 panic!("content should deserialize to a value");
1314 };
1315 assert_eq!(content.len(), 1);
1316
1317 let MaybeUndefined::Value(locations) = deserialized.locations else {
1318 panic!("locations should deserialize to a value");
1319 };
1320 assert_eq!(locations.len(), 1);
1321 }
1322
1323 #[test]
1324 fn tool_call_content_chunk_serializes_single_content_item() {
1325 let chunk = ToolCallContentChunk::new(
1326 "tc_1",
1327 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1328 );
1329
1330 assert_eq!(
1331 serde_json::to_value(chunk).unwrap(),
1332 serde_json::json!({
1333 "toolCallId": "tc_1",
1334 "content": {
1335 "type": "content",
1336 "content": {
1337 "type": "text",
1338 "text": "partial output"
1339 }
1340 }
1341 })
1342 );
1343 }
1344
1345 #[test]
1346 fn terminal_content_serializes_as_display_reference() {
1347 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1348
1349 assert_eq!(
1350 serde_json::to_value(terminal).unwrap(),
1351 serde_json::json!({
1352 "type": "terminal",
1353 "terminalId": "term_1"
1354 })
1355 );
1356 }
1357
1358 #[test]
1359 fn diff_patch_serializes_git_patch_with_structured_changes() {
1360 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";
1361 let diff = ToolCallContent::Diff(Diff::patch(
1362 patch_text,
1363 vec![
1364 DiffChange::modify("/repo/config.json")
1365 .file_type(DiffFileType::Text)
1366 .mime_type("application/json"),
1367 ],
1368 ));
1369
1370 assert_eq!(
1371 serde_json::to_value(diff).unwrap(),
1372 serde_json::json!({
1373 "type": "diff",
1374 "changes": [
1375 {
1376 "operation": "modify",
1377 "path": "/repo/config.json",
1378 "fileType": "text",
1379 "mimeType": "application/json"
1380 }
1381 ],
1382 "patch": {
1383 "format": "git_patch",
1384 "text": patch_text
1385 }
1386 })
1387 );
1388 }
1389
1390 #[test]
1391 fn diff_patch_requires_text() {
1392 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1393 "format": "git_patch",
1394 "diff": "diff --git /repo/config.json /repo/config.json\n"
1395 }));
1396
1397 assert!(result.is_err());
1398 }
1399
1400 #[test]
1401 fn diff_serializes_binary_modify_without_patch_text() {
1402 let diff = ToolCallContent::Diff(Diff::new(vec![
1403 DiffChange::modify("/repo/assets/logo.png")
1404 .file_type(DiffFileType::Binary)
1405 .mime_type("image/png"),
1406 ]));
1407
1408 assert_eq!(
1409 serde_json::to_value(diff).unwrap(),
1410 serde_json::json!({
1411 "type": "diff",
1412 "changes": [
1413 {
1414 "operation": "modify",
1415 "path": "/repo/assets/logo.png",
1416 "fileType": "binary",
1417 "mimeType": "image/png"
1418 }
1419 ]
1420 })
1421 );
1422 }
1423
1424 #[test]
1425 fn diff_move_serializes_shared_fields_with_operation_payload() {
1426 let diff = ToolCallContent::Diff(Diff::new(vec![
1427 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1428 .file_type(DiffFileType::Text)
1429 .mime_type("text/rust"),
1430 ]));
1431
1432 assert_eq!(
1433 serde_json::to_value(diff).unwrap(),
1434 serde_json::json!({
1435 "type": "diff",
1436 "changes": [
1437 {
1438 "operation": "move",
1439 "oldPath": "/repo/src/old.rs",
1440 "path": "/repo/src/new.rs",
1441 "fileType": "text",
1442 "mimeType": "text/rust"
1443 }
1444 ]
1445 })
1446 );
1447 }
1448
1449 #[test]
1450 fn diff_changes_skip_malformed_list_items() {
1451 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1452 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1453 "type": "diff",
1454 "changes": [
1455 {
1456 "operation": "modify"
1457 },
1458 {
1459 "operation": "delete",
1460 "path": "/ok"
1461 }
1462 ],
1463 "patch": {
1464 "format": "git_patch",
1465 "text": patch_text
1466 }
1467 }))
1468 .unwrap();
1469
1470 let ToolCallContent::Diff(diff) = content else {
1471 panic!("expected diff content");
1472 };
1473 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1474 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1475 }
1476
1477 #[test]
1478 fn tool_kind_preserves_unknown_variant() {
1479 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1480 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1481 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1482 }
1483
1484 #[test]
1485 fn tool_call_status_preserves_unknown_variant() {
1486 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1487 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1488 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1489 }
1490
1491 #[test]
1492 fn tool_call_status_recognizes_cancelled_variant() {
1493 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1494 assert_eq!(status, ToolCallStatus::Cancelled);
1495 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1496 }
1497
1498 #[test]
1499 fn tool_call_content_preserves_unknown_variant() {
1500 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1501 "type": "_chart",
1502 "title": "Tests",
1503 "data": [1, 2, 3]
1504 }))
1505 .unwrap();
1506
1507 let ToolCallContent::Other(unknown) = content else {
1508 panic!("expected unknown tool call content");
1509 };
1510
1511 assert_eq!(unknown.type_, "_chart");
1512 assert_eq!(
1513 unknown.fields.get("title"),
1514 Some(&serde_json::json!("Tests"))
1515 );
1516 assert_eq!(
1517 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1518 serde_json::json!({
1519 "type": "_chart",
1520 "title": "Tests",
1521 "data": [1, 2, 3]
1522 })
1523 );
1524 }
1525
1526 #[test]
1527 fn tool_call_content_does_not_hide_malformed_known_variant() {
1528 assert!(
1529 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1530 "type": "diff"
1531 }))
1532 .is_err()
1533 );
1534 assert!(
1535 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1536 "type": "terminal"
1537 }))
1538 .is_err()
1539 );
1540 }
1541}