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")]
53 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
54 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
55 pub title: MaybeUndefined<String>,
56 #[serde_as(deserialize_as = "DefaultOnError")]
59 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
60 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
61 pub kind: MaybeUndefined<ToolKind>,
62 #[serde_as(deserialize_as = "DefaultOnError")]
64 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
65 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
66 pub status: MaybeUndefined<ToolCallStatus>,
67 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
69 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
70 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
71 pub content: MaybeUndefined<Vec<ToolCallContent>>,
72 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_>>>")]
75 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
76 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
77 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
78 #[serde_as(deserialize_as = "DefaultOnError")]
80 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
81 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
82 pub raw_input: MaybeUndefined<serde_json::Value>,
83 #[serde_as(deserialize_as = "DefaultOnError")]
85 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
86 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
87 pub raw_output: MaybeUndefined<serde_json::Value>,
88 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
97 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
98 #[serde(
99 rename = "_meta",
100 default,
101 skip_serializing_if = "MaybeUndefined::is_undefined"
102 )]
103 pub meta: MaybeUndefined<Meta>,
104}
105
106impl ToolCallUpdate {
107 #[must_use]
109 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
110 Self {
111 tool_call_id: tool_call_id.into(),
112 name: MaybeUndefined::Undefined,
113 title: MaybeUndefined::Undefined,
114 kind: MaybeUndefined::Undefined,
115 status: MaybeUndefined::Undefined,
116 content: MaybeUndefined::Undefined,
117 locations: MaybeUndefined::Undefined,
118 raw_input: MaybeUndefined::Undefined,
119 raw_output: MaybeUndefined::Undefined,
120 meta: MaybeUndefined::Undefined,
121 }
122 }
123
124 #[must_use]
126 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
127 self.name = name.into_maybe_undefined();
128 self
129 }
130
131 #[must_use]
133 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
134 self.title = title.into_maybe_undefined();
135 self
136 }
137
138 #[must_use]
141 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
142 self.kind = kind.into_maybe_undefined();
143 self
144 }
145
146 #[must_use]
148 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
149 self.status = status.into_maybe_undefined();
150 self
151 }
152
153 #[must_use]
155 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
156 self.content = content.into_maybe_undefined();
157 self
158 }
159
160 #[must_use]
163 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
164 self.locations = locations.into_maybe_undefined();
165 self
166 }
167
168 #[must_use]
170 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
171 self.raw_input = raw_input.into_maybe_undefined();
172 self
173 }
174
175 #[must_use]
177 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
178 self.raw_output = raw_output.into_maybe_undefined();
179 self
180 }
181
182 #[must_use]
187 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
188 self.meta = meta.into_maybe_undefined();
189 self
190 }
191
192 pub fn apply_update(&mut self, update: ToolCallUpdate) {
198 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
199 if !update.name.is_undefined() {
200 self.name = update.name;
201 }
202 if !update.title.is_undefined() {
203 self.title = update.title;
204 }
205 if !update.kind.is_undefined() {
206 self.kind = update.kind;
207 }
208 if !update.status.is_undefined() {
209 self.status = update.status;
210 }
211 if !update.content.is_undefined() {
212 self.content = update.content;
213 }
214 if !update.locations.is_undefined() {
215 self.locations = update.locations;
216 }
217 if !update.raw_input.is_undefined() {
218 self.raw_input = update.raw_input;
219 }
220 if !update.raw_output.is_undefined() {
221 self.raw_output = update.raw_output;
222 }
223 apply_meta_update(&mut self.meta, update.meta);
224 }
225}
226
227#[serde_as]
234#[skip_serializing_none]
235#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238#[non_exhaustive]
239pub struct ToolCallContentChunk {
240 pub tool_call_id: ToolCallId,
242 pub content: ToolCallContent,
244 #[serde_as(deserialize_as = "DefaultOnError")]
250 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
251 #[serde(default)]
252 #[serde(rename = "_meta")]
253 pub meta: Option<Meta>,
254}
255
256impl ToolCallContentChunk {
257 #[must_use]
259 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
260 Self {
261 tool_call_id: tool_call_id.into(),
262 content: content.into(),
263 meta: None,
264 }
265 }
266
267 #[must_use]
273 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
274 self.meta = meta.into_option();
275 self
276 }
277}
278
279#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
282#[serde(transparent)]
283#[from(forward)]
284#[non_exhaustive]
285pub struct ToolCallId(pub Arc<str>);
286
287impl ToolCallId {
288 #[must_use]
290 pub fn new(id: impl Into<Self>) -> Self {
291 id.into()
292 }
293}
294
295impl IntoOption<ToolCallId> for &str {
296 fn into_option(self) -> Option<ToolCallId> {
297 Some(ToolCallId::new(self))
298 }
299}
300
301#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "snake_case")]
310#[non_exhaustive]
311pub enum ToolKind {
312 Read,
314 Edit,
316 Delete,
318 Move,
320 Search,
322 Execute,
324 Think,
326 Fetch,
328 SwitchMode,
330 #[default]
332 Other,
333 #[serde(untagged)]
339 Unknown(String),
340}
341
342#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
348#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
349#[serde(rename_all = "snake_case")]
350#[non_exhaustive]
351pub enum ToolCallStatus {
352 #[default]
355 Pending,
356 InProgress,
358 Completed,
360 Failed,
362 Cancelled,
364 #[serde(untagged)]
370 Other(String),
371}
372
373#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(tag = "type", rename_all = "snake_case")]
382#[non_exhaustive]
383pub enum ToolCallContent {
384 Content(Box<Content>),
386 Diff(Diff),
388 Terminal(Terminal),
390 #[serde(untagged)]
400 Other(OtherToolCallContent),
401}
402
403#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
405#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
406#[cfg_attr(feature = "schemars", schemars(inline))]
407#[cfg_attr(feature = "schemars", schemars(transform = other_tool_call_content_schema))]
408#[serde(rename_all = "camelCase")]
409#[non_exhaustive]
410pub struct OtherToolCallContent {
411 #[serde(rename = "type")]
417 pub type_: String,
418 #[serde(flatten)]
420 pub fields: BTreeMap<String, serde_json::Value>,
421}
422
423impl OtherToolCallContent {
424 #[must_use]
426 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
427 fields.remove("type");
428 Self {
429 type_: type_.into(),
430 fields,
431 }
432 }
433}
434
435impl<'de> Deserialize<'de> for OtherToolCallContent {
436 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
437 where
438 D: serde::Deserializer<'de>,
439 {
440 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
441 let type_ = fields
442 .remove("type")
443 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
444 let serde_json::Value::String(type_) = type_ else {
445 return Err(serde::de::Error::custom("`type` must be a string"));
446 };
447
448 if is_known_tool_call_content_type(&type_) {
449 return Err(serde::de::Error::custom(format!(
450 "known tool call content `{type_}` did not match its schema"
451 )));
452 }
453
454 Ok(Self { type_, fields })
455 }
456}
457
458fn is_known_tool_call_content_type(type_: &str) -> bool {
459 matches!(type_, "content" | "diff" | "terminal")
460}
461
462#[cfg(feature = "schemars")]
463fn other_tool_call_content_schema(schema: &mut Schema) {
464 super::schema_util::reject_known_string_discriminators(
465 schema,
466 "type",
467 &["content", "diff", "terminal"],
468 );
469}
470
471impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
472 fn from(content: T) -> Self {
473 ToolCallContent::Content(Box::new(Content::new(content)))
474 }
475}
476
477impl From<Diff> for ToolCallContent {
478 fn from(diff: Diff) -> Self {
479 ToolCallContent::Diff(diff)
480 }
481}
482
483impl From<Terminal> for ToolCallContent {
484 fn from(terminal: Terminal) -> Self {
485 ToolCallContent::Terminal(terminal)
486 }
487}
488
489#[serde_as]
491#[skip_serializing_none]
492#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494#[serde(rename_all = "camelCase")]
495#[non_exhaustive]
496pub struct Content {
497 pub content: ContentBlock,
499 #[serde_as(deserialize_as = "DefaultOnError")]
505 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
506 #[serde(default)]
507 #[serde(rename = "_meta")]
508 pub meta: Option<Meta>,
509}
510
511impl Content {
512 #[must_use]
514 pub fn new(content: impl Into<ContentBlock>) -> Self {
515 Self {
516 content: content.into(),
517 meta: None,
518 }
519 }
520
521 #[must_use]
527 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
528 self.meta = meta.into_option();
529 self
530 }
531}
532
533#[serde_as]
542#[skip_serializing_none]
543#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
544#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
545#[serde(rename_all = "camelCase")]
546#[non_exhaustive]
547pub struct Diff {
548 #[serde_as(deserialize_as = "VecSkipError<_>")]
552 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
553 pub changes: Vec<DiffChange>,
554 #[serde_as(deserialize_as = "DefaultOnError")]
559 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
560 #[serde(default)]
561 pub patch: Option<DiffPatch>,
562 #[serde_as(deserialize_as = "DefaultOnError")]
568 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
569 #[serde(default)]
570 #[serde(rename = "_meta")]
571 pub meta: Option<Meta>,
572}
573
574impl Diff {
575 #[must_use]
577 pub fn new(changes: Vec<DiffChange>) -> Self {
578 Self {
579 changes,
580 patch: None,
581 meta: None,
582 }
583 }
584
585 #[must_use]
587 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
588 Self::new(changes).with_patch(DiffPatch::new(text))
589 }
590
591 #[must_use]
593 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
594 self.patch = patch.into_option();
595 self
596 }
597
598 #[must_use]
604 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
605 self.meta = meta.into_option();
606 self
607 }
608}
609
610#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
612#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
613#[serde(rename_all = "camelCase")]
614#[non_exhaustive]
615pub struct DiffPatch {
616 pub format: DiffPatchFormat,
618 pub text: String,
620}
621
622impl DiffPatch {
623 #[must_use]
625 pub fn new(text: impl Into<String>) -> Self {
626 Self {
627 format: DiffPatchFormat::GitPatch,
628 text: text.into(),
629 }
630 }
631}
632
633#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(rename_all = "snake_case")]
637#[non_exhaustive]
638pub enum DiffPatchFormat {
639 GitPatch,
650 #[serde(untagged)]
656 Other(String),
657}
658
659#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
662#[serde(rename_all = "snake_case")]
663#[non_exhaustive]
664pub enum DiffFileType {
665 Text,
667 Binary,
669 Directory,
671 Symlink,
673 #[serde(untagged)]
679 Other(String),
680}
681
682#[serde_as]
687#[skip_serializing_none]
688#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
690#[serde(rename_all = "camelCase")]
691#[non_exhaustive]
692pub struct DiffChange {
693 #[serde_as(deserialize_as = "DefaultOnError")]
697 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
698 #[serde(default)]
699 pub file_type: Option<DiffFileType>,
700 #[serde_as(deserialize_as = "DefaultOnError")]
704 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
705 #[serde(default)]
706 pub mime_type: Option<MediaType>,
707 #[serde(flatten)]
709 pub operation: DiffChangeOperation,
710 #[serde_as(deserialize_as = "DefaultOnError")]
716 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
717 #[serde(default)]
718 #[serde(rename = "_meta")]
719 pub meta: Option<Meta>,
720}
721
722impl DiffChange {
723 #[must_use]
725 pub fn new(operation: DiffChangeOperation) -> Self {
726 Self {
727 file_type: None,
728 mime_type: None,
729 operation,
730 meta: None,
731 }
732 }
733
734 #[must_use]
736 pub fn add(path: impl Into<AbsolutePath>) -> Self {
737 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
738 }
739
740 #[must_use]
742 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
743 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
744 }
745
746 #[must_use]
748 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
749 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
750 }
751
752 #[must_use]
754 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
755 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
756 old_path, path,
757 )))
758 }
759
760 #[must_use]
762 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
763 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
764 old_path, path,
765 )))
766 }
767
768 #[must_use]
772 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
773 self.file_type = file_type.into_option();
774 self
775 }
776
777 #[must_use]
781 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
782 self.mime_type = mime_type.into_option();
783 self
784 }
785
786 #[must_use]
792 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
793 self.meta = meta.into_option();
794 self
795 }
796}
797
798#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
801#[serde(tag = "operation", rename_all = "snake_case")]
802#[non_exhaustive]
803pub enum DiffChangeOperation {
804 Add(DiffPathChange),
806 Delete(DiffPathChange),
808 Modify(DiffPathChange),
810 Move(DiffPathPairChange),
812 Copy(DiffPathPairChange),
814 #[serde(untagged)]
820 Other(OtherDiffChange),
821}
822
823#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
826#[serde(rename_all = "camelCase")]
827#[non_exhaustive]
828pub struct DiffPathChange {
829 pub path: AbsolutePath,
831}
832
833impl DiffPathChange {
834 #[must_use]
836 pub fn new(path: impl Into<AbsolutePath>) -> Self {
837 Self { path: path.into() }
838 }
839}
840
841#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
843#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845#[non_exhaustive]
846pub struct DiffPathPairChange {
847 pub old_path: AbsolutePath,
849 pub path: AbsolutePath,
851}
852
853impl DiffPathPairChange {
854 #[must_use]
856 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
857 Self {
858 old_path: old_path.into(),
859 path: path.into(),
860 }
861 }
862}
863
864#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
866#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
867#[cfg_attr(feature = "schemars", schemars(inline))]
868#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
869#[serde(rename_all = "camelCase")]
870#[non_exhaustive]
871pub struct OtherDiffChange {
872 pub operation: String,
878 #[serde(flatten)]
880 pub fields: BTreeMap<String, serde_json::Value>,
881}
882
883impl OtherDiffChange {
884 #[must_use]
886 pub fn new(
887 operation: impl Into<String>,
888 mut fields: BTreeMap<String, serde_json::Value>,
889 ) -> Self {
890 fields.remove("operation");
891 fields.remove("fileType");
892 fields.remove("mimeType");
893 fields.remove("_meta");
894 Self {
895 operation: operation.into(),
896 fields,
897 }
898 }
899}
900
901impl<'de> Deserialize<'de> for OtherDiffChange {
902 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
903 where
904 D: serde::Deserializer<'de>,
905 {
906 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
907 let operation = fields
908 .remove("operation")
909 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
910 let serde_json::Value::String(operation) = operation else {
911 return Err(serde::de::Error::custom("`operation` must be a string"));
912 };
913
914 if is_known_diff_change_operation(&operation) {
915 return Err(serde::de::Error::custom(format!(
916 "known diff change operation `{operation}` did not match its schema"
917 )));
918 }
919 fields.remove("fileType");
920 fields.remove("mimeType");
921 fields.remove("_meta");
922
923 Ok(Self { operation, fields })
924 }
925}
926
927fn is_known_diff_change_operation(operation: &str) -> bool {
928 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
929}
930
931#[cfg(feature = "schemars")]
932fn other_diff_change_schema(schema: &mut Schema) {
933 super::schema_util::reject_known_string_discriminators(
934 schema,
935 "operation",
936 &["add", "delete", "modify", "move", "copy"],
937 );
938}
939
940#[serde_as]
947#[skip_serializing_none]
948#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
949#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
950#[serde(rename_all = "camelCase")]
951#[non_exhaustive]
952pub struct ToolCallLocation {
953 pub path: AbsolutePath,
955 #[serde_as(deserialize_as = "DefaultOnError")]
957 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
958 #[serde(default)]
959 pub line: Option<u32>,
960 #[serde_as(deserialize_as = "DefaultOnError")]
966 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
967 #[serde(default)]
968 #[serde(rename = "_meta")]
969 pub meta: Option<Meta>,
970}
971
972impl ToolCallLocation {
973 #[must_use]
975 pub fn new(path: impl Into<AbsolutePath>) -> Self {
976 Self {
977 path: path.into(),
978 line: None,
979 meta: None,
980 }
981 }
982
983 #[must_use]
985 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
986 self.line = line.into_option();
987 self
988 }
989
990 #[must_use]
996 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
997 self.meta = meta.into_option();
998 self
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use crate::MaybeUndefined;
1006 use serde_json::{from_value, json, to_value};
1007
1008 #[test]
1009 fn tool_call_serializes_as_upsert() {
1010 let tool_call = ToolCallUpdate::new("tc_1")
1011 .title("Reading configuration")
1012 .status(ToolCallStatus::InProgress)
1013 .raw_input(serde_json::json!({"path": "settings.json"}));
1014
1015 assert_eq!(
1016 serde_json::to_value(tool_call).unwrap(),
1017 serde_json::json!({
1018 "toolCallId": "tc_1",
1019 "title": "Reading configuration",
1020 "status": "in_progress",
1021 "rawInput": {
1022 "path": "settings.json"
1023 }
1024 })
1025 );
1026 }
1027
1028 #[test]
1029 fn tool_call_update_distinguishes_omitted_null_and_value() {
1030 let tool_call = ToolCallUpdate::new("tc_1")
1031 .status(ToolCallStatus::Completed)
1032 .content(None::<Vec<ToolCallContent>>);
1033
1034 assert_eq!(
1035 serde_json::to_value(tool_call).unwrap(),
1036 serde_json::json!({
1037 "toolCallId": "tc_1",
1038 "status": "completed",
1039 "content": null
1040 })
1041 );
1042
1043 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1044 "toolCallId": "tc_1",
1045 "status": null,
1046 "locations": []
1047 }))
1048 .unwrap();
1049 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1050 assert_eq!(deserialized.status, MaybeUndefined::Null);
1051 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1052 }
1053
1054 #[test]
1055 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1056 let named = ToolCallUpdate::new("tc_1").name("read_file");
1057 assert_eq!(
1058 serde_json::to_value(named).unwrap(),
1059 serde_json::json!({
1060 "toolCallId": "tc_1",
1061 "name": "read_file"
1062 })
1063 );
1064
1065 let omitted = ToolCallUpdate::new("tc_1");
1066 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1067
1068 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1069 "toolCallId": "tc_1",
1070 "name": null
1071 }))
1072 .unwrap();
1073 assert_eq!(from_null.name, MaybeUndefined::Null);
1074
1075 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1076 stored.apply_update(ToolCallUpdate::new("tc_1"));
1077 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1078
1079 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1080 assert_eq!(stored.name, MaybeUndefined::Null);
1081
1082 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1083 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1084 }
1085
1086 #[test]
1087 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1088 let mut meta = Meta::new();
1089 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1090
1091 assert_eq!(
1092 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1093 serde_json::json!({
1094 "toolCallId": "tc_1",
1095 "_meta": {
1096 "source": "tool-call"
1097 }
1098 })
1099 );
1100
1101 assert_eq!(
1102 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1103 serde_json::json!({
1104 "toolCallId": "tc_1",
1105 "_meta": null
1106 })
1107 );
1108
1109 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1110 "toolCallId": "tc_1",
1111 "_meta": null
1112 }))
1113 .unwrap();
1114 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1115
1116 let patch = ToolCallUpdate::new("tc_1");
1117 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1118
1119 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1120 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1121 assert_eq!(stored.meta, MaybeUndefined::Null);
1122 }
1123
1124 #[test]
1125 fn tool_call_update_patches_meta_by_top_level_key() {
1126 let mut stored: ToolCallUpdate = from_value(json!({
1127 "toolCallId": "tc_1",
1128 "_meta": {"zed.dev/a": 1, "acme.dev/b": {"x": 1}}
1129 }))
1130 .unwrap();
1131
1132 stored.apply_update(
1134 from_value(json!({"toolCallId": "tc_1", "_meta": {"proxy.dev/c": true}})).unwrap(),
1135 );
1136 stored.apply_update(
1137 from_value(json!({
1138 "toolCallId": "tc_1",
1139 "_meta": {"zed.dev/a": null, "acme.dev/b": {"y": 2}}
1140 }))
1141 .unwrap(),
1142 );
1143
1144 assert_eq!(
1145 to_value(&stored).unwrap(),
1146 json!({
1147 "toolCallId": "tc_1",
1148 "_meta": {"acme.dev/b": {"y": 2}, "proxy.dev/c": true}
1149 })
1150 );
1151 }
1152
1153 #[test]
1154 fn tool_call_wire_patches_preserve_omitted_fields_and_replace_values() {
1155 let initial = json!({
1156 "toolCallId": "tc_1",
1157 "_meta": {"source": "replay", "opaque": {"sequence": 1}}
1158 });
1159 let mut stored: ToolCallUpdate = from_value(initial.clone()).unwrap();
1160 assert_eq!(to_value(&stored).unwrap(), initial);
1162
1163 let populated = json!({
1164 "toolCallId": "tc_1",
1165 "name": "read_file",
1166 "title": "Reading configuration",
1167 "kind": "read",
1168 "status": "in_progress",
1169 "content": [{
1170 "type": "content",
1171 "content": {
1172 "type": "text",
1173 "text": "old",
1174 "_meta": {"source": "tool"}
1175 }
1176 }],
1177 "locations": [{"path": "/workspace/config.json", "line": 3}],
1178 "rawInput": {"path": "/workspace/config.json"},
1179 "rawOutput": {"text": "old"}
1180 });
1181 stored.apply_update(from_value(populated.clone()).unwrap());
1182 let mut expected = populated;
1183 expected["_meta"] = initial["_meta"].clone();
1184 assert_eq!(to_value(&stored).unwrap(), expected);
1185
1186 for (field, empty) in [
1187 ("content", json!([])),
1188 ("locations", json!([])),
1189 ("rawInput", json!({})),
1190 ("rawOutput", json!({})),
1191 ] {
1192 let original = expected[field].clone();
1193 for replacement in [empty, json!(null), original] {
1196 stored.apply_update(
1197 from_value(json!({"toolCallId": "tc_1", (field): replacement})).unwrap(),
1198 );
1199 expected[field] = replacement;
1200 assert_eq!(to_value(&stored).unwrap(), expected, "patching {field}");
1201
1202 stored.apply_update(from_value(json!({"toolCallId": "tc_1"})).unwrap());
1203 assert_eq!(to_value(&stored).unwrap(), expected, "omitting {field}");
1204 }
1205 }
1206 }
1207
1208 #[test]
1209 fn tool_call_wire_patches_preserve_unknown_statuses() {
1210 for status in ["deferred", "_awaiting_review"] {
1211 let mut stored = ToolCallUpdate::new("tc_1").status(ToolCallStatus::InProgress);
1212 stored
1213 .apply_update(from_value(json!({"toolCallId": "tc_1", "status": status})).unwrap());
1214 let expected_status = MaybeUndefined::Value(ToolCallStatus::Other(status.to_owned()));
1215 assert_eq!(stored.status, expected_status);
1216
1217 stored.apply_update(
1220 from_value(json!({"toolCallId": "tc_1", "title": "Still waiting"})).unwrap(),
1221 );
1222 assert_eq!(stored.status, expected_status);
1223 assert_eq!(
1224 to_value(&stored).unwrap(),
1225 json!({"toolCallId": "tc_1", "title": "Still waiting", "status": status})
1226 );
1227 }
1228 }
1229
1230 #[test]
1231 fn tool_call_update_skips_malformed_list_items() {
1232 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1233 "toolCallId": "tc_1",
1234 "content": [
1235 {
1236 "type": "content",
1237 "content": {
1238 "type": "text",
1239 "text": "ok"
1240 }
1241 },
1242 {
1243 "type": "diff",
1244 "path": "/bad"
1245 }
1246 ],
1247 "locations": [
1248 {
1249 "path": "/ok",
1250 "line": 3
1251 },
1252 {
1253 "line": 4
1254 }
1255 ]
1256 }))
1257 .unwrap();
1258
1259 let MaybeUndefined::Value(content) = deserialized.content else {
1260 panic!("content should deserialize to a value");
1261 };
1262 assert_eq!(content.len(), 1);
1263
1264 let MaybeUndefined::Value(locations) = deserialized.locations else {
1265 panic!("locations should deserialize to a value");
1266 };
1267 assert_eq!(locations.len(), 1);
1268 }
1269
1270 #[test]
1271 fn tool_call_content_chunk_serializes_single_content_item() {
1272 let chunk = ToolCallContentChunk::new(
1273 "tc_1",
1274 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1275 );
1276
1277 assert_eq!(
1278 serde_json::to_value(chunk).unwrap(),
1279 serde_json::json!({
1280 "toolCallId": "tc_1",
1281 "content": {
1282 "type": "content",
1283 "content": {
1284 "type": "text",
1285 "text": "partial output"
1286 }
1287 }
1288 })
1289 );
1290 }
1291
1292 #[test]
1293 fn terminal_content_serializes_as_display_reference() {
1294 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1295
1296 assert_eq!(
1297 serde_json::to_value(terminal).unwrap(),
1298 serde_json::json!({
1299 "type": "terminal",
1300 "terminalId": "term_1"
1301 })
1302 );
1303 }
1304
1305 #[test]
1306 fn diff_patch_serializes_git_patch_with_structured_changes() {
1307 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";
1308 let diff = ToolCallContent::Diff(Diff::patch(
1309 patch_text,
1310 vec![
1311 DiffChange::modify("/repo/config.json")
1312 .file_type(DiffFileType::Text)
1313 .mime_type("application/json"),
1314 ],
1315 ));
1316
1317 assert_eq!(
1318 serde_json::to_value(diff).unwrap(),
1319 serde_json::json!({
1320 "type": "diff",
1321 "changes": [
1322 {
1323 "operation": "modify",
1324 "path": "/repo/config.json",
1325 "fileType": "text",
1326 "mimeType": "application/json"
1327 }
1328 ],
1329 "patch": {
1330 "format": "git_patch",
1331 "text": patch_text
1332 }
1333 })
1334 );
1335 }
1336
1337 #[test]
1338 fn diff_patch_requires_text() {
1339 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1340 "format": "git_patch",
1341 "diff": "diff --git /repo/config.json /repo/config.json\n"
1342 }));
1343
1344 assert!(result.is_err());
1345 }
1346
1347 #[test]
1348 fn diff_serializes_binary_modify_without_patch_text() {
1349 let diff = ToolCallContent::Diff(Diff::new(vec![
1350 DiffChange::modify("/repo/assets/logo.png")
1351 .file_type(DiffFileType::Binary)
1352 .mime_type("image/png"),
1353 ]));
1354
1355 assert_eq!(
1356 serde_json::to_value(diff).unwrap(),
1357 serde_json::json!({
1358 "type": "diff",
1359 "changes": [
1360 {
1361 "operation": "modify",
1362 "path": "/repo/assets/logo.png",
1363 "fileType": "binary",
1364 "mimeType": "image/png"
1365 }
1366 ]
1367 })
1368 );
1369 }
1370
1371 #[test]
1372 fn diff_move_serializes_shared_fields_with_operation_payload() {
1373 let diff = ToolCallContent::Diff(Diff::new(vec![
1374 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1375 .file_type(DiffFileType::Text)
1376 .mime_type("text/rust"),
1377 ]));
1378
1379 assert_eq!(
1380 serde_json::to_value(diff).unwrap(),
1381 serde_json::json!({
1382 "type": "diff",
1383 "changes": [
1384 {
1385 "operation": "move",
1386 "oldPath": "/repo/src/old.rs",
1387 "path": "/repo/src/new.rs",
1388 "fileType": "text",
1389 "mimeType": "text/rust"
1390 }
1391 ]
1392 })
1393 );
1394 }
1395
1396 #[test]
1397 fn diff_changes_skip_malformed_list_items() {
1398 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1399 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1400 "type": "diff",
1401 "changes": [
1402 {
1403 "operation": "modify"
1404 },
1405 {
1406 "operation": "delete",
1407 "path": "/ok"
1408 }
1409 ],
1410 "patch": {
1411 "format": "git_patch",
1412 "text": patch_text
1413 }
1414 }))
1415 .unwrap();
1416
1417 let ToolCallContent::Diff(diff) = content else {
1418 panic!("expected diff content");
1419 };
1420 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1421 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1422 }
1423
1424 #[test]
1425 fn tool_kind_preserves_unknown_variant() {
1426 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1427 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1428 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1429 }
1430
1431 #[test]
1432 fn tool_call_status_preserves_unknown_variant() {
1433 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1434 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1435 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1436 }
1437
1438 #[test]
1439 fn tool_call_status_recognizes_cancelled_variant() {
1440 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1441 assert_eq!(status, ToolCallStatus::Cancelled);
1442 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1443 }
1444
1445 #[test]
1446 fn tool_call_content_preserves_unknown_variant() {
1447 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1448 "type": "_chart",
1449 "title": "Tests",
1450 "data": [1, 2, 3]
1451 }))
1452 .unwrap();
1453
1454 let ToolCallContent::Other(unknown) = content else {
1455 panic!("expected unknown tool call content");
1456 };
1457
1458 assert_eq!(unknown.type_, "_chart");
1459 assert_eq!(
1460 unknown.fields.get("title"),
1461 Some(&serde_json::json!("Tests"))
1462 );
1463 assert_eq!(
1464 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1465 serde_json::json!({
1466 "type": "_chart",
1467 "title": "Tests",
1468 "data": [1, 2, 3]
1469 })
1470 );
1471 }
1472
1473 #[test]
1474 fn tool_call_content_does_not_hide_malformed_known_variant() {
1475 assert!(
1476 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1477 "type": "diff"
1478 }))
1479 .is_err()
1480 );
1481 assert!(
1482 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1483 "type": "terminal"
1484 }))
1485 .is_err()
1486 );
1487 }
1488}