1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::{Message, Metadata, ToolArguments, ToolDirective, ToolResultStatus};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct ToolCall {
8 pub id: String,
9 pub name: String,
10 pub arguments: ToolArguments,
11 pub extra_content: Option<Value>,
12}
13
14impl ToolCall {
15 pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: ToolArguments) -> Self {
16 Self {
17 id: id.into(),
18 name: name.into(),
19 arguments,
20 extra_content: None,
21 }
22 }
23
24 pub fn from_raw_arguments(
25 id: impl Into<String>,
26 name: impl Into<String>,
27 raw_arguments: Value,
28 ) -> Self {
29 let id = id.into();
30 let name = name.into();
31 match parse_raw_tool_arguments(&raw_arguments) {
32 Ok(arguments) => Self {
33 id,
34 name,
35 arguments,
36 extra_content: None,
37 },
38 Err((error_code, error)) => Self {
39 id,
40 name,
41 arguments: ToolArguments::new(),
42 extra_content: Some(Value::Object(
43 [
44 ("raw_arguments".to_string(), raw_arguments),
45 ("argument_error_code".to_string(), Value::String(error_code)),
46 ("argument_error".to_string(), Value::String(error)),
47 ]
48 .into_iter()
49 .collect(),
50 )),
51 },
52 }
53 }
54}
55
56fn parse_raw_tool_arguments(raw_arguments: &Value) -> Result<ToolArguments, (String, String)> {
57 match raw_arguments {
58 Value::Null => Ok(ToolArguments::new()),
59 Value::Object(object) => Ok(object.clone().into_iter().collect()),
60 Value::String(raw) => {
61 let stripped = raw.trim();
62 if stripped.is_empty() {
63 return Ok(ToolArguments::new());
64 }
65 let parsed = serde_json::from_str::<Value>(stripped).map_err(|error| {
66 (
67 "invalid_arguments_json".to_string(),
68 format!("Invalid tool arguments JSON: {error}"),
69 )
70 })?;
71 match parsed {
72 Value::Object(object) => Ok(object.into_iter().collect()),
73 _ => Err((
74 "invalid_arguments_payload".to_string(),
75 "Tool arguments must decode to an object".to_string(),
76 )),
77 }
78 }
79 other => Err((
80 "invalid_arguments_type".to_string(),
81 format!("Unsupported tool argument type: {}", json_type_name(other)),
82 )),
83 }
84}
85
86fn json_type_name(value: &Value) -> &'static str {
87 match value {
88 Value::Null => "null",
89 Value::Bool(_) => "bool",
90 Value::Number(_) => "number",
91 Value::String(_) => "string",
92 Value::Array(_) => "array",
93 Value::Object(_) => "object",
94 }
95}
96
97#[derive(Debug, Clone, PartialEq)]
98pub struct ToolExecutionResult {
99 pub tool_call_id: String,
100 pub content: String,
101 pub status: ToolResultStatus,
102 pub directive: ToolDirective,
103 pub error_code: Option<String>,
104 pub metadata: Metadata,
105 pub image_url: Option<String>,
106 pub image_path: Option<String>,
107 pub truncated: bool,
108 pub truncation_reason: Option<ToolTruncationReason>,
109 pub original_bytes: Option<u64>,
110 pub visible_bytes: Option<u64>,
111 pub artifact: Option<ToolArtifactRef>,
112 pub cursor: Option<ToolResultCursor>,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum ToolTruncationReason {
118 OutputLimit,
119 ReadLimit,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct ToolArtifactRef {
125 pub path: String,
126 pub media_type: String,
127 pub encoding: String,
128 pub size_bytes: u64,
129 pub sha256: String,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct ToolResultCursor {
135 pub kind: String,
136 pub path: String,
137 pub offset_chars: u64,
138 pub sha256: String,
139}
140
141impl Serialize for ToolExecutionResult {
142 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
143 where
144 S: serde::Serializer,
145 {
146 self.validate().map_err(serde::ser::Error::custom)?;
147 self.to_dict().serialize(serializer)
148 }
149}
150
151impl<'de> Deserialize<'de> for ToolExecutionResult {
152 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153 where
154 D: serde::Deserializer<'de>,
155 {
156 let value = Value::deserialize(deserializer)?;
157 Self::from_dict(&value).map_err(serde::de::Error::custom)
158 }
159}
160
161impl ToolExecutionResult {
162 pub fn success(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
163 Self {
164 tool_call_id: tool_call_id.into(),
165 content: content.into(),
166 status: ToolResultStatus::Success,
167 directive: ToolDirective::Continue,
168 error_code: None,
169 metadata: Metadata::new(),
170 image_url: None,
171 image_path: None,
172 truncated: false,
173 truncation_reason: None,
174 original_bytes: None,
175 visible_bytes: None,
176 artifact: None,
177 cursor: None,
178 }
179 }
180
181 pub fn error(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
182 Self {
183 status: ToolResultStatus::Error,
184 ..Self::success(tool_call_id, content)
185 }
186 }
187
188 pub fn to_message(&self) -> Message {
189 let mut content = self.content.clone();
190 if self.truncated {
191 let recovery = self.recovery_value();
192 let encoded = serde_json_canonicalizer::to_string(&recovery)
193 .expect("validated tool recovery fields are canonical JSON");
194 content.push('\n');
195 content.push_str(&encoded);
196 }
197 Message::tool(content, self.tool_call_id.clone())
198 }
199
200 pub fn to_tool_message(&self) -> Message {
201 self.to_message()
202 }
203
204 pub fn validate(&self) -> Result<(), String> {
205 for forbidden in ["content", "instructions", "output", "stderr", "stdout"] {
206 if self.metadata.contains_key(forbidden) {
207 return Err(format!(
208 "tool_result_invalid: metadata key {forbidden:?} may not repeat bulk output"
209 ));
210 }
211 }
212 if !self.truncated {
213 if self.truncation_reason.is_some()
214 || self.original_bytes.is_some()
215 || self.visible_bytes.is_some()
216 || self.artifact.is_some()
217 || self.cursor.is_some()
218 {
219 return Err(
220 "tool_result_invalid: ordinary results cannot contain recovery fields"
221 .to_string(),
222 );
223 }
224 return Ok(());
225 }
226
227 let reason = self.truncation_reason.ok_or_else(|| {
228 "tool_result_invalid: truncated result requires truncation_reason".to_string()
229 })?;
230 let original_bytes = self.original_bytes.ok_or_else(|| {
231 "tool_result_invalid: truncated result requires original_bytes".to_string()
232 })?;
233 let visible_bytes = self.visible_bytes.ok_or_else(|| {
234 "tool_result_invalid: truncated result requires visible_bytes".to_string()
235 })?;
236 if visible_bytes != self.content.len() as u64 || visible_bytes > original_bytes {
237 return Err(
238 "tool_result_invalid: truncated result byte counts are invalid".to_string(),
239 );
240 }
241 match reason {
242 ToolTruncationReason::OutputLimit => {
243 let artifact = self.artifact.as_ref().ok_or_else(|| {
244 "tool_result_invalid: output_limit requires artifact".to_string()
245 })?;
246 if self.cursor.is_some() {
247 return Err(
248 "tool_result_invalid: output_limit cannot contain cursor".to_string()
249 );
250 }
251 artifact.validate()?;
252 }
253 ToolTruncationReason::ReadLimit => {
254 let cursor = self
255 .cursor
256 .as_ref()
257 .ok_or_else(|| "tool_result_invalid: read_limit requires cursor".to_string())?;
258 if self.artifact.is_some() {
259 return Err(
260 "tool_result_invalid: read_limit cannot contain artifact".to_string()
261 );
262 }
263 cursor.validate()?;
264 }
265 }
266 Ok(())
267 }
268
269 fn recovery_value(&self) -> Value {
270 let mut recovery = serde_json::Map::new();
271 recovery.insert("truncated".to_string(), Value::Bool(true));
272 recovery.insert(
273 "truncation_reason".to_string(),
274 Value::String(
275 match self.truncation_reason.expect("validated truncated result") {
276 ToolTruncationReason::OutputLimit => "output_limit",
277 ToolTruncationReason::ReadLimit => "read_limit",
278 }
279 .to_string(),
280 ),
281 );
282 recovery.insert(
283 "original_bytes".to_string(),
284 Value::from(self.original_bytes.expect("validated truncated result")),
285 );
286 recovery.insert(
287 "visible_bytes".to_string(),
288 Value::from(self.visible_bytes.expect("validated truncated result")),
289 );
290 if let Some(artifact) = &self.artifact {
291 recovery.insert(
292 "artifact".to_string(),
293 serde_json::to_value(artifact).expect("ToolArtifactRef is serializable"),
294 );
295 }
296 if let Some(cursor) = &self.cursor {
297 recovery.insert(
298 "cursor".to_string(),
299 serde_json::to_value(cursor).expect("ToolResultCursor is serializable"),
300 );
301 }
302 Value::Object(serde_json::Map::from_iter([(
303 "vv_agent_recovery".to_string(),
304 Value::Object(recovery),
305 )]))
306 }
307}
308
309impl ToolArtifactRef {
310 pub fn validate(&self) -> Result<(), String> {
311 if !valid_artifact_path(&self.path) {
312 return Err("artifact_path_invalid".to_string());
313 }
314 if self.media_type != "text/plain" || self.encoding != "utf-8" {
315 return Err(
316 "tool_result_invalid: artifact media type or encoding is invalid".to_string(),
317 );
318 }
319 if !valid_sha256(&self.sha256) {
320 return Err("tool_result_invalid: artifact sha256 is invalid".to_string());
321 }
322 Ok(())
323 }
324}
325
326impl ToolResultCursor {
327 pub fn validate(&self) -> Result<(), String> {
328 if self.kind != "read_file"
329 || self.path.trim().is_empty()
330 || self.path.contains(['\\', '\0'])
331 || self.offset_chars > crate::budget::MAX_WIRE_INTEGER
332 || !valid_sha256(&self.sha256)
333 {
334 return Err("tool_result_invalid: read_file cursor is invalid".to_string());
335 }
336 Ok(())
337 }
338}
339
340fn valid_artifact_path(path: &str) -> bool {
341 const PREFIX: &str = ".vv-agent/artifacts/";
342 if path.len() > 512 || !path.starts_with(PREFIX) || path.contains(['\\', '\0']) {
343 return false;
344 }
345 path[PREFIX.len()..].split('/').all(|segment| {
346 !segment.is_empty()
347 && segment.len() <= 128
348 && segment
349 .bytes()
350 .next()
351 .is_some_and(|byte| byte.is_ascii_alphanumeric())
352 && segment
353 .bytes()
354 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
355 })
356}
357
358fn valid_sha256(value: &str) -> bool {
359 value.len() == 64
360 && value
361 .bytes()
362 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
363}