1use objectiveai_sdk::cli::command::agents::logs::read::all::{
31 AssistantResponsePart, ClientNotificationPart, ClientNotificationPartType, ResponseItem,
32 ToolResponsePart, ToolResponsePartType,
33};
34use sqlx::Row as _;
35
36use super::super::{Error, Pool};
37use super::row::MessageTable;
38
39struct MsgRow {
42 id: i64,
45 response_id: String,
46 table_kind: MessageTable,
47 agent_instance_hierarchy: String,
48 timestamp_delivered: i64,
49 sender_agent_instance_hierarchy: Option<String>,
55 message_queue_id: Option<i64>,
60 timestamp_queued: Option<i64>,
64 message_queue_key: Option<String>,
71 function_name: Option<String>,
77 tool_call_id: Option<String>,
85 row_sub_index: Option<i64>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96enum BlockClass {
97 AgentCompletionRequest,
98 VectorCompletionRequest,
99 FunctionExecutionRequest,
100 ClientNotification,
101 AssistantResponse,
102 ToolResponse,
103}
104
105fn block_class(t: MessageTable) -> BlockClass {
106 match t {
107 MessageTable::AgentCompletionRequest => BlockClass::AgentCompletionRequest,
108 MessageTable::VectorCompletionRequest => BlockClass::VectorCompletionRequest,
109 MessageTable::FunctionExecutionRequest => BlockClass::FunctionExecutionRequest,
110 MessageTable::MessageQueueText
111 | MessageTable::MessageQueueImage
112 | MessageTable::MessageQueueAudio
113 | MessageTable::MessageQueueVideo
114 | MessageTable::MessageQueueFile => BlockClass::ClientNotification,
115 MessageTable::ToolResponseContentText
116 | MessageTable::ToolResponseContentImage
117 | MessageTable::ToolResponseContentAudio
118 | MessageTable::ToolResponseContentVideo
119 | MessageTable::ToolResponseContentFile => BlockClass::ToolResponse,
120 MessageTable::AssistantResponseRefusal
121 | MessageTable::AssistantResponseReasoning
122 | MessageTable::AssistantResponseToolCalls
123 | MessageTable::AssistantResponseContentText
124 | MessageTable::AssistantResponseContentImage
125 | MessageTable::AssistantResponseContentAudio
126 | MessageTable::AssistantResponseContentVideo
127 | MessageTable::AssistantResponseContentFile => BlockClass::AssistantResponse,
128 }
129}
130
131fn client_notification_kind(t: MessageTable) -> Option<ClientNotificationPartType> {
132 match t {
133 MessageTable::MessageQueueText => Some(ClientNotificationPartType::Text),
134 MessageTable::MessageQueueImage => Some(ClientNotificationPartType::Image),
135 MessageTable::MessageQueueAudio => Some(ClientNotificationPartType::Audio),
136 MessageTable::MessageQueueVideo => Some(ClientNotificationPartType::Video),
137 MessageTable::MessageQueueFile => Some(ClientNotificationPartType::File),
138 _ => None,
139 }
140}
141
142fn assistant_response_part(row: &MsgRow) -> Option<AssistantResponsePart> {
147 let id = row.id;
148 let timestamp_delivered = row.timestamp_delivered;
149 Some(match row.table_kind {
150 MessageTable::AssistantResponseToolCalls => AssistantResponsePart::ToolCall {
151 id,
152 timestamp_delivered,
153 function_name: row.function_name.clone().unwrap_or_default(),
154 tool_call_id: row.tool_call_id.clone().unwrap_or_default(),
155 tool_call_index: row.row_sub_index.unwrap_or_default(),
156 },
157 MessageTable::AssistantResponseRefusal => {
158 AssistantResponsePart::Refusal { id, timestamp_delivered }
159 }
160 MessageTable::AssistantResponseReasoning => {
161 AssistantResponsePart::Reasoning { id, timestamp_delivered }
162 }
163 MessageTable::AssistantResponseContentText => {
164 AssistantResponsePart::Text { id, timestamp_delivered }
165 }
166 MessageTable::AssistantResponseContentImage => {
167 AssistantResponsePart::Image { id, timestamp_delivered }
168 }
169 MessageTable::AssistantResponseContentAudio => {
170 AssistantResponsePart::Audio { id, timestamp_delivered }
171 }
172 MessageTable::AssistantResponseContentVideo => {
173 AssistantResponsePart::Video { id, timestamp_delivered }
174 }
175 MessageTable::AssistantResponseContentFile => {
176 AssistantResponsePart::File { id, timestamp_delivered }
177 }
178 _ => return None,
179 })
180}
181
182fn tool_response_kind(t: MessageTable) -> Option<ToolResponsePartType> {
183 match t {
184 MessageTable::ToolResponseContentText => Some(ToolResponsePartType::Text),
185 MessageTable::ToolResponseContentImage => Some(ToolResponsePartType::Image),
186 MessageTable::ToolResponseContentAudio => Some(ToolResponsePartType::Audio),
187 MessageTable::ToolResponseContentVideo => Some(ToolResponsePartType::Video),
188 MessageTable::ToolResponseContentFile => Some(ToolResponsePartType::File),
189 _ => None,
190 }
191}
192
193const SELECT_SHAPE: &str = "SELECT \
199 m.\"index\" AS id, \
200 m.response_id, \
201 m.\"table\" AS table_kind, \
202 m.agent_instance_hierarchy, \
203 m.\"timestamp\" AS timestamp_delivered, \
204 CASE m.\"table\" \
205 WHEN 'message_queue_text' THEN mq.sender_agent_instance_hierarchy \
206 WHEN 'message_queue_image' THEN mq.sender_agent_instance_hierarchy \
207 WHEN 'message_queue_audio' THEN mq.sender_agent_instance_hierarchy \
208 WHEN 'message_queue_video' THEN mq.sender_agent_instance_hierarchy \
209 WHEN 'message_queue_file' THEN mq.sender_agent_instance_hierarchy \
210 WHEN 'agent_completion_request' THEN acr.sender_agent_instance_hierarchy \
211 WHEN 'vector_completion_request' THEN vcr.sender_agent_instance_hierarchy \
212 WHEN 'function_execution_request' THEN fer.sender_agent_instance_hierarchy \
213 ELSE NULL \
214 END AS sender_agent_instance_hierarchy, \
215 mq.id AS message_queue_id, \
216 mq.enqueued_at AS timestamp_queued, \
217 mq.key AS message_queue_key, \
218 atc.function_name AS function_name, \
219 COALESCE(atc.tool_call_id, tr.tool_call_id) AS tool_call_id, \
220 m.row_sub_index AS row_sub_index";
221
222const FROM_JOINS: &str = "FROM objectiveai.messages m \
223 LEFT JOIN objectiveai.message_queue_contents mqc \
224 ON m.row_index = mqc.id \
225 AND m.\"table\" IN ( \
226 'message_queue_text', 'message_queue_image', 'message_queue_audio', \
227 'message_queue_video', 'message_queue_file' \
228 ) \
229 LEFT JOIN objectiveai.message_queue mq ON mqc.message_queue_id = mq.id \
230 LEFT JOIN objectiveai.agent_completion_requests acr \
231 ON m.response_id = acr.response_id \
232 AND m.\"table\" = 'agent_completion_request' \
233 LEFT JOIN objectiveai.vector_completion_requests vcr \
234 ON m.response_id = vcr.response_id \
235 AND m.\"table\" = 'vector_completion_request' \
236 LEFT JOIN objectiveai.function_execution_requests fer \
237 ON m.response_id = fer.response_id \
238 AND m.\"table\" = 'function_execution_request' \
239 LEFT JOIN objectiveai.assistant_response_tool_calls atc \
240 ON m.response_id = atc.response_id \
241 AND m.row_index = atc.\"index\" \
242 AND m.row_sub_index = atc.tool_call_index \
243 AND m.\"table\" = 'assistant_response_tool_calls' \
244 LEFT JOIN objectiveai.tool_response tr \
245 ON m.response_id = tr.response_id \
246 AND m.row_index = tr.\"index\" \
247 AND m.\"table\" IN ( \
248 'tool_response_content_text', 'tool_response_content_image', \
249 'tool_response_content_audio', 'tool_response_content_video', \
250 'tool_response_content_file' \
251 )";
252
253fn row_into_msg(r: &sqlx::postgres::PgRow) -> Result<MsgRow, Error> {
254 Ok(MsgRow {
255 id: r.try_get("id")?,
256 response_id: r.try_get("response_id")?,
257 table_kind: r.try_get("table_kind")?,
258 agent_instance_hierarchy: r.try_get("agent_instance_hierarchy")?,
259 timestamp_delivered: r.try_get("timestamp_delivered")?,
260 sender_agent_instance_hierarchy: r.try_get("sender_agent_instance_hierarchy")?,
261 message_queue_id: r.try_get("message_queue_id")?,
262 timestamp_queued: r.try_get("timestamp_queued")?,
263 message_queue_key: r.try_get("message_queue_key")?,
264 function_name: r.try_get("function_name")?,
265 tool_call_id: r.try_get("tool_call_id")?,
266 row_sub_index: r.try_get("row_sub_index")?,
267 })
268}
269
270fn coalesce_into_blocks(rows: Vec<MsgRow>) -> Vec<ResponseItem> {
273 let mut out: Vec<ResponseItem> = Vec::new();
274 let mut cur_class: Option<BlockClass> = None;
275 let mut cur_aih: String = String::new();
276 let mut cur_rid: String = String::new();
277 let mut cur_sender: Option<String> = None;
280 let mut cur_mq_id: Option<i64> = None;
285 let mut cur_timestamp_queued: Option<i64> = None;
288 let mut cur_key: Option<String> = None;
292 let mut cur_tool_call_id: Option<String> = None;
296 let mut cur_notification_parts: Vec<ClientNotificationPart> = Vec::new();
297 let mut cur_assistant_parts: Vec<AssistantResponsePart> = Vec::new();
298 let mut cur_tool_parts: Vec<ToolResponsePart> = Vec::new();
299
300 let flush = |class: Option<BlockClass>,
301 aih: &mut String,
302 rid: &mut String,
303 sender: &mut Option<String>,
304 mq_id: &mut Option<i64>,
305 timestamp_queued: &mut Option<i64>,
306 key: &mut Option<String>,
307 tool_call_id: &mut Option<String>,
308 notification_parts: &mut Vec<ClientNotificationPart>,
309 assistant_parts: &mut Vec<AssistantResponsePart>,
310 tool_parts: &mut Vec<ToolResponsePart>,
311 out: &mut Vec<ResponseItem>| {
312 match class {
313 Some(BlockClass::ClientNotification) if !notification_parts.is_empty() => {
314 out.push(ResponseItem::ClientNotification {
315 agent_instance_hierarchy: std::mem::take(aih),
316 sender_agent_instance_hierarchy: sender.take().unwrap_or_default(),
317 response_id: std::mem::take(rid),
318 timestamp_queued: timestamp_queued.take().unwrap_or_default(),
319 key: key.take(),
320 parts: std::mem::take(notification_parts),
321 });
322 *mq_id = None;
323 }
324 Some(BlockClass::AssistantResponse) if !assistant_parts.is_empty() => {
325 out.push(ResponseItem::AssistantResponse {
326 agent_instance_hierarchy: std::mem::take(aih),
327 response_id: std::mem::take(rid),
328 parts: std::mem::take(assistant_parts),
329 });
330 }
331 Some(BlockClass::ToolResponse) if !tool_parts.is_empty() => {
332 out.push(ResponseItem::ToolResponse {
333 agent_instance_hierarchy: std::mem::take(aih),
334 response_id: std::mem::take(rid),
335 tool_call_id: tool_call_id.take().unwrap_or_default(),
336 parts: std::mem::take(tool_parts),
337 });
338 }
339 _ => {
340 aih.clear();
341 rid.clear();
342 *sender = None;
343 *mq_id = None;
344 *timestamp_queued = None;
345 *key = None;
346 *tool_call_id = None;
347 notification_parts.clear();
348 assistant_parts.clear();
349 tool_parts.clear();
350 }
351 }
352 };
353
354 for row in rows {
355 let class = block_class(row.table_kind);
356
357 match class {
359 BlockClass::AgentCompletionRequest => {
360 flush(
361 cur_class, &mut cur_aih, &mut cur_rid, &mut cur_sender,
362 &mut cur_mq_id, &mut cur_timestamp_queued, &mut cur_key,
363 &mut cur_tool_call_id,
364 &mut cur_notification_parts, &mut cur_assistant_parts,
365 &mut cur_tool_parts, &mut out,
366 );
367 out.push(ResponseItem::AgentCompletionRequest {
368 id: row.id,
369 agent_instance_hierarchy: row.agent_instance_hierarchy,
370 sender_agent_instance_hierarchy: row
371 .sender_agent_instance_hierarchy
372 .unwrap_or_default(),
373 timestamp_delivered: row.timestamp_delivered,
374 response_id: row.response_id,
375 });
376 cur_class = None;
377 continue;
378 }
379 BlockClass::VectorCompletionRequest => {
380 flush(
381 cur_class, &mut cur_aih, &mut cur_rid, &mut cur_sender,
382 &mut cur_mq_id, &mut cur_timestamp_queued, &mut cur_key,
383 &mut cur_tool_call_id,
384 &mut cur_notification_parts, &mut cur_assistant_parts,
385 &mut cur_tool_parts, &mut out,
386 );
387 out.push(ResponseItem::VectorCompletionRequest {
388 id: row.id,
389 agent_instance_hierarchy: row.agent_instance_hierarchy,
390 sender_agent_instance_hierarchy: row
391 .sender_agent_instance_hierarchy
392 .unwrap_or_default(),
393 timestamp_delivered: row.timestamp_delivered,
394 response_id: row.response_id,
395 });
396 cur_class = None;
397 continue;
398 }
399 BlockClass::FunctionExecutionRequest => {
400 flush(
401 cur_class, &mut cur_aih, &mut cur_rid, &mut cur_sender,
402 &mut cur_mq_id, &mut cur_timestamp_queued, &mut cur_key,
403 &mut cur_tool_call_id,
404 &mut cur_notification_parts, &mut cur_assistant_parts,
405 &mut cur_tool_parts, &mut out,
406 );
407 out.push(ResponseItem::FunctionExecutionRequest {
408 id: row.id,
409 agent_instance_hierarchy: row.agent_instance_hierarchy,
410 sender_agent_instance_hierarchy: row
411 .sender_agent_instance_hierarchy
412 .unwrap_or_default(),
413 timestamp_delivered: row.timestamp_delivered,
414 response_id: row.response_id,
415 });
416 cur_class = None;
417 continue;
418 }
419 _ => {}
420 }
421
422 let boundary = cur_class != Some(class)
428 || cur_aih != row.agent_instance_hierarchy
429 || cur_rid != row.response_id
430 || (class == BlockClass::ClientNotification
431 && (cur_sender.as_deref() != row.sender_agent_instance_hierarchy.as_deref()
432 || cur_mq_id != row.message_queue_id))
433 || (class == BlockClass::ToolResponse
434 && cur_tool_call_id.as_deref() != row.tool_call_id.as_deref());
435 if boundary {
436 flush(
437 cur_class, &mut cur_aih, &mut cur_rid, &mut cur_sender,
438 &mut cur_mq_id, &mut cur_timestamp_queued, &mut cur_key,
439 &mut cur_tool_call_id,
440 &mut cur_notification_parts, &mut cur_assistant_parts,
441 &mut cur_tool_parts, &mut out,
442 );
443 cur_class = Some(class);
444 cur_aih = row.agent_instance_hierarchy.clone();
445 cur_rid = row.response_id.clone();
446 if class == BlockClass::ClientNotification {
447 cur_sender = row.sender_agent_instance_hierarchy.clone();
448 cur_mq_id = row.message_queue_id;
449 cur_timestamp_queued = row.timestamp_queued;
450 cur_key = row.message_queue_key.clone();
451 cur_tool_call_id = None;
452 } else if class == BlockClass::ToolResponse {
453 cur_sender = None;
454 cur_mq_id = None;
455 cur_timestamp_queued = None;
456 cur_key = None;
457 cur_tool_call_id = row.tool_call_id.clone();
458 } else {
459 cur_sender = None;
460 cur_mq_id = None;
461 cur_timestamp_queued = None;
462 cur_key = None;
463 cur_tool_call_id = None;
464 }
465 }
466
467 match class {
468 BlockClass::ClientNotification => {
469 let r#type = client_notification_kind(row.table_kind)
470 .expect("class invariant: ClientNotification maps to message_queue_*");
471 cur_notification_parts.push(ClientNotificationPart {
472 id: row.id,
473 timestamp_delivered: row.timestamp_delivered,
474 r#type,
475 });
476 }
477 BlockClass::AssistantResponse => {
478 let part = assistant_response_part(&row)
479 .expect("class invariant: AssistantResponse maps to assistant_response_*");
480 cur_assistant_parts.push(part);
481 }
482 BlockClass::ToolResponse => {
483 let r#type = tool_response_kind(row.table_kind)
484 .expect("class invariant: ToolResponse maps to tool_response*");
485 cur_tool_parts.push(ToolResponsePart {
486 id: row.id,
487 timestamp_delivered: row.timestamp_delivered,
488 r#type,
489 });
490 }
491 _ => unreachable!("request classes handled above"),
492 }
493 }
494
495 flush(
496 cur_class, &mut cur_aih, &mut cur_rid, &mut cur_sender,
497 &mut cur_mq_id, &mut cur_timestamp_queued, &mut cur_key,
498 &mut cur_tool_call_id,
499 &mut cur_notification_parts, &mut cur_assistant_parts,
500 &mut cur_tool_parts, &mut out,
501 );
502
503 out
504}
505
506pub async fn read_all_for_hierarchy(
510 pool: &Pool,
511 agent_instance_hierarchy: &str,
512 after_id: Option<i64>,
513 limit: Option<i64>,
514) -> Result<Vec<ResponseItem>, Error> {
515 let sql = format!(
516 "{select} {from} \
517 WHERE m.agent_instance_hierarchy = $1 \
518 AND m.\"index\" > COALESCE($2, 0) \
519 ORDER BY m.\"index\" ASC \
520 LIMIT $3",
521 select = SELECT_SHAPE,
522 from = FROM_JOINS,
523 );
524 let rows = sqlx::query(&sql)
525 .bind(agent_instance_hierarchy)
526 .bind(after_id)
527 .bind(limit)
528 .fetch_all(&**pool)
529 .await?;
530
531 let msg_rows: Vec<MsgRow> = rows.iter().map(row_into_msg).collect::<Result<_, _>>()?;
532 Ok(coalesce_into_blocks(msg_rows))
533}
534
535pub async fn read_pending_for_parent(
542 pool: &Pool,
543 parent_agent_instance_hierarchy: &str,
544 after_id: Option<i64>,
545 limit: Option<i64>,
546) -> Result<Vec<ResponseItem>, Error> {
547 let sql = format!(
559 "WITH selected AS ( \
560 {select} \
561 {from} \
562 JOIN objectiveai.messages_queue q \
563 ON q.spawned_agent_instance_hierarchy = m.agent_instance_hierarchy \
564 WHERE q.parent_agent_instance_hierarchy = $1 \
565 AND m.\"index\" > GREATEST(q.read_index, COALESCE($2, 0)) \
566 ORDER BY m.\"index\" ASC \
567 LIMIT $3 \
568 ), \
569 maxes AS ( \
570 SELECT agent_instance_hierarchy AS spawned, MAX(id) AS max_id \
571 FROM selected \
572 GROUP BY agent_instance_hierarchy \
573 ), \
574 bump AS ( \
575 UPDATE objectiveai.messages_queue qq \
576 SET read_index = GREATEST(qq.read_index, mx.max_id) \
577 FROM maxes mx \
578 WHERE qq.parent_agent_instance_hierarchy = $1 \
579 AND qq.spawned_agent_instance_hierarchy = mx.spawned \
580 RETURNING 1 \
581 ) \
582 SELECT s.id, s.response_id, s.table_kind, \
583 s.agent_instance_hierarchy, s.timestamp_delivered, \
584 s.sender_agent_instance_hierarchy, \
585 s.message_queue_id, s.timestamp_queued, \
586 s.message_queue_key, s.function_name, \
587 s.tool_call_id, s.row_sub_index \
588 FROM selected s \
589 ORDER BY s.id ASC",
590 select = SELECT_SHAPE,
591 from = FROM_JOINS,
592 );
593 let rows = sqlx::query(&sql)
594 .bind(parent_agent_instance_hierarchy)
595 .bind(after_id)
596 .bind(limit)
597 .fetch_all(&**pool)
598 .await?;
599
600 let msg_rows: Vec<MsgRow> = rows.iter().map(row_into_msg).collect::<Result<_, _>>()?;
601 Ok(coalesce_into_blocks(msg_rows))
602}
603
604pub async fn any_pending_matching_kinds(
620 pool: &Pool,
621 parent_agent_instance_hierarchy: &str,
622 after_id: Option<i64>,
623 kinds: Option<&[MessageTable]>,
624) -> Result<bool, Error> {
625 let kinds_clause = match kinds {
626 Some(ks) if !ks.is_empty() => {
627 let list = ks
628 .iter()
629 .map(|k| format!("'{}'", k.schema_name()))
630 .collect::<Vec<_>>()
631 .join(", ");
632 format!("AND m.\"table\" IN ({list})")
633 }
634 _ => String::new(),
635 };
636 let sql = format!(
637 "SELECT EXISTS( \
638 SELECT 1 FROM objectiveai.messages m \
639 JOIN objectiveai.messages_queue q \
640 ON q.spawned_agent_instance_hierarchy = m.agent_instance_hierarchy \
641 WHERE q.parent_agent_instance_hierarchy = $1 \
642 AND m.\"index\" > GREATEST(q.read_index, COALESCE($2, 0)) \
643 {kinds_clause} \
644 )"
645 );
646 let exists: bool = sqlx::query_scalar(&sql)
647 .bind(parent_agent_instance_hierarchy)
648 .bind(after_id)
649 .fetch_one(&**pool)
650 .await?;
651 Ok(exists)
652}