1use super::*;
3use tokio::sync::OwnedSemaphorePermit;
4
5pub(super) const TREE_BYTES: usize = 64 * 1024 * 1024;
6#[derive(Default)]
7pub(super) struct Store(HashMap<String, Retained>);
8struct Retained {
9 native: crate::AgentToolResult,
10 is_error: bool,
11 encoded: Box<str>,
12 _bytes: OwnedSemaphorePermit,
13}
14impl Store {
15 pub(super) fn clear(&mut self) {
16 self.0.clear();
17 }
18}
19fn value_overhead(value: &Value) -> Option<usize> {
22 let add = |sum: usize, child: &Value| sum.checked_add(value_overhead(child)?);
23 match value {
24 Value::Array(values) => values.iter().try_fold(128, add),
25 Value::Object(values) => values.values().try_fold(128, add),
26 _ => Some(128),
27 }
28}
29impl ToolProgram {
30 pub(super) fn retain_result(
31 &self,
32 id: &str,
33 native: crate::AgentToolResult,
34 is_error: bool,
35 encoded: String,
36 ) -> Result<Value, String> {
37 let charge = encoded
40 .len()
41 .checked_mul(2)
42 .and_then(|n| n.checked_add(4096))
43 .and_then(|n| n.checked_add(value_overhead(&native.details)?))
44 .and_then(|n| n.checked_add(native.content.len().checked_mul(256)?))
45 .filter(|n| *n <= TREE_BYTES)
46 .ok_or("ToolProgram result reference byte quota exceeded")?;
47 let mut state = self.state.lock().unwrap();
48 if state.results.0.len() >= 16 {
49 return Err("ToolProgram retained result reference limit exceeded".into());
50 }
51 let bytes = self
52 .result_bytes
53 .clone()
54 .try_acquire_many_owned(charge as u32)
55 .map_err(|_| "ToolProgram invocation-tree result reference byte quota exceeded")?;
56 let manifest = json!({"requestId":id,"status":"done","isError":is_error,
57 "resultReference":{"version":1,"id":id,"byteLength":encoded.len(),
58 "contentBlocks":native.content.len(),"isError":is_error,"terminate":native.terminate}});
59 state.results.0.insert(
60 id.to_owned(),
61 Retained {
62 native,
63 is_error,
64 encoded: encoded.into_boxed_str(),
65 _bytes: bytes,
66 },
67 );
68 Ok(manifest)
69 }
70 pub(super) fn read_result(&self, args: &Value) -> Result<Value, String> {
71 let id = args["resultId"].as_str().ok_or("resultId is required")?;
72 let offset = integer(args, "offset", 0, 0, u64::MAX)?;
73 let limit = integer(args, "limit", 65536, 4, 65536)? as usize;
74 let state = self.state.lock().unwrap();
75 if self.signal.is_cancelled() {
76 return Err("parent invocation is no longer active".into());
77 }
78 let retained = state
79 .results
80 .0
81 .get(id)
82 .ok_or("unknown result reference for this invocation")?;
83 let text = &retained.encoded;
84 let offset = usize::try_from(offset).map_err(|_| "offset is past EOF")?;
85 if offset > text.len() || !text.is_char_boundary(offset) {
86 return Err("offset must be a UTF-8 boundary at or before EOF".into());
87 }
88 let mut end = offset.saturating_add(limit).min(text.len());
89 while !text.is_char_boundary(end) {
90 end -= 1;
91 }
92 let reply = json!({"resultId":id,"offset":offset,"data":&text[offset..end],
93 "byteLength":text.len(),"nextOffset":if end < text.len() {Some(end)} else {None}});
94 bounded(&reply)?;
96 Ok(reply)
97 }
98 pub(super) fn release_result(&self, args: &Value) -> Result<Value, String> {
99 let id = args["resultId"].as_str().ok_or("resultId is required")?;
100 self.state
101 .lock()
102 .unwrap()
103 .results
104 .0
105 .remove(id)
106 .ok_or("unknown result reference for this invocation")?;
107 Ok(json!({"resultId":id,"status":"released"}))
108 }
109 pub fn resolve_result_reference(
113 &self,
114 id: &str,
115 ) -> Result<(crate::AgentToolResult, bool), String> {
116 let state = self.state.lock().unwrap();
117 if self.signal.is_cancelled() {
118 return Err("parent invocation is no longer active".into());
119 }
120 let retained = state
121 .results
122 .0
123 .get(id)
124 .ok_or("unknown result reference for this invocation")?;
125 let mut result = retained.native.clone();
126 result.usage = None;
127 result.added_tool_names.clear();
128 Ok((result, retained.is_error))
129 }
130}
131
132const CONTENT_BLOCKS: usize = 256;
133const CONTENT_BYTES: usize = 32 * 1024 * 1024;
134
135#[derive(Default)]
137struct ContentBudget {
138 blocks: usize,
139 encoded_bytes: usize,
140}
141impl ContentBudget {
142 fn block(&mut self, block: &TextContentOrImage) -> Result<(), String> {
143 if self.blocks == CONTENT_BLOCKS {
144 return Err("content exceeds 256 blocks".into());
145 }
146 let mut counter = ByteCounter {
148 bytes: 0,
149 remaining: CONTENT_BYTES
150 .checked_sub(self.encoded_bytes + self.blocks + 2)
151 .ok_or("content array exceeds 32 MiB")?,
152 };
153 match block {
154 TextContentOrImage::Text(text) => serde_json::to_writer(&mut counter, text),
155 TextContentOrImage::Image(image) => serde_json::to_writer(&mut counter, image),
156 }
157 .map_err(|e| e.to_string())?;
158 self.encoded_bytes += counter.bytes;
159 self.blocks += 1;
160 Ok(())
161 }
162}
163struct ByteCounter {
164 bytes: usize,
165 remaining: usize,
166}
167impl std::io::Write for ByteCounter {
168 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
169 if bytes.len() > self.remaining {
170 return Err(std::io::Error::other("content array exceeds 32 MiB"));
171 }
172 self.bytes += bytes.len();
173 self.remaining -= bytes.len();
174 Ok(bytes.len())
175 }
176 fn flush(&mut self) -> std::io::Result<()> {
177 Ok(())
178 }
179}
180fn visit_selected(
181 blocks: &[TextContentOrImage],
182 indices: Option<&[usize]>,
183 mut visit: impl FnMut(&TextContentOrImage) -> Result<(), String>,
184) -> Result<(), String> {
185 let count = indices.map_or(blocks.len(), <[usize]>::len);
186 if count > CONTENT_BLOCKS {
187 return Err("content selection exceeds 256 blocks".into());
188 }
189 for position in 0..count {
190 let index = indices.map_or(position, |indices| indices[position]);
191 visit(
192 blocks
193 .get(index)
194 .ok_or("content index is past native content")?,
195 )?;
196 }
197 Ok(())
198}
199impl ToolProgram {
200 fn check_content_live(&self) -> Result<(), String> {
201 if self.signal.is_cancelled() {
202 Err("parent invocation is no longer active".into())
203 } else {
204 Ok(())
205 }
206 }
207 pub(super) fn measure_result_content(&self, args: &Value) -> Result<Value, String> {
208 self.check_content_live()?;
209 let id = args["resultId"].as_str().ok_or("resultId is required")?;
210 let indices = match args.get("indices") {
211 None | Some(Value::Null) => None,
212 Some(Value::Array(values)) if values.len() <= CONTENT_BLOCKS => Some(
213 values
214 .iter()
215 .map(|v| {
216 v.as_u64()
217 .and_then(|n| usize::try_from(n).ok())
218 .ok_or_else(|| "indices must contain nonnegative integers".to_owned())
219 })
220 .collect::<Result<Vec<_>, _>>()?,
221 ),
222 _ => return Err("indices must be null or an array of at most 256 integers".into()),
223 };
224 let state = self.state.lock().unwrap();
225 let retained = state
226 .results
227 .0
228 .get(id)
229 .ok_or("unknown result reference for this invocation")?;
230 let mut budget = ContentBudget::default();
231 visit_selected(&retained.native.content, indices.as_deref(), |block| {
232 self.check_content_live()?;
233 budget.block(block)
234 })?;
235 self.check_content_live()?;
236 let reply = json!({"resultId":id,"contentBlocks":budget.blocks,"encodedBytes":budget.encoded_bytes});
237 bounded(&reply)?;
238 Ok(reply)
239 }
240 pub fn resolve_content_parts(
244 &self,
245 parts: Vec<ToolProgramContentPart>,
246 ) -> Result<Vec<TextContentOrImage>, String> {
247 self.check_content_live()?;
248 if parts.len() > CONTENT_BLOCKS {
249 return Err("content exceeds 256 parts".into());
250 }
251 let state = self.state.lock().unwrap();
252 let mut budget = ContentBudget::default();
253 let mut content = Vec::new();
254 for part in parts {
255 self.check_content_live()?;
256 match part {
257 ToolProgramContentPart::Inline(blocks) => {
258 for block in blocks {
259 self.check_content_live()?;
260 budget.block(&block)?;
261 content.push(block);
262 }
263 }
264 ToolProgramContentPart::Reference { id, indices } => {
265 let retained = state
266 .results
267 .0
268 .get(&id)
269 .ok_or("unknown result reference for this invocation")?;
270 visit_selected(&retained.native.content, indices.as_deref(), |block| {
271 self.check_content_live()?;
272 budget.block(block)?;
273 content.push(block.clone());
274 Ok(())
275 })?;
276 }
277 }
278 }
279 self.check_content_live()?;
280 Ok(content)
281 }
282}