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::open::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::open::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::AssistantResponseRefusal => {
165            let index = require_row_index(table_kind, row_index)?;
166            let text = fetch_indexed_text(
167                pool,
168                "objectiveai.assistant_response_refusal",
169                response_id,
170                index,
171            )
172            .await?;
173            Ok(Response::Text { text })
174        }
175        MessageTable::AssistantResponseReasoning => {
176            let index = require_row_index(table_kind, row_index)?;
177            let text = fetch_indexed_text(
178                pool,
179                "objectiveai.assistant_response_reasoning",
180                response_id,
181                index,
182            )
183            .await?;
184            Ok(Response::Text { text })
185        }
186        // A tool-call row reads back as its `arguments` (text). The
187        // call's metadata (function_name / tool_call_id /
188        // tool_call_index) is surfaced inline by `agents logs read all`
189        // on `AssistantResponsePart::ToolCall`, so it isn't repeated
190        // here. `tool_call_index` stays in the WHERE — it's part of the
191        // row PK.
192        MessageTable::AssistantResponseToolCalls => {
193            let index = require_row_index(table_kind, row_index)?;
194            let tool_call_index = require_row_sub_index(table_kind, row_sub_index)?;
195            let row = sqlx::query(
196                "SELECT arguments \
197                 FROM objectiveai.assistant_response_tool_calls \
198                 WHERE response_id = $1 AND \"index\" = $2 AND tool_call_index = $3",
199            )
200            .bind(response_id)
201            .bind(index)
202            .bind(tool_call_index)
203            .fetch_one(&**pool)
204            .await?;
205            Ok(Response::Text { text: row.try_get("arguments")? })
206        }
207        MessageTable::AssistantResponseContentText => {
208            let (index, part_index) =
209                require_full_indices(table_kind, row_index, row_sub_index)?;
210            let text = fetch_content_text(
211                pool,
212                "objectiveai.assistant_response_content_text",
213                response_id,
214                index,
215                part_index,
216            )
217            .await?;
218            Ok(Response::Text { text })
219        }
220        MessageTable::AssistantResponseContentImage => {
221            let (index, part_index) =
222                require_full_indices(table_kind, row_index, row_sub_index)?;
223            let image_url = fetch_content_image(
224                pool,
225                "objectiveai.assistant_response_content_image",
226                response_id,
227                index,
228                part_index,
229            )
230            .await?;
231            Ok(Response::Image(image_url))
232        }
233        MessageTable::AssistantResponseContentAudio => {
234            let (index, part_index) =
235                require_full_indices(table_kind, row_index, row_sub_index)?;
236            let input_audio = fetch_content_audio(
237                pool,
238                "objectiveai.assistant_response_content_audio",
239                response_id,
240                index,
241                part_index,
242            )
243            .await?;
244            Ok(Response::Audio(input_audio))
245        }
246        MessageTable::AssistantResponseContentVideo => {
247            let (index, part_index) =
248                require_full_indices(table_kind, row_index, row_sub_index)?;
249            let video_url = fetch_content_video(
250                pool,
251                "objectiveai.assistant_response_content_video",
252                response_id,
253                index,
254                part_index,
255            )
256            .await?;
257            Ok(Response::Video(video_url))
258        }
259        MessageTable::AssistantResponseContentFile => {
260            let (index, part_index) =
261                require_full_indices(table_kind, row_index, row_sub_index)?;
262            let file = fetch_content_file(
263                pool,
264                "objectiveai.assistant_response_content_file",
265                response_id,
266                index,
267                part_index,
268            )
269            .await?;
270            Ok(Response::File(file))
271        }
272        MessageTable::ToolResponseContentText => {
273            let (index, part_index) =
274                require_full_indices(table_kind, row_index, row_sub_index)?;
275            let text = fetch_content_text(
276                pool,
277                "objectiveai.tool_response_content_text",
278                response_id,
279                index,
280                part_index,
281            )
282            .await?;
283            Ok(Response::Text { text })
284        }
285        MessageTable::ToolResponseContentImage => {
286            let (index, part_index) =
287                require_full_indices(table_kind, row_index, row_sub_index)?;
288            let image_url = fetch_content_image(
289                pool,
290                "objectiveai.tool_response_content_image",
291                response_id,
292                index,
293                part_index,
294            )
295            .await?;
296            Ok(Response::Image(image_url))
297        }
298        MessageTable::ToolResponseContentAudio => {
299            let (index, part_index) =
300                require_full_indices(table_kind, row_index, row_sub_index)?;
301            let input_audio = fetch_content_audio(
302                pool,
303                "objectiveai.tool_response_content_audio",
304                response_id,
305                index,
306                part_index,
307            )
308            .await?;
309            Ok(Response::Audio(input_audio))
310        }
311        MessageTable::ToolResponseContentVideo => {
312            let (index, part_index) =
313                require_full_indices(table_kind, row_index, row_sub_index)?;
314            let video_url = fetch_content_video(
315                pool,
316                "objectiveai.tool_response_content_video",
317                response_id,
318                index,
319                part_index,
320            )
321            .await?;
322            Ok(Response::Video(video_url))
323        }
324        MessageTable::ToolResponseContentFile => {
325            let (index, part_index) =
326                require_full_indices(table_kind, row_index, row_sub_index)?;
327            let file = fetch_content_file(
328                pool,
329                "objectiveai.tool_response_content_file",
330                response_id,
331                index,
332                part_index,
333            )
334            .await?;
335            Ok(Response::File(file))
336        }
337    }
338}
339
340async fn fetch_request_blob(
341    pool: &Pool,
342    table: &str,
343    response_id: &str,
344) -> Result<(serde_json::Value, i64, String), Error> {
345    let sql = format!(
346        "SELECT body, created_at, sender_agent_instance_hierarchy \
347         FROM {table} WHERE response_id = $1",
348    );
349    let row = sqlx::query(&sql)
350        .bind(response_id)
351        .fetch_one(&**pool)
352        .await?;
353    let body: sqlx::types::Json<serde_json::Value> = row.try_get("body")?;
354    let created_at: i64 = row.try_get("created_at")?;
355    let sender: String = row.try_get("sender_agent_instance_hierarchy")?;
356    Ok((body.0, created_at, sender))
357}
358
359async fn fetch_indexed_text(
360    pool: &Pool,
361    table: &str,
362    response_id: &str,
363    index: i64,
364) -> Result<String, Error> {
365    let sql = format!(
366        "SELECT text FROM {table} \
367         WHERE response_id = $1 AND \"index\" = $2",
368    );
369    let row = sqlx::query(&sql)
370        .bind(response_id)
371        .bind(index)
372        .fetch_one(&**pool)
373        .await?;
374    Ok(row.try_get("text")?)
375}
376
377async fn fetch_content_text(
378    pool: &Pool,
379    table: &str,
380    response_id: &str,
381    index: i64,
382    part_index: i64,
383) -> Result<String, Error> {
384    let sql = format!(
385        "SELECT text FROM {table} \
386         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
387    );
388    let row = sqlx::query(&sql)
389        .bind(response_id)
390        .bind(index)
391        .bind(part_index)
392        .fetch_one(&**pool)
393        .await?;
394    Ok(row.try_get("text")?)
395}
396
397async fn fetch_content_image(
398    pool: &Pool,
399    table: &str,
400    response_id: &str,
401    index: i64,
402    part_index: i64,
403) -> Result<ImageUrl, Error> {
404    let sql = format!(
405        "SELECT url, detail FROM {table} \
406         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
407    );
408    let row = sqlx::query(&sql)
409        .bind(response_id)
410        .bind(index)
411        .bind(part_index)
412        .fetch_one(&**pool)
413        .await?;
414    let url: String = row.try_get("url")?;
415    let detail_str: Option<String> = row.try_get("detail")?;
416    let detail = match detail_str {
417        Some(s) => serde_json::from_value(serde_json::Value::String(s))?,
418        None => None,
419    };
420    Ok(ImageUrl { url, detail })
421}
422
423async fn fetch_content_audio(
424    pool: &Pool,
425    table: &str,
426    response_id: &str,
427    index: i64,
428    part_index: i64,
429) -> Result<InputAudio, Error> {
430    let sql = format!(
431        "SELECT data, format FROM {table} \
432         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
433    );
434    let row = sqlx::query(&sql)
435        .bind(response_id)
436        .bind(index)
437        .bind(part_index)
438        .fetch_one(&**pool)
439        .await?;
440    Ok(InputAudio {
441        data: row.try_get("data")?,
442        format: row.try_get("format")?,
443    })
444}
445
446async fn fetch_content_video(
447    pool: &Pool,
448    table: &str,
449    response_id: &str,
450    index: i64,
451    part_index: i64,
452) -> Result<VideoUrl, Error> {
453    let sql = format!(
454        "SELECT url FROM {table} \
455         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
456    );
457    let row = sqlx::query(&sql)
458        .bind(response_id)
459        .bind(index)
460        .bind(part_index)
461        .fetch_one(&**pool)
462        .await?;
463    Ok(VideoUrl { url: row.try_get("url")? })
464}
465
466async fn fetch_content_file(
467    pool: &Pool,
468    table: &str,
469    response_id: &str,
470    index: i64,
471    part_index: i64,
472) -> Result<File, Error> {
473    let sql = format!(
474        "SELECT file_data, file_id, filename, file_url FROM {table} \
475         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
476    );
477    let row = sqlx::query(&sql)
478        .bind(response_id)
479        .bind(index)
480        .bind(part_index)
481        .fetch_one(&**pool)
482        .await?;
483    Ok(File {
484        file_data: row.try_get("file_data")?,
485        file_id: row.try_get("file_id")?,
486        filename: row.try_get("filename")?,
487        file_url: row.try_get("file_url")?,
488    })
489}
490
491fn require_row_index(
492    table_kind: MessageTable,
493    row_index: Option<i64>,
494) -> Result<i64, Error> {
495    row_index.ok_or_else(|| {
496        Error::InvalidData(format!(
497            "objectiveai.messages row for table {table_kind:?} is missing row_index"
498        ))
499    })
500}
501
502fn require_row_sub_index(
503    table_kind: MessageTable,
504    row_sub_index: Option<i64>,
505) -> Result<i64, Error> {
506    row_sub_index.ok_or_else(|| {
507        Error::InvalidData(format!(
508            "objectiveai.messages row for table {table_kind:?} is missing row_sub_index"
509        ))
510    })
511}
512
513fn require_full_indices(
514    table_kind: MessageTable,
515    row_index: Option<i64>,
516    row_sub_index: Option<i64>,
517) -> Result<(i64, i64), Error> {
518    Ok((
519        require_row_index(table_kind, row_index)?,
520        require_row_sub_index(table_kind, row_sub_index)?,
521    ))
522}