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}