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]
76 pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
77 Self {
78 tool_call_id: tool_call_id.into(),
79 title: MaybeUndefined::Undefined,
80 kind: MaybeUndefined::Undefined,
81 status: MaybeUndefined::Undefined,
82 content: MaybeUndefined::Undefined,
83 locations: MaybeUndefined::Undefined,
84 raw_input: MaybeUndefined::Undefined,
85 raw_output: MaybeUndefined::Undefined,
86 meta: None,
87 }
88 }
89
90 #[must_use]
92 pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
93 self.title = title.into_maybe_undefined();
94 self
95 }
96
97 #[must_use]
100 pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
101 self.kind = kind.into_maybe_undefined();
102 self
103 }
104
105 #[must_use]
107 pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
108 self.status = status.into_maybe_undefined();
109 self
110 }
111
112 #[must_use]
114 pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
115 self.content = content.into_maybe_undefined();
116 self
117 }
118
119 #[must_use]
122 pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
123 self.locations = locations.into_maybe_undefined();
124 self
125 }
126
127 #[must_use]
129 pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
130 self.raw_input = raw_input.into_maybe_undefined();
131 self
132 }
133
134 #[must_use]
136 pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
137 self.raw_output = raw_output.into_maybe_undefined();
138 self
139 }
140
141 #[must_use]
147 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
148 self.meta = meta.into_option();
149 self
150 }
151
152 pub fn apply_update(&mut self, update: ToolCallUpdate) {
157 debug_assert_eq!(self.tool_call_id, update.tool_call_id);
158 if !update.title.is_undefined() {
159 self.title = update.title;
160 }
161 if !update.kind.is_undefined() {
162 self.kind = update.kind;
163 }
164 if !update.status.is_undefined() {
165 self.status = update.status;
166 }
167 if !update.content.is_undefined() {
168 self.content = update.content;
169 }
170 if !update.locations.is_undefined() {
171 self.locations = update.locations;
172 }
173 if !update.raw_input.is_undefined() {
174 self.raw_input = update.raw_input;
175 }
176 if !update.raw_output.is_undefined() {
177 self.raw_output = update.raw_output;
178 }
179 }
180}
181
182#[skip_serializing_none]
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct ToolCallContentChunk {
193 pub tool_call_id: ToolCallId,
195 pub content: ToolCallContent,
197 #[serde(rename = "_meta")]
204 pub meta: Option<Meta>,
205}
206
207impl ToolCallContentChunk {
208 #[must_use]
209 pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
210 Self {
211 tool_call_id: tool_call_id.into(),
212 content: content.into(),
213 meta: None,
214 }
215 }
216
217 #[must_use]
223 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
224 self.meta = meta.into_option();
225 self
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
231#[serde(transparent)]
232#[from(Arc<str>, String, &'static str)]
233#[non_exhaustive]
234pub struct ToolCallId(pub Arc<str>);
235
236impl ToolCallId {
237 #[must_use]
238 pub fn new(id: impl Into<Arc<str>>) -> Self {
239 Self(id.into())
240 }
241}
242
243impl IntoOption<ToolCallId> for &str {
244 fn into_option(self) -> Option<ToolCallId> {
245 Some(ToolCallId::new(self))
246 }
247}
248
249#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
256#[serde(rename_all = "snake_case")]
257#[non_exhaustive]
258pub enum ToolKind {
259 Read,
261 Edit,
263 Delete,
265 Move,
267 Search,
269 Execute,
271 Think,
273 Fetch,
275 SwitchMode,
277 #[default]
279 Other,
280 #[serde(untagged)]
286 Unknown(String),
287}
288
289#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
295#[serde(rename_all = "snake_case")]
296#[non_exhaustive]
297pub enum ToolCallStatus {
298 #[default]
301 Pending,
302 InProgress,
304 Completed,
306 Failed,
308 #[serde(untagged)]
314 Other(String),
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
324#[serde(tag = "type", rename_all = "snake_case")]
325#[schemars(extend("discriminator" = {"propertyName": "type"}))]
326#[non_exhaustive]
327pub enum ToolCallContent {
328 Content(Box<Content>),
330 Diff(Diff),
332 #[serde(untagged)]
342 Other(OtherToolCallContent),
343}
344
345#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
347#[schemars(inline)]
348#[schemars(transform = other_tool_call_content_schema)]
349#[serde(rename_all = "camelCase")]
350#[non_exhaustive]
351pub struct OtherToolCallContent {
352 #[serde(rename = "type")]
358 pub type_: String,
359 #[serde(flatten)]
361 pub fields: BTreeMap<String, serde_json::Value>,
362}
363
364impl OtherToolCallContent {
365 #[must_use]
366 pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
367 fields.remove("type");
368 Self {
369 type_: type_.into(),
370 fields,
371 }
372 }
373}
374
375impl<'de> Deserialize<'de> for OtherToolCallContent {
376 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
377 where
378 D: serde::Deserializer<'de>,
379 {
380 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
381 let type_ = fields
382 .remove("type")
383 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
384 let serde_json::Value::String(type_) = type_ else {
385 return Err(serde::de::Error::custom("`type` must be a string"));
386 };
387
388 if is_known_tool_call_content_type(&type_) {
389 return Err(serde::de::Error::custom(format!(
390 "known tool call content `{type_}` did not match its schema"
391 )));
392 }
393
394 Ok(Self { type_, fields })
395 }
396}
397
398fn is_known_tool_call_content_type(type_: &str) -> bool {
399 matches!(type_, "content" | "diff")
400}
401
402fn other_tool_call_content_schema(schema: &mut Schema) {
403 super::schema_util::reject_known_string_discriminators(schema, "type", &["content", "diff"]);
404}
405
406impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
407 fn from(content: T) -> Self {
408 ToolCallContent::Content(Box::new(Content::new(content)))
409 }
410}
411
412impl From<Diff> for ToolCallContent {
413 fn from(diff: Diff) -> Self {
414 ToolCallContent::Diff(diff)
415 }
416}
417
418#[skip_serializing_none]
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
421#[serde(rename_all = "camelCase")]
422#[non_exhaustive]
423pub struct Content {
424 pub content: ContentBlock,
426 #[serde(rename = "_meta")]
432 pub meta: Option<Meta>,
433}
434
435impl Content {
436 #[must_use]
437 pub fn new(content: impl Into<ContentBlock>) -> Self {
438 Self {
439 content: content.into(),
440 meta: None,
441 }
442 }
443
444 #[must_use]
450 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
451 self.meta = meta.into_option();
452 self
453 }
454}
455
456#[skip_serializing_none]
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464#[non_exhaustive]
465pub struct Diff {
466 pub path: PathBuf,
468 pub old_text: Option<String>,
470 pub new_text: String,
472 #[serde(rename = "_meta")]
478 pub meta: Option<Meta>,
479}
480
481impl Diff {
482 #[must_use]
483 pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
484 Self {
485 path: path.into(),
486 old_text: None,
487 new_text: new_text.into(),
488 meta: None,
489 }
490 }
491
492 #[must_use]
494 pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
495 self.old_text = old_text.into_option();
496 self
497 }
498
499 #[must_use]
505 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
506 self.meta = meta.into_option();
507 self
508 }
509}
510
511#[skip_serializing_none]
518#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
519#[serde(rename_all = "camelCase")]
520#[non_exhaustive]
521pub struct ToolCallLocation {
522 pub path: PathBuf,
524 #[serde(default)]
526 pub line: Option<u32>,
527 #[serde(rename = "_meta")]
533 pub meta: Option<Meta>,
534}
535
536impl ToolCallLocation {
537 #[must_use]
538 pub fn new(path: impl Into<PathBuf>) -> Self {
539 Self {
540 path: path.into(),
541 line: None,
542 meta: None,
543 }
544 }
545
546 #[must_use]
548 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
549 self.line = line.into_option();
550 self
551 }
552
553 #[must_use]
559 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
560 self.meta = meta.into_option();
561 self
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::MaybeUndefined;
569
570 #[test]
571 fn tool_call_serializes_as_upsert() {
572 let tool_call = ToolCallUpdate::new("tc_1")
573 .title("Reading configuration")
574 .status(ToolCallStatus::InProgress)
575 .raw_input(serde_json::json!({"path": "settings.json"}));
576
577 assert_eq!(
578 serde_json::to_value(tool_call).unwrap(),
579 serde_json::json!({
580 "toolCallId": "tc_1",
581 "title": "Reading configuration",
582 "status": "in_progress",
583 "rawInput": {
584 "path": "settings.json"
585 }
586 })
587 );
588 }
589
590 #[test]
591 fn tool_call_update_distinguishes_omitted_null_and_value() {
592 let tool_call = ToolCallUpdate::new("tc_1")
593 .status(ToolCallStatus::Completed)
594 .content(None::<Vec<ToolCallContent>>);
595
596 assert_eq!(
597 serde_json::to_value(tool_call).unwrap(),
598 serde_json::json!({
599 "toolCallId": "tc_1",
600 "status": "completed",
601 "content": null
602 })
603 );
604
605 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
606 "toolCallId": "tc_1",
607 "status": null,
608 "locations": []
609 }))
610 .unwrap();
611 assert_eq!(deserialized.title, MaybeUndefined::Undefined);
612 assert_eq!(deserialized.status, MaybeUndefined::Null);
613 assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
614 }
615
616 #[test]
617 fn tool_call_update_skips_malformed_list_items() {
618 let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
619 "toolCallId": "tc_1",
620 "content": [
621 {
622 "type": "content",
623 "content": {
624 "type": "text",
625 "text": "ok"
626 }
627 },
628 {
629 "type": "diff",
630 "path": "/bad"
631 }
632 ],
633 "locations": [
634 {
635 "path": "/ok",
636 "line": 3
637 },
638 {
639 "line": 4
640 }
641 ]
642 }))
643 .unwrap();
644
645 let MaybeUndefined::Value(content) = deserialized.content else {
646 panic!("content should deserialize to a value");
647 };
648 assert_eq!(content.len(), 1);
649
650 let MaybeUndefined::Value(locations) = deserialized.locations else {
651 panic!("locations should deserialize to a value");
652 };
653 assert_eq!(locations.len(), 1);
654 }
655
656 #[test]
657 fn tool_call_content_chunk_serializes_single_content_item() {
658 let chunk = ToolCallContentChunk::new(
659 "tc_1",
660 ContentBlock::Text(crate::v2::TextContent::new("partial output")),
661 );
662
663 assert_eq!(
664 serde_json::to_value(chunk).unwrap(),
665 serde_json::json!({
666 "toolCallId": "tc_1",
667 "content": {
668 "type": "content",
669 "content": {
670 "type": "text",
671 "text": "partial output"
672 }
673 }
674 })
675 );
676 }
677
678 #[test]
679 fn tool_kind_preserves_unknown_variant() {
680 let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
681 assert_eq!(kind, ToolKind::Unknown("review".to_string()));
682 assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
683 }
684
685 #[test]
686 fn tool_call_status_preserves_unknown_variant() {
687 let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
688 assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
689 assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
690 }
691
692 #[test]
693 fn tool_call_content_preserves_unknown_variant() {
694 let content: ToolCallContent = serde_json::from_value(serde_json::json!({
695 "type": "_chart",
696 "title": "Tests",
697 "data": [1, 2, 3]
698 }))
699 .unwrap();
700
701 let ToolCallContent::Other(unknown) = content else {
702 panic!("expected unknown tool call content");
703 };
704
705 assert_eq!(unknown.type_, "_chart");
706 assert_eq!(
707 unknown.fields.get("title"),
708 Some(&serde_json::json!("Tests"))
709 );
710 assert_eq!(
711 serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
712 serde_json::json!({
713 "type": "_chart",
714 "title": "Tests",
715 "data": [1, 2, 3]
716 })
717 );
718 }
719
720 #[test]
721 fn tool_call_content_does_not_hide_malformed_known_variant() {
722 assert!(
723 serde_json::from_value::<ToolCallContent>(serde_json::json!({
724 "type": "diff"
725 }))
726 .is_err()
727 );
728 }
729}