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//! Sources, in order:
5//!
6//! - **agent definition** — read from `objectiveai.agent_refs`, the
7//! definition-source registry keyed by the full AIH (EITHER the
8//! `RemotePath` the WF was fetched from OR the inline spec —
9//! blindly upserted at spawn-by-spec lock acquisition and by the
10//! log writer whenever a chunk carries `agent_inline`). When the
11//! registry has no row — agents whose last activity predates the
12//! table — the MIGRATION FALLBACK extracts the definition from
13//! the most recent `agent_completion_requests` blob, keyed by the
14//! `response_id` suffix embedded in the AIH. Any post-table run
15//! repopulates the registry, so the fallback decays naturally.
16//! - **latest continuation** — read straight from the
17//! `agent_continuations` table keyed by the full AIH. The
18//! chunk-yielder loops (`agents spawn` + `functions execute`)
19//! upsert into that table per chunk, so this is the
20//! authoritative latest-continuation source — no more parsing it
21//! out of the cumulative response blob.
22//!
23//! Used by `agents message`'s stream-true path after it acquires
24//! the hierarchy's lock: it needs the agent definition to drive
25//! `spawn::run_multi_pass` and the latest continuation to seed the
26//! resumed conversation.
27
28use objectiveai_sdk::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional;
29use sqlx::Row as _;
30
31use super::super::{Error, Pool};
32
33/// What [`lookup_session`] returns when a prior session exists for
34/// the queried `agent_instance_hierarchy`.
35#[derive(Debug, Clone)]
36pub struct SessionLookup {
37 pub agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional,
38 /// `None` when the `agent_continuations` row hasn't been
39 /// written yet — typically because the stream errored before
40 /// any chunk that carried a continuation landed.
41 pub continuation: Option<String>,
42}
43
44/// Resolve the session for `agent_instance_hierarchy`.
45/// `Ok(None)` means neither the `agent_refs` registry nor the
46/// legacy request-blob fallback holds a definition for that
47/// hierarchy (no prior session recorded).
48pub async fn lookup_session(
49 pool: &Pool,
50 agent_instance_hierarchy: &str,
51) -> Result<Option<SessionLookup>, Error> {
52 // One row per AIH in `agent_refs` (remote XOR inline, CHECK-
53 // enforced); LEFT JOIN the continuation keyed by the same AIH. A
54 // NULL `continuation` column means there's no row in
55 // `agent_continuations` yet for this AIH.
56 let row = sqlx::query(
57 "SELECT refs.remote AS remote, refs.inline AS inline, \
58 cont.continuation AS continuation \
59 FROM objectiveai.agent_refs refs \
60 LEFT JOIN objectiveai.agent_continuations cont \
61 ON cont.agent_instance_hierarchy = refs.agent_instance_hierarchy \
62 WHERE refs.agent_instance_hierarchy = $1",
63 )
64 .bind(agent_instance_hierarchy)
65 .fetch_optional(&**pool)
66 .await?;
67
68 let Some(row) = row else {
69 // Migration fallback: pre-`agent_refs` agents resolve from
70 // their most recent request blob.
71 return lookup_session_from_request_blob(pool, agent_instance_hierarchy)
72 .await;
73 };
74
75 let remote: Option<String> = row.try_get("remote")?;
76 let inline: Option<serde_json::Value> = row.try_get("inline")?;
77 let continuation: Option<String> = row.try_get("continuation")?;
78
79 // Both variants round-trip through the request-side untagged
80 // union: a remote is its wire string, an inline spec its object.
81 // The log writer stores the VALIDATED `InlineAgentWithFallbacks`
82 // (which serializes its computed `id`s alongside the flattened
83 // base); deserializing into the base type simply ignores those
84 // extra fields.
85 let agent_value = match (remote, inline) {
86 (Some(remote), _) => serde_json::Value::String(remote),
87 (None, Some(inline)) => inline,
88 (None, None) => {
89 return Err(Error::InvalidData(format!(
90 "agent_refs row for {agent_instance_hierarchy} has neither \
91 remote nor inline",
92 )));
93 }
94 };
95 let agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional =
96 serde_json::from_value(agent_value)?;
97
98 Ok(Some(SessionLookup { agent, continuation }))
99}
100
101/// The pre-`agent_refs` resolution, kept as a migration fallback:
102/// extract the definition from
103/// `objectiveai.agent_completion_requests.body.agent` (the request
104/// blob is a serialized `AgentCompletionCreateParams`). The request
105/// row is PK'd by `response_id` — the trailing suffix of the AIH
106/// after the final `-`
107/// (`{ctx lineage}/{agent_full_id}-{response_id}`).
108async fn lookup_session_from_request_blob(
109 pool: &Pool,
110 agent_instance_hierarchy: &str,
111) -> Result<Option<SessionLookup>, Error> {
112 // Split on the FINAL `-`: everything after is the response_id.
113 // No `-` at all means the hierarchy doesn't carry a response_id
114 // suffix and can't be resolved — return None.
115 let Some((_, response_id)) = agent_instance_hierarchy.rsplit_once('-') else {
116 return Ok(None);
117 };
118
119 let row = sqlx::query(
120 "SELECT req.body AS request_body, cont.continuation AS continuation \
121 FROM objectiveai.agent_completion_requests req \
122 LEFT JOIN objectiveai.agent_continuations cont \
123 ON cont.agent_instance_hierarchy = $2 \
124 WHERE req.response_id = $1",
125 )
126 .bind(response_id)
127 .bind(agent_instance_hierarchy)
128 .fetch_optional(&**pool)
129 .await?;
130
131 let Some(row) = row else { return Ok(None) };
132
133 let request_body: serde_json::Value = row.try_get("request_body")?;
134 let continuation: Option<String> = row.try_get("continuation")?;
135
136 // The request blob is a serialized
137 // `AgentCompletionCreateParams`; the agent field there is what
138 // spawn's `Request.agent` carries.
139 let agent_value = request_body.get("agent").cloned().ok_or_else(|| {
140 Error::InvalidData(format!(
141 "agent_completion_requests.body missing `agent` field for response_id {response_id}",
142 ))
143 })?;
144 let agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional =
145 serde_json::from_value(agent_value)?;
146
147 Ok(Some(SessionLookup { agent, continuation }))
148}