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")]
102 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
103 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
104 pub capabilities: MaybeUndefined<ToolCallCapabilities>,
105 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
114 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
115 #[serde(
116 rename = "_meta",
117 default,
118 skip_serializing_if = "MaybeUndefined::is_undefined"
119 )]
120 pub meta: MaybeUndefined<Meta>,
121}
122
123impl ToolCallUpdate {
124 #[must_use]
126 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
127 Self {
128 tool_call_id: tool_call_id.into(),
129 name: MaybeUndefined::Undefined,
130 title: MaybeUndefined::Undefined,
131 kind: MaybeUndefined::Undefined,
132 status: MaybeUndefined::Undefined,
133 content: MaybeUndefined::Undefined,
134 locations: MaybeUndefined::Undefined,
135 raw_input: MaybeUndefined::Undefined,
136 raw_output: MaybeUndefined::Undefined,
137 capabilities: MaybeUndefined::Undefined,
138 meta: MaybeUndefined::Undefined,
139 }
140 }
141
142 #[must_use]
144 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
145 self.name = name.into_maybe_undefined();
146 self
147 }
148
149 #[must_use]
151 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
152 self.title = title.into_maybe_undefined();
153 self
154 }
155
156 #[must_use]
159 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
160 self.kind = kind.into_maybe_undefined();
161 self
162 }
163
164 #[must_use]
166 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
167 self.status = status.into_maybe_undefined();
168 self
169 }
170
171 #[must_use]
173 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
174 self.content = content.into_maybe_undefined();
175 self
176 }
177
178 #[must_use]
181 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
182 self.locations = locations.into_maybe_undefined();
183 self
184 }
185
186 #[must_use]
188 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
189 self.raw_input = raw_input.into_maybe_undefined();
190 self
191 }
192
193 #[must_use]
195 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
196 self.raw_output = raw_output.into_maybe_undefined();
197 self
198 }
199
200 #[must_use]
202 pub fn capabilities(
203 mut self,
204 capabilities: impl IntoMaybeUndefined<ToolCallCapabilities>,
205 ) -> Self {
206 self.capabilities = capabilities.into_maybe_undefined();
207 self
208 }
209
210 #[must_use]
215 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
216 self.meta = meta.into_maybe_undefined();
217 self
218 }
219
220 pub fn apply_update(&mut self, update: ToolCallUpdate) {
226 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
227 if !update.name.is_undefined() {
228 self.name = update.name;
229 }
230 if !update.title.is_undefined() {
231 self.title = update.title;
232 }
233 if !update.kind.is_undefined() {
234 self.kind = update.kind;
235 }
236 if !update.status.is_undefined() {
237 self.status = update.status;
238 }
239 if !update.content.is_undefined() {
240 self.content = update.content;
241 }
242 if !update.locations.is_undefined() {
243 self.locations = update.locations;
244 }
245 if !update.raw_input.is_undefined() {
246 self.raw_input = update.raw_input;
247 }
248 if !update.raw_output.is_undefined() {
249 self.raw_output = update.raw_output;
250 }
251 if !update.capabilities.is_undefined() {
252 self.capabilities = update.capabilities;
253 }
254 apply_meta_update(&mut self.meta, update.meta);
255 }
256}
257
258#[serde_as]
265#[skip_serializing_none]
266#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
267#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "camelCase")]
269#[non_exhaustive]
270pub struct ToolCallCapabilities {
271 #[serde_as(deserialize_as = "DefaultOnError")]
276 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
277 #[serde(default)]
278 pub cancel: Option<ToolCallCancelCapabilities>,
279 #[serde_as(deserialize_as = "DefaultOnError")]
285 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
286 #[serde(default)]
287 #[serde(rename = "_meta")]
288 pub meta: Option<Meta>,
289}
290
291impl ToolCallCapabilities {
292 #[must_use]
294 pub fn new() -> Self {
295 Self::default()
296 }
297
298 #[must_use]
300 pub fn cancel(mut self, cancel: impl IntoOption<ToolCallCancelCapabilities>) -> Self {
301 self.cancel = cancel.into_option();
302 self
303 }
304
305 #[must_use]
311 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
312 self.meta = meta.into_option();
313 self
314 }
315}
316
317#[serde_as]
321#[skip_serializing_none]
322#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
323#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
324#[non_exhaustive]
325pub struct ToolCallCancelCapabilities {
326 #[serde_as(deserialize_as = "DefaultOnError")]
332 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
333 #[serde(default)]
334 #[serde(rename = "_meta")]
335 pub meta: Option<Meta>,
336}
337
338impl ToolCallCancelCapabilities {
339 #[must_use]
341 pub fn new() -> Self {
342 Self::default()
343 }
344
345 #[must_use]
351 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
352 self.meta = meta.into_option();
353 self
354 }
355}
356
357#[serde_as]
364#[skip_serializing_none]
365#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368#[non_exhaustive]
369pub struct ToolCallContentChunk {
370 pub tool_call_id: ToolCallId,
372 pub content: ToolCallContent,
374 #[serde_as(deserialize_as = "DefaultOnError")]
381 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
382 #[serde(default)]
383 #[serde(rename = "_meta")]
384 pub meta: Option<Meta>,
385}
386
387impl ToolCallContentChunk {
388 #[must_use]
390 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
391 Self {
392 tool_call_id: tool_call_id.into(),
393 content: content.into(),
394 meta: None,
395 }
396 }
397
398 #[must_use]
404 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
405 self.meta = meta.into_option();
406 self
407 }
408}
409
410#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
413#[serde(transparent)]
414#[from(forward)]
415#[non_exhaustive]
416pub struct ToolCallId(pub Arc<str>);
417
418impl ToolCallId {
419 #[must_use]
421 pub fn new(id: impl Into<Self>) -> Self {
422 id.into()
423 }
424}
425
426impl IntoOption<ToolCallId> for &str {
427 fn into_option(self) -> Option<ToolCallId> {
428 Some(ToolCallId::new(self))
429 }
430}
431
432#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
439#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
440#[serde(rename_all = "snake_case")]
441#[non_exhaustive]
442pub enum ToolKind {
443 Read,
445 Edit,
447 Delete,
449 Move,
451 Search,
453 Execute,
455 Think,
457 Fetch,
459 SwitchMode,
461 #[default]
463 Other,
464 #[serde(untagged)]
470 Unknown(String),
471}
472
473#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
479#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
480#[serde(rename_all = "snake_case")]
481#[non_exhaustive]
482pub enum ToolCallStatus {
483 #[default]
486 Pending,
487 InProgress,
489 Paused,
492 Completed,
494 Failed,
496 Cancelled,
498 #[serde(untagged)]
506 Other(String),
507}
508
509#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
517#[serde(tag = "type", rename_all = "snake_case")]
518#[non_exhaustive]
519pub enum ToolCallContent {
520 Content(Box<Content>),
522 Diff(Diff),
524 Terminal(Terminal),
526 #[serde(untagged)]
536 Other(OtherToolCallContent),
537}
538
539#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
541#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
542#[cfg_attr(feature = "schemars", schemars(inline))]
543#[cfg_attr(feature = "schemars", schemars(transform = other_tool_call_content_schema))]
544#[serde(rename_all = "camelCase")]
545#[non_exhaustive]
546pub struct OtherToolCallContent {
547 #[serde(rename = "type")]
553 pub type_: String,
554 #[serde(flatten)]
556 pub fields: BTreeMap<String, serde_json::Value>,
557}
558
559impl OtherToolCallContent {
560 #[must_use]
562 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
563 fields.remove("type");
564 Self {
565 type_: type_.into(),
566 fields,
567 }
568 }
569}
570
571impl<'de> Deserialize<'de> for OtherToolCallContent {
572 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
573 where
574 D: serde::Deserializer<'de>,
575 {
576 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
577 let type_ = fields
578 .remove("type")
579 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
580 let serde_json::Value::String(type_) = type_ else {
581 return Err(serde::de::Error::custom("`type` must be a string"));
582 };
583
584 if is_known_tool_call_content_type(&type_) {
585 return Err(serde::de::Error::custom(format!(
586 "known tool call content `{type_}` did not match its schema"
587 )));
588 }
589
590 Ok(Self { type_, fields })
591 }
592}
593
594fn is_known_tool_call_content_type(type_: &str) -> bool {
595 matches!(type_, "content" | "diff" | "terminal")
596}
597
598#[cfg(feature = "schemars")]
599fn other_tool_call_content_schema(schema: &mut Schema) {
600 super::schema_util::reject_known_string_discriminators(
601 schema,
602 "type",
603 &["content", "diff", "terminal"],
604 );
605}
606
607impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
608 fn from(content: T) -> Self {
609 ToolCallContent::Content(Box::new(Content::new(content)))
610 }
611}
612
613impl From<Diff> for ToolCallContent {
614 fn from(diff: Diff) -> Self {
615 ToolCallContent::Diff(diff)
616 }
617}
618
619impl From<Terminal> for ToolCallContent {
620 fn from(terminal: Terminal) -> Self {
621 ToolCallContent::Terminal(terminal)
622 }
623}
624
625#[serde_as]
627#[skip_serializing_none]
628#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
630#[serde(rename_all = "camelCase")]
631#[non_exhaustive]
632pub struct Content {
633 pub content: ContentBlock,
635 #[serde_as(deserialize_as = "DefaultOnError")]
641 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
642 #[serde(default)]
643 #[serde(rename = "_meta")]
644 pub meta: Option<Meta>,
645}
646
647impl Content {
648 #[must_use]
650 pub fn new(content: impl Into<ContentBlock>) -> Self {
651 Self {
652 content: content.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#[serde_as]
678#[skip_serializing_none]
679#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
681#[serde(rename_all = "camelCase")]
682#[non_exhaustive]
683pub struct Diff {
684 #[serde_as(deserialize_as = "VecSkipError<_>")]
688 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
689 pub changes: Vec<DiffChange>,
690 #[serde_as(deserialize_as = "DefaultOnError")]
695 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
696 #[serde(default)]
697 pub patch: Option<DiffPatch>,
698 #[serde_as(deserialize_as = "DefaultOnError")]
704 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
705 #[serde(default)]
706 #[serde(rename = "_meta")]
707 pub meta: Option<Meta>,
708}
709
710impl Diff {
711 #[must_use]
713 pub fn new(changes: Vec<DiffChange>) -> Self {
714 Self {
715 changes,
716 patch: None,
717 meta: None,
718 }
719 }
720
721 #[must_use]
723 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
724 Self::new(changes).with_patch(DiffPatch::new(text))
725 }
726
727 #[must_use]
729 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
730 self.patch = patch.into_option();
731 self
732 }
733
734 #[must_use]
740 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
741 self.meta = meta.into_option();
742 self
743 }
744}
745
746#[serde_as]
748#[skip_serializing_none]
749#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
751#[serde(rename_all = "camelCase")]
752#[non_exhaustive]
753pub struct DiffPatch {
754 pub format: DiffPatchFormat,
756 pub text: String,
758 #[serde_as(deserialize_as = "DefaultOnError")]
764 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
765 #[serde(default)]
766 #[serde(rename = "_meta")]
767 pub meta: Option<Meta>,
768}
769
770impl DiffPatch {
771 #[must_use]
773 pub fn new(text: impl Into<String>) -> Self {
774 Self {
775 format: DiffPatchFormat::GitPatch,
776 text: text.into(),
777 meta: None,
778 }
779 }
780
781 #[must_use]
787 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
788 self.meta = meta.into_option();
789 self
790 }
791}
792
793#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
795#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
796#[serde(rename_all = "snake_case")]
797#[non_exhaustive]
798pub enum DiffPatchFormat {
799 GitPatch,
811 #[serde(untagged)]
817 Other(String),
818}
819
820#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
822#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
823#[serde(rename_all = "snake_case")]
824#[non_exhaustive]
825pub enum DiffFileType {
826 Text,
828 Binary,
830 Directory,
832 Symlink,
834 #[serde(untagged)]
840 Other(String),
841}
842
843#[serde_as]
848#[skip_serializing_none]
849#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
850#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
851#[serde(rename_all = "camelCase")]
852#[non_exhaustive]
853pub struct DiffChange {
854 #[serde_as(deserialize_as = "DefaultOnError")]
858 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
859 #[serde(default)]
860 pub file_type: Option<DiffFileType>,
861 #[serde_as(deserialize_as = "DefaultOnError")]
865 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
866 #[serde(default)]
867 pub mime_type: Option<MediaType>,
868 #[serde(flatten)]
870 pub operation: DiffChangeOperation,
871 #[serde_as(deserialize_as = "DefaultOnError")]
877 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
878 #[serde(default)]
879 #[serde(rename = "_meta")]
880 pub meta: Option<Meta>,
881}
882
883impl DiffChange {
884 #[must_use]
886 pub fn new(operation: DiffChangeOperation) -> Self {
887 Self {
888 file_type: None,
889 mime_type: None,
890 operation,
891 meta: None,
892 }
893 }
894
895 #[must_use]
897 pub fn add(path: impl Into<AbsolutePath>) -> Self {
898 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
899 }
900
901 #[must_use]
903 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
904 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
905 }
906
907 #[must_use]
909 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
910 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
911 }
912
913 #[must_use]
915 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
916 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
917 old_path, path,
918 )))
919 }
920
921 #[must_use]
923 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
924 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
925 old_path, path,
926 )))
927 }
928
929 #[must_use]
933 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
934 self.file_type = file_type.into_option();
935 self
936 }
937
938 #[must_use]
942 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
943 self.mime_type = mime_type.into_option();
944 self
945 }
946
947 #[must_use]
953 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
954 self.meta = meta.into_option();
955 self
956 }
957}
958
959#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
961#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(tag = "operation", rename_all = "snake_case")]
963#[non_exhaustive]
964pub enum DiffChangeOperation {
965 Add(DiffPathChange),
967 Delete(DiffPathChange),
969 Modify(DiffPathChange),
971 Move(DiffPathPairChange),
973 Copy(DiffPathPairChange),
975 #[serde(untagged)]
981 Other(OtherDiffChange),
982}
983
984#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
986#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(rename_all = "camelCase")]
988#[non_exhaustive]
989pub struct DiffPathChange {
990 pub path: AbsolutePath,
992}
993
994impl DiffPathChange {
995 #[must_use]
997 pub fn new(path: impl Into<AbsolutePath>) -> Self {
998 Self { path: path.into() }
999 }
1000}
1001
1002#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1005#[serde(rename_all = "camelCase")]
1006#[non_exhaustive]
1007pub struct DiffPathPairChange {
1008 pub old_path: AbsolutePath,
1010 pub path: AbsolutePath,
1012}
1013
1014impl DiffPathPairChange {
1015 #[must_use]
1017 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
1018 Self {
1019 old_path: old_path.into(),
1020 path: path.into(),
1021 }
1022 }
1023}
1024
1025#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1027#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1028#[cfg_attr(feature = "schemars", schemars(inline))]
1029#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
1030#[serde(rename_all = "camelCase")]
1031#[non_exhaustive]
1032pub struct OtherDiffChange {
1033 pub operation: String,
1039 #[serde(flatten)]
1041 pub fields: BTreeMap<String, serde_json::Value>,
1042}
1043
1044impl OtherDiffChange {
1045 #[must_use]
1047 pub fn new(
1048 operation: impl Into<String>,
1049 mut fields: BTreeMap<String, serde_json::Value>,
1050 ) -> Self {
1051 fields.remove("operation");
1052 fields.remove("fileType");
1053 fields.remove("mimeType");
1054 fields.remove("_meta");
1055 Self {
1056 operation: operation.into(),
1057 fields,
1058 }
1059 }
1060}
1061
1062impl<'de> Deserialize<'de> for OtherDiffChange {
1063 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1064 where
1065 D: serde::Deserializer<'de>,
1066 {
1067 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1068 let operation = fields
1069 .remove("operation")
1070 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
1071 let serde_json::Value::String(operation) = operation else {
1072 return Err(serde::de::Error::custom("`operation` must be a string"));
1073 };
1074
1075 if is_known_diff_change_operation(&operation) {
1076 return Err(serde::de::Error::custom(format!(
1077 "known diff change operation `{operation}` did not match its schema"
1078 )));
1079 }
1080 fields.remove("fileType");
1081 fields.remove("mimeType");
1082 fields.remove("_meta");
1083
1084 Ok(Self { operation, fields })
1085 }
1086}
1087
1088fn is_known_diff_change_operation(operation: &str) -> bool {
1089 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
1090}
1091
1092#[cfg(feature = "schemars")]
1093fn other_diff_change_schema(schema: &mut Schema) {
1094 super::schema_util::reject_known_string_discriminators(
1095 schema,
1096 "operation",
1097 &["add", "delete", "modify", "move", "copy"],
1098 );
1099}
1100
1101#[serde_as]
1108#[skip_serializing_none]
1109#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1110#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1111#[serde(rename_all = "camelCase")]
1112#[non_exhaustive]
1113pub struct ToolCallLocation {
1114 pub path: AbsolutePath,
1116 #[serde_as(deserialize_as = "DefaultOnError")]
1119 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1120 #[serde(default)]
1121 pub line: Option<std::num::NonZeroU32>,
1122 #[serde_as(deserialize_as = "DefaultOnError")]
1128 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1129 #[serde(default)]
1130 #[serde(rename = "_meta")]
1131 pub meta: Option<Meta>,
1132}
1133
1134impl ToolCallLocation {
1135 #[must_use]
1137 pub fn new(path: impl Into<AbsolutePath>) -> Self {
1138 Self {
1139 path: path.into(),
1140 line: None,
1141 meta: None,
1142 }
1143 }
1144
1145 #[must_use]
1150 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
1151 self.line = line.into_option().and_then(std::num::NonZeroU32::new);
1152 self
1153 }
1154
1155 #[must_use]
1161 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1162 self.meta = meta.into_option();
1163 self
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169
1170 #[test]
1171 fn location_line_is_one_based() {
1172 let location: ToolCallLocation =
1173 serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 0 })).unwrap();
1174 assert_eq!(location.line, None);
1175 let location: ToolCallLocation =
1176 serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 3 })).unwrap();
1177 assert_eq!(location.line.map(std::num::NonZeroU32::get), Some(3));
1178 assert_eq!(ToolCallLocation::new("/a.rs").line(0).line, None);
1179 }
1180
1181 #[test]
1182 fn toolcall_capabilities_patch_and_tolerate_malformed_values() {
1183 use serde_json::json;
1184 let update = ToolCallUpdate::new("call_012")
1185 .capabilities(ToolCallCapabilities::new().cancel(ToolCallCancelCapabilities::new()));
1186 assert_eq!(
1187 serde_json::to_value(&update).unwrap(),
1188 json!({ "toolCallId": "call_012", "capabilities": { "cancel": {} } })
1189 );
1190
1191 let mut stored = update;
1192 stored.apply_update(ToolCallUpdate::new("call_012"));
1193 assert!(stored.capabilities.is_value());
1194 stored.apply_update(ToolCallUpdate::new("call_012").capabilities(MaybeUndefined::Null));
1195 assert!(stored.capabilities.is_null());
1196
1197 for bad in [json!("yes"), json!(1)] {
1198 let parsed: ToolCallUpdate =
1199 serde_json::from_value(json!({ "toolCallId": "call_012", "capabilities": bad }))
1200 .unwrap();
1201 assert!(parsed.capabilities.is_undefined());
1202 }
1203 let parsed: ToolCallCapabilities =
1204 serde_json::from_value(json!({ "cancel": "yes" })).unwrap();
1205 assert_eq!(parsed.cancel, None);
1206 }
1207
1208 #[test]
1209 fn paused_is_a_known_tool_call_status() {
1210 use serde_json::json;
1211 assert_eq!(
1212 serde_json::to_value(ToolCallStatus::Paused).unwrap(),
1213 json!("paused")
1214 );
1215 assert_eq!(
1216 serde_json::from_value::<ToolCallStatus>(json!("paused")).unwrap(),
1217 ToolCallStatus::Paused
1218 );
1219 }
1220
1221 use super::*;
1222 use crate::MaybeUndefined;
1223 use serde_json::{from_value, json, to_value};
1224
1225 #[test]
1226 fn tool_call_serializes_as_upsert() {
1227 let tool_call = ToolCallUpdate::new("tc_1")
1228 .title("Reading configuration")
1229 .status(ToolCallStatus::InProgress)
1230 .raw_input(serde_json::json!({"path": "settings.json"}));
1231
1232 assert_eq!(
1233 serde_json::to_value(tool_call).unwrap(),
1234 serde_json::json!({
1235 "toolCallId": "tc_1",
1236 "title": "Reading configuration",
1237 "status": "in_progress",
1238 "rawInput": {
1239 "path": "settings.json"
1240 }
1241 })
1242 );
1243 }
1244
1245 #[test]
1246 fn tool_call_update_distinguishes_omitted_null_and_value() {
1247 let tool_call = ToolCallUpdate::new("tc_1")
1248 .status(ToolCallStatus::Completed)
1249 .content(None::<Vec<ToolCallContent>>);
1250
1251 assert_eq!(
1252 serde_json::to_value(tool_call).unwrap(),
1253 serde_json::json!({
1254 "toolCallId": "tc_1",
1255 "status": "completed",
1256 "content": null
1257 })
1258 );
1259
1260 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1261 "toolCallId": "tc_1",
1262 "status": null,
1263 "locations": []
1264 }))
1265 .unwrap();
1266 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1267 assert_eq!(deserialized.status, MaybeUndefined::Null);
1268 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1269 }
1270
1271 #[test]
1272 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1273 let named = ToolCallUpdate::new("tc_1").name("read_file");
1274 assert_eq!(
1275 serde_json::to_value(named).unwrap(),
1276 serde_json::json!({
1277 "toolCallId": "tc_1",
1278 "name": "read_file"
1279 })
1280 );
1281
1282 let omitted = ToolCallUpdate::new("tc_1");
1283 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1284
1285 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1286 "toolCallId": "tc_1",
1287 "name": null
1288 }))
1289 .unwrap();
1290 assert_eq!(from_null.name, MaybeUndefined::Null);
1291
1292 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1293 stored.apply_update(ToolCallUpdate::new("tc_1"));
1294 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1295
1296 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1297 assert_eq!(stored.name, MaybeUndefined::Null);
1298
1299 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1300 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1301 }
1302
1303 #[test]
1304 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1305 let mut meta = Meta::new();
1306 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1307
1308 assert_eq!(
1309 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1310 serde_json::json!({
1311 "toolCallId": "tc_1",
1312 "_meta": {
1313 "source": "tool-call"
1314 }
1315 })
1316 );
1317
1318 assert_eq!(
1319 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1320 serde_json::json!({
1321 "toolCallId": "tc_1",
1322 "_meta": null
1323 })
1324 );
1325
1326 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1327 "toolCallId": "tc_1",
1328 "_meta": null
1329 }))
1330 .unwrap();
1331 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1332
1333 let patch = ToolCallUpdate::new("tc_1");
1334 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1335
1336 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1337 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1338 assert_eq!(stored.meta, MaybeUndefined::Null);
1339 }
1340
1341 #[test]
1342 fn tool_call_update_patches_meta_by_top_level_key() {
1343 let mut stored: ToolCallUpdate = from_value(json!({
1344 "toolCallId": "tc_1",
1345 "_meta": {"zed.dev/a": 1, "acme.dev/b": {"x": 1}}
1346 }))
1347 .unwrap();
1348
1349 stored.apply_update(
1351 from_value(json!({"toolCallId": "tc_1", "_meta": {"proxy.dev/c": true}})).unwrap(),
1352 );
1353 stored.apply_update(
1354 from_value(json!({
1355 "toolCallId": "tc_1",
1356 "_meta": {"zed.dev/a": null, "acme.dev/b": {"y": 2}}
1357 }))
1358 .unwrap(),
1359 );
1360
1361 assert_eq!(
1362 to_value(&stored).unwrap(),
1363 json!({
1364 "toolCallId": "tc_1",
1365 "_meta": {"acme.dev/b": {"y": 2}, "proxy.dev/c": true}
1366 })
1367 );
1368 }
1369
1370 #[test]
1371 fn tool_call_wire_patches_preserve_omitted_fields_and_replace_values() {
1372 let initial = json!({
1373 "toolCallId": "tc_1",
1374 "_meta": {"source": "replay", "opaque": {"sequence": 1}}
1375 });
1376 let mut stored: ToolCallUpdate = from_value(initial.clone()).unwrap();
1377 assert_eq!(to_value(&stored).unwrap(), initial);
1379
1380 let populated = json!({
1381 "toolCallId": "tc_1",
1382 "name": "read_file",
1383 "title": "Reading configuration",
1384 "kind": "read",
1385 "status": "in_progress",
1386 "content": [{
1387 "type": "content",
1388 "content": {
1389 "type": "text",
1390 "text": "old",
1391 "_meta": {"source": "tool"}
1392 }
1393 }],
1394 "locations": [{"path": "/workspace/config.json", "line": 3}],
1395 "rawInput": {"path": "/workspace/config.json"},
1396 "rawOutput": {"text": "old"}
1397 });
1398 stored.apply_update(from_value(populated.clone()).unwrap());
1399 let mut expected = populated;
1400 expected["_meta"] = initial["_meta"].clone();
1401 assert_eq!(to_value(&stored).unwrap(), expected);
1402
1403 for (field, empty) in [
1404 ("content", json!([])),
1405 ("locations", json!([])),
1406 ("rawInput", json!({})),
1407 ("rawOutput", json!({})),
1408 ] {
1409 let original = expected[field].clone();
1410 for replacement in [empty, json!(null), original] {
1413 stored.apply_update(
1414 from_value(json!({"toolCallId": "tc_1", (field): replacement})).unwrap(),
1415 );
1416 expected[field] = replacement;
1417 assert_eq!(to_value(&stored).unwrap(), expected, "patching {field}");
1418
1419 stored.apply_update(from_value(json!({"toolCallId": "tc_1"})).unwrap());
1420 assert_eq!(to_value(&stored).unwrap(), expected, "omitting {field}");
1421 }
1422 }
1423 }
1424
1425 #[test]
1426 fn tool_call_wire_patches_preserve_unknown_statuses() {
1427 for status in ["deferred", "_awaiting_review"] {
1428 let mut stored = ToolCallUpdate::new("tc_1").status(ToolCallStatus::InProgress);
1429 stored
1430 .apply_update(from_value(json!({"toolCallId": "tc_1", "status": status})).unwrap());
1431 let expected_status = MaybeUndefined::Value(ToolCallStatus::Other(status.to_owned()));
1432 assert_eq!(stored.status, expected_status);
1433
1434 stored.apply_update(
1437 from_value(json!({"toolCallId": "tc_1", "title": "Still waiting"})).unwrap(),
1438 );
1439 assert_eq!(stored.status, expected_status);
1440 assert_eq!(
1441 to_value(&stored).unwrap(),
1442 json!({"toolCallId": "tc_1", "title": "Still waiting", "status": status})
1443 );
1444 }
1445 }
1446
1447 #[test]
1448 fn tool_call_update_skips_malformed_list_items() {
1449 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1450 "toolCallId": "tc_1",
1451 "content": [
1452 {
1453 "type": "content",
1454 "content": {
1455 "type": "text",
1456 "text": "ok"
1457 }
1458 },
1459 {
1460 "type": "diff",
1461 "path": "/bad"
1462 }
1463 ],
1464 "locations": [
1465 {
1466 "path": "/ok",
1467 "line": 3
1468 },
1469 {
1470 "line": 4
1471 }
1472 ]
1473 }))
1474 .unwrap();
1475
1476 let MaybeUndefined::Value(content) = deserialized.content else {
1477 panic!("content should deserialize to a value");
1478 };
1479 assert_eq!(content.len(), 1);
1480
1481 let MaybeUndefined::Value(locations) = deserialized.locations else {
1482 panic!("locations should deserialize to a value");
1483 };
1484 assert_eq!(locations.len(), 1);
1485 }
1486
1487 #[test]
1488 fn tool_call_content_chunk_serializes_single_content_item() {
1489 let chunk = ToolCallContentChunk::new(
1490 "tc_1",
1491 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1492 );
1493
1494 assert_eq!(
1495 serde_json::to_value(chunk).unwrap(),
1496 serde_json::json!({
1497 "toolCallId": "tc_1",
1498 "content": {
1499 "type": "content",
1500 "content": {
1501 "type": "text",
1502 "text": "partial output"
1503 }
1504 }
1505 })
1506 );
1507 }
1508
1509 #[test]
1510 fn terminal_content_serializes_as_display_reference() {
1511 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1512
1513 assert_eq!(
1514 serde_json::to_value(terminal).unwrap(),
1515 serde_json::json!({
1516 "type": "terminal",
1517 "terminalId": "term_1"
1518 })
1519 );
1520 }
1521
1522 #[test]
1523 fn diff_patch_serializes_git_patch_with_structured_changes() {
1524 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";
1525 let diff = ToolCallContent::Diff(Diff::patch(
1526 patch_text,
1527 vec![
1528 DiffChange::modify("/repo/config.json")
1529 .file_type(DiffFileType::Text)
1530 .mime_type("application/json"),
1531 ],
1532 ));
1533
1534 assert_eq!(
1535 serde_json::to_value(diff).unwrap(),
1536 serde_json::json!({
1537 "type": "diff",
1538 "changes": [
1539 {
1540 "operation": "modify",
1541 "path": "/repo/config.json",
1542 "fileType": "text",
1543 "mimeType": "application/json"
1544 }
1545 ],
1546 "patch": {
1547 "format": "git_patch",
1548 "text": patch_text
1549 }
1550 })
1551 );
1552 }
1553
1554 #[test]
1555 fn diff_patch_requires_text() {
1556 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1557 "format": "git_patch",
1558 "diff": "diff --git /repo/config.json /repo/config.json\n"
1559 }));
1560
1561 assert!(result.is_err());
1562 }
1563
1564 #[test]
1565 fn diff_serializes_binary_modify_without_patch_text() {
1566 let diff = ToolCallContent::Diff(Diff::new(vec![
1567 DiffChange::modify("/repo/assets/logo.png")
1568 .file_type(DiffFileType::Binary)
1569 .mime_type("image/png"),
1570 ]));
1571
1572 assert_eq!(
1573 serde_json::to_value(diff).unwrap(),
1574 serde_json::json!({
1575 "type": "diff",
1576 "changes": [
1577 {
1578 "operation": "modify",
1579 "path": "/repo/assets/logo.png",
1580 "fileType": "binary",
1581 "mimeType": "image/png"
1582 }
1583 ]
1584 })
1585 );
1586 }
1587
1588 #[test]
1589 fn diff_move_serializes_shared_fields_with_operation_payload() {
1590 let diff = ToolCallContent::Diff(Diff::new(vec![
1591 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1592 .file_type(DiffFileType::Text)
1593 .mime_type("text/rust"),
1594 ]));
1595
1596 assert_eq!(
1597 serde_json::to_value(diff).unwrap(),
1598 serde_json::json!({
1599 "type": "diff",
1600 "changes": [
1601 {
1602 "operation": "move",
1603 "oldPath": "/repo/src/old.rs",
1604 "path": "/repo/src/new.rs",
1605 "fileType": "text",
1606 "mimeType": "text/rust"
1607 }
1608 ]
1609 })
1610 );
1611 }
1612
1613 #[test]
1614 fn diff_changes_skip_malformed_list_items() {
1615 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1616 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1617 "type": "diff",
1618 "changes": [
1619 {
1620 "operation": "modify"
1621 },
1622 {
1623 "operation": "delete",
1624 "path": "/ok"
1625 }
1626 ],
1627 "patch": {
1628 "format": "git_patch",
1629 "text": patch_text
1630 }
1631 }))
1632 .unwrap();
1633
1634 let ToolCallContent::Diff(diff) = content else {
1635 panic!("expected diff content");
1636 };
1637 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1638 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1639 }
1640
1641 #[test]
1642 fn tool_kind_preserves_unknown_variant() {
1643 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1644 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1645 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1646 }
1647
1648 #[test]
1649 fn tool_call_status_preserves_unknown_variant() {
1650 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1651 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1652 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1653 }
1654
1655 #[test]
1656 fn tool_call_status_recognizes_cancelled_variant() {
1657 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1658 assert_eq!(status, ToolCallStatus::Cancelled);
1659 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1660 }
1661
1662 #[test]
1663 fn tool_call_content_preserves_unknown_variant() {
1664 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1665 "type": "_chart",
1666 "title": "Tests",
1667 "data": [1, 2, 3]
1668 }))
1669 .unwrap();
1670
1671 let ToolCallContent::Other(unknown) = content else {
1672 panic!("expected unknown tool call content");
1673 };
1674
1675 assert_eq!(unknown.type_, "_chart");
1676 assert_eq!(
1677 unknown.fields.get("title"),
1678 Some(&serde_json::json!("Tests"))
1679 );
1680 assert_eq!(
1681 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1682 serde_json::json!({
1683 "type": "_chart",
1684 "title": "Tests",
1685 "data": [1, 2, 3]
1686 })
1687 );
1688 }
1689
1690 #[test]
1691 fn tool_call_content_does_not_hide_malformed_known_variant() {
1692 assert!(
1693 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1694 "type": "diff"
1695 }))
1696 .is_err()
1697 );
1698 assert!(
1699 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1700 "type": "terminal"
1701 }))
1702 .is_err()
1703 );
1704 }
1705}