1use objectiveai_sdk::agent::completions::message::{
23 AssistantToolCall, Message, RichContent, RichContentPart,
24};
25use objectiveai_sdk::agent::completions::response::ToolResponse;
26use objectiveai_sdk::agent::completions::response::streaming::{
27 AgentCompletionChunk, AssistantResponseChunk, MessageChunk,
28};
29use objectiveai_sdk::functions::executions::response::streaming::{
30 FunctionExecutionChunk, TaskChunk, VectorCompletionTaskChunk,
31};
32use objectiveai_sdk::vector::completions::response::Vote;
33use objectiveai_sdk::vector::completions::response::streaming::VectorCompletionChunk;
34
35use super::row::{RowValue, RowsIter, WriterItem, WriterItems};
36
37pub fn agent_completion_chunk_rows<'a>(
42 chunk: &'a AgentCompletionChunk,
43) -> WriterItems<'a> {
44 let response_id = chunk.id.as_str();
45 let agent_hierarchy = chunk.agent_instance_hierarchy.as_str();
46 let usage = chunk.usage.as_ref().map(move |u| WriterItem::Usage {
47 agent_instance_hierarchy: agent_hierarchy,
48 total_tokens: u.total_tokens,
49 });
50 let error = chunk.error.as_ref().map(move |error| WriterItem::Error {
54 agent_instance_hierarchy: agent_hierarchy,
55 response_id,
56 error,
57 });
58 let rows = chunk
59 .messages
60 .iter()
61 .flat_map(move |msg| message_chunk_rows(response_id, agent_hierarchy, msg))
62 .map(WriterItem::Row);
63 Box::new(usage.into_iter().chain(error).chain(rows))
64}
65
66pub fn vector_completion_chunk_rows<'a>(
69 chunk: &'a VectorCompletionChunk,
70) -> WriterItems<'a> {
71 Box::new(
72 chunk
73 .completions
74 .iter()
75 .flat_map(|c| agent_completion_chunk_rows(&c.inner)),
76 )
77}
78
79pub fn function_execution_chunk_rows<'a>(
84 chunk: &'a FunctionExecutionChunk,
85) -> WriterItems<'a> {
86 let task_iter = chunk
87 .tasks
88 .iter()
89 .flat_map(|task| task_chunk_rows(task));
90 let reasoning_iter = chunk
91 .reasoning
92 .iter()
93 .flat_map(|r| agent_completion_chunk_rows(&r.inner));
94 Box::new(task_iter.chain(reasoning_iter))
95}
96
97pub struct CompletionStatus<'a> {
104 pub agent_instance_hierarchy: &'a str,
105 pub response_id: &'a str,
106 pub finished: bool,
107 pub errored: bool,
108}
109
110pub type CompletionStatuses<'a> = Box<dyn Iterator<Item = CompletionStatus<'a>> + Send + 'a>;
112
113pub fn agent_completion_statuses<'a>(
115 chunk: &'a AgentCompletionChunk,
116) -> CompletionStatuses<'a> {
117 Box::new(std::iter::once(CompletionStatus {
118 agent_instance_hierarchy: chunk.agent_instance_hierarchy.as_str(),
119 response_id: chunk.id.as_str(),
120 finished: chunk.usage.is_some(),
121 errored: chunk.error.is_some(),
122 }))
123}
124
125pub fn vector_completion_statuses<'a>(
127 chunk: &'a VectorCompletionChunk,
128) -> CompletionStatuses<'a> {
129 Box::new(
130 chunk
131 .completions
132 .iter()
133 .flat_map(|c| agent_completion_statuses(&c.inner)),
134 )
135}
136
137pub fn function_execution_statuses<'a>(
141 chunk: &'a FunctionExecutionChunk,
142) -> CompletionStatuses<'a> {
143 let tasks = chunk.tasks.iter().flat_map(|task| match task {
144 TaskChunk::FunctionExecution(wrapper) => {
145 function_execution_statuses(&wrapper.inner)
146 }
147 TaskChunk::VectorCompletion(wrapper) => {
148 vector_completion_statuses(&wrapper.inner)
149 }
150 });
151 let reasoning = chunk
152 .reasoning
153 .iter()
154 .flat_map(|r| agent_completion_statuses(&r.inner));
155 Box::new(tasks.chain(reasoning))
156}
157
158fn task_chunk_rows<'a>(task: &'a TaskChunk) -> WriterItems<'a> {
161 match task {
162 TaskChunk::FunctionExecution(wrapper) => {
163 function_execution_chunk_rows(&wrapper.inner)
164 }
165 TaskChunk::VectorCompletion(wrapper) => vector_task_rows(wrapper),
166 }
167}
168
169fn vector_task_rows<'a>(
177 wrapper: &'a VectorCompletionTaskChunk,
178) -> WriterItems<'a> {
179 let vc = &wrapper.inner;
180 let request_messages = wrapper.request_messages.as_deref();
181 let request_choices = wrapper.request_choices.as_deref();
182 Box::new(vc.completions.iter().flat_map(move |c| {
183 let agent = &c.inner;
184 let response_id = agent.id.as_str();
185 let aih = agent.agent_instance_hierarchy.as_str();
186
187 let req_msgs: RowsIter<'a> = match request_messages {
188 Some(msgs) => request_message_rows(response_id, aih, msgs),
189 None => Box::new(std::iter::empty()),
190 };
191 let choices: RowsIter<'a> =
192 match (request_choices, c.request_choice_keys.as_deref()) {
193 (Some(chs), Some(keys)) => {
194 vector_choice_rows(response_id, aih, chs, keys)
195 }
196 _ => Box::new(std::iter::empty()),
197 };
198 let vote = vote_row(response_id, aih, &vc.votes, c.index)
199 .map(WriterItem::Row);
200
201 Box::new(
202 req_msgs
203 .map(WriterItem::Row)
204 .chain(choices.map(WriterItem::Row))
205 .chain(agent_completion_chunk_rows(agent))
206 .chain(vote),
207 ) as WriterItems<'a>
208 }))
209}
210
211fn vote_row<'a>(
216 response_id: &'a str,
217 agent_instance_hierarchy: &'a str,
218 votes: &'a [Vote],
219 completion_index: u64,
220) -> Option<RowValue<'a>> {
221 votes
222 .iter()
223 .find(|v| v.completion_index == Some(completion_index))
224 .map(|v| RowValue::ResponseVectorVote {
225 response_id,
226 agent_instance_hierarchy,
227 vote: v.vote.as_slice(),
228 })
229}
230
231fn vector_choice_rows<'a>(
234 response_id: &'a str,
235 agent_instance_hierarchy: &'a str,
236 choices: &'a [RichContent],
237 keys: &'a [String],
238) -> RowsIter<'a> {
239 Box::new(choices.iter().enumerate().flat_map(move |(i, content)| {
240 let choice_index = i as u64;
241 let key = keys.get(i).map(|k| k.as_str()).unwrap_or("");
242 let head = std::iter::once(RowValue::RequestVectorChoice {
243 response_id,
244 agent_instance_hierarchy,
245 choice_index,
246 key,
247 });
248 Box::new(head.chain(request_content_rows(
249 RequestTarget::VectorChoice,
250 response_id,
251 agent_instance_hierarchy,
252 choice_index,
253 content,
254 ))) as RowsIter<'a>
255 }))
256}
257
258pub(super) fn request_message_rows<'a>(
264 response_id: &'a str,
265 aih: &'a str,
266 messages: &'a [Message],
267) -> RowsIter<'a> {
268 Box::new(messages.iter().enumerate().flat_map(move |(i, msg)| {
269 let index = i as u64;
270 let out: RowsIter<'a> = match msg {
271 Message::User(u) => {
272 request_content_rows(RequestTarget::User, response_id, aih, index, &u.content)
273 }
274 Message::Assistant(a) => {
275 let reasoning = a.reasoning.iter().map(move |t| {
277 RowValue::RequestMessageAssistantReasoning {
278 response_id,
279 agent_instance_hierarchy: aih,
280 index,
281 text: t.as_str(),
282 }
283 });
284 let tool_calls =
285 a.tool_calls.iter().flatten().enumerate().map(move |(tci, tc)| {
286 let AssistantToolCall::Function { id, function } = tc;
287 RowValue::RequestMessageAssistantToolCalls {
288 response_id,
289 agent_instance_hierarchy: aih,
290 index,
291 tool_call_index: tci as u64,
292 tool_call_id: id.as_str(),
293 function_name: function.name.as_str(),
294 arguments: function.arguments.as_str(),
295 }
296 });
297 let content = a.content.iter().flat_map(move |c| {
298 request_content_rows(RequestTarget::Assistant, response_id, aih, index, c)
299 });
300 let refusal = a.refusal.iter().map(move |t| {
301 RowValue::RequestMessageAssistantRefusal {
302 response_id,
303 agent_instance_hierarchy: aih,
304 index,
305 text: t.as_str(),
306 }
307 });
308 Box::new(reasoning.chain(tool_calls).chain(content).chain(refusal))
309 }
310 Message::Tool(t) => {
311 let head = std::iter::once(RowValue::RequestMessageTool {
312 response_id,
313 agent_instance_hierarchy: aih,
314 index,
315 tool_call_id: t.tool_call_id.as_str(),
316 });
317 Box::new(head.chain(request_content_rows(
318 RequestTarget::Tool,
319 response_id,
320 aih,
321 index,
322 &t.content,
323 )))
324 }
325 };
326 out
327 }))
328}
329
330#[derive(Debug, Clone, Copy)]
332enum RequestTarget {
333 User,
334 Assistant,
335 Tool,
336 VectorChoice,
337}
338
339fn request_content_rows<'a>(
340 target: RequestTarget,
341 response_id: &'a str,
342 aih: &'a str,
343 index: u64,
344 content: &'a RichContent,
345) -> RowsIter<'a> {
346 match content {
347 RichContent::Text(text) => Box::new(std::iter::once(request_content_text(
348 target, response_id, aih, index, 0, text.as_str(),
349 ))),
350 RichContent::Parts(parts) => {
351 Box::new(parts.iter().enumerate().map(move |(pi, part)| {
352 request_content_part(target, response_id, aih, index, pi as u64, part)
353 }))
354 }
355 }
356}
357
358fn request_content_text<'a>(
359 target: RequestTarget,
360 response_id: &'a str,
361 agent_instance_hierarchy: &'a str,
362 index: u64,
363 part_index: u64,
364 text: &'a str,
365) -> RowValue<'a> {
366 match target {
367 RequestTarget::User => RowValue::RequestMessageUserContentText { response_id, agent_instance_hierarchy, index, part_index, text },
368 RequestTarget::Assistant => RowValue::RequestMessageAssistantContentText { response_id, agent_instance_hierarchy, index, part_index, text },
369 RequestTarget::Tool => RowValue::RequestMessageToolContentText { response_id, agent_instance_hierarchy, index, part_index, text },
370 RequestTarget::VectorChoice => RowValue::RequestVectorChoiceContentText { response_id, agent_instance_hierarchy, choice_index: index, part_index, text },
371 }
372}
373
374fn request_content_part<'a>(
375 target: RequestTarget,
376 response_id: &'a str,
377 agent_instance_hierarchy: &'a str,
378 index: u64,
379 part_index: u64,
380 part: &'a RichContentPart,
381) -> RowValue<'a> {
382 match part {
383 RichContentPart::Text { text } => {
384 request_content_text(target, response_id, agent_instance_hierarchy, index, part_index, text.as_str())
385 }
386 RichContentPart::ImageUrl { image_url } => match target {
387 RequestTarget::User => RowValue::RequestMessageUserContentImage { response_id, agent_instance_hierarchy, index, part_index, image_url },
388 RequestTarget::Assistant => RowValue::RequestMessageAssistantContentImage { response_id, agent_instance_hierarchy, index, part_index, image_url },
389 RequestTarget::Tool => RowValue::RequestMessageToolContentImage { response_id, agent_instance_hierarchy, index, part_index, image_url },
390 RequestTarget::VectorChoice => RowValue::RequestVectorChoiceContentImage { response_id, agent_instance_hierarchy, choice_index: index, part_index, image_url },
391 },
392 RichContentPart::InputAudio { input_audio } => match target {
393 RequestTarget::User => RowValue::RequestMessageUserContentAudio { response_id, agent_instance_hierarchy, index, part_index, input_audio },
394 RequestTarget::Assistant => RowValue::RequestMessageAssistantContentAudio { response_id, agent_instance_hierarchy, index, part_index, input_audio },
395 RequestTarget::Tool => RowValue::RequestMessageToolContentAudio { response_id, agent_instance_hierarchy, index, part_index, input_audio },
396 RequestTarget::VectorChoice => RowValue::RequestVectorChoiceContentAudio { response_id, agent_instance_hierarchy, choice_index: index, part_index, input_audio },
397 },
398 RichContentPart::InputVideo { video_url } | RichContentPart::VideoUrl { video_url } => match target {
399 RequestTarget::User => RowValue::RequestMessageUserContentVideo { response_id, agent_instance_hierarchy, index, part_index, video_url },
400 RequestTarget::Assistant => RowValue::RequestMessageAssistantContentVideo { response_id, agent_instance_hierarchy, index, part_index, video_url },
401 RequestTarget::Tool => RowValue::RequestMessageToolContentVideo { response_id, agent_instance_hierarchy, index, part_index, video_url },
402 RequestTarget::VectorChoice => RowValue::RequestVectorChoiceContentVideo { response_id, agent_instance_hierarchy, choice_index: index, part_index, video_url },
403 },
404 RichContentPart::File { file } => match target {
405 RequestTarget::User => RowValue::RequestMessageUserContentFile { response_id, agent_instance_hierarchy, index, part_index, file },
406 RequestTarget::Assistant => RowValue::RequestMessageAssistantContentFile { response_id, agent_instance_hierarchy, index, part_index, file },
407 RequestTarget::Tool => RowValue::RequestMessageToolContentFile { response_id, agent_instance_hierarchy, index, part_index, file },
408 RequestTarget::VectorChoice => RowValue::RequestVectorChoiceContentFile { response_id, agent_instance_hierarchy, choice_index: index, part_index, file },
409 },
410 }
411}
412
413fn message_chunk_rows<'a>(
414 response_id: &'a str,
415 agent_instance_hierarchy: &'a str,
416 msg: &'a MessageChunk,
417) -> RowsIter<'a> {
418 match msg {
419 MessageChunk::Assistant(a) => {
420 assistant_response_chunk_rows(response_id, agent_instance_hierarchy, a)
421 }
422 MessageChunk::Tool(t) => {
423 tool_response_rows(response_id, agent_instance_hierarchy, t)
424 }
425 }
426}
427
428fn assistant_response_chunk_rows<'a>(
429 response_id: &'a str,
430 agent_instance_hierarchy: &'a str,
431 chunk: &'a AssistantResponseChunk,
432) -> RowsIter<'a> {
433 let index = chunk.index;
434
435 let message_queue_iter = chunk
440 .request_message_ids
441 .as_deref()
442 .unwrap_or(&[])
443 .iter()
444 .copied()
445 .map(move |message_queue_content_id| RowValue::MessageQueueContent {
446 response_id,
447 agent_instance_hierarchy,
448 message_queue_content_id,
449 });
450
451 let reasoning_iter = chunk.reasoning.iter().map(move |text| {
457 RowValue::AssistantResponseReasoning {
458 response_id,
459 agent_instance_hierarchy,
460 index,
461 text: text.as_str(),
462 }
463 });
464 let tool_calls_iter = chunk
465 .tool_calls
466 .iter()
467 .flatten()
468 .filter_map(move |tc| {
469 let id = tc.id.as_deref()?;
470 let name = tc.function.as_ref().and_then(|f| f.name.as_deref())?;
471 let args = tc.function.as_ref().and_then(|f| f.arguments.as_deref())?;
472 Some(RowValue::AssistantResponseToolCalls {
473 response_id,
474 agent_instance_hierarchy,
475 index,
476 tool_call_index: tc.index,
485 tool_call_id: id,
486 function_name: name,
487 arguments: args,
488 })
489 });
490 let content_iter = chunk
491 .content
492 .iter()
493 .flat_map(move |c| assistant_content_rows(response_id, agent_instance_hierarchy, index, c));
494 let refusal_iter = chunk.refusal.iter().map(move |text| {
495 RowValue::AssistantResponseRefusal {
496 response_id,
497 agent_instance_hierarchy,
498 index,
499 text: text.as_str(),
500 }
501 });
502
503 Box::new(
504 message_queue_iter
505 .chain(reasoning_iter)
506 .chain(tool_calls_iter)
507 .chain(content_iter)
508 .chain(refusal_iter),
509 )
510}
511
512fn tool_response_rows<'a>(
513 response_id: &'a str,
514 agent_instance_hierarchy: &'a str,
515 response: &'a ToolResponse,
516) -> RowsIter<'a> {
517 let index = response.index;
518 let message_queue_iter = response
523 .request_message_ids
524 .as_deref()
525 .unwrap_or(&[])
526 .iter()
527 .copied()
528 .map(move |message_queue_content_id| RowValue::MessageQueueContent {
529 response_id,
530 agent_instance_hierarchy,
531 message_queue_content_id,
532 });
533 let head = std::iter::once(RowValue::ToolResponse {
534 response_id,
535 agent_instance_hierarchy,
536 index,
537 tool_call_id: response.inner.tool_call_id.as_str(),
538 });
539 Box::new(
540 message_queue_iter.chain(head).chain(tool_content_rows(
541 response_id,
542 agent_instance_hierarchy,
543 index,
544 &response.inner.content,
545 )),
546 )
547}
548
549fn assistant_content_rows<'a>(
550 response_id: &'a str,
551 agent_instance_hierarchy: &'a str,
552 index: u64,
553 content: &'a RichContent,
554) -> RowsIter<'a> {
555 match content {
556 RichContent::Text(text) => Box::new(std::iter::once(RowValue::AssistantResponseContentText {
557 response_id,
558 agent_instance_hierarchy,
559 index,
560 part_index: 0,
561 text: text.as_str(),
562 })),
563 RichContent::Parts(parts) => Box::new(parts.iter().enumerate().map(move |(part_index, part)| {
564 assistant_content_part(response_id, agent_instance_hierarchy, index, part_index as u64, part)
565 })),
566 }
567}
568
569fn assistant_content_part<'a>(
570 response_id: &'a str,
571 agent_instance_hierarchy: &'a str,
572 index: u64,
573 part_index: u64,
574 part: &'a RichContentPart,
575) -> RowValue<'a> {
576 match part {
577 RichContentPart::Text { text } => RowValue::AssistantResponseContentText {
578 response_id, agent_instance_hierarchy, index, part_index, text: text.as_str(),
579 },
580 RichContentPart::ImageUrl { image_url } => RowValue::AssistantResponseContentImage {
581 response_id, agent_instance_hierarchy, index, part_index, image_url,
582 },
583 RichContentPart::InputAudio { input_audio } => RowValue::AssistantResponseContentAudio {
584 response_id, agent_instance_hierarchy, index, part_index, input_audio,
585 },
586 RichContentPart::InputVideo { video_url }
587 | RichContentPart::VideoUrl { video_url } => RowValue::AssistantResponseContentVideo {
588 response_id, agent_instance_hierarchy, index, part_index, video_url,
589 },
590 RichContentPart::File { file } => RowValue::AssistantResponseContentFile {
591 response_id, agent_instance_hierarchy, index, part_index, file,
592 },
593 }
594}
595
596fn tool_content_rows<'a>(
597 response_id: &'a str,
598 agent_instance_hierarchy: &'a str,
599 index: u64,
600 content: &'a RichContent,
601) -> RowsIter<'a> {
602 match content {
603 RichContent::Text(text) => Box::new(std::iter::once(RowValue::ToolResponseContentText {
604 response_id,
605 agent_instance_hierarchy,
606 index,
607 part_index: 0,
608 text: text.as_str(),
609 })),
610 RichContent::Parts(parts) => Box::new(parts.iter().enumerate().map(move |(part_index, part)| {
611 tool_content_part(response_id, agent_instance_hierarchy, index, part_index as u64, part)
612 })),
613 }
614}
615
616fn tool_content_part<'a>(
617 response_id: &'a str,
618 agent_instance_hierarchy: &'a str,
619 index: u64,
620 part_index: u64,
621 part: &'a RichContentPart,
622) -> RowValue<'a> {
623 match part {
624 RichContentPart::Text { text } => RowValue::ToolResponseContentText {
625 response_id, agent_instance_hierarchy, index, part_index, text: text.as_str(),
626 },
627 RichContentPart::ImageUrl { image_url } => RowValue::ToolResponseContentImage {
628 response_id, agent_instance_hierarchy, index, part_index, image_url,
629 },
630 RichContentPart::InputAudio { input_audio } => RowValue::ToolResponseContentAudio {
631 response_id, agent_instance_hierarchy, index, part_index, input_audio,
632 },
633 RichContentPart::InputVideo { video_url }
634 | RichContentPart::VideoUrl { video_url } => RowValue::ToolResponseContentVideo {
635 response_id, agent_instance_hierarchy, index, part_index, video_url,
636 },
637 RichContentPart::File { file } => RowValue::ToolResponseContentFile {
638 response_id, agent_instance_hierarchy, index, part_index, file,
639 },
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646 use objectiveai_sdk::agent::completions::response::Usage;
647
648 fn agent_chunk(aih: &str, total: Option<u64>) -> AgentCompletionChunk {
649 AgentCompletionChunk {
650 agent_instance_hierarchy: aih.to_string(),
651 usage: total.map(|t| Usage { total_tokens: t, ..Default::default() }),
652 ..Default::default()
653 }
654 }
655
656 fn usages<'a>(items: WriterItems<'a>) -> Vec<(&'a str, u64)> {
658 items
659 .filter_map(|item| match item {
660 WriterItem::Usage { agent_instance_hierarchy, total_tokens } => {
661 Some((agent_instance_hierarchy, total_tokens))
662 }
663 WriterItem::Row(_) | WriterItem::Error { .. } => None,
664 })
665 .collect()
666 }
667
668 #[test]
669 fn agent_walk_emits_usage_only_when_present() {
670 assert_eq!(
671 usages(agent_completion_chunk_rows(&agent_chunk("a/b", Some(42)))),
672 vec![("a/b", 42)]
673 );
674 assert!(usages(agent_completion_chunk_rows(&agent_chunk("a/b", None))).is_empty());
675 }
676
677 #[test]
678 fn vector_walk_surfaces_nested_usage_and_skips_none() {
679 use objectiveai_sdk::vector::completions::response::streaming::AgentCompletionChunk as VecAgent;
680 let chunk = VectorCompletionChunk {
681 completions: vec![
682 VecAgent { index: 0, inner: agent_chunk("a/x", Some(10)), ..Default::default() },
683 VecAgent { index: 1, inner: agent_chunk("a/y", None), ..Default::default() },
684 VecAgent { index: 2, inner: agent_chunk("a/z", Some(7)), ..Default::default() },
685 ],
686 ..Default::default()
687 };
688 assert_eq!(
689 usages(vector_completion_chunk_rows(&chunk)),
690 vec![("a/x", 10), ("a/z", 7)]
691 );
692 }
693}