Skip to main content

cacp_proto/
tool_call.rs

1//! Tool calls and the incremental updates an agent reports for them.
2
3use crate::{ContentBlock, Meta, TerminalId, ToolCallId};
4use serde::{Deserialize, Serialize};
5use std::{collections::BTreeMap, path::PathBuf};
6
7/// A tool invocation the agent is reporting to the client.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct ToolCall {
11    pub tool_call_id: ToolCallId,
12    pub title: String,
13    /// The tool's own name, where `title` is what the user is shown.
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub name: Option<String>,
16    #[serde(default, skip_serializing_if = "ToolKind::is_default")]
17    pub kind: ToolKind,
18    #[serde(default, skip_serializing_if = "ToolCallStatus::is_default")]
19    pub status: ToolCallStatus,
20    #[serde(default, skip_serializing_if = "Vec::is_empty")]
21    pub content: Vec<ToolCallContent>,
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub locations: Vec<ToolCallLocation>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub raw_input: Option<serde_json::Value>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub raw_output: Option<serde_json::Value>,
28    #[serde(default, rename = "_meta", skip_serializing_if = "Option::is_none")]
29    pub meta: Option<Meta>,
30}
31
32impl ToolCall {
33    pub fn new(tool_call_id: impl Into<ToolCallId>, title: impl Into<String>) -> Self {
34        Self {
35            tool_call_id: tool_call_id.into(),
36            title: title.into(),
37            name: None,
38            kind: ToolKind::default(),
39            status: ToolCallStatus::default(),
40            content: Vec::new(),
41            locations: Vec::new(),
42            raw_input: None,
43            raw_output: None,
44            meta: None,
45        }
46    }
47}
48
49/// A partial update to an in-flight tool call; absent fields are unchanged.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct ToolCallUpdate {
53    pub tool_call_id: ToolCallId,
54    #[serde(flatten)]
55    pub fields: ToolCallUpdateFields,
56    #[serde(default, rename = "_meta", skip_serializing_if = "Option::is_none")]
57    pub meta: Option<Meta>,
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct ToolCallUpdateFields {
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub kind: Option<ToolKind>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub status: Option<ToolCallStatus>,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub title: Option<String>,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub name: Option<String>,
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub content: Option<Vec<ToolCallContent>>,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub locations: Option<Vec<ToolCallLocation>>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub raw_input: Option<serde_json::Value>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub raw_output: Option<serde_json::Value>,
79}
80
81impl ToolCallUpdate {
82    /// Fold this update into an existing tool call.
83    pub fn apply(self, target: &mut ToolCall) {
84        let ToolCallUpdateFields {
85            kind,
86            status,
87            title,
88            name,
89            content,
90            locations,
91            raw_input,
92            raw_output,
93        } = self.fields;
94        if let Some(kind) = kind {
95            target.kind = kind;
96        }
97        if let Some(status) = status {
98            target.status = status;
99        }
100        if let Some(title) = title {
101            target.title = title;
102        }
103        if name.is_some() {
104            target.name = name;
105        }
106        if let Some(content) = content {
107            target.content = content;
108        }
109        if let Some(locations) = locations {
110            target.locations = locations;
111        }
112        if raw_input.is_some() {
113            target.raw_input = raw_input;
114        }
115        if raw_output.is_some() {
116            target.raw_output = raw_output;
117        }
118    }
119}
120
121impl From<ToolCall> for ToolCallUpdate {
122    fn from(call: ToolCall) -> Self {
123        Self {
124            tool_call_id: call.tool_call_id,
125            fields: ToolCallUpdateFields {
126                kind: Some(call.kind),
127                status: Some(call.status),
128                title: Some(call.title),
129                name: call.name,
130                content: Some(call.content),
131                locations: Some(call.locations),
132                raw_input: call.raw_input,
133                raw_output: call.raw_output,
134            },
135            meta: None,
136        }
137    }
138}
139
140/// What a tool does, so the client can pick an icon and a phrasing.
141///
142/// `Other` is the `#[serde(other)]` catch-all: a kind added in a later
143/// revision deserializes instead of failing the whole update.
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum ToolKind {
147    Read,
148    Edit,
149    Delete,
150    Move,
151    Search,
152    Execute,
153    Think,
154    Fetch,
155    SwitchMode,
156    #[default]
157    #[serde(other)]
158    Other,
159}
160
161impl ToolKind {
162    fn is_default(&self) -> bool {
163        matches!(self, Self::Other)
164    }
165}
166
167#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum ToolCallStatus {
170    #[default]
171    Pending,
172    InProgress,
173    Completed,
174    Failed,
175    #[serde(untagged)]
176    Other(String),
177}
178
179impl ToolCallStatus {
180    fn is_default(&self) -> bool {
181        matches!(self, Self::Pending)
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[serde(tag = "type", rename_all = "snake_case")]
187pub enum ToolCallContent {
188    Content {
189        content: ContentBlock,
190    },
191    Diff(Diff),
192    // `rename_all` on the container renames variants, not their fields.
193    #[serde(rename_all = "camelCase")]
194    Terminal {
195        terminal_id: TerminalId,
196    },
197    #[serde(untagged)]
198    Other(OtherToolCallContent),
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202pub struct OtherToolCallContent {
203    #[serde(rename = "type")]
204    pub kind: String,
205    #[serde(flatten)]
206    pub fields: BTreeMap<String, serde_json::Value>,
207}
208
209impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
210    fn from(content: T) -> Self {
211        Self::Content {
212            content: content.into(),
213        }
214    }
215}
216
217/// A proposed edit, rendered by the client as a diff.
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct Diff {
221    pub path: PathBuf,
222    pub new_text: String,
223    /// Absent for a newly created file.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub old_text: Option<String>,
226    #[serde(default, rename = "_meta", skip_serializing_if = "Option::is_none")]
227    pub meta: Option<Meta>,
228}
229
230/// A file the tool call touches, so the client can follow along.
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct ToolCallLocation {
234    pub path: PathBuf,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub line: Option<u32>,
237    #[serde(default, rename = "_meta", skip_serializing_if = "Option::is_none")]
238    pub meta: Option<Meta>,
239}