1use objectiveai_sdk::agent::completions::message::{RichContent, RichContentPart};
23use objectiveai_sdk::agent::completions::response::ToolResponse;
24use objectiveai_sdk::agent::completions::response::streaming::{
25 AgentCompletionChunk, AssistantResponseChunk, MessageChunk,
26};
27use objectiveai_sdk::functions::executions::response::streaming::{
28 FunctionExecutionChunk, TaskChunk,
29};
30use objectiveai_sdk::vector::completions::response::streaming::VectorCompletionChunk;
31
32use super::row::{RowValue, RowsIter, WriterItem, WriterItems};
33
34pub fn agent_completion_chunk_rows<'a>(
39 chunk: &'a AgentCompletionChunk,
40) -> WriterItems<'a> {
41 let response_id = chunk.id.as_str();
42 let agent_hierarchy = chunk.agent_instance_hierarchy.as_str();
43 let usage = chunk.usage.as_ref().map(move |u| WriterItem::Usage {
44 agent_instance_hierarchy: agent_hierarchy,
45 total_tokens: u.total_tokens,
46 });
47 let rows = chunk
48 .messages
49 .iter()
50 .flat_map(move |msg| message_chunk_rows(response_id, agent_hierarchy, msg))
51 .map(WriterItem::Row);
52 Box::new(usage.into_iter().chain(rows))
53}
54
55pub fn vector_completion_chunk_rows<'a>(
58 chunk: &'a VectorCompletionChunk,
59) -> WriterItems<'a> {
60 Box::new(
61 chunk
62 .completions
63 .iter()
64 .flat_map(|c| agent_completion_chunk_rows(&c.inner)),
65 )
66}
67
68pub fn function_execution_chunk_rows<'a>(
73 chunk: &'a FunctionExecutionChunk,
74) -> WriterItems<'a> {
75 let task_iter = chunk
76 .tasks
77 .iter()
78 .flat_map(|task| task_chunk_rows(task));
79 let reasoning_iter = chunk
80 .reasoning
81 .iter()
82 .flat_map(|r| agent_completion_chunk_rows(&r.inner));
83 Box::new(task_iter.chain(reasoning_iter))
84}
85
86fn task_chunk_rows<'a>(task: &'a TaskChunk) -> WriterItems<'a> {
89 match task {
90 TaskChunk::FunctionExecution(wrapper) => {
91 function_execution_chunk_rows(&wrapper.inner)
92 }
93 TaskChunk::VectorCompletion(wrapper) => {
94 vector_completion_chunk_rows(&wrapper.inner)
95 }
96 }
97}
98
99fn message_chunk_rows<'a>(
100 response_id: &'a str,
101 agent_instance_hierarchy: &'a str,
102 msg: &'a MessageChunk,
103) -> RowsIter<'a> {
104 match msg {
105 MessageChunk::Assistant(a) => {
106 assistant_response_chunk_rows(response_id, agent_instance_hierarchy, a)
107 }
108 MessageChunk::Tool(t) => {
109 tool_response_rows(response_id, agent_instance_hierarchy, t)
110 }
111 }
112}
113
114fn assistant_response_chunk_rows<'a>(
115 response_id: &'a str,
116 agent_instance_hierarchy: &'a str,
117 chunk: &'a AssistantResponseChunk,
118) -> RowsIter<'a> {
119 let index = chunk.index;
120
121 let message_queue_iter = chunk
126 .request_message_ids
127 .as_deref()
128 .unwrap_or(&[])
129 .iter()
130 .copied()
131 .map(move |message_queue_content_id| RowValue::MessageQueueContent {
132 response_id,
133 agent_instance_hierarchy,
134 message_queue_content_id,
135 });
136
137 let reasoning_iter = chunk.reasoning.iter().map(move |text| {
143 RowValue::AssistantResponseReasoning {
144 response_id,
145 agent_instance_hierarchy,
146 index,
147 text: text.as_str(),
148 }
149 });
150 let tool_calls_iter = chunk
151 .tool_calls
152 .iter()
153 .flatten()
154 .filter_map(move |tc| {
155 let id = tc.id.as_deref()?;
156 let name = tc.function.as_ref().and_then(|f| f.name.as_deref())?;
157 let args = tc.function.as_ref().and_then(|f| f.arguments.as_deref())?;
158 Some(RowValue::AssistantResponseToolCalls {
159 response_id,
160 agent_instance_hierarchy,
161 index,
162 tool_call_index: tc.index,
171 tool_call_id: id,
172 function_name: name,
173 arguments: args,
174 })
175 });
176 let content_iter = chunk
177 .content
178 .iter()
179 .flat_map(move |c| assistant_content_rows(response_id, agent_instance_hierarchy, index, c));
180 let refusal_iter = chunk.refusal.iter().map(move |text| {
181 RowValue::AssistantResponseRefusal {
182 response_id,
183 agent_instance_hierarchy,
184 index,
185 text: text.as_str(),
186 }
187 });
188
189 Box::new(
190 message_queue_iter
191 .chain(reasoning_iter)
192 .chain(tool_calls_iter)
193 .chain(content_iter)
194 .chain(refusal_iter),
195 )
196}
197
198fn tool_response_rows<'a>(
199 response_id: &'a str,
200 agent_instance_hierarchy: &'a str,
201 response: &'a ToolResponse,
202) -> RowsIter<'a> {
203 let index = response.index;
204 let message_queue_iter = response
209 .request_message_ids
210 .as_deref()
211 .unwrap_or(&[])
212 .iter()
213 .copied()
214 .map(move |message_queue_content_id| RowValue::MessageQueueContent {
215 response_id,
216 agent_instance_hierarchy,
217 message_queue_content_id,
218 });
219 let head = std::iter::once(RowValue::ToolResponse {
220 response_id,
221 agent_instance_hierarchy,
222 index,
223 tool_call_id: response.inner.tool_call_id.as_str(),
224 });
225 Box::new(
226 message_queue_iter.chain(head).chain(tool_content_rows(
227 response_id,
228 agent_instance_hierarchy,
229 index,
230 &response.inner.content,
231 )),
232 )
233}
234
235fn assistant_content_rows<'a>(
236 response_id: &'a str,
237 agent_instance_hierarchy: &'a str,
238 index: u64,
239 content: &'a RichContent,
240) -> RowsIter<'a> {
241 match content {
242 RichContent::Text(text) => Box::new(std::iter::once(RowValue::AssistantResponseContentText {
243 response_id,
244 agent_instance_hierarchy,
245 index,
246 part_index: 0,
247 text: text.as_str(),
248 })),
249 RichContent::Parts(parts) => Box::new(parts.iter().enumerate().map(move |(part_index, part)| {
250 assistant_content_part(response_id, agent_instance_hierarchy, index, part_index as u64, part)
251 })),
252 }
253}
254
255fn assistant_content_part<'a>(
256 response_id: &'a str,
257 agent_instance_hierarchy: &'a str,
258 index: u64,
259 part_index: u64,
260 part: &'a RichContentPart,
261) -> RowValue<'a> {
262 match part {
263 RichContentPart::Text { text } => RowValue::AssistantResponseContentText {
264 response_id, agent_instance_hierarchy, index, part_index, text: text.as_str(),
265 },
266 RichContentPart::ImageUrl { image_url } => RowValue::AssistantResponseContentImage {
267 response_id, agent_instance_hierarchy, index, part_index, image_url,
268 },
269 RichContentPart::InputAudio { input_audio } => RowValue::AssistantResponseContentAudio {
270 response_id, agent_instance_hierarchy, index, part_index, input_audio,
271 },
272 RichContentPart::InputVideo { video_url }
273 | RichContentPart::VideoUrl { video_url } => RowValue::AssistantResponseContentVideo {
274 response_id, agent_instance_hierarchy, index, part_index, video_url,
275 },
276 RichContentPart::File { file } => RowValue::AssistantResponseContentFile {
277 response_id, agent_instance_hierarchy, index, part_index, file,
278 },
279 }
280}
281
282fn tool_content_rows<'a>(
283 response_id: &'a str,
284 agent_instance_hierarchy: &'a str,
285 index: u64,
286 content: &'a RichContent,
287) -> RowsIter<'a> {
288 match content {
289 RichContent::Text(text) => Box::new(std::iter::once(RowValue::ToolResponseContentText {
290 response_id,
291 agent_instance_hierarchy,
292 index,
293 part_index: 0,
294 text: text.as_str(),
295 })),
296 RichContent::Parts(parts) => Box::new(parts.iter().enumerate().map(move |(part_index, part)| {
297 tool_content_part(response_id, agent_instance_hierarchy, index, part_index as u64, part)
298 })),
299 }
300}
301
302fn tool_content_part<'a>(
303 response_id: &'a str,
304 agent_instance_hierarchy: &'a str,
305 index: u64,
306 part_index: u64,
307 part: &'a RichContentPart,
308) -> RowValue<'a> {
309 match part {
310 RichContentPart::Text { text } => RowValue::ToolResponseContentText {
311 response_id, agent_instance_hierarchy, index, part_index, text: text.as_str(),
312 },
313 RichContentPart::ImageUrl { image_url } => RowValue::ToolResponseContentImage {
314 response_id, agent_instance_hierarchy, index, part_index, image_url,
315 },
316 RichContentPart::InputAudio { input_audio } => RowValue::ToolResponseContentAudio {
317 response_id, agent_instance_hierarchy, index, part_index, input_audio,
318 },
319 RichContentPart::InputVideo { video_url }
320 | RichContentPart::VideoUrl { video_url } => RowValue::ToolResponseContentVideo {
321 response_id, agent_instance_hierarchy, index, part_index, video_url,
322 },
323 RichContentPart::File { file } => RowValue::ToolResponseContentFile {
324 response_id, agent_instance_hierarchy, index, part_index, file,
325 },
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use objectiveai_sdk::agent::completions::response::Usage;
333
334 fn agent_chunk(aih: &str, total: Option<u64>) -> AgentCompletionChunk {
335 AgentCompletionChunk {
336 agent_instance_hierarchy: aih.to_string(),
337 usage: total.map(|t| Usage { total_tokens: t, ..Default::default() }),
338 ..Default::default()
339 }
340 }
341
342 fn usages<'a>(items: WriterItems<'a>) -> Vec<(&'a str, u64)> {
344 items
345 .filter_map(|item| match item {
346 WriterItem::Usage { agent_instance_hierarchy, total_tokens } => {
347 Some((agent_instance_hierarchy, total_tokens))
348 }
349 WriterItem::Row(_) => None,
350 })
351 .collect()
352 }
353
354 #[test]
355 fn agent_walk_emits_usage_only_when_present() {
356 assert_eq!(
357 usages(agent_completion_chunk_rows(&agent_chunk("a/b", Some(42)))),
358 vec![("a/b", 42)]
359 );
360 assert!(usages(agent_completion_chunk_rows(&agent_chunk("a/b", None))).is_empty());
361 }
362
363 #[test]
364 fn vector_walk_surfaces_nested_usage_and_skips_none() {
365 use objectiveai_sdk::vector::completions::response::streaming::AgentCompletionChunk as VecAgent;
366 let chunk = VectorCompletionChunk {
367 completions: vec![
368 VecAgent { index: 0, inner: agent_chunk("a/x", Some(10)), ..Default::default() },
369 VecAgent { index: 1, inner: agent_chunk("a/y", None), ..Default::default() },
370 VecAgent { index: 2, inner: agent_chunk("a/z", Some(7)), ..Default::default() },
371 ],
372 ..Default::default()
373 };
374 assert_eq!(
375 usages(vector_completion_chunk_rows(&chunk)),
376 vec![("a/x", 10), ("a/z", 7)]
377 );
378 }
379}