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,
686 #[serde(untagged)]
692 Other(String),
693}
694
695#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "snake_case")]
699#[non_exhaustive]
700pub enum DiffFileType {
701 Text,
703 Binary,
705 Directory,
707 Symlink,
709 #[serde(untagged)]
715 Other(String),
716}
717
718#[serde_as]
723#[skip_serializing_none]
724#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(rename_all = "camelCase")]
727#[non_exhaustive]
728pub struct DiffChange {
729 #[serde_as(deserialize_as = "DefaultOnError")]
733 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
734 #[serde(default)]
735 pub file_type: Option<DiffFileType>,
736 #[serde_as(deserialize_as = "DefaultOnError")]
740 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
741 #[serde(default)]
742 pub mime_type: Option<MediaType>,
743 #[serde(flatten)]
745 pub operation: DiffChangeOperation,
746 #[serde_as(deserialize_as = "DefaultOnError")]
752 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
753 #[serde(default)]
754 #[serde(rename = "_meta")]
755 pub meta: Option<Meta>,
756}
757
758impl DiffChange {
759 #[must_use]
761 pub fn new(operation: DiffChangeOperation) -> Self {
762 Self {
763 file_type: None,
764 mime_type: None,
765 operation,
766 meta: None,
767 }
768 }
769
770 #[must_use]
772 pub fn add(path: impl Into<AbsolutePath>) -> Self {
773 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
774 }
775
776 #[must_use]
778 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
779 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
780 }
781
782 #[must_use]
784 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
785 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
786 }
787
788 #[must_use]
790 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
791 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
792 old_path, path,
793 )))
794 }
795
796 #[must_use]
798 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
799 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
800 old_path, path,
801 )))
802 }
803
804 #[must_use]
808 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
809 self.file_type = file_type.into_option();
810 self
811 }
812
813 #[must_use]
817 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
818 self.mime_type = mime_type.into_option();
819 self
820 }
821
822 #[must_use]
828 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
829 self.meta = meta.into_option();
830 self
831 }
832}
833
834#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
836#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
837#[serde(tag = "operation", rename_all = "snake_case")]
838#[non_exhaustive]
839pub enum DiffChangeOperation {
840 Add(DiffPathChange),
842 Delete(DiffPathChange),
844 Modify(DiffPathChange),
846 Move(DiffPathPairChange),
848 Copy(DiffPathPairChange),
850 #[serde(untagged)]
856 Other(OtherDiffChange),
857}
858
859#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
861#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
862#[serde(rename_all = "camelCase")]
863#[non_exhaustive]
864pub struct DiffPathChange {
865 pub path: AbsolutePath,
867}
868
869impl DiffPathChange {
870 #[must_use]
872 pub fn new(path: impl Into<AbsolutePath>) -> Self {
873 Self { path: path.into() }
874 }
875}
876
877#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
879#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880#[serde(rename_all = "camelCase")]
881#[non_exhaustive]
882pub struct DiffPathPairChange {
883 pub old_path: AbsolutePath,
885 pub path: AbsolutePath,
887}
888
889impl DiffPathPairChange {
890 #[must_use]
892 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
893 Self {
894 old_path: old_path.into(),
895 path: path.into(),
896 }
897 }
898}
899
900#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
902#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
903#[cfg_attr(feature = "schemars", schemars(inline))]
904#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
905#[serde(rename_all = "camelCase")]
906#[non_exhaustive]
907pub struct OtherDiffChange {
908 pub operation: String,
914 #[serde(flatten)]
916 pub fields: BTreeMap<String, serde_json::Value>,
917}
918
919impl OtherDiffChange {
920 #[must_use]
922 pub fn new(
923 operation: impl Into<String>,
924 mut fields: BTreeMap<String, serde_json::Value>,
925 ) -> Self {
926 fields.remove("operation");
927 fields.remove("fileType");
928 fields.remove("mimeType");
929 fields.remove("_meta");
930 Self {
931 operation: operation.into(),
932 fields,
933 }
934 }
935}
936
937impl<'de> Deserialize<'de> for OtherDiffChange {
938 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
939 where
940 D: serde::Deserializer<'de>,
941 {
942 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
943 let operation = fields
944 .remove("operation")
945 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
946 let serde_json::Value::String(operation) = operation else {
947 return Err(serde::de::Error::custom("`operation` must be a string"));
948 };
949
950 if is_known_diff_change_operation(&operation) {
951 return Err(serde::de::Error::custom(format!(
952 "known diff change operation `{operation}` did not match its schema"
953 )));
954 }
955 fields.remove("fileType");
956 fields.remove("mimeType");
957 fields.remove("_meta");
958
959 Ok(Self { operation, fields })
960 }
961}
962
963fn is_known_diff_change_operation(operation: &str) -> bool {
964 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
965}
966
967#[cfg(feature = "schemars")]
968fn other_diff_change_schema(schema: &mut Schema) {
969 super::schema_util::reject_known_string_discriminators(
970 schema,
971 "operation",
972 &["add", "delete", "modify", "move", "copy"],
973 );
974}
975
976#[serde_as]
983#[skip_serializing_none]
984#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
985#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
986#[serde(rename_all = "camelCase")]
987#[non_exhaustive]
988pub struct ToolCallLocation {
989 pub path: AbsolutePath,
991 #[serde_as(deserialize_as = "DefaultOnError")]
994 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
995 #[serde(default)]
996 pub line: Option<std::num::NonZeroU32>,
997 #[serde_as(deserialize_as = "DefaultOnError")]
1003 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1004 #[serde(default)]
1005 #[serde(rename = "_meta")]
1006 pub meta: Option<Meta>,
1007}
1008
1009impl ToolCallLocation {
1010 #[must_use]
1012 pub fn new(path: impl Into<AbsolutePath>) -> Self {
1013 Self {
1014 path: path.into(),
1015 line: None,
1016 meta: None,
1017 }
1018 }
1019
1020 #[must_use]
1024 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
1025 self.line = line.into_option().and_then(std::num::NonZeroU32::new);
1026 self
1027 }
1028
1029 #[must_use]
1035 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1036 self.meta = meta.into_option();
1037 self
1038 }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043
1044 #[test]
1045 fn location_line_is_one_based() {
1046 let location: ToolCallLocation =
1047 serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 0 })).unwrap();
1048 assert_eq!(location.line, None);
1049 let location: ToolCallLocation =
1050 serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 3 })).unwrap();
1051 assert_eq!(location.line.map(std::num::NonZeroU32::get), Some(3));
1052 assert_eq!(ToolCallLocation::new("/a.rs").line(0).line, None);
1053 }
1054
1055 use super::*;
1056 use crate::MaybeUndefined;
1057 use serde_json::{from_value, json, to_value};
1058
1059 #[test]
1060 fn tool_call_serializes_as_upsert() {
1061 let tool_call = ToolCallUpdate::new("tc_1")
1062 .title("Reading configuration")
1063 .status(ToolCallStatus::InProgress)
1064 .raw_input(serde_json::json!({"path": "settings.json"}));
1065
1066 assert_eq!(
1067 serde_json::to_value(tool_call).unwrap(),
1068 serde_json::json!({
1069 "toolCallId": "tc_1",
1070 "title": "Reading configuration",
1071 "status": "in_progress",
1072 "rawInput": {
1073 "path": "settings.json"
1074 }
1075 })
1076 );
1077 }
1078
1079 #[test]
1080 fn tool_call_update_distinguishes_omitted_null_and_value() {
1081 let tool_call = ToolCallUpdate::new("tc_1")
1082 .status(ToolCallStatus::Completed)
1083 .content(None::<Vec<ToolCallContent>>);
1084
1085 assert_eq!(
1086 serde_json::to_value(tool_call).unwrap(),
1087 serde_json::json!({
1088 "toolCallId": "tc_1",
1089 "status": "completed",
1090 "content": null
1091 })
1092 );
1093
1094 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1095 "toolCallId": "tc_1",
1096 "status": null,
1097 "locations": []
1098 }))
1099 .unwrap();
1100 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1101 assert_eq!(deserialized.status, MaybeUndefined::Null);
1102 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1103 }
1104
1105 #[test]
1106 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1107 let named = ToolCallUpdate::new("tc_1").name("read_file");
1108 assert_eq!(
1109 serde_json::to_value(named).unwrap(),
1110 serde_json::json!({
1111 "toolCallId": "tc_1",
1112 "name": "read_file"
1113 })
1114 );
1115
1116 let omitted = ToolCallUpdate::new("tc_1");
1117 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1118
1119 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1120 "toolCallId": "tc_1",
1121 "name": null
1122 }))
1123 .unwrap();
1124 assert_eq!(from_null.name, MaybeUndefined::Null);
1125
1126 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1127 stored.apply_update(ToolCallUpdate::new("tc_1"));
1128 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1129
1130 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1131 assert_eq!(stored.name, MaybeUndefined::Null);
1132
1133 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1134 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1135 }
1136
1137 #[test]
1138 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1139 let mut meta = Meta::new();
1140 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1141
1142 assert_eq!(
1143 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1144 serde_json::json!({
1145 "toolCallId": "tc_1",
1146 "_meta": {
1147 "source": "tool-call"
1148 }
1149 })
1150 );
1151
1152 assert_eq!(
1153 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1154 serde_json::json!({
1155 "toolCallId": "tc_1",
1156 "_meta": null
1157 })
1158 );
1159
1160 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1161 "toolCallId": "tc_1",
1162 "_meta": null
1163 }))
1164 .unwrap();
1165 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1166
1167 let patch = ToolCallUpdate::new("tc_1");
1168 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1169
1170 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1171 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1172 assert_eq!(stored.meta, MaybeUndefined::Null);
1173 }
1174
1175 #[test]
1176 fn tool_call_update_patches_meta_by_top_level_key() {
1177 let mut stored: ToolCallUpdate = from_value(json!({
1178 "toolCallId": "tc_1",
1179 "_meta": {"zed.dev/a": 1, "acme.dev/b": {"x": 1}}
1180 }))
1181 .unwrap();
1182
1183 stored.apply_update(
1185 from_value(json!({"toolCallId": "tc_1", "_meta": {"proxy.dev/c": true}})).unwrap(),
1186 );
1187 stored.apply_update(
1188 from_value(json!({
1189 "toolCallId": "tc_1",
1190 "_meta": {"zed.dev/a": null, "acme.dev/b": {"y": 2}}
1191 }))
1192 .unwrap(),
1193 );
1194
1195 assert_eq!(
1196 to_value(&stored).unwrap(),
1197 json!({
1198 "toolCallId": "tc_1",
1199 "_meta": {"acme.dev/b": {"y": 2}, "proxy.dev/c": true}
1200 })
1201 );
1202 }
1203
1204 #[test]
1205 fn tool_call_wire_patches_preserve_omitted_fields_and_replace_values() {
1206 let initial = json!({
1207 "toolCallId": "tc_1",
1208 "_meta": {"source": "replay", "opaque": {"sequence": 1}}
1209 });
1210 let mut stored: ToolCallUpdate = from_value(initial.clone()).unwrap();
1211 assert_eq!(to_value(&stored).unwrap(), initial);
1213
1214 let populated = json!({
1215 "toolCallId": "tc_1",
1216 "name": "read_file",
1217 "title": "Reading configuration",
1218 "kind": "read",
1219 "status": "in_progress",
1220 "content": [{
1221 "type": "content",
1222 "content": {
1223 "type": "text",
1224 "text": "old",
1225 "_meta": {"source": "tool"}
1226 }
1227 }],
1228 "locations": [{"path": "/workspace/config.json", "line": 3}],
1229 "rawInput": {"path": "/workspace/config.json"},
1230 "rawOutput": {"text": "old"}
1231 });
1232 stored.apply_update(from_value(populated.clone()).unwrap());
1233 let mut expected = populated;
1234 expected["_meta"] = initial["_meta"].clone();
1235 assert_eq!(to_value(&stored).unwrap(), expected);
1236
1237 for (field, empty) in [
1238 ("content", json!([])),
1239 ("locations", json!([])),
1240 ("rawInput", json!({})),
1241 ("rawOutput", json!({})),
1242 ] {
1243 let original = expected[field].clone();
1244 for replacement in [empty, json!(null), original] {
1247 stored.apply_update(
1248 from_value(json!({"toolCallId": "tc_1", (field): replacement})).unwrap(),
1249 );
1250 expected[field] = replacement;
1251 assert_eq!(to_value(&stored).unwrap(), expected, "patching {field}");
1252
1253 stored.apply_update(from_value(json!({"toolCallId": "tc_1"})).unwrap());
1254 assert_eq!(to_value(&stored).unwrap(), expected, "omitting {field}");
1255 }
1256 }
1257 }
1258
1259 #[test]
1260 fn tool_call_wire_patches_preserve_unknown_statuses() {
1261 for status in ["deferred", "_awaiting_review"] {
1262 let mut stored = ToolCallUpdate::new("tc_1").status(ToolCallStatus::InProgress);
1263 stored
1264 .apply_update(from_value(json!({"toolCallId": "tc_1", "status": status})).unwrap());
1265 let expected_status = MaybeUndefined::Value(ToolCallStatus::Other(status.to_owned()));
1266 assert_eq!(stored.status, expected_status);
1267
1268 stored.apply_update(
1271 from_value(json!({"toolCallId": "tc_1", "title": "Still waiting"})).unwrap(),
1272 );
1273 assert_eq!(stored.status, expected_status);
1274 assert_eq!(
1275 to_value(&stored).unwrap(),
1276 json!({"toolCallId": "tc_1", "title": "Still waiting", "status": status})
1277 );
1278 }
1279 }
1280
1281 #[test]
1282 fn tool_call_update_skips_malformed_list_items() {
1283 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1284 "toolCallId": "tc_1",
1285 "content": [
1286 {
1287 "type": "content",
1288 "content": {
1289 "type": "text",
1290 "text": "ok"
1291 }
1292 },
1293 {
1294 "type": "diff",
1295 "path": "/bad"
1296 }
1297 ],
1298 "locations": [
1299 {
1300 "path": "/ok",
1301 "line": 3
1302 },
1303 {
1304 "line": 4
1305 }
1306 ]
1307 }))
1308 .unwrap();
1309
1310 let MaybeUndefined::Value(content) = deserialized.content else {
1311 panic!("content should deserialize to a value");
1312 };
1313 assert_eq!(content.len(), 1);
1314
1315 let MaybeUndefined::Value(locations) = deserialized.locations else {
1316 panic!("locations should deserialize to a value");
1317 };
1318 assert_eq!(locations.len(), 1);
1319 }
1320
1321 #[test]
1322 fn tool_call_content_chunk_serializes_single_content_item() {
1323 let chunk = ToolCallContentChunk::new(
1324 "tc_1",
1325 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1326 );
1327
1328 assert_eq!(
1329 serde_json::to_value(chunk).unwrap(),
1330 serde_json::json!({
1331 "toolCallId": "tc_1",
1332 "content": {
1333 "type": "content",
1334 "content": {
1335 "type": "text",
1336 "text": "partial output"
1337 }
1338 }
1339 })
1340 );
1341 }
1342
1343 #[test]
1344 fn terminal_content_serializes_as_display_reference() {
1345 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1346
1347 assert_eq!(
1348 serde_json::to_value(terminal).unwrap(),
1349 serde_json::json!({
1350 "type": "terminal",
1351 "terminalId": "term_1"
1352 })
1353 );
1354 }
1355
1356 #[test]
1357 fn diff_patch_serializes_git_patch_with_structured_changes() {
1358 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";
1359 let diff = ToolCallContent::Diff(Diff::patch(
1360 patch_text,
1361 vec![
1362 DiffChange::modify("/repo/config.json")
1363 .file_type(DiffFileType::Text)
1364 .mime_type("application/json"),
1365 ],
1366 ));
1367
1368 assert_eq!(
1369 serde_json::to_value(diff).unwrap(),
1370 serde_json::json!({
1371 "type": "diff",
1372 "changes": [
1373 {
1374 "operation": "modify",
1375 "path": "/repo/config.json",
1376 "fileType": "text",
1377 "mimeType": "application/json"
1378 }
1379 ],
1380 "patch": {
1381 "format": "git_patch",
1382 "text": patch_text
1383 }
1384 })
1385 );
1386 }
1387
1388 #[test]
1389 fn diff_patch_requires_text() {
1390 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1391 "format": "git_patch",
1392 "diff": "diff --git /repo/config.json /repo/config.json\n"
1393 }));
1394
1395 assert!(result.is_err());
1396 }
1397
1398 #[test]
1399 fn diff_serializes_binary_modify_without_patch_text() {
1400 let diff = ToolCallContent::Diff(Diff::new(vec![
1401 DiffChange::modify("/repo/assets/logo.png")
1402 .file_type(DiffFileType::Binary)
1403 .mime_type("image/png"),
1404 ]));
1405
1406 assert_eq!(
1407 serde_json::to_value(diff).unwrap(),
1408 serde_json::json!({
1409 "type": "diff",
1410 "changes": [
1411 {
1412 "operation": "modify",
1413 "path": "/repo/assets/logo.png",
1414 "fileType": "binary",
1415 "mimeType": "image/png"
1416 }
1417 ]
1418 })
1419 );
1420 }
1421
1422 #[test]
1423 fn diff_move_serializes_shared_fields_with_operation_payload() {
1424 let diff = ToolCallContent::Diff(Diff::new(vec![
1425 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1426 .file_type(DiffFileType::Text)
1427 .mime_type("text/rust"),
1428 ]));
1429
1430 assert_eq!(
1431 serde_json::to_value(diff).unwrap(),
1432 serde_json::json!({
1433 "type": "diff",
1434 "changes": [
1435 {
1436 "operation": "move",
1437 "oldPath": "/repo/src/old.rs",
1438 "path": "/repo/src/new.rs",
1439 "fileType": "text",
1440 "mimeType": "text/rust"
1441 }
1442 ]
1443 })
1444 );
1445 }
1446
1447 #[test]
1448 fn diff_changes_skip_malformed_list_items() {
1449 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1450 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1451 "type": "diff",
1452 "changes": [
1453 {
1454 "operation": "modify"
1455 },
1456 {
1457 "operation": "delete",
1458 "path": "/ok"
1459 }
1460 ],
1461 "patch": {
1462 "format": "git_patch",
1463 "text": patch_text
1464 }
1465 }))
1466 .unwrap();
1467
1468 let ToolCallContent::Diff(diff) = content else {
1469 panic!("expected diff content");
1470 };
1471 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1472 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1473 }
1474
1475 #[test]
1476 fn tool_kind_preserves_unknown_variant() {
1477 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1478 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1479 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1480 }
1481
1482 #[test]
1483 fn tool_call_status_preserves_unknown_variant() {
1484 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1485 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1486 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1487 }
1488
1489 #[test]
1490 fn tool_call_status_recognizes_cancelled_variant() {
1491 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1492 assert_eq!(status, ToolCallStatus::Cancelled);
1493 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1494 }
1495
1496 #[test]
1497 fn tool_call_content_preserves_unknown_variant() {
1498 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1499 "type": "_chart",
1500 "title": "Tests",
1501 "data": [1, 2, 3]
1502 }))
1503 .unwrap();
1504
1505 let ToolCallContent::Other(unknown) = content else {
1506 panic!("expected unknown tool call content");
1507 };
1508
1509 assert_eq!(unknown.type_, "_chart");
1510 assert_eq!(
1511 unknown.fields.get("title"),
1512 Some(&serde_json::json!("Tests"))
1513 );
1514 assert_eq!(
1515 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1516 serde_json::json!({
1517 "type": "_chart",
1518 "title": "Tests",
1519 "data": [1, 2, 3]
1520 })
1521 );
1522 }
1523
1524 #[test]
1525 fn tool_call_content_does_not_hide_malformed_known_variant() {
1526 assert!(
1527 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1528 "type": "diff"
1529 }))
1530 .is_err()
1531 );
1532 assert!(
1533 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1534 "type": "terminal"
1535 }))
1536 .is_err()
1537 );
1538 }
1539}