Skip to main content

objectiveai_cli/db/logs/
rows.rs

1//! Free functions that walk a chunk and yield every streaming-content
2//! [`RowValue`] it implies. One entry point per top-level chunk type:
3//!
4//! - [`agent_completion_chunk_rows`]
5//! - [`vector_completion_chunk_rows`]
6//! - [`function_execution_chunk_rows`]
7//!
8//! The function-execution walker is recursive — it forwards into the
9//! vector walker (and back into itself for nested function tasks),
10//! which in turn forwards into the agent walker. No collection
11//! happens: each yielded row borrows from the input chunk and the
12//! writer drains the iterator one element at a time.
13//!
14//! Every yielded `RowValue` carries the enclosing agent-completion
15//! chunk's `response_id` AND `agent_instance_hierarchy`, so the
16//! writer can populate `objectiveai.messages` / `objectiveai.messages_queue`
17//! without a side-channel.
18
19use objectiveai_sdk::agent::completions::message::{RichContent, RichContentPart};
20use objectiveai_sdk::agent::completions::response::ToolResponse;
21use objectiveai_sdk::agent::completions::response::streaming::{
22    AgentCompletionChunk, AssistantResponseChunk, MessageChunk,
23};
24use objectiveai_sdk::functions::executions::response::streaming::{
25    FunctionExecutionChunk, TaskChunk,
26};
27use objectiveai_sdk::vector::completions::response::streaming::VectorCompletionChunk;
28
29use super::row::{RowValue, RowsIter};
30
31/// Entry: walk an agent-completion chunk's `messages` and yield every
32/// streaming-content row keyed by the chunk's own `id` and
33/// `agent_instance_hierarchy`.
34pub fn agent_completion_chunk_rows<'a>(
35    chunk: &'a AgentCompletionChunk,
36) -> RowsIter<'a> {
37    let response_id = chunk.id.as_str();
38    let agent_hierarchy = chunk.agent_instance_hierarchy.as_str();
39    Box::new(
40        chunk
41            .messages
42            .iter()
43            .flat_map(move |msg| message_chunk_rows(response_id, agent_hierarchy, msg)),
44    )
45}
46
47/// Entry: walk every embedded per-agent completion in a vector chunk
48/// and forward to [`agent_completion_chunk_rows`].
49pub fn vector_completion_chunk_rows<'a>(
50    chunk: &'a VectorCompletionChunk,
51) -> RowsIter<'a> {
52    Box::new(
53        chunk
54            .completions
55            .iter()
56            .flat_map(|c| agent_completion_chunk_rows(&c.inner)),
57    )
58}
59
60/// Entry: walk every task in a function chunk and forward to the
61/// matching tier walker. Reasoning summary's inner agent completion
62/// also flows through. Recursive: function tasks chain back into this
63/// function.
64pub fn function_execution_chunk_rows<'a>(
65    chunk: &'a FunctionExecutionChunk,
66) -> RowsIter<'a> {
67    let task_iter = chunk
68        .tasks
69        .iter()
70        .flat_map(|task| task_chunk_rows(task));
71    let reasoning_iter = chunk
72        .reasoning
73        .iter()
74        .flat_map(|r| agent_completion_chunk_rows(&r.inner));
75    Box::new(task_iter.chain(reasoning_iter))
76}
77
78// ---- internal helpers -------------------------------------------------
79
80fn task_chunk_rows<'a>(task: &'a TaskChunk) -> RowsIter<'a> {
81    match task {
82        TaskChunk::FunctionExecution(wrapper) => {
83            function_execution_chunk_rows(&wrapper.inner)
84        }
85        TaskChunk::VectorCompletion(wrapper) => {
86            vector_completion_chunk_rows(&wrapper.inner)
87        }
88    }
89}
90
91fn message_chunk_rows<'a>(
92    response_id: &'a str,
93    agent_instance_hierarchy: &'a str,
94    msg: &'a MessageChunk,
95) -> RowsIter<'a> {
96    match msg {
97        MessageChunk::Assistant(a) => {
98            assistant_response_chunk_rows(response_id, agent_instance_hierarchy, a)
99        }
100        MessageChunk::Tool(t) => {
101            tool_response_rows(response_id, agent_instance_hierarchy, t)
102        }
103    }
104}
105
106fn assistant_response_chunk_rows<'a>(
107    response_id: &'a str,
108    agent_instance_hierarchy: &'a str,
109    chunk: &'a AssistantResponseChunk,
110) -> RowsIter<'a> {
111    let index = chunk.index;
112
113    // Prepend: `MessageQueueContent` rows for every consumed
114    // `message_queue_contents.id` the API stamped. Yielded ahead
115    // of the message body so `objectiveai.messages` chronicles
116    // consumption before the body the agent produced from it.
117    let message_queue_iter = chunk
118        .request_message_ids
119        .as_deref()
120        .unwrap_or(&[])
121        .iter()
122        .copied()
123        .map(move |message_queue_content_id| RowValue::MessageQueueContent {
124            response_id,
125            agent_instance_hierarchy,
126            message_queue_content_id,
127        });
128
129    // Emission order: reasoning → tool_calls → content → refusal.
130    // Refusal goes last on purpose — when a model refuses it's
131    // typically the terminal signal of the turn, so readers see all
132    // the actual work (reasoning / tool calls / content parts) before
133    // the refusal stamp.
134    let reasoning_iter = chunk.reasoning.iter().map(move |text| {
135        RowValue::AssistantResponseReasoning {
136            response_id,
137            agent_instance_hierarchy,
138            index,
139            text: text.as_str(),
140        }
141    });
142    let tool_calls_iter = chunk
143        .tool_calls
144        .iter()
145        .flatten()
146        .filter_map(move |tc| {
147            let id = tc.id.as_deref()?;
148            let name = tc.function.as_ref().and_then(|f| f.name.as_deref())?;
149            let args = tc.function.as_ref().and_then(|f| f.arguments.as_deref())?;
150            Some(RowValue::AssistantResponseToolCalls {
151                response_id,
152                agent_instance_hierarchy,
153                index,
154                // The tool call's own wire `index`, NOT its position in
155                // the merged Vec. `push` correlates streamed tool-call
156                // deltas by `index`, so a call whose wire index is e.g.
157                // 2 can sit at Vec position 0 (or move as earlier-index
158                // deltas arrive). The row PK is
159                // (response_id, index, tool_call_index) — keying it on
160                // Vec position would mislabel calls and let the shadow
161                // key drift across chunks; the wire index is stable.
162                tool_call_index: tc.index,
163                tool_call_id: id,
164                function_name: name,
165                arguments: args,
166            })
167        });
168    let content_iter = chunk
169        .content
170        .iter()
171        .flat_map(move |c| assistant_content_rows(response_id, agent_instance_hierarchy, index, c));
172    let refusal_iter = chunk.refusal.iter().map(move |text| {
173        RowValue::AssistantResponseRefusal {
174            response_id,
175            agent_instance_hierarchy,
176            index,
177            text: text.as_str(),
178        }
179    });
180
181    Box::new(
182        message_queue_iter
183            .chain(reasoning_iter)
184            .chain(tool_calls_iter)
185            .chain(content_iter)
186            .chain(refusal_iter),
187    )
188}
189
190fn tool_response_rows<'a>(
191    response_id: &'a str,
192    agent_instance_hierarchy: &'a str,
193    response: &'a ToolResponse,
194) -> RowsIter<'a> {
195    let index = response.index;
196    // Same MessageQueueContent prepend as the assistant path —
197    // surfaced for wire-shape symmetry. Currently the API never
198    // populates `ToolResponse.request_message_ids`, so this iter
199    // is empty in practice.
200    let message_queue_iter = response
201        .request_message_ids
202        .as_deref()
203        .unwrap_or(&[])
204        .iter()
205        .copied()
206        .map(move |message_queue_content_id| RowValue::MessageQueueContent {
207            response_id,
208            agent_instance_hierarchy,
209            message_queue_content_id,
210        });
211    let head = std::iter::once(RowValue::ToolResponse {
212        response_id,
213        agent_instance_hierarchy,
214        index,
215        tool_call_id: response.inner.tool_call_id.as_str(),
216    });
217    Box::new(
218        message_queue_iter.chain(head).chain(tool_content_rows(
219            response_id,
220            agent_instance_hierarchy,
221            index,
222            &response.inner.content,
223        )),
224    )
225}
226
227fn assistant_content_rows<'a>(
228    response_id: &'a str,
229    agent_instance_hierarchy: &'a str,
230    index: u64,
231    content: &'a RichContent,
232) -> RowsIter<'a> {
233    match content {
234        RichContent::Text(text) => Box::new(std::iter::once(RowValue::AssistantResponseContentText {
235            response_id,
236            agent_instance_hierarchy,
237            index,
238            part_index: 0,
239            text: text.as_str(),
240        })),
241        RichContent::Parts(parts) => Box::new(parts.iter().enumerate().map(move |(part_index, part)| {
242            assistant_content_part(response_id, agent_instance_hierarchy, index, part_index as u64, part)
243        })),
244    }
245}
246
247fn assistant_content_part<'a>(
248    response_id: &'a str,
249    agent_instance_hierarchy: &'a str,
250    index: u64,
251    part_index: u64,
252    part: &'a RichContentPart,
253) -> RowValue<'a> {
254    match part {
255        RichContentPart::Text { text } => RowValue::AssistantResponseContentText {
256            response_id, agent_instance_hierarchy, index, part_index, text: text.as_str(),
257        },
258        RichContentPart::ImageUrl { image_url } => RowValue::AssistantResponseContentImage {
259            response_id, agent_instance_hierarchy, index, part_index, image_url,
260        },
261        RichContentPart::InputAudio { input_audio } => RowValue::AssistantResponseContentAudio {
262            response_id, agent_instance_hierarchy, index, part_index, input_audio,
263        },
264        RichContentPart::InputVideo { video_url }
265        | RichContentPart::VideoUrl { video_url } => RowValue::AssistantResponseContentVideo {
266            response_id, agent_instance_hierarchy, index, part_index, video_url,
267        },
268        RichContentPart::File { file } => RowValue::AssistantResponseContentFile {
269            response_id, agent_instance_hierarchy, index, part_index, file,
270        },
271    }
272}
273
274fn tool_content_rows<'a>(
275    response_id: &'a str,
276    agent_instance_hierarchy: &'a str,
277    index: u64,
278    content: &'a RichContent,
279) -> RowsIter<'a> {
280    match content {
281        RichContent::Text(text) => Box::new(std::iter::once(RowValue::ToolResponseContentText {
282            response_id,
283            agent_instance_hierarchy,
284            index,
285            part_index: 0,
286            text: text.as_str(),
287        })),
288        RichContent::Parts(parts) => Box::new(parts.iter().enumerate().map(move |(part_index, part)| {
289            tool_content_part(response_id, agent_instance_hierarchy, index, part_index as u64, part)
290        })),
291    }
292}
293
294fn tool_content_part<'a>(
295    response_id: &'a str,
296    agent_instance_hierarchy: &'a str,
297    index: u64,
298    part_index: u64,
299    part: &'a RichContentPart,
300) -> RowValue<'a> {
301    match part {
302        RichContentPart::Text { text } => RowValue::ToolResponseContentText {
303            response_id, agent_instance_hierarchy, index, part_index, text: text.as_str(),
304        },
305        RichContentPart::ImageUrl { image_url } => RowValue::ToolResponseContentImage {
306            response_id, agent_instance_hierarchy, index, part_index, image_url,
307        },
308        RichContentPart::InputAudio { input_audio } => RowValue::ToolResponseContentAudio {
309            response_id, agent_instance_hierarchy, index, part_index, input_audio,
310        },
311        RichContentPart::InputVideo { video_url }
312        | RichContentPart::VideoUrl { video_url } => RowValue::ToolResponseContentVideo {
313            response_id, agent_instance_hierarchy, index, part_index, video_url,
314        },
315        RichContentPart::File { file } => RowValue::ToolResponseContentFile {
316            response_id, agent_instance_hierarchy, index, part_index, file,
317        },
318    }
319}