Skip to main content

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}