Skip to main content

vv_agent/types/dict/
tools.rs

1use super::common::*;
2use super::*;
3
4impl ToolExecutionResult {
5    pub fn to_dict(&self) -> Value {
6        let mut payload = serde_json::Map::from_iter([
7            (
8                "tool_call_id".to_string(),
9                Value::String(self.tool_call_id.clone()),
10            ),
11            ("content".to_string(), Value::String(self.content.clone())),
12            (
13                "directive".to_string(),
14                Value::String(tool_directive_value(self.directive).to_string()),
15            ),
16            (
17                "status_code".to_string(),
18                Value::String(tool_result_status_value(self.status).to_string()),
19            ),
20        ]);
21        insert_optional_string(&mut payload, "error_code", &self.error_code);
22        if !self.metadata.is_empty() {
23            payload.insert("metadata".to_string(), metadata_to_value(&self.metadata));
24        }
25        insert_optional_string(&mut payload, "image_url", &self.image_url);
26        insert_optional_string(&mut payload, "image_path", &self.image_path);
27        if self.truncated {
28            payload.insert("truncated".to_string(), Value::Bool(true));
29        }
30        if let Some(reason) = self.truncation_reason {
31            payload.insert(
32                "truncation_reason".to_string(),
33                Value::String(
34                    match reason {
35                        ToolTruncationReason::OutputLimit => "output_limit",
36                        ToolTruncationReason::ReadLimit => "read_limit",
37                    }
38                    .to_string(),
39                ),
40            );
41        }
42        if let Some(original_bytes) = self.original_bytes {
43            payload.insert("original_bytes".to_string(), Value::from(original_bytes));
44        }
45        if let Some(visible_bytes) = self.visible_bytes {
46            payload.insert("visible_bytes".to_string(), Value::from(visible_bytes));
47        }
48        if let Some(artifact) = &self.artifact {
49            payload.insert(
50                "artifact".to_string(),
51                serde_json::to_value(artifact).expect("ToolArtifactRef is serializable"),
52            );
53        }
54        if let Some(cursor) = &self.cursor {
55            payload.insert(
56                "cursor".to_string(),
57                serde_json::to_value(cursor).expect("ToolResultCursor is serializable"),
58            );
59        }
60        Value::Object(payload)
61    }
62
63    pub fn from_dict(data: &Value) -> Result<Self, String> {
64        let object = expect_object(data, "ToolExecutionResult")?;
65        let required = ["tool_call_id", "content", "status_code", "directive"];
66        let allowed = [
67            "tool_call_id",
68            "content",
69            "status_code",
70            "directive",
71            "error_code",
72            "metadata",
73            "image_url",
74            "image_path",
75            "truncated",
76            "truncation_reason",
77            "original_bytes",
78            "visible_bytes",
79            "artifact",
80            "cursor",
81        ];
82        let mut missing = required
83            .iter()
84            .filter(|field| !object.contains_key(**field))
85            .copied()
86            .collect::<Vec<_>>();
87        let mut unknown = object
88            .keys()
89            .filter(|field| !allowed.contains(&field.as_str()))
90            .map(String::as_str)
91            .collect::<Vec<_>>();
92        missing.sort_unstable();
93        unknown.sort_unstable();
94        if !missing.is_empty() || !unknown.is_empty() {
95            return Err(format!(
96                "tool_result_invalid: ToolExecutionResult fields do not match the current wire: missing={missing:?}, unknown={unknown:?}"
97            ));
98        }
99
100        let metadata = match object.get("metadata") {
101            None => Metadata::new(),
102            Some(Value::Object(metadata)) => metadata.clone().into_iter().collect(),
103            Some(_) => return Err("ToolExecutionResult metadata must be an object".to_string()),
104        };
105        let truncated = match object.get("truncated") {
106            None => false,
107            Some(Value::Bool(true)) => true,
108            Some(_) => {
109                return Err("tool_result_invalid: truncated must be omitted or true".to_string())
110            }
111        };
112        let truncation_reason = match strict_optional_string(object, "truncation_reason")?
113            .as_deref()
114        {
115            None => None,
116            Some("output_limit") => Some(ToolTruncationReason::OutputLimit),
117            Some("read_limit") => Some(ToolTruncationReason::ReadLimit),
118            Some(_) => return Err("tool_result_invalid: truncation_reason is invalid".to_string()),
119        };
120        let result = Self {
121            tool_call_id: read_required_string(object, "tool_call_id")?.to_string(),
122            content: read_required_string(object, "content")?.to_string(),
123            status: parse_tool_result_status(read_required_string(object, "status_code")?)?,
124            directive: parse_tool_directive(read_required_string(object, "directive")?)?,
125            error_code: strict_optional_string(object, "error_code")?,
126            metadata,
127            image_url: strict_optional_string(object, "image_url")?,
128            image_path: strict_optional_string(object, "image_path")?,
129            truncated,
130            truncation_reason,
131            original_bytes: strict_optional_u64(object, "original_bytes")?,
132            visible_bytes: strict_optional_u64(object, "visible_bytes")?,
133            artifact: strict_optional_object(object, "artifact")?,
134            cursor: strict_optional_object(object, "cursor")?,
135        };
136        result.validate()?;
137        Ok(result)
138    }
139}
140
141fn strict_optional_string(
142    object: &serde_json::Map<String, Value>,
143    key: &str,
144) -> Result<Option<String>, String> {
145    match object.get(key) {
146        None => Ok(None),
147        Some(Value::String(value)) => Ok(Some(value.clone())),
148        Some(_) => Err(format!("ToolExecutionResult {key} must be a string")),
149    }
150}
151
152fn strict_optional_u64(
153    object: &serde_json::Map<String, Value>,
154    key: &str,
155) -> Result<Option<u64>, String> {
156    match object.get(key) {
157        None => Ok(None),
158        Some(Value::Number(value)) => value
159            .as_u64()
160            .filter(|value| *value <= crate::budget::MAX_WIRE_INTEGER)
161            .map(Some)
162            .ok_or_else(|| {
163                format!("ToolExecutionResult {key} must be a JSON-safe unsigned integer")
164            }),
165        Some(_) => Err(format!(
166            "ToolExecutionResult {key} must be a JSON-safe unsigned integer"
167        )),
168    }
169}
170
171fn strict_optional_object<T>(
172    object: &serde_json::Map<String, Value>,
173    key: &str,
174) -> Result<Option<T>, String>
175where
176    T: serde::de::DeserializeOwned,
177{
178    match object.get(key) {
179        None => Ok(None),
180        Some(Value::Object(_)) => serde_json::from_value(object[key].clone())
181            .map(Some)
182            .map_err(|error| {
183                format!("tool_result_invalid: ToolExecutionResult {key} is invalid: {error}")
184            }),
185        Some(_) => Err(format!("ToolExecutionResult {key} must be an object")),
186    }
187}