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};
16use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
17
18#[serde_as]
32#[skip_serializing_none]
33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36#[non_exhaustive]
37pub struct ToolCallUpdate {
38 pub tool_call_id: ToolCallId,
40 #[serde_as(deserialize_as = "DefaultOnError")]
47 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
48 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
49 pub name: MaybeUndefined<String>,
50 #[serde_as(deserialize_as = "DefaultOnError")]
52 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
53 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
54 pub title: MaybeUndefined<String>,
55 #[serde_as(deserialize_as = "DefaultOnError")]
58 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
59 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
60 pub kind: MaybeUndefined<ToolKind>,
61 #[serde_as(deserialize_as = "DefaultOnError")]
63 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
64 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
65 pub status: MaybeUndefined<ToolCallStatus>,
66 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
68 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
69 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
70 pub content: MaybeUndefined<Vec<ToolCallContent>>,
71 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
74 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
75 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
76 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
77 #[serde_as(deserialize_as = "DefaultOnError")]
79 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
80 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
81 pub raw_input: MaybeUndefined<serde_json::Value>,
82 #[serde_as(deserialize_as = "DefaultOnError")]
84 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
85 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
86 pub raw_output: MaybeUndefined<serde_json::Value>,
87 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
93 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
94 #[serde(
95 rename = "_meta",
96 default,
97 skip_serializing_if = "MaybeUndefined::is_undefined"
98 )]
99 pub meta: MaybeUndefined<Meta>,
100}
101
102impl ToolCallUpdate {
103 #[must_use]
105 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
106 Self {
107 tool_call_id: tool_call_id.into(),
108 name: MaybeUndefined::Undefined,
109 title: MaybeUndefined::Undefined,
110 kind: MaybeUndefined::Undefined,
111 status: MaybeUndefined::Undefined,
112 content: MaybeUndefined::Undefined,
113 locations: MaybeUndefined::Undefined,
114 raw_input: MaybeUndefined::Undefined,
115 raw_output: MaybeUndefined::Undefined,
116 meta: MaybeUndefined::Undefined,
117 }
118 }
119
120 #[must_use]
122 pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
123 self.name = name.into_maybe_undefined();
124 self
125 }
126
127 #[must_use]
129 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
130 self.title = title.into_maybe_undefined();
131 self
132 }
133
134 #[must_use]
137 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
138 self.kind = kind.into_maybe_undefined();
139 self
140 }
141
142 #[must_use]
144 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
145 self.status = status.into_maybe_undefined();
146 self
147 }
148
149 #[must_use]
151 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
152 self.content = content.into_maybe_undefined();
153 self
154 }
155
156 #[must_use]
159 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
160 self.locations = locations.into_maybe_undefined();
161 self
162 }
163
164 #[must_use]
166 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
167 self.raw_input = raw_input.into_maybe_undefined();
168 self
169 }
170
171 #[must_use]
173 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
174 self.raw_output = raw_output.into_maybe_undefined();
175 self
176 }
177
178 #[must_use]
184 pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
185 self.meta = meta.into_maybe_undefined();
186 self
187 }
188
189 pub fn apply_update(&mut self, update: ToolCallUpdate) {
194 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
195 if !update.name.is_undefined() {
196 self.name = update.name;
197 }
198 if !update.title.is_undefined() {
199 self.title = update.title;
200 }
201 if !update.kind.is_undefined() {
202 self.kind = update.kind;
203 }
204 if !update.status.is_undefined() {
205 self.status = update.status;
206 }
207 if !update.content.is_undefined() {
208 self.content = update.content;
209 }
210 if !update.locations.is_undefined() {
211 self.locations = update.locations;
212 }
213 if !update.raw_input.is_undefined() {
214 self.raw_input = update.raw_input;
215 }
216 if !update.raw_output.is_undefined() {
217 self.raw_output = update.raw_output;
218 }
219 if !update.meta.is_undefined() {
220 self.meta = update.meta;
221 }
222 }
223}
224
225#[serde_as]
232#[skip_serializing_none]
233#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236#[non_exhaustive]
237pub struct ToolCallContentChunk {
238 pub tool_call_id: ToolCallId,
240 pub content: ToolCallContent,
242 #[serde_as(deserialize_as = "DefaultOnError")]
248 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
249 #[serde(default)]
250 #[serde(rename = "_meta")]
251 pub meta: Option<Meta>,
252}
253
254impl ToolCallContentChunk {
255 #[must_use]
257 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
258 Self {
259 tool_call_id: tool_call_id.into(),
260 content: content.into(),
261 meta: None,
262 }
263 }
264
265 #[must_use]
271 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
272 self.meta = meta.into_option();
273 self
274 }
275}
276
277#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
280#[serde(transparent)]
281#[from(forward)]
282#[non_exhaustive]
283pub struct ToolCallId(pub Arc<str>);
284
285impl ToolCallId {
286 #[must_use]
288 pub fn new(id: impl Into<Self>) -> Self {
289 id.into()
290 }
291}
292
293impl IntoOption<ToolCallId> for &str {
294 fn into_option(self) -> Option<ToolCallId> {
295 Some(ToolCallId::new(self))
296 }
297}
298
299#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
306#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
307#[serde(rename_all = "snake_case")]
308#[non_exhaustive]
309pub enum ToolKind {
310 Read,
312 Edit,
314 Delete,
316 Move,
318 Search,
320 Execute,
322 Think,
324 Fetch,
326 SwitchMode,
328 #[default]
330 Other,
331 #[serde(untagged)]
337 Unknown(String),
338}
339
340#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
346#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
347#[serde(rename_all = "snake_case")]
348#[non_exhaustive]
349pub enum ToolCallStatus {
350 #[default]
353 Pending,
354 InProgress,
356 Completed,
358 Failed,
360 Cancelled,
362 #[serde(untagged)]
368 Other(String),
369}
370
371#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379#[serde(tag = "type", rename_all = "snake_case")]
380#[non_exhaustive]
381pub enum ToolCallContent {
382 Content(Box<Content>),
384 Diff(Diff),
386 Terminal(Terminal),
388 #[serde(untagged)]
398 Other(OtherToolCallContent),
399}
400
401#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
403#[derive(Debug, Clone, PartialEq, Serialize)]
404#[cfg_attr(feature = "schemars", schemars(inline))]
405#[cfg_attr(feature = "schemars", schemars(transform = other_tool_call_content_schema))]
406#[serde(rename_all = "camelCase")]
407#[non_exhaustive]
408pub struct OtherToolCallContent {
409 #[serde(rename = "type")]
415 pub type_: String,
416 #[serde(flatten)]
418 pub fields: BTreeMap<String, serde_json::Value>,
419}
420
421impl OtherToolCallContent {
422 #[must_use]
424 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
425 fields.remove("type");
426 Self {
427 type_: type_.into(),
428 fields,
429 }
430 }
431}
432
433impl<'de> Deserialize<'de> for OtherToolCallContent {
434 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435 where
436 D: serde::Deserializer<'de>,
437 {
438 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
439 let type_ = fields
440 .remove("type")
441 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
442 let serde_json::Value::String(type_) = type_ else {
443 return Err(serde::de::Error::custom("`type` must be a string"));
444 };
445
446 if is_known_tool_call_content_type(&type_) {
447 return Err(serde::de::Error::custom(format!(
448 "known tool call content `{type_}` did not match its schema"
449 )));
450 }
451
452 Ok(Self { type_, fields })
453 }
454}
455
456fn is_known_tool_call_content_type(type_: &str) -> bool {
457 matches!(type_, "content" | "diff" | "terminal")
458}
459
460#[cfg(feature = "schemars")]
461fn other_tool_call_content_schema(schema: &mut Schema) {
462 super::schema_util::reject_known_string_discriminators(
463 schema,
464 "type",
465 &["content", "diff", "terminal"],
466 );
467}
468
469impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
470 fn from(content: T) -> Self {
471 ToolCallContent::Content(Box::new(Content::new(content)))
472 }
473}
474
475impl From<Diff> for ToolCallContent {
476 fn from(diff: Diff) -> Self {
477 ToolCallContent::Diff(diff)
478 }
479}
480
481impl From<Terminal> for ToolCallContent {
482 fn from(terminal: Terminal) -> Self {
483 ToolCallContent::Terminal(terminal)
484 }
485}
486
487#[serde_as]
489#[skip_serializing_none]
490#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493#[non_exhaustive]
494pub struct Content {
495 pub content: ContentBlock,
497 #[serde_as(deserialize_as = "DefaultOnError")]
503 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
504 #[serde(default)]
505 #[serde(rename = "_meta")]
506 pub meta: Option<Meta>,
507}
508
509impl Content {
510 #[must_use]
512 pub fn new(content: impl Into<ContentBlock>) -> Self {
513 Self {
514 content: content.into(),
515 meta: None,
516 }
517 }
518
519 #[must_use]
525 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
526 self.meta = meta.into_option();
527 self
528 }
529}
530
531#[serde_as]
540#[skip_serializing_none]
541#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543#[serde(rename_all = "camelCase")]
544#[non_exhaustive]
545pub struct Diff {
546 #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
550 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
551 pub changes: Vec<DiffChange>,
552 #[serde_as(deserialize_as = "DefaultOnError")]
557 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
558 #[serde(default)]
559 pub patch: Option<DiffPatch>,
560 #[serde_as(deserialize_as = "DefaultOnError")]
566 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
567 #[serde(default)]
568 #[serde(rename = "_meta")]
569 pub meta: Option<Meta>,
570}
571
572impl Diff {
573 #[must_use]
575 pub fn new(changes: Vec<DiffChange>) -> Self {
576 Self {
577 changes,
578 patch: None,
579 meta: None,
580 }
581 }
582
583 #[must_use]
585 pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
586 Self::new(changes).with_patch(DiffPatch::new(text))
587 }
588
589 #[must_use]
591 pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
592 self.patch = patch.into_option();
593 self
594 }
595
596 #[must_use]
602 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
603 self.meta = meta.into_option();
604 self
605 }
606}
607
608#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611#[serde(rename_all = "camelCase")]
612#[non_exhaustive]
613pub struct DiffPatch {
614 pub format: DiffPatchFormat,
616 pub text: String,
618}
619
620impl DiffPatch {
621 #[must_use]
623 pub fn new(text: impl Into<String>) -> Self {
624 Self {
625 format: DiffPatchFormat::GitPatch,
626 text: text.into(),
627 }
628 }
629}
630
631#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "snake_case")]
635#[non_exhaustive]
636pub enum DiffPatchFormat {
637 GitPatch,
642 #[serde(untagged)]
648 Other(String),
649}
650
651#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(rename_all = "snake_case")]
655#[non_exhaustive]
656pub enum DiffFileType {
657 Text,
659 Binary,
661 Directory,
663 Symlink,
665 #[serde(untagged)]
671 Other(String),
672}
673
674#[serde_as]
679#[skip_serializing_none]
680#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682#[serde(rename_all = "camelCase")]
683#[non_exhaustive]
684pub struct DiffChange {
685 #[serde_as(deserialize_as = "DefaultOnError")]
689 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
690 #[serde(default)]
691 pub file_type: Option<DiffFileType>,
692 #[serde_as(deserialize_as = "DefaultOnError")]
696 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
697 #[serde(default)]
698 pub mime_type: Option<MediaType>,
699 #[serde(flatten)]
701 pub operation: DiffChangeOperation,
702 #[serde_as(deserialize_as = "DefaultOnError")]
708 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
709 #[serde(default)]
710 #[serde(rename = "_meta")]
711 pub meta: Option<Meta>,
712}
713
714impl DiffChange {
715 #[must_use]
717 pub fn new(operation: DiffChangeOperation) -> Self {
718 Self {
719 file_type: None,
720 mime_type: None,
721 operation,
722 meta: None,
723 }
724 }
725
726 #[must_use]
728 pub fn add(path: impl Into<AbsolutePath>) -> Self {
729 Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
730 }
731
732 #[must_use]
734 pub fn delete(path: impl Into<AbsolutePath>) -> Self {
735 Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
736 }
737
738 #[must_use]
740 pub fn modify(path: impl Into<AbsolutePath>) -> Self {
741 Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
742 }
743
744 #[must_use]
746 pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
747 Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
748 old_path, path,
749 )))
750 }
751
752 #[must_use]
754 pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
755 Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
756 old_path, path,
757 )))
758 }
759
760 #[must_use]
764 pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
765 self.file_type = file_type.into_option();
766 self
767 }
768
769 #[must_use]
773 pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
774 self.mime_type = mime_type.into_option();
775 self
776 }
777
778 #[must_use]
784 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
785 self.meta = meta.into_option();
786 self
787 }
788}
789
790#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(tag = "operation", rename_all = "snake_case")]
794#[non_exhaustive]
795pub enum DiffChangeOperation {
796 Add(DiffPathChange),
798 Delete(DiffPathChange),
800 Modify(DiffPathChange),
802 Move(DiffPathPairChange),
804 Copy(DiffPathPairChange),
806 #[serde(untagged)]
812 Other(OtherDiffChange),
813}
814
815#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818#[serde(rename_all = "camelCase")]
819#[non_exhaustive]
820pub struct DiffPathChange {
821 pub path: AbsolutePath,
823}
824
825impl DiffPathChange {
826 #[must_use]
828 pub fn new(path: impl Into<AbsolutePath>) -> Self {
829 Self { path: path.into() }
830 }
831}
832
833#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
835#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
836#[serde(rename_all = "camelCase")]
837#[non_exhaustive]
838pub struct DiffPathPairChange {
839 pub old_path: AbsolutePath,
841 pub path: AbsolutePath,
843}
844
845impl DiffPathPairChange {
846 #[must_use]
848 pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
849 Self {
850 old_path: old_path.into(),
851 path: path.into(),
852 }
853 }
854}
855
856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
858#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
859#[cfg_attr(feature = "schemars", schemars(inline))]
860#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
861#[serde(rename_all = "camelCase")]
862#[non_exhaustive]
863pub struct OtherDiffChange {
864 pub operation: String,
870 #[serde(flatten)]
872 pub fields: BTreeMap<String, serde_json::Value>,
873}
874
875impl OtherDiffChange {
876 #[must_use]
878 pub fn new(
879 operation: impl Into<String>,
880 mut fields: BTreeMap<String, serde_json::Value>,
881 ) -> Self {
882 fields.remove("operation");
883 fields.remove("fileType");
884 fields.remove("mimeType");
885 fields.remove("_meta");
886 Self {
887 operation: operation.into(),
888 fields,
889 }
890 }
891}
892
893impl<'de> Deserialize<'de> for OtherDiffChange {
894 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
895 where
896 D: serde::Deserializer<'de>,
897 {
898 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
899 let operation = fields
900 .remove("operation")
901 .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
902 let serde_json::Value::String(operation) = operation else {
903 return Err(serde::de::Error::custom("`operation` must be a string"));
904 };
905
906 if is_known_diff_change_operation(&operation) {
907 return Err(serde::de::Error::custom(format!(
908 "known diff change operation `{operation}` did not match its schema"
909 )));
910 }
911 fields.remove("fileType");
912 fields.remove("mimeType");
913 fields.remove("_meta");
914
915 Ok(Self { operation, fields })
916 }
917}
918
919fn is_known_diff_change_operation(operation: &str) -> bool {
920 matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
921}
922
923#[cfg(feature = "schemars")]
924fn other_diff_change_schema(schema: &mut Schema) {
925 super::schema_util::reject_known_string_discriminators(
926 schema,
927 "operation",
928 &["add", "delete", "modify", "move", "copy"],
929 );
930}
931
932#[serde_as]
939#[skip_serializing_none]
940#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
941#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
942#[serde(rename_all = "camelCase")]
943#[non_exhaustive]
944pub struct ToolCallLocation {
945 pub path: AbsolutePath,
947 #[serde_as(deserialize_as = "DefaultOnError")]
949 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
950 #[serde(default)]
951 pub line: Option<u32>,
952 #[serde_as(deserialize_as = "DefaultOnError")]
958 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
959 #[serde(default)]
960 #[serde(rename = "_meta")]
961 pub meta: Option<Meta>,
962}
963
964impl ToolCallLocation {
965 #[must_use]
967 pub fn new(path: impl Into<AbsolutePath>) -> Self {
968 Self {
969 path: path.into(),
970 line: None,
971 meta: None,
972 }
973 }
974
975 #[must_use]
977 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
978 self.line = line.into_option();
979 self
980 }
981
982 #[must_use]
988 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
989 self.meta = meta.into_option();
990 self
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997 use crate::MaybeUndefined;
998 use serde_json::{from_value, json, to_value};
999
1000 #[test]
1001 fn tool_call_serializes_as_upsert() {
1002 let tool_call = ToolCallUpdate::new("tc_1")
1003 .title("Reading configuration")
1004 .status(ToolCallStatus::InProgress)
1005 .raw_input(serde_json::json!({"path": "settings.json"}));
1006
1007 assert_eq!(
1008 serde_json::to_value(tool_call).unwrap(),
1009 serde_json::json!({
1010 "toolCallId": "tc_1",
1011 "title": "Reading configuration",
1012 "status": "in_progress",
1013 "rawInput": {
1014 "path": "settings.json"
1015 }
1016 })
1017 );
1018 }
1019
1020 #[test]
1021 fn tool_call_update_distinguishes_omitted_null_and_value() {
1022 let tool_call = ToolCallUpdate::new("tc_1")
1023 .status(ToolCallStatus::Completed)
1024 .content(None::<Vec<ToolCallContent>>);
1025
1026 assert_eq!(
1027 serde_json::to_value(tool_call).unwrap(),
1028 serde_json::json!({
1029 "toolCallId": "tc_1",
1030 "status": "completed",
1031 "content": null
1032 })
1033 );
1034
1035 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1036 "toolCallId": "tc_1",
1037 "status": null,
1038 "locations": []
1039 }))
1040 .unwrap();
1041 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1042 assert_eq!(deserialized.status, MaybeUndefined::Null);
1043 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1044 }
1045
1046 #[test]
1047 fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1048 let named = ToolCallUpdate::new("tc_1").name("read_file");
1049 assert_eq!(
1050 serde_json::to_value(named).unwrap(),
1051 serde_json::json!({
1052 "toolCallId": "tc_1",
1053 "name": "read_file"
1054 })
1055 );
1056
1057 let omitted = ToolCallUpdate::new("tc_1");
1058 assert_eq!(omitted.name, MaybeUndefined::Undefined);
1059
1060 let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1061 "toolCallId": "tc_1",
1062 "name": null
1063 }))
1064 .unwrap();
1065 assert_eq!(from_null.name, MaybeUndefined::Null);
1066
1067 let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1068 stored.apply_update(ToolCallUpdate::new("tc_1"));
1069 assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1070
1071 stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1072 assert_eq!(stored.name, MaybeUndefined::Null);
1073
1074 stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1075 assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1076 }
1077
1078 #[test]
1079 fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1080 let mut meta = Meta::new();
1081 meta.insert("source".to_string(), serde_json::json!("tool-call"));
1082
1083 assert_eq!(
1084 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1085 serde_json::json!({
1086 "toolCallId": "tc_1",
1087 "_meta": {
1088 "source": "tool-call"
1089 }
1090 })
1091 );
1092
1093 assert_eq!(
1094 serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1095 serde_json::json!({
1096 "toolCallId": "tc_1",
1097 "_meta": null
1098 })
1099 );
1100
1101 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1102 "toolCallId": "tc_1",
1103 "_meta": null
1104 }))
1105 .unwrap();
1106 assert_eq!(deserialized.meta, MaybeUndefined::Null);
1107
1108 let patch = ToolCallUpdate::new("tc_1");
1109 assert_eq!(patch.meta, MaybeUndefined::Undefined);
1110
1111 let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1112 stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1113 assert_eq!(stored.meta, MaybeUndefined::Null);
1114 }
1115
1116 #[test]
1117 fn tool_call_wire_patches_preserve_omitted_fields_and_replace_values() {
1118 let initial = json!({
1119 "toolCallId": "tc_1",
1120 "_meta": {"source": "replay", "opaque": {"sequence": 1}}
1121 });
1122 let mut stored: ToolCallUpdate = from_value(initial.clone()).unwrap();
1123 assert_eq!(to_value(&stored).unwrap(), initial);
1125
1126 let populated = json!({
1127 "toolCallId": "tc_1",
1128 "name": "read_file",
1129 "title": "Reading configuration",
1130 "kind": "read",
1131 "status": "in_progress",
1132 "content": [{
1133 "type": "content",
1134 "content": {
1135 "type": "text",
1136 "text": "old",
1137 "_meta": {"source": "tool"}
1138 }
1139 }],
1140 "locations": [{"path": "/workspace/config.json", "line": 3}],
1141 "rawInput": {"path": "/workspace/config.json"},
1142 "rawOutput": {"text": "old"}
1143 });
1144 stored.apply_update(from_value(populated.clone()).unwrap());
1145 let mut expected = populated;
1146 expected["_meta"] = initial["_meta"].clone();
1147 assert_eq!(to_value(&stored).unwrap(), expected);
1148
1149 for (field, empty) in [
1150 ("content", json!([])),
1151 ("locations", json!([])),
1152 ("rawInput", json!({})),
1153 ("rawOutput", json!({})),
1154 ("_meta", json!({})),
1155 ] {
1156 let original = expected[field].clone();
1157 for replacement in [empty, json!(null), original] {
1160 stored.apply_update(
1161 from_value(json!({"toolCallId": "tc_1", (field): replacement})).unwrap(),
1162 );
1163 expected[field] = replacement;
1164 assert_eq!(to_value(&stored).unwrap(), expected, "patching {field}");
1165
1166 stored.apply_update(from_value(json!({"toolCallId": "tc_1"})).unwrap());
1167 assert_eq!(to_value(&stored).unwrap(), expected, "omitting {field}");
1168 }
1169 }
1170 }
1171
1172 #[test]
1173 fn tool_call_wire_patches_preserve_unknown_statuses() {
1174 for status in ["deferred", "_awaiting_review"] {
1175 let mut stored = ToolCallUpdate::new("tc_1").status(ToolCallStatus::InProgress);
1176 stored
1177 .apply_update(from_value(json!({"toolCallId": "tc_1", "status": status})).unwrap());
1178 let expected_status = MaybeUndefined::Value(ToolCallStatus::Other(status.to_owned()));
1179 assert_eq!(stored.status, expected_status);
1180
1181 stored.apply_update(
1184 from_value(json!({"toolCallId": "tc_1", "title": "Still waiting"})).unwrap(),
1185 );
1186 assert_eq!(stored.status, expected_status);
1187 assert_eq!(
1188 to_value(&stored).unwrap(),
1189 json!({"toolCallId": "tc_1", "title": "Still waiting", "status": status})
1190 );
1191 }
1192 }
1193
1194 #[test]
1195 fn tool_call_update_skips_malformed_list_items() {
1196 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1197 "toolCallId": "tc_1",
1198 "content": [
1199 {
1200 "type": "content",
1201 "content": {
1202 "type": "text",
1203 "text": "ok"
1204 }
1205 },
1206 {
1207 "type": "diff",
1208 "path": "/bad"
1209 }
1210 ],
1211 "locations": [
1212 {
1213 "path": "/ok",
1214 "line": 3
1215 },
1216 {
1217 "line": 4
1218 }
1219 ]
1220 }))
1221 .unwrap();
1222
1223 let MaybeUndefined::Value(content) = deserialized.content else {
1224 panic!("content should deserialize to a value");
1225 };
1226 assert_eq!(content.len(), 1);
1227
1228 let MaybeUndefined::Value(locations) = deserialized.locations else {
1229 panic!("locations should deserialize to a value");
1230 };
1231 assert_eq!(locations.len(), 1);
1232 }
1233
1234 #[test]
1235 fn tool_call_content_chunk_serializes_single_content_item() {
1236 let chunk = ToolCallContentChunk::new(
1237 "tc_1",
1238 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1239 );
1240
1241 assert_eq!(
1242 serde_json::to_value(chunk).unwrap(),
1243 serde_json::json!({
1244 "toolCallId": "tc_1",
1245 "content": {
1246 "type": "content",
1247 "content": {
1248 "type": "text",
1249 "text": "partial output"
1250 }
1251 }
1252 })
1253 );
1254 }
1255
1256 #[test]
1257 fn terminal_content_serializes_as_display_reference() {
1258 let terminal = ToolCallContent::from(Terminal::new("term_1"));
1259
1260 assert_eq!(
1261 serde_json::to_value(terminal).unwrap(),
1262 serde_json::json!({
1263 "type": "terminal",
1264 "terminalId": "term_1"
1265 })
1266 );
1267 }
1268
1269 #[test]
1270 fn diff_patch_serializes_git_patch_with_structured_changes() {
1271 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";
1272 let diff = ToolCallContent::Diff(Diff::patch(
1273 patch_text,
1274 vec![
1275 DiffChange::modify("/repo/config.json")
1276 .file_type(DiffFileType::Text)
1277 .mime_type("application/json"),
1278 ],
1279 ));
1280
1281 assert_eq!(
1282 serde_json::to_value(diff).unwrap(),
1283 serde_json::json!({
1284 "type": "diff",
1285 "changes": [
1286 {
1287 "operation": "modify",
1288 "path": "/repo/config.json",
1289 "fileType": "text",
1290 "mimeType": "application/json"
1291 }
1292 ],
1293 "patch": {
1294 "format": "git_patch",
1295 "text": patch_text
1296 }
1297 })
1298 );
1299 }
1300
1301 #[test]
1302 fn diff_patch_requires_text() {
1303 let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1304 "format": "git_patch",
1305 "diff": "diff --git /repo/config.json /repo/config.json\n"
1306 }));
1307
1308 assert!(result.is_err());
1309 }
1310
1311 #[test]
1312 fn diff_serializes_binary_modify_without_patch_text() {
1313 let diff = ToolCallContent::Diff(Diff::new(vec![
1314 DiffChange::modify("/repo/assets/logo.png")
1315 .file_type(DiffFileType::Binary)
1316 .mime_type("image/png"),
1317 ]));
1318
1319 assert_eq!(
1320 serde_json::to_value(diff).unwrap(),
1321 serde_json::json!({
1322 "type": "diff",
1323 "changes": [
1324 {
1325 "operation": "modify",
1326 "path": "/repo/assets/logo.png",
1327 "fileType": "binary",
1328 "mimeType": "image/png"
1329 }
1330 ]
1331 })
1332 );
1333 }
1334
1335 #[test]
1336 fn diff_move_serializes_shared_fields_with_operation_payload() {
1337 let diff = ToolCallContent::Diff(Diff::new(vec![
1338 DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1339 .file_type(DiffFileType::Text)
1340 .mime_type("text/rust"),
1341 ]));
1342
1343 assert_eq!(
1344 serde_json::to_value(diff).unwrap(),
1345 serde_json::json!({
1346 "type": "diff",
1347 "changes": [
1348 {
1349 "operation": "move",
1350 "oldPath": "/repo/src/old.rs",
1351 "path": "/repo/src/new.rs",
1352 "fileType": "text",
1353 "mimeType": "text/rust"
1354 }
1355 ]
1356 })
1357 );
1358 }
1359
1360 #[test]
1361 fn diff_changes_skip_malformed_list_items() {
1362 let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1363 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1364 "type": "diff",
1365 "changes": [
1366 {
1367 "operation": "modify"
1368 },
1369 {
1370 "operation": "delete",
1371 "path": "/ok"
1372 }
1373 ],
1374 "patch": {
1375 "format": "git_patch",
1376 "text": patch_text
1377 }
1378 }))
1379 .unwrap();
1380
1381 let ToolCallContent::Diff(diff) = content else {
1382 panic!("expected diff content");
1383 };
1384 assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1385 assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1386 }
1387
1388 #[test]
1389 fn tool_kind_preserves_unknown_variant() {
1390 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1391 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1392 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1393 }
1394
1395 #[test]
1396 fn tool_call_status_preserves_unknown_variant() {
1397 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1398 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1399 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1400 }
1401
1402 #[test]
1403 fn tool_call_status_recognizes_cancelled_variant() {
1404 let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1405 assert_eq!(status, ToolCallStatus::Cancelled);
1406 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1407 }
1408
1409 #[test]
1410 fn tool_call_content_preserves_unknown_variant() {
1411 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1412 "type": "_chart",
1413 "title": "Tests",
1414 "data": [1, 2, 3]
1415 }))
1416 .unwrap();
1417
1418 let ToolCallContent::Other(unknown) = content else {
1419 panic!("expected unknown tool call content");
1420 };
1421
1422 assert_eq!(unknown.type_, "_chart");
1423 assert_eq!(
1424 unknown.fields.get("title"),
1425 Some(&serde_json::json!("Tests"))
1426 );
1427 assert_eq!(
1428 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1429 serde_json::json!({
1430 "type": "_chart",
1431 "title": "Tests",
1432 "data": [1, 2, 3]
1433 })
1434 );
1435 }
1436
1437 #[test]
1438 fn tool_call_content_does_not_hide_malformed_known_variant() {
1439 assert!(
1440 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1441 "type": "diff"
1442 }))
1443 .is_err()
1444 );
1445 assert!(
1446 serde_json::from_value::<ToolCallContent>(serde_json::json!({
1447 "type": "terminal"
1448 }))
1449 .is_err()
1450 );
1451 }
1452}