Skip to main content

objectiveai_cli/db/logs/
read_id.rs

1//! `agents logs read id` backend: take a `objectiveai.messages."index"`
2//! BIGSERIAL, dispatch to the per-tier target table, and build the
3//! SDK `agents::logs::read::id::Response` variant with a typed
4//! payload.
5//!
6//! Lookup is two round-trips: one to resolve the message row
7//! (table discriminator + row PK fragments), one to load the
8//! payload from the resolved target table. Each variant assembles
9//! the SDK `Response` directly so the CLI handler stays a thin
10//! pass-through.
11
12use objectiveai_sdk::agent::completions::message::{File, ImageUrl, InputAudio, VideoUrl};
13use objectiveai_sdk::agent::completions::request::AgentCompletionCreateParams;
14use objectiveai_sdk::cli::command::agents::logs::read::id::Response;
15use objectiveai_sdk::functions::executions::request::FunctionExecutionCreateParams;
16use objectiveai_sdk::vector::completions::request::VectorCompletionCreateParams;
17use sqlx::Row as _;
18
19use super::super::{Error, Pool};
20use super::row::MessageTable;
21
22/// Resolve one `objectiveai.messages."index"` to the matching SDK
23/// `Response` variant. Returns `Ok(None)` when no row exists at
24/// that index.
25pub async fn read_by_id(pool: &Pool, id: i64) -> Result<Option<Response>, Error> {
26    let Some(msg) = sqlx::query(
27        "SELECT response_id, \"table\" AS table_kind, row_index, row_sub_index \
28         FROM objectiveai.messages \
29         WHERE \"index\" = $1",
30    )
31    .bind(id)
32    .fetch_optional(&**pool)
33    .await?
34    else {
35        return Ok(None);
36    };
37
38    let response_id: String = msg.try_get("response_id")?;
39    let table_kind: MessageTable = msg.try_get("table_kind")?;
40    let row_index: Option<i64> = msg.try_get("row_index")?;
41    let row_sub_index: Option<i64> = msg.try_get("row_sub_index")?;
42
43    Ok(Some(load_payload(
44        pool,
45        table_kind,
46        &response_id,
47        row_index,
48        row_sub_index,
49    ).await?))
50}
51
52async fn load_payload(
53    pool: &Pool,
54    table_kind: MessageTable,
55    response_id: &str,
56    row_index: Option<i64>,
57    row_sub_index: Option<i64>,
58) -> Result<Response, Error> {
59    match table_kind {
60        // Five message_queue_* variants: `row_index` carries
61        // `message_queue_contents.id`. We dispatch directly to the
62        // matching per-kind table (`message_queue_texts`,
63        // `_images`, etc.) and map onto the same `Text` / `Image`
64        // / `Audio` / `Video` / `File` variants the assistant /
65        // tool content rows use — no separate response shape.
66        MessageTable::MessageQueueText => {
67            let id = require_row_index(table_kind, row_index)?;
68            let text: String = sqlx::query_scalar(
69                "SELECT text FROM objectiveai.message_queue_texts WHERE id = $1",
70            )
71            .bind(id)
72            .fetch_one(&**pool)
73            .await?;
74            Ok(Response::Text { text })
75        }
76        MessageTable::MessageQueueImage => {
77            let id = require_row_index(table_kind, row_index)?;
78            let row = sqlx::query(
79                "SELECT url, detail FROM objectiveai.message_queue_images WHERE id = $1",
80            )
81            .bind(id)
82            .fetch_one(&**pool)
83            .await?;
84            let url: String = row.try_get("url")?;
85            let detail_str: Option<String> = row.try_get("detail")?;
86            let detail = match detail_str {
87                Some(s) => serde_json::from_value(serde_json::Value::String(s))?,
88                None => None,
89            };
90            Ok(Response::Image(ImageUrl { url, detail }))
91        }
92        MessageTable::MessageQueueAudio => {
93            let id = require_row_index(table_kind, row_index)?;
94            let row = sqlx::query(
95                "SELECT data, format FROM objectiveai.message_queue_audios WHERE id = $1",
96            )
97            .bind(id)
98            .fetch_one(&**pool)
99            .await?;
100            Ok(Response::Audio(InputAudio {
101                data: row.try_get("data")?,
102                format: row.try_get("format")?,
103            }))
104        }
105        MessageTable::MessageQueueVideo => {
106            let id = require_row_index(table_kind, row_index)?;
107            let url: String = sqlx::query_scalar(
108                "SELECT url FROM objectiveai.message_queue_videos WHERE id = $1",
109            )
110            .bind(id)
111            .fetch_one(&**pool)
112            .await?;
113            Ok(Response::Video(VideoUrl { url }))
114        }
115        MessageTable::MessageQueueFile => {
116            let id = require_row_index(table_kind, row_index)?;
117            let row = sqlx::query(
118                "SELECT file_data, file_id, filename, file_url \
119                 FROM objectiveai.message_queue_files WHERE id = $1",
120            )
121            .bind(id)
122            .fetch_one(&**pool)
123            .await?;
124            Ok(Response::File(File {
125                file_data: row.try_get("file_data")?,
126                file_id: row.try_get("file_id")?,
127                filename: row.try_get("filename")?,
128                file_url: row.try_get("file_url")?,
129            }))
130        }
131        MessageTable::AgentCompletionRequest => {
132            let (body, created_at, sender) =
133                fetch_request_blob(pool, "objectiveai.agent_completion_requests", response_id).await?;
134            let body: AgentCompletionCreateParams = serde_json::from_value(body)?;
135            Ok(Response::AgentCompletionRequest {
136                response_id: response_id.to_string(),
137                sender_agent_instance_hierarchy: sender,
138                body,
139                created_at,
140            })
141        }
142        MessageTable::VectorCompletionRequest => {
143            let (body, created_at, sender) =
144                fetch_request_blob(pool, "objectiveai.vector_completion_requests", response_id).await?;
145            let body: VectorCompletionCreateParams = serde_json::from_value(body)?;
146            Ok(Response::VectorCompletionRequest {
147                response_id: response_id.to_string(),
148                sender_agent_instance_hierarchy: sender,
149                body,
150                created_at,
151            })
152        }
153        MessageTable::FunctionExecutionRequest => {
154            let (body, created_at, sender) =
155                fetch_request_blob(pool, "objectiveai.function_execution_requests", response_id).await?;
156            let body: FunctionExecutionCreateParams = serde_json::from_value(body)?;
157            Ok(Response::FunctionExecutionRequest {
158                response_id: response_id.to_string(),
159                sender_agent_instance_hierarchy: sender,
160                body,
161                created_at,
162            })
163        }
164        MessageTable::ToolResponse => {
165            let index = require_row_index(table_kind, row_index)?;
166            let row = sqlx::query(
167                "SELECT tool_call_id FROM objectiveai.tool_response \
168                 WHERE response_id = $1 AND \"index\" = $2",
169            )
170            .bind(response_id)
171            .bind(index)
172            .fetch_one(&**pool)
173            .await?;
174            Ok(Response::ToolResponse {
175                response_id: response_id.to_string(),
176                index,
177                tool_call_id: row.try_get("tool_call_id")?,
178            })
179        }
180        MessageTable::AssistantResponseRefusal => {
181            let index = require_row_index(table_kind, row_index)?;
182            let text = fetch_indexed_text(
183                pool,
184                "objectiveai.assistant_response_refusal",
185                response_id,
186                index,
187            )
188            .await?;
189            Ok(Response::Text { text })
190        }
191        MessageTable::AssistantResponseReasoning => {
192            let index = require_row_index(table_kind, row_index)?;
193            let text = fetch_indexed_text(
194                pool,
195                "objectiveai.assistant_response_reasoning",
196                response_id,
197                index,
198            )
199            .await?;
200            Ok(Response::Text { text })
201        }
202        MessageTable::AssistantResponseToolCalls => {
203            let index = require_row_index(table_kind, row_index)?;
204            let tool_call_index = require_row_sub_index(table_kind, row_sub_index)?;
205            let row = sqlx::query(
206                "SELECT tool_call_id, function_name, arguments \
207                 FROM objectiveai.assistant_response_tool_calls \
208                 WHERE response_id = $1 AND \"index\" = $2 AND tool_call_index = $3",
209            )
210            .bind(response_id)
211            .bind(index)
212            .bind(tool_call_index)
213            .fetch_one(&**pool)
214            .await?;
215            Ok(Response::ToolCall {
216                response_id: response_id.to_string(),
217                index,
218                tool_call_index,
219                tool_call_id: row.try_get("tool_call_id")?,
220                function_name: row.try_get("function_name")?,
221                arguments: row.try_get("arguments")?,
222            })
223        }
224        MessageTable::AssistantResponseContentText => {
225            let (index, part_index) =
226                require_full_indices(table_kind, row_index, row_sub_index)?;
227            let text = fetch_content_text(
228                pool,
229                "objectiveai.assistant_response_content_text",
230                response_id,
231                index,
232                part_index,
233            )
234            .await?;
235            Ok(Response::Text { text })
236        }
237        MessageTable::AssistantResponseContentImage => {
238            let (index, part_index) =
239                require_full_indices(table_kind, row_index, row_sub_index)?;
240            let image_url = fetch_content_image(
241                pool,
242                "objectiveai.assistant_response_content_image",
243                response_id,
244                index,
245                part_index,
246            )
247            .await?;
248            Ok(Response::Image(image_url))
249        }
250        MessageTable::AssistantResponseContentAudio => {
251            let (index, part_index) =
252                require_full_indices(table_kind, row_index, row_sub_index)?;
253            let input_audio = fetch_content_audio(
254                pool,
255                "objectiveai.assistant_response_content_audio",
256                response_id,
257                index,
258                part_index,
259            )
260            .await?;
261            Ok(Response::Audio(input_audio))
262        }
263        MessageTable::AssistantResponseContentVideo => {
264            let (index, part_index) =
265                require_full_indices(table_kind, row_index, row_sub_index)?;
266            let video_url = fetch_content_video(
267                pool,
268                "objectiveai.assistant_response_content_video",
269                response_id,
270                index,
271                part_index,
272            )
273            .await?;
274            Ok(Response::Video(video_url))
275        }
276        MessageTable::AssistantResponseContentFile => {
277            let (index, part_index) =
278                require_full_indices(table_kind, row_index, row_sub_index)?;
279            let file = fetch_content_file(
280                pool,
281                "objectiveai.assistant_response_content_file",
282                response_id,
283                index,
284                part_index,
285            )
286            .await?;
287            Ok(Response::File(file))
288        }
289        MessageTable::ToolResponseContentText => {
290            let (index, part_index) =
291                require_full_indices(table_kind, row_index, row_sub_index)?;
292            let text = fetch_content_text(
293                pool,
294                "objectiveai.tool_response_content_text",
295                response_id,
296                index,
297                part_index,
298            )
299            .await?;
300            Ok(Response::Text { text })
301        }
302        MessageTable::ToolResponseContentImage => {
303            let (index, part_index) =
304                require_full_indices(table_kind, row_index, row_sub_index)?;
305            let image_url = fetch_content_image(
306                pool,
307                "objectiveai.tool_response_content_image",
308                response_id,
309                index,
310                part_index,
311            )
312            .await?;
313            Ok(Response::Image(image_url))
314        }
315        MessageTable::ToolResponseContentAudio => {
316            let (index, part_index) =
317                require_full_indices(table_kind, row_index, row_sub_index)?;
318            let input_audio = fetch_content_audio(
319                pool,
320                "objectiveai.tool_response_content_audio",
321                response_id,
322                index,
323                part_index,
324            )
325            .await?;
326            Ok(Response::Audio(input_audio))
327        }
328        MessageTable::ToolResponseContentVideo => {
329            let (index, part_index) =
330                require_full_indices(table_kind, row_index, row_sub_index)?;
331            let video_url = fetch_content_video(
332                pool,
333                "objectiveai.tool_response_content_video",
334                response_id,
335                index,
336                part_index,
337            )
338            .await?;
339            Ok(Response::Video(video_url))
340        }
341        MessageTable::ToolResponseContentFile => {
342            let (index, part_index) =
343                require_full_indices(table_kind, row_index, row_sub_index)?;
344            let file = fetch_content_file(
345                pool,
346                "objectiveai.tool_response_content_file",
347                response_id,
348                index,
349                part_index,
350            )
351            .await?;
352            Ok(Response::File(file))
353        }
354    }
355}
356
357async fn fetch_request_blob(
358    pool: &Pool,
359    table: &str,
360    response_id: &str,
361) -> Result<(serde_json::Value, i64, String), Error> {
362    let sql = format!(
363        "SELECT body, created_at, sender_agent_instance_hierarchy \
364         FROM {table} WHERE response_id = $1",
365    );
366    let row = sqlx::query(&sql)
367        .bind(response_id)
368        .fetch_one(&**pool)
369        .await?;
370    let body: sqlx::types::Json<serde_json::Value> = row.try_get("body")?;
371    let created_at: i64 = row.try_get("created_at")?;
372    let sender: String = row.try_get("sender_agent_instance_hierarchy")?;
373    Ok((body.0, created_at, sender))
374}
375
376async fn fetch_indexed_text(
377    pool: &Pool,
378    table: &str,
379    response_id: &str,
380    index: i64,
381) -> Result<String, Error> {
382    let sql = format!(
383        "SELECT text FROM {table} \
384         WHERE response_id = $1 AND \"index\" = $2",
385    );
386    let row = sqlx::query(&sql)
387        .bind(response_id)
388        .bind(index)
389        .fetch_one(&**pool)
390        .await?;
391    Ok(row.try_get("text")?)
392}
393
394async fn fetch_content_text(
395    pool: &Pool,
396    table: &str,
397    response_id: &str,
398    index: i64,
399    part_index: i64,
400) -> Result<String, Error> {
401    let sql = format!(
402        "SELECT text FROM {table} \
403         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
404    );
405    let row = sqlx::query(&sql)
406        .bind(response_id)
407        .bind(index)
408        .bind(part_index)
409        .fetch_one(&**pool)
410        .await?;
411    Ok(row.try_get("text")?)
412}
413
414async fn fetch_content_image(
415    pool: &Pool,
416    table: &str,
417    response_id: &str,
418    index: i64,
419    part_index: i64,
420) -> Result<ImageUrl, Error> {
421    let sql = format!(
422        "SELECT url, detail FROM {table} \
423         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
424    );
425    let row = sqlx::query(&sql)
426        .bind(response_id)
427        .bind(index)
428        .bind(part_index)
429        .fetch_one(&**pool)
430        .await?;
431    let url: String = row.try_get("url")?;
432    let detail_str: Option<String> = row.try_get("detail")?;
433    let detail = match detail_str {
434        Some(s) => serde_json::from_value(serde_json::Value::String(s))?,
435        None => None,
436    };
437    Ok(ImageUrl { url, detail })
438}
439
440async fn fetch_content_audio(
441    pool: &Pool,
442    table: &str,
443    response_id: &str,
444    index: i64,
445    part_index: i64,
446) -> Result<InputAudio, Error> {
447    let sql = format!(
448        "SELECT data, format FROM {table} \
449         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
450    );
451    let row = sqlx::query(&sql)
452        .bind(response_id)
453        .bind(index)
454        .bind(part_index)
455        .fetch_one(&**pool)
456        .await?;
457    Ok(InputAudio {
458        data: row.try_get("data")?,
459        format: row.try_get("format")?,
460    })
461}
462
463async fn fetch_content_video(
464    pool: &Pool,
465    table: &str,
466    response_id: &str,
467    index: i64,
468    part_index: i64,
469) -> Result<VideoUrl, Error> {
470    let sql = format!(
471        "SELECT url FROM {table} \
472         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
473    );
474    let row = sqlx::query(&sql)
475        .bind(response_id)
476        .bind(index)
477        .bind(part_index)
478        .fetch_one(&**pool)
479        .await?;
480    Ok(VideoUrl { url: row.try_get("url")? })
481}
482
483async fn fetch_content_file(
484    pool: &Pool,
485    table: &str,
486    response_id: &str,
487    index: i64,
488    part_index: i64,
489) -> Result<File, Error> {
490    let sql = format!(
491        "SELECT file_data, file_id, filename, file_url FROM {table} \
492         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
493    );
494    let row = sqlx::query(&sql)
495        .bind(response_id)
496        .bind(index)
497        .bind(part_index)
498        .fetch_one(&**pool)
499        .await?;
500    Ok(File {
501        file_data: row.try_get("file_data")?,
502        file_id: row.try_get("file_id")?,
503        filename: row.try_get("filename")?,
504        file_url: row.try_get("file_url")?,
505    })
506}
507
508fn require_row_index(
509    table_kind: MessageTable,
510    row_index: Option<i64>,
511) -> Result<i64, Error> {
512    row_index.ok_or_else(|| {
513        Error::InvalidData(format!(
514            "objectiveai.messages row for table {table_kind:?} is missing row_index"
515        ))
516    })
517}
518
519fn require_row_sub_index(
520    table_kind: MessageTable,
521    row_sub_index: Option<i64>,
522) -> Result<i64, Error> {
523    row_sub_index.ok_or_else(|| {
524        Error::InvalidData(format!(
525            "objectiveai.messages row for table {table_kind:?} is missing row_sub_index"
526        ))
527    })
528}
529
530fn require_full_indices(
531    table_kind: MessageTable,
532    row_index: Option<i64>,
533    row_sub_index: Option<i64>,
534) -> Result<(i64, i64), Error> {
535    Ok((
536        require_row_index(table_kind, row_index)?,
537        require_row_sub_index(table_kind, row_sub_index)?,
538    ))
539}