objectiveai_cli/db/logs/lookup.rs
1//! Look up the agent definition + latest continuation that
2//! belong to a given `agent_instance_hierarchy`.
3//!
4//! Two sources, one round-trip:
5//!
6//! - **agent definition** — extracted from
7//! `objectiveai.agent_completion_requests.body.agent` (the request blob
8//! is a serialized `AgentCompletionCreateParams`). The request
9//! row is PK'd by `response_id`, which is the trailing suffix of
10//! the AIH after the final `-`
11//! (`{ctx lineage}/{agent_full_id}-{response_id}`).
12//! - **latest continuation** — read straight from the
13//! `agent_continuations` table keyed by the full AIH. The
14//! chunk-yielder loops (`agents spawn` + `functions execute`)
15//! upsert into that table per chunk, so this is the
16//! authoritative latest-continuation source — no more parsing it
17//! out of the cumulative response blob.
18//!
19//! Used by `agents message`'s stream-true path after it acquires
20//! the hierarchy's lock: it needs the agent definition to drive
21//! `spawn::run_multi_pass` and the latest continuation to seed the
22//! resumed conversation.
23
24use objectiveai_sdk::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional;
25use sqlx::Row as _;
26
27use super::super::{Error, Pool};
28
29/// What [`lookup_session`] returns when a prior session exists for
30/// the queried `agent_instance_hierarchy`.
31#[derive(Debug, Clone)]
32pub struct SessionLookup {
33 pub agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional,
34 /// `None` when the `agent_continuations` row hasn't been
35 /// written yet — typically because the stream errored before
36 /// any chunk that carried a continuation landed.
37 pub continuation: Option<String>,
38}
39
40/// Resolve the session for `agent_instance_hierarchy`.
41/// `Ok(None)` means there's no logged request for that
42/// hierarchy's embedded `response_id` (no prior session).
43pub async fn lookup_session(
44 pool: &Pool,
45 agent_instance_hierarchy: &str,
46) -> Result<Option<SessionLookup>, Error> {
47 // Split on the FINAL `-`: everything after is the response_id.
48 // No `-` at all means the hierarchy doesn't carry a response_id
49 // suffix and can't be resolved — return None.
50 let Some((_, response_id)) = agent_instance_hierarchy.rsplit_once('-') else {
51 return Ok(None);
52 };
53
54 // LEFT JOIN `agent_continuations` onto the request row. The
55 // request row is PK'd by `response_id`; the continuation row is
56 // PK'd by the full AIH. Both keys are bound separately. A NULL
57 // `continuation` column means there's no row in
58 // `agent_continuations` yet for this AIH.
59 let row = sqlx::query(
60 "SELECT req.body AS request_body, cont.continuation AS continuation \
61 FROM objectiveai.agent_completion_requests req \
62 LEFT JOIN objectiveai.agent_continuations cont \
63 ON cont.agent_instance_hierarchy = $2 \
64 WHERE req.response_id = $1",
65 )
66 .bind(response_id)
67 .bind(agent_instance_hierarchy)
68 .fetch_optional(&**pool)
69 .await?;
70
71 let Some(row) = row else { return Ok(None) };
72
73 let request_body: serde_json::Value = row.try_get("request_body")?;
74 let continuation: Option<String> = row.try_get("continuation")?;
75
76 // The request blob is a serialized
77 // `AgentCompletionCreateParams`; the agent field there is what
78 // spawn's `Request.agent` carries.
79 let agent_value = request_body.get("agent").cloned().ok_or_else(|| {
80 Error::InvalidData(format!(
81 "agent_completion_requests.body missing `agent` field for response_id {response_id}",
82 ))
83 })?;
84 let agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional =
85 serde_json::from_value(agent_value)?;
86
87 Ok(Some(SessionLookup { agent, continuation }))
88}