1use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
8
9use derive_more::{Display, From};
10use schemars::{JsonSchema, Schema};
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14use super::{ContentBlock, Meta};
15use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
16
17#[serde_as]
31#[skip_serializing_none]
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "camelCase")]
34#[non_exhaustive]
35pub struct ToolCallUpdate {
36 pub tool_call_id: ToolCallId,
38 #[serde_as(deserialize_as = "DefaultOnError")]
40 #[schemars(extend("x-deserialize-default-on-error" = true))]
41 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
42 pub title: MaybeUndefined<String>,
43 #[serde_as(deserialize_as = "DefaultOnError")]
46 #[schemars(extend("x-deserialize-default-on-error" = true))]
47 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
48 pub kind: MaybeUndefined<ToolKind>,
49 #[serde_as(deserialize_as = "DefaultOnError")]
51 #[schemars(extend("x-deserialize-default-on-error" = true))]
52 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
53 pub status: MaybeUndefined<ToolCallStatus>,
54 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
56 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
57 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
58 pub content: MaybeUndefined<Vec<ToolCallContent>>,
59 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
62 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
63 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
64 pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
65 #[serde_as(deserialize_as = "DefaultOnError")]
67 #[schemars(extend("x-deserialize-default-on-error" = true))]
68 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
69 pub raw_input: MaybeUndefined<serde_json::Value>,
70 #[serde_as(deserialize_as = "DefaultOnError")]
72 #[schemars(extend("x-deserialize-default-on-error" = true))]
73 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
74 pub raw_output: MaybeUndefined<serde_json::Value>,
75 #[serde_as(deserialize_as = "DefaultOnError")]
81 #[schemars(extend("x-deserialize-default-on-error" = true))]
82 #[serde(default)]
83 #[serde(rename = "_meta")]
84 pub meta: Option<Meta>,
85}
86
87impl ToolCallUpdate {
88 #[must_use]
90 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
91 Self {
92 tool_call_id: tool_call_id.into(),
93 title: MaybeUndefined::Undefined,
94 kind: MaybeUndefined::Undefined,
95 status: MaybeUndefined::Undefined,
96 content: MaybeUndefined::Undefined,
97 locations: MaybeUndefined::Undefined,
98 raw_input: MaybeUndefined::Undefined,
99 raw_output: MaybeUndefined::Undefined,
100 meta: None,
101 }
102 }
103
104 #[must_use]
106 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
107 self.title = title.into_maybe_undefined();
108 self
109 }
110
111 #[must_use]
114 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
115 self.kind = kind.into_maybe_undefined();
116 self
117 }
118
119 #[must_use]
121 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
122 self.status = status.into_maybe_undefined();
123 self
124 }
125
126 #[must_use]
128 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
129 self.content = content.into_maybe_undefined();
130 self
131 }
132
133 #[must_use]
136 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
137 self.locations = locations.into_maybe_undefined();
138 self
139 }
140
141 #[must_use]
143 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
144 self.raw_input = raw_input.into_maybe_undefined();
145 self
146 }
147
148 #[must_use]
150 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
151 self.raw_output = raw_output.into_maybe_undefined();
152 self
153 }
154
155 #[must_use]
161 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
162 self.meta = meta.into_option();
163 self
164 }
165
166 pub fn apply_update(&mut self, update: ToolCallUpdate) {
171 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
172 if !update.title.is_undefined() {
173 self.title = update.title;
174 }
175 if !update.kind.is_undefined() {
176 self.kind = update.kind;
177 }
178 if !update.status.is_undefined() {
179 self.status = update.status;
180 }
181 if !update.content.is_undefined() {
182 self.content = update.content;
183 }
184 if !update.locations.is_undefined() {
185 self.locations = update.locations;
186 }
187 if !update.raw_input.is_undefined() {
188 self.raw_input = update.raw_input;
189 }
190 if !update.raw_output.is_undefined() {
191 self.raw_output = update.raw_output;
192 }
193 }
194}
195
196#[serde_as]
203#[skip_serializing_none]
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
205#[serde(rename_all = "camelCase")]
206#[non_exhaustive]
207pub struct ToolCallContentChunk {
208 pub tool_call_id: ToolCallId,
210 pub content: ToolCallContent,
212 #[serde_as(deserialize_as = "DefaultOnError")]
219 #[schemars(extend("x-deserialize-default-on-error" = true))]
220 #[serde(default)]
221 #[serde(rename = "_meta")]
222 pub meta: Option<Meta>,
223}
224
225impl ToolCallContentChunk {
226 #[must_use]
228 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
229 Self {
230 tool_call_id: tool_call_id.into(),
231 content: content.into(),
232 meta: None,
233 }
234 }
235
236 #[must_use]
242 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
243 self.meta = meta.into_option();
244 self
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
250#[serde(transparent)]
251#[from(Arc<str>, String, &'static str)]
252#[non_exhaustive]
253pub struct ToolCallId(pub Arc<str>);
254
255impl ToolCallId {
256 #[must_use]
258 pub fn new(id: impl Into<Arc<str>>) -> Self {
259 Self(id.into())
260 }
261}
262
263impl IntoOption<ToolCallId> for &str {
264 fn into_option(self) -> Option<ToolCallId> {
265 Some(ToolCallId::new(self))
266 }
267}
268
269#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
276#[serde(rename_all = "snake_case")]
277#[non_exhaustive]
278pub enum ToolKind {
279 Read,
281 Edit,
283 Delete,
285 Move,
287 Search,
289 Execute,
291 Think,
293 Fetch,
295 SwitchMode,
297 #[default]
299 Other,
300 #[serde(untagged)]
306 Unknown(String),
307}
308
309#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
315#[serde(rename_all = "snake_case")]
316#[non_exhaustive]
317pub enum ToolCallStatus {
318 #[default]
321 Pending,
322 InProgress,
324 Completed,
326 Failed,
328 #[serde(untagged)]
334 Other(String),
335}
336
337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
344#[serde(tag = "type", rename_all = "snake_case")]
345#[schemars(extend("discriminator" = {"propertyName": "type"}))]
346#[non_exhaustive]
347pub enum ToolCallContent {
348 Content(Box<Content>),
350 Diff(Diff),
352 #[serde(untagged)]
362 Other(OtherToolCallContent),
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
367#[schemars(inline)]
368#[schemars(transform = other_tool_call_content_schema)]
369#[serde(rename_all = "camelCase")]
370#[non_exhaustive]
371pub struct OtherToolCallContent {
372 #[serde(rename = "type")]
378 pub type_: String,
379 #[serde(flatten)]
381 pub fields: BTreeMap<String, serde_json::Value>,
382}
383
384impl OtherToolCallContent {
385 #[must_use]
387 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
388 fields.remove("type");
389 Self {
390 type_: type_.into(),
391 fields,
392 }
393 }
394}
395
396impl<'de> Deserialize<'de> for OtherToolCallContent {
397 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398 where
399 D: serde::Deserializer<'de>,
400 {
401 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
402 let type_ = fields
403 .remove("type")
404 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
405 let serde_json::Value::String(type_) = type_ else {
406 return Err(serde::de::Error::custom("`type` must be a string"));
407 };
408
409 if is_known_tool_call_content_type(&type_) {
410 return Err(serde::de::Error::custom(format!(
411 "known tool call content `{type_}` did not match its schema"
412 )));
413 }
414
415 Ok(Self { type_, fields })
416 }
417}
418
419fn is_known_tool_call_content_type(type_: &str) -> bool {
420 matches!(type_, "content" | "diff")
421}
422
423fn other_tool_call_content_schema(schema: &mut Schema) {
424 super::schema_util::reject_known_string_discriminators(schema, "type", &["content", "diff"]);
425}
426
427impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
428 fn from(content: T) -> Self {
429 ToolCallContent::Content(Box::new(Content::new(content)))
430 }
431}
432
433impl From<Diff> for ToolCallContent {
434 fn from(diff: Diff) -> Self {
435 ToolCallContent::Diff(diff)
436 }
437}
438
439#[serde_as]
441#[skip_serializing_none]
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
443#[serde(rename_all = "camelCase")]
444#[non_exhaustive]
445pub struct Content {
446 pub content: ContentBlock,
448 #[serde_as(deserialize_as = "DefaultOnError")]
454 #[schemars(extend("x-deserialize-default-on-error" = true))]
455 #[serde(default)]
456 #[serde(rename = "_meta")]
457 pub meta: Option<Meta>,
458}
459
460impl Content {
461 #[must_use]
463 pub fn new(content: impl Into<ContentBlock>) -> Self {
464 Self {
465 content: content.into(),
466 meta: None,
467 }
468 }
469
470 #[must_use]
476 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
477 self.meta = meta.into_option();
478 self
479 }
480}
481
482#[serde_as]
488#[skip_serializing_none]
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
490#[serde(rename_all = "camelCase")]
491#[non_exhaustive]
492pub struct Diff {
493 pub path: PathBuf,
495 #[serde_as(deserialize_as = "DefaultOnError")]
497 #[schemars(extend("x-deserialize-default-on-error" = true))]
498 #[serde(default)]
499 pub old_text: Option<String>,
500 pub new_text: String,
502 #[serde_as(deserialize_as = "DefaultOnError")]
508 #[schemars(extend("x-deserialize-default-on-error" = true))]
509 #[serde(default)]
510 #[serde(rename = "_meta")]
511 pub meta: Option<Meta>,
512}
513
514impl Diff {
515 #[must_use]
517 pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
518 Self {
519 path: path.into(),
520 old_text: None,
521 new_text: new_text.into(),
522 meta: None,
523 }
524 }
525
526 #[must_use]
528 pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
529 self.old_text = old_text.into_option();
530 self
531 }
532
533 #[must_use]
539 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
540 self.meta = meta.into_option();
541 self
542 }
543}
544
545#[serde_as]
552#[skip_serializing_none]
553#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
554#[serde(rename_all = "camelCase")]
555#[non_exhaustive]
556pub struct ToolCallLocation {
557 pub path: PathBuf,
559 #[serde_as(deserialize_as = "DefaultOnError")]
561 #[schemars(extend("x-deserialize-default-on-error" = true))]
562 #[serde(default)]
563 pub line: Option<u32>,
564 #[serde_as(deserialize_as = "DefaultOnError")]
570 #[schemars(extend("x-deserialize-default-on-error" = true))]
571 #[serde(default)]
572 #[serde(rename = "_meta")]
573 pub meta: Option<Meta>,
574}
575
576impl ToolCallLocation {
577 #[must_use]
579 pub fn new(path: impl Into<PathBuf>) -> Self {
580 Self {
581 path: path.into(),
582 line: None,
583 meta: None,
584 }
585 }
586
587 #[must_use]
589 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
590 self.line = line.into_option();
591 self
592 }
593
594 #[must_use]
600 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
601 self.meta = meta.into_option();
602 self
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use crate::MaybeUndefined;
610
611 #[test]
612 fn tool_call_serializes_as_upsert() {
613 let tool_call = ToolCallUpdate::new("tc_1")
614 .title("Reading configuration")
615 .status(ToolCallStatus::InProgress)
616 .raw_input(serde_json::json!({"path": "settings.json"}));
617
618 assert_eq!(
619 serde_json::to_value(tool_call).unwrap(),
620 serde_json::json!({
621 "toolCallId": "tc_1",
622 "title": "Reading configuration",
623 "status": "in_progress",
624 "rawInput": {
625 "path": "settings.json"
626 }
627 })
628 );
629 }
630
631 #[test]
632 fn tool_call_update_distinguishes_omitted_null_and_value() {
633 let tool_call = ToolCallUpdate::new("tc_1")
634 .status(ToolCallStatus::Completed)
635 .content(None::<Vec<ToolCallContent>>);
636
637 assert_eq!(
638 serde_json::to_value(tool_call).unwrap(),
639 serde_json::json!({
640 "toolCallId": "tc_1",
641 "status": "completed",
642 "content": null
643 })
644 );
645
646 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
647 "toolCallId": "tc_1",
648 "status": null,
649 "locations": []
650 }))
651 .unwrap();
652 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
653 assert_eq!(deserialized.status, MaybeUndefined::Null);
654 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
655 }
656
657 #[test]
658 fn tool_call_update_skips_malformed_list_items() {
659 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
660 "toolCallId": "tc_1",
661 "content": [
662 {
663 "type": "content",
664 "content": {
665 "type": "text",
666 "text": "ok"
667 }
668 },
669 {
670 "type": "diff",
671 "path": "/bad"
672 }
673 ],
674 "locations": [
675 {
676 "path": "/ok",
677 "line": 3
678 },
679 {
680 "line": 4
681 }
682 ]
683 }))
684 .unwrap();
685
686 let MaybeUndefined::Value(content) = deserialized.content else {
687 panic!("content should deserialize to a value");
688 };
689 assert_eq!(content.len(), 1);
690
691 let MaybeUndefined::Value(locations) = deserialized.locations else {
692 panic!("locations should deserialize to a value");
693 };
694 assert_eq!(locations.len(), 1);
695 }
696
697 #[test]
698 fn tool_call_content_chunk_serializes_single_content_item() {
699 let chunk = ToolCallContentChunk::new(
700 "tc_1",
701 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
702 );
703
704 assert_eq!(
705 serde_json::to_value(chunk).unwrap(),
706 serde_json::json!({
707 "toolCallId": "tc_1",
708 "content": {
709 "type": "content",
710 "content": {
711 "type": "text",
712 "text": "partial output"
713 }
714 }
715 })
716 );
717 }
718
719 #[test]
720 fn tool_kind_preserves_unknown_variant() {
721 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
722 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
723 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
724 }
725
726 #[test]
727 fn tool_call_status_preserves_unknown_variant() {
728 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
729 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
730 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
731 }
732
733 #[test]
734 fn tool_call_content_preserves_unknown_variant() {
735 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
736 "type": "_chart",
737 "title": "Tests",
738 "data": [1, 2, 3]
739 }))
740 .unwrap();
741
742 let ToolCallContent::Other(unknown) = content else {
743 panic!("expected unknown tool call content");
744 };
745
746 assert_eq!(unknown.type_, "_chart");
747 assert_eq!(
748 unknown.fields.get("title"),
749 Some(&serde_json::json!("Tests"))
750 );
751 assert_eq!(
752 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
753 serde_json::json!({
754 "type": "_chart",
755 "title": "Tests",
756 "data": [1, 2, 3]
757 })
758 );
759 }
760
761 #[test]
762 fn tool_call_content_does_not_hide_malformed_known_variant() {
763 assert!(
764 serde_json::from_value::<ToolCallContent>(serde_json::json!({
765 "type": "diff"
766 }))
767 .is_err()
768 );
769 }
770}