1use std::{path::PathBuf, sync::Arc};
8
9use derive_more::{Display, From};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14use crate::{IntoOption, SkipListener};
15
16use super::{ContentBlock, Error, Meta, TerminalId};
17
18#[serde_as]
25#[skip_serializing_none]
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
27#[serde(rename_all = "camelCase")]
28#[non_exhaustive]
29pub struct ToolCall {
30 pub tool_call_id: ToolCallId,
32 pub title: String,
34 #[cfg(feature = "unstable_tool_call_name")]
43 #[serde_as(deserialize_as = "DefaultOnError")]
44 #[schemars(extend("x-deserialize-default-on-error" = true))]
45 #[serde(default)]
46 pub name: Option<String>,
47 #[serde_as(deserialize_as = "DefaultOnError")]
50 #[schemars(extend("x-deserialize-default-on-error" = true))]
51 #[serde(default, skip_serializing_if = "ToolKind::is_default")]
52 pub kind: ToolKind,
53 #[serde_as(deserialize_as = "DefaultOnError")]
55 #[schemars(extend("x-deserialize-default-on-error" = true))]
56 #[serde(default, skip_serializing_if = "ToolCallStatus::is_default")]
57 pub status: ToolCallStatus,
58 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
60 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub content: Vec<ToolCallContent>,
63 #[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
66 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub locations: Vec<ToolCallLocation>,
69 #[serde_as(deserialize_as = "DefaultOnError")]
71 #[schemars(extend("x-deserialize-default-on-error" = true))]
72 #[serde(default)]
73 pub raw_input: Option<serde_json::Value>,
74 #[serde_as(deserialize_as = "DefaultOnError")]
76 #[schemars(extend("x-deserialize-default-on-error" = true))]
77 #[serde(default)]
78 pub raw_output: Option<serde_json::Value>,
79 #[serde_as(deserialize_as = "DefaultOnError")]
85 #[schemars(extend("x-deserialize-default-on-error" = true))]
86 #[serde(default)]
87 #[serde(rename = "_meta")]
88 pub meta: Option<Meta>,
89}
90
91impl ToolCall {
92 #[must_use]
94 pub fn new(tool_call_id: impl Into<ToolCallId>, title: impl Into<String>) -> Self {
95 Self {
96 tool_call_id: tool_call_id.into(),
97 title: title.into(),
98 #[cfg(feature = "unstable_tool_call_name")]
99 name: None,
100 kind: ToolKind::default(),
101 status: ToolCallStatus::default(),
102 content: Vec::default(),
103 locations: Vec::default(),
104 raw_input: None,
105 raw_output: None,
106 meta: None,
107 }
108 }
109
110 #[cfg(feature = "unstable_tool_call_name")]
116 #[must_use]
117 pub fn name(mut self, name: impl IntoOption<String>) -> Self {
118 self.name = name.into_option();
119 self
120 }
121
122 #[must_use]
125 pub fn kind(mut self, kind: ToolKind) -> Self {
126 self.kind = kind;
127 self
128 }
129
130 #[must_use]
132 pub fn status(mut self, status: ToolCallStatus) -> Self {
133 self.status = status;
134 self
135 }
136
137 #[must_use]
139 pub fn content(mut self, content: Vec<ToolCallContent>) -> Self {
140 self.content = content;
141 self
142 }
143
144 #[must_use]
147 pub fn locations(mut self, locations: Vec<ToolCallLocation>) -> Self {
148 self.locations = locations;
149 self
150 }
151
152 #[must_use]
154 pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
155 self.raw_input = raw_input.into_option();
156 self
157 }
158
159 #[must_use]
161 pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
162 self.raw_output = raw_output.into_option();
163 self
164 }
165
166 #[must_use]
172 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
173 self.meta = meta.into_option();
174 self
175 }
176
177 pub fn update(&mut self, fields: ToolCallUpdateFields) {
180 if let Some(title) = fields.title {
181 self.title = title;
182 }
183 #[cfg(feature = "unstable_tool_call_name")]
184 if let Some(name) = fields.name {
185 self.name = Some(name);
186 }
187 if let Some(kind) = fields.kind {
188 self.kind = kind;
189 }
190 if let Some(status) = fields.status {
191 self.status = status;
192 }
193 if let Some(content) = fields.content {
194 self.content = content;
195 }
196 if let Some(locations) = fields.locations {
197 self.locations = locations;
198 }
199 if let Some(raw_input) = fields.raw_input {
200 self.raw_input = Some(raw_input);
201 }
202 if let Some(raw_output) = fields.raw_output {
203 self.raw_output = Some(raw_output);
204 }
205 }
206}
207
208#[serde_as]
215#[skip_serializing_none]
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
217#[serde(rename_all = "camelCase")]
218#[non_exhaustive]
219pub struct ToolCallUpdate {
220 pub tool_call_id: ToolCallId,
222 #[serde(flatten)]
224 pub fields: ToolCallUpdateFields,
225 #[serde_as(deserialize_as = "DefaultOnError")]
231 #[schemars(extend("x-deserialize-default-on-error" = true))]
232 #[serde(default)]
233 #[serde(rename = "_meta")]
234 pub meta: Option<Meta>,
235}
236
237impl ToolCallUpdate {
238 #[must_use]
240 pub fn new(tool_call_id: impl Into<ToolCallId>, fields: ToolCallUpdateFields) -> Self {
241 Self {
242 tool_call_id: tool_call_id.into(),
243 fields,
244 meta: None,
245 }
246 }
247
248 #[must_use]
254 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
255 self.meta = meta.into_option();
256 self
257 }
258}
259
260#[serde_as]
267#[skip_serializing_none]
268#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
269#[serde(rename_all = "camelCase")]
270#[non_exhaustive]
271pub struct ToolCallUpdateFields {
272 #[serde_as(deserialize_as = "DefaultOnError")]
274 #[schemars(extend("x-deserialize-default-on-error" = true))]
275 #[serde(default)]
276 pub kind: Option<ToolKind>,
277 #[serde_as(deserialize_as = "DefaultOnError")]
279 #[schemars(extend("x-deserialize-default-on-error" = true))]
280 #[serde(default)]
281 pub status: Option<ToolCallStatus>,
282 #[serde_as(deserialize_as = "DefaultOnError")]
284 #[schemars(extend("x-deserialize-default-on-error" = true))]
285 #[serde(default)]
286 pub title: Option<String>,
287 #[cfg(feature = "unstable_tool_call_name")]
296 #[serde_as(deserialize_as = "DefaultOnError")]
297 #[schemars(extend("x-deserialize-default-on-error" = true))]
298 #[serde(default)]
299 pub name: Option<String>,
300 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
302 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
303 #[serde(default)]
304 pub content: Option<Vec<ToolCallContent>>,
305 #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
307 #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
308 #[serde(default)]
309 pub locations: Option<Vec<ToolCallLocation>>,
310 #[serde_as(deserialize_as = "DefaultOnError")]
312 #[schemars(extend("x-deserialize-default-on-error" = true))]
313 #[serde(default)]
314 pub raw_input: Option<serde_json::Value>,
315 #[serde_as(deserialize_as = "DefaultOnError")]
317 #[schemars(extend("x-deserialize-default-on-error" = true))]
318 #[serde(default)]
319 pub raw_output: Option<serde_json::Value>,
320}
321
322impl ToolCallUpdateFields {
323 #[must_use]
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 #[must_use]
331 pub fn kind(mut self, kind: impl IntoOption<ToolKind>) -> Self {
332 self.kind = kind.into_option();
333 self
334 }
335
336 #[must_use]
338 pub fn status(mut self, status: impl IntoOption<ToolCallStatus>) -> Self {
339 self.status = status.into_option();
340 self
341 }
342
343 #[must_use]
345 pub fn title(mut self, title: impl IntoOption<String>) -> Self {
346 self.title = title.into_option();
347 self
348 }
349
350 #[cfg(feature = "unstable_tool_call_name")]
356 #[must_use]
357 pub fn name(mut self, name: impl IntoOption<String>) -> Self {
358 self.name = name.into_option();
359 self
360 }
361
362 #[must_use]
364 pub fn content(mut self, content: impl IntoOption<Vec<ToolCallContent>>) -> Self {
365 self.content = content.into_option();
366 self
367 }
368
369 #[must_use]
371 pub fn locations(mut self, locations: impl IntoOption<Vec<ToolCallLocation>>) -> Self {
372 self.locations = locations.into_option();
373 self
374 }
375
376 #[must_use]
378 pub fn raw_input(mut self, raw_input: impl IntoOption<serde_json::Value>) -> Self {
379 self.raw_input = raw_input.into_option();
380 self
381 }
382
383 #[must_use]
385 pub fn raw_output(mut self, raw_output: impl IntoOption<serde_json::Value>) -> Self {
386 self.raw_output = raw_output.into_option();
387 self
388 }
389}
390
391impl TryFrom<ToolCallUpdate> for ToolCall {
394 type Error = Error;
395
396 fn try_from(update: ToolCallUpdate) -> Result<Self, Self::Error> {
397 let ToolCallUpdate {
398 tool_call_id,
399 fields:
400 ToolCallUpdateFields {
401 kind,
402 status,
403 title,
404 #[cfg(feature = "unstable_tool_call_name")]
405 name,
406 content,
407 locations,
408 raw_input,
409 raw_output,
410 },
411 meta,
412 } = update;
413
414 Ok(Self {
415 tool_call_id,
416 title: title.ok_or_else(|| {
417 Error::invalid_params().data(serde_json::json!("title is required for a tool call"))
418 })?,
419 #[cfg(feature = "unstable_tool_call_name")]
420 name,
421 kind: kind.unwrap_or_default(),
422 status: status.unwrap_or_default(),
423 content: content.unwrap_or_default(),
424 locations: locations.unwrap_or_default(),
425 raw_input,
426 raw_output,
427 meta,
428 })
429 }
430}
431
432impl From<ToolCall> for ToolCallUpdate {
433 fn from(value: ToolCall) -> Self {
434 let ToolCall {
435 tool_call_id,
436 title,
437 #[cfg(feature = "unstable_tool_call_name")]
438 name,
439 kind,
440 status,
441 content,
442 locations,
443 raw_input,
444 raw_output,
445 meta,
446 } = value;
447 Self {
448 tool_call_id,
449 fields: ToolCallUpdateFields {
450 kind: Some(kind),
451 status: Some(status),
452 title: Some(title),
453 #[cfg(feature = "unstable_tool_call_name")]
454 name,
455 content: Some(content),
456 locations: Some(locations),
457 raw_input,
458 raw_output,
459 },
460 meta,
461 }
462 }
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
467#[serde(transparent)]
468#[from(Arc<str>, String, &'static str)]
469#[non_exhaustive]
470pub struct ToolCallId(pub Arc<str>);
471
472impl ToolCallId {
473 #[must_use]
475 pub fn new(id: impl Into<Arc<str>>) -> Self {
476 Self(id.into())
477 }
478}
479
480impl IntoOption<ToolCallId> for &str {
481 fn into_option(self) -> Option<ToolCallId> {
482 Some(ToolCallId::new(self))
483 }
484}
485
486#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
493#[serde(rename_all = "snake_case")]
494#[non_exhaustive]
495pub enum ToolKind {
496 Read,
498 Edit,
500 Delete,
502 Move,
504 Search,
506 Execute,
508 Think,
510 Fetch,
512 SwitchMode,
514 #[default]
516 #[serde(other)]
517 Other,
518}
519
520impl ToolKind {
521 #[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
522 fn is_default(&self) -> bool {
523 matches!(self, ToolKind::Other)
524 }
525}
526
527#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
533#[serde(rename_all = "snake_case")]
534#[non_exhaustive]
535pub enum ToolCallStatus {
536 #[default]
539 Pending,
540 InProgress,
542 Completed,
544 Failed,
546}
547
548impl ToolCallStatus {
549 #[expect(clippy::trivially_copy_pass_by_ref, reason = "Required by serde")]
550 fn is_default(&self) -> bool {
551 matches!(self, ToolCallStatus::Pending)
552 }
553}
554
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
562#[serde(tag = "type", rename_all = "snake_case")]
563#[schemars(extend("discriminator" = {"propertyName": "type"}))]
564#[non_exhaustive]
565#[expect(clippy::large_enum_variant)]
566pub enum ToolCallContent {
567 Content(Content),
569 Diff(Diff),
571 Terminal(Terminal),
577}
578
579impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
580 fn from(content: T) -> Self {
581 ToolCallContent::Content(Content::new(content))
582 }
583}
584
585impl From<Diff> for ToolCallContent {
586 fn from(diff: Diff) -> Self {
587 ToolCallContent::Diff(diff)
588 }
589}
590
591#[serde_as]
593#[skip_serializing_none]
594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
595#[serde(rename_all = "camelCase")]
596#[non_exhaustive]
597pub struct Content {
598 pub content: ContentBlock,
600 #[serde_as(deserialize_as = "DefaultOnError")]
606 #[schemars(extend("x-deserialize-default-on-error" = true))]
607 #[serde(default)]
608 #[serde(rename = "_meta")]
609 pub meta: Option<Meta>,
610}
611
612impl Content {
613 #[must_use]
615 pub fn new(content: impl Into<ContentBlock>) -> Self {
616 Self {
617 content: content.into(),
618 meta: None,
619 }
620 }
621
622 #[must_use]
628 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
629 self.meta = meta.into_option();
630 self
631 }
632}
633
634#[serde_as]
640#[skip_serializing_none]
641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
642#[serde(rename_all = "camelCase")]
643#[non_exhaustive]
644pub struct Terminal {
645 pub terminal_id: TerminalId,
647 #[serde_as(deserialize_as = "DefaultOnError")]
653 #[schemars(extend("x-deserialize-default-on-error" = true))]
654 #[serde(default)]
655 #[serde(rename = "_meta")]
656 pub meta: Option<Meta>,
657}
658
659impl Terminal {
660 #[must_use]
662 pub fn new(terminal_id: impl Into<TerminalId>) -> Self {
663 Self {
664 terminal_id: terminal_id.into(),
665 meta: None,
666 }
667 }
668
669 #[must_use]
675 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
676 self.meta = meta.into_option();
677 self
678 }
679}
680
681#[serde_as]
687#[skip_serializing_none]
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
689#[serde(rename_all = "camelCase")]
690#[non_exhaustive]
691pub struct Diff {
692 pub path: PathBuf,
694 #[serde_as(deserialize_as = "DefaultOnError")]
696 #[schemars(extend("x-deserialize-default-on-error" = true))]
697 #[serde(default)]
698 pub old_text: Option<String>,
699 pub new_text: String,
701 #[serde_as(deserialize_as = "DefaultOnError")]
707 #[schemars(extend("x-deserialize-default-on-error" = true))]
708 #[serde(default)]
709 #[serde(rename = "_meta")]
710 pub meta: Option<Meta>,
711}
712
713impl Diff {
714 #[must_use]
716 pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
717 Self {
718 path: path.into(),
719 old_text: None,
720 new_text: new_text.into(),
721 meta: None,
722 }
723 }
724
725 #[must_use]
727 pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
728 self.old_text = old_text.into_option();
729 self
730 }
731
732 #[must_use]
738 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
739 self.meta = meta.into_option();
740 self
741 }
742}
743
744#[serde_as]
751#[skip_serializing_none]
752#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
753#[serde(rename_all = "camelCase")]
754#[non_exhaustive]
755pub struct ToolCallLocation {
756 pub path: PathBuf,
758 #[serde_as(deserialize_as = "DefaultOnError")]
760 #[schemars(extend("x-deserialize-default-on-error" = true))]
761 #[serde(default)]
762 pub line: Option<u32>,
763 #[serde_as(deserialize_as = "DefaultOnError")]
769 #[schemars(extend("x-deserialize-default-on-error" = true))]
770 #[serde(default)]
771 #[serde(rename = "_meta")]
772 pub meta: Option<Meta>,
773}
774
775impl ToolCallLocation {
776 #[must_use]
778 pub fn new(path: impl Into<PathBuf>) -> Self {
779 Self {
780 path: path.into(),
781 line: None,
782 meta: None,
783 }
784 }
785
786 #[must_use]
788 pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
789 self.line = line.into_option();
790 self
791 }
792
793 #[must_use]
799 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
800 self.meta = meta.into_option();
801 self
802 }
803}
804
805#[cfg(all(test, feature = "unstable_tool_call_name"))]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn tool_call_name_is_optional_and_null_is_equivalent_to_omission() {
811 let named = ToolCall::new("tc_1", "Reading configuration").name("read_file");
812 assert_eq!(
813 serde_json::to_value(named).unwrap(),
814 serde_json::json!({
815 "toolCallId": "tc_1",
816 "title": "Reading configuration",
817 "name": "read_file"
818 })
819 );
820
821 let unnamed = ToolCall::new("tc_1", "Reading configuration");
822 assert_eq!(unnamed.name, None);
823 assert_eq!(
824 serde_json::to_value(unnamed).unwrap(),
825 serde_json::json!({
826 "toolCallId": "tc_1",
827 "title": "Reading configuration"
828 })
829 );
830
831 let from_null: ToolCall = serde_json::from_value(serde_json::json!({
832 "toolCallId": "tc_1",
833 "title": "Reading configuration",
834 "name": null
835 }))
836 .unwrap();
837 assert_eq!(from_null.name, None);
838 }
839
840 #[test]
841 fn tool_call_name_update_replaces_a_name_but_cannot_clear_it() {
842 let mut stored = ToolCall::new("tc_1", "Reading configuration").name("read_file");
843
844 stored.update(ToolCallUpdateFields::new());
845 assert_eq!(stored.name.as_deref(), Some("read_file"));
846
847 let null_update: ToolCallUpdateFields =
848 serde_json::from_value(serde_json::json!({"name": null})).unwrap();
849 stored.update(null_update);
850 assert_eq!(stored.name.as_deref(), Some("read_file"));
851
852 stored.update(ToolCallUpdateFields::new().name("read_many_files"));
853 assert_eq!(stored.name.as_deref(), Some("read_many_files"));
854 }
855
856 #[test]
857 fn tool_call_name_survives_v1_upsert_conversion() {
858 let tool_call = ToolCall::new("tc_1", "Reading configuration").name("read_file");
859
860 let update = ToolCallUpdate::from(tool_call.clone());
861 assert_eq!(update.fields.name.as_deref(), Some("read_file"));
862
863 let rebuilt = ToolCall::try_from(update).unwrap();
864 assert_eq!(rebuilt, tool_call);
865 }
866}