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(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
40 pub title: MaybeUndefined<String>,
41 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
44 pub kind: MaybeUndefined<ToolKind>,
45 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
47 pub status: MaybeUndefined<ToolCallStatus>,
48 #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
50 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
51 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
52 pub content: MaybeUndefined<Vec<ToolCallContent>>,
53 #[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 locations: MaybeUndefined<Vec<ToolCallLocation>>,
59 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
61 pub raw_input: MaybeUndefined<serde_json::Value>,
62 #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
64 pub raw_output: MaybeUndefined<serde_json::Value>,
65 #[serde(rename = "_meta")]
71 pub meta: Option<Meta>,
72}
73
74impl ToolCallUpdate {
75 #[must_use]
77 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
78 Self {
79 tool_call_id: tool_call_id.into(),
80 title: MaybeUndefined::Undefined,
81 kind: MaybeUndefined::Undefined,
82 status: MaybeUndefined::Undefined,
83 content: MaybeUndefined::Undefined,
84 locations: MaybeUndefined::Undefined,
85 raw_input: MaybeUndefined::Undefined,
86 raw_output: MaybeUndefined::Undefined,
87 meta: None,
88 }
89 }
90
91 #[must_use]
93 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
94 self.title = title.into_maybe_undefined();
95 self
96 }
97
98 #[must_use]
101 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
102 self.kind = kind.into_maybe_undefined();
103 self
104 }
105
106 #[must_use]
108 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
109 self.status = status.into_maybe_undefined();
110 self
111 }
112
113 #[must_use]
115 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
116 self.content = content.into_maybe_undefined();
117 self
118 }
119
120 #[must_use]
123 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
124 self.locations = locations.into_maybe_undefined();
125 self
126 }
127
128 #[must_use]
130 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
131 self.raw_input = raw_input.into_maybe_undefined();
132 self
133 }
134
135 #[must_use]
137 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
138 self.raw_output = raw_output.into_maybe_undefined();
139 self
140 }
141
142 #[must_use]
148 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
149 self.meta = meta.into_option();
150 self
151 }
152
153 pub fn apply_update(&mut self, update: ToolCallUpdate) {
158 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
159 if !update.title.is_undefined() {
160 self.title = update.title;
161 }
162 if !update.kind.is_undefined() {
163 self.kind = update.kind;
164 }
165 if !update.status.is_undefined() {
166 self.status = update.status;
167 }
168 if !update.content.is_undefined() {
169 self.content = update.content;
170 }
171 if !update.locations.is_undefined() {
172 self.locations = update.locations;
173 }
174 if !update.raw_input.is_undefined() {
175 self.raw_input = update.raw_input;
176 }
177 if !update.raw_output.is_undefined() {
178 self.raw_output = update.raw_output;
179 }
180 }
181}
182
183#[skip_serializing_none]
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
191#[serde(rename_all = "camelCase")]
192#[non_exhaustive]
193pub struct ToolCallContentChunk {
194 pub tool_call_id: ToolCallId,
196 pub content: ToolCallContent,
198 #[serde(rename = "_meta")]
205 pub meta: Option<Meta>,
206}
207
208impl ToolCallContentChunk {
209 #[must_use]
211 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
212 Self {
213 tool_call_id: tool_call_id.into(),
214 content: content.into(),
215 meta: None,
216 }
217 }
218
219 #[must_use]
225 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
226 self.meta = meta.into_option();
227 self
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
233#[serde(transparent)]
234#[from(Arc<str>, String, &'static str)]
235#[non_exhaustive]
236pub struct ToolCallId(pub Arc<str>);
237
238impl ToolCallId {
239 #[must_use]
241 pub fn new(id: impl Into<Arc<str>>) -> Self {
242 Self(id.into())
243 }
244}
245
246impl IntoOption<ToolCallId> for &str {
247 fn into_option(self) -> Option<ToolCallId> {
248 Some(ToolCallId::new(self))
249 }
250}
251
252#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
259#[serde(rename_all = "snake_case")]
260#[non_exhaustive]
261pub enum ToolKind {
262 Read,
264 Edit,
266 Delete,
268 Move,
270 Search,
272 Execute,
274 Think,
276 Fetch,
278 SwitchMode,
280 #[default]
282 Other,
283 #[serde(untagged)]
289 Unknown(String),
290}
291
292#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
298#[serde(rename_all = "snake_case")]
299#[non_exhaustive]
300pub enum ToolCallStatus {
301 #[default]
304 Pending,
305 InProgress,
307 Completed,
309 Failed,
311 #[serde(untagged)]
317 Other(String),
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
327#[serde(tag = "type", rename_all = "snake_case")]
328#[schemars(extend("discriminator" = {"propertyName": "type"}))]
329#[non_exhaustive]
330pub enum ToolCallContent {
331 Content(Box<Content>),
333 Diff(Diff),
335 #[serde(untagged)]
345 Other(OtherToolCallContent),
346}
347
348#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
350#[schemars(inline)]
351#[schemars(transform = other_tool_call_content_schema)]
352#[serde(rename_all = "camelCase")]
353#[non_exhaustive]
354pub struct OtherToolCallContent {
355 #[serde(rename = "type")]
361 pub type_: String,
362 #[serde(flatten)]
364 pub fields: BTreeMap<String, serde_json::Value>,
365}
366
367impl OtherToolCallContent {
368 #[must_use]
370 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
371 fields.remove("type");
372 Self {
373 type_: type_.into(),
374 fields,
375 }
376 }
377}
378
379impl<'de> Deserialize<'de> for OtherToolCallContent {
380 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
381 where
382 D: serde::Deserializer<'de>,
383 {
384 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
385 let type_ = fields
386 .remove("type")
387 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
388 let serde_json::Value::String(type_) = type_ else {
389 return Err(serde::de::Error::custom("`type` must be a string"));
390 };
391
392 if is_known_tool_call_content_type(&type_) {
393 return Err(serde::de::Error::custom(format!(
394 "known tool call content `{type_}` did not match its schema"
395 )));
396 }
397
398 Ok(Self { type_, fields })
399 }
400}
401
402fn is_known_tool_call_content_type(type_: &str) -> bool {
403 matches!(type_, "content" | "diff")
404}
405
406fn other_tool_call_content_schema(schema: &mut Schema) {
407 super::schema_util::reject_known_string_discriminators(schema, "type", &["content", "diff"]);
408}
409
410impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
411 fn from(content: T) -> Self {
412 ToolCallContent::Content(Box::new(Content::new(content)))
413 }
414}
415
416impl From<Diff> for ToolCallContent {
417 fn from(diff: Diff) -> Self {
418 ToolCallContent::Diff(diff)
419 }
420}
421
422#[skip_serializing_none]
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
425#[serde(rename_all = "camelCase")]
426#[non_exhaustive]
427pub struct Content {
428 pub content: ContentBlock,
430 #[serde(rename = "_meta")]
436 pub meta: Option<Meta>,
437}
438
439impl Content {
440 #[must_use]
442 pub fn new(content: impl Into<ContentBlock>) -> Self {
443 Self {
444 content: content.into(),
445 meta: None,
446 }
447 }
448
449 #[must_use]
455 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
456 self.meta = meta.into_option();
457 self
458 }
459}
460
461#[skip_serializing_none]
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
468#[serde(rename_all = "camelCase")]
469#[non_exhaustive]
470pub struct Diff {
471 pub path: PathBuf,
473 pub old_text: Option<String>,
475 pub new_text: String,
477 #[serde(rename = "_meta")]
483 pub meta: Option<Meta>,
484}
485
486impl Diff {
487 #[must_use]
489 pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
490 Self {
491 path: path.into(),
492 old_text: None,
493 new_text: new_text.into(),
494 meta: None,
495 }
496 }
497
498 #[must_use]
500 pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
501 self.old_text = old_text.into_option();
502 self
503 }
504
505 #[must_use]
511 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
512 self.meta = meta.into_option();
513 self
514 }
515}
516
517#[skip_serializing_none]
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
525#[serde(rename_all = "camelCase")]
526#[non_exhaustive]
527pub struct ToolCallLocation {
528 pub path: PathBuf,
530 #[serde(default)]
532 pub line: Option<u32>,
533 #[serde(rename = "_meta")]
539 pub meta: Option<Meta>,
540}
541
542impl ToolCallLocation {
543 #[must_use]
545 pub fn new(path: impl Into<PathBuf>) -> Self {
546 Self {
547 path: path.into(),
548 line: None,
549 meta: None,
550 }
551 }
552
553 #[must_use]
555 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
556 self.line = line.into_option();
557 self
558 }
559
560 #[must_use]
566 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
567 self.meta = meta.into_option();
568 self
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use crate::MaybeUndefined;
576
577 #[test]
578 fn tool_call_serializes_as_upsert() {
579 let tool_call = ToolCallUpdate::new("tc_1")
580 .title("Reading configuration")
581 .status(ToolCallStatus::InProgress)
582 .raw_input(serde_json::json!({"path": "settings.json"}));
583
584 assert_eq!(
585 serde_json::to_value(tool_call).unwrap(),
586 serde_json::json!({
587 "toolCallId": "tc_1",
588 "title": "Reading configuration",
589 "status": "in_progress",
590 "rawInput": {
591 "path": "settings.json"
592 }
593 })
594 );
595 }
596
597 #[test]
598 fn tool_call_update_distinguishes_omitted_null_and_value() {
599 let tool_call = ToolCallUpdate::new("tc_1")
600 .status(ToolCallStatus::Completed)
601 .content(None::<Vec<ToolCallContent>>);
602
603 assert_eq!(
604 serde_json::to_value(tool_call).unwrap(),
605 serde_json::json!({
606 "toolCallId": "tc_1",
607 "status": "completed",
608 "content": null
609 })
610 );
611
612 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
613 "toolCallId": "tc_1",
614 "status": null,
615 "locations": []
616 }))
617 .unwrap();
618 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
619 assert_eq!(deserialized.status, MaybeUndefined::Null);
620 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
621 }
622
623 #[test]
624 fn tool_call_update_skips_malformed_list_items() {
625 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
626 "toolCallId": "tc_1",
627 "content": [
628 {
629 "type": "content",
630 "content": {
631 "type": "text",
632 "text": "ok"
633 }
634 },
635 {
636 "type": "diff",
637 "path": "/bad"
638 }
639 ],
640 "locations": [
641 {
642 "path": "/ok",
643 "line": 3
644 },
645 {
646 "line": 4
647 }
648 ]
649 }))
650 .unwrap();
651
652 let MaybeUndefined::Value(content) = deserialized.content else {
653 panic!("content should deserialize to a value");
654 };
655 assert_eq!(content.len(), 1);
656
657 let MaybeUndefined::Value(locations) = deserialized.locations else {
658 panic!("locations should deserialize to a value");
659 };
660 assert_eq!(locations.len(), 1);
661 }
662
663 #[test]
664 fn tool_call_content_chunk_serializes_single_content_item() {
665 let chunk = ToolCallContentChunk::new(
666 "tc_1",
667 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
668 );
669
670 assert_eq!(
671 serde_json::to_value(chunk).unwrap(),
672 serde_json::json!({
673 "toolCallId": "tc_1",
674 "content": {
675 "type": "content",
676 "content": {
677 "type": "text",
678 "text": "partial output"
679 }
680 }
681 })
682 );
683 }
684
685 #[test]
686 fn tool_kind_preserves_unknown_variant() {
687 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
688 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
689 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
690 }
691
692 #[test]
693 fn tool_call_status_preserves_unknown_variant() {
694 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
695 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
696 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
697 }
698
699 #[test]
700 fn tool_call_content_preserves_unknown_variant() {
701 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
702 "type": "_chart",
703 "title": "Tests",
704 "data": [1, 2, 3]
705 }))
706 .unwrap();
707
708 let ToolCallContent::Other(unknown) = content else {
709 panic!("expected unknown tool call content");
710 };
711
712 assert_eq!(unknown.type_, "_chart");
713 assert_eq!(
714 unknown.fields.get("title"),
715 Some(&serde_json::json!("Tests"))
716 );
717 assert_eq!(
718 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
719 serde_json::json!({
720 "type": "_chart",
721 "title": "Tests",
722 "data": [1, 2, 3]
723 })
724 );
725 }
726
727 #[test]
728 fn tool_call_content_does_not_hide_malformed_known_variant() {
729 assert!(
730 serde_json::from_value::<ToolCallContent>(serde_json::json!({
731 "type": "diff"
732 }))
733 .is_err()
734 );
735 }
736}