Skip to main content

objectiveai_cli/db/
instances.rs

1//! `instances` tier — aggregate enumeration backing
2//! `agents instances {list, get}`.
3//!
4//! Reports per-agent stats joined across three tiers: `objectiveai.messages`
5//! (spawn/active timestamps + total logged), `message_queue` (active
6//! queued count, resolving tag-targeted rows through `tags`), and
7//! `tags` (the tag names bound to each agent).
8//!
9//! Two entry points share one aggregate, differing only in which
10//! agents they scope to:
11//! - [`list_under_parent`] — the DIRECT children of a parent AIH.
12//! - [`get_exact`] — one exact agent AIH.
13
14use std::collections::HashMap;
15
16use objectiveai_sdk::cli::command::agents::instances::list::ResponseItem;
17use sqlx::Row as _;
18
19use super::{Error, Pool};
20
21/// Aggregate per-agent stats for the agents selected by the AIH
22/// predicate. Exactly one of `exact` / `prefix` is `Some`:
23/// - `exact = Some(aih)` → the single agent whose AIH equals `aih`.
24/// - `prefix = Some("{p}/%")` + `prefix_deep = Some("{p}/%/%")` → the
25///   direct children of `p` (starts with `{p}/`, no further `/`).
26///
27/// The same 3-bind predicate gates `log_agg`, `queue_agg`, and the
28/// tags subtree query. Returns SDK `ResponseItem`s sorted by AIH.
29async fn aggregate(
30    pool: &Pool,
31    exact: Option<&str>,
32    prefix: Option<&str>,
33    prefix_deep: Option<&str>,
34) -> Result<Vec<ResponseItem>, Error> {
35    // logs (spawned/active/logged) FULL OUTER JOINed with active queue
36    // counts (queued). A queue row targeting a tag resolves to that
37    // tag's BOUND AIH via `tags`; unbound/grouped tags drop out (NULL
38    // eff_aih fails the predicate). FULL OUTER JOIN unions agents seen
39    // in either tier.
40    let rows = sqlx::query(
41        "WITH log_agg AS ( \
42             SELECT agent_instance_hierarchy AS aih, \
43                    MIN(\"timestamp\") AS spawned, \
44                    MAX(\"timestamp\") AS active, \
45                    COUNT(*) AS logged \
46             FROM objectiveai.messages \
47             WHERE ( \
48                 ($1::text IS NOT NULL AND agent_instance_hierarchy = $1) \
49                 OR ($2::text IS NOT NULL \
50                     AND agent_instance_hierarchy LIKE $2 \
51                     AND agent_instance_hierarchy NOT LIKE $3) \
52             ) \
53             GROUP BY agent_instance_hierarchy \
54         ), \
55         queue_agg AS ( \
56             SELECT eff_aih AS aih, COUNT(*) AS queued \
57             FROM ( \
58                 SELECT COALESCE( \
59                            mq.agent_instance_hierarchy, \
60                            t.agent_instance_hierarchy \
61                        ) AS eff_aih \
62                 FROM objectiveai.message_queue mq \
63                 LEFT JOIN objectiveai.tags t \
64                     ON mq.agent_tag IS NOT NULL AND t.name = mq.agent_tag \
65                 WHERE mq.active = TRUE \
66             ) x \
67             WHERE ( \
68                 ($1::text IS NOT NULL AND x.eff_aih = $1) \
69                 OR ($2::text IS NOT NULL \
70                     AND x.eff_aih LIKE $2 \
71                     AND x.eff_aih NOT LIKE $3) \
72             ) \
73             GROUP BY eff_aih \
74         ) \
75         SELECT COALESCE(l.aih, q.aih)   AS aih, \
76                l.spawned                AS spawned, \
77                l.active                 AS active, \
78                COALESCE(l.logged, 0)::bigint AS logged, \
79                COALESCE(q.queued, 0)::bigint AS queued \
80         FROM log_agg l \
81         FULL OUTER JOIN queue_agg q ON q.aih = l.aih \
82         ORDER BY aih ASC",
83    )
84    .bind(exact)
85    .bind(prefix)
86    .bind(prefix_deep)
87    .fetch_all(&**pool)
88    .await?;
89
90    // Tag bindings for the same scope in one query, grouped per AIH
91    // (newest-bound first), so we avoid a per-agent round trip.
92    let tag_rows = sqlx::query(
93        "SELECT agent_instance_hierarchy, name FROM objectiveai.tags \
94         WHERE ( \
95             ($1::text IS NOT NULL AND agent_instance_hierarchy = $1) \
96             OR ($2::text IS NOT NULL \
97                 AND agent_instance_hierarchy LIKE $2 \
98                 AND agent_instance_hierarchy NOT LIKE $3) \
99         ) \
100         ORDER BY updated_at DESC",
101    )
102    .bind(exact)
103    .bind(prefix)
104    .bind(prefix_deep)
105    .fetch_all(&**pool)
106    .await?;
107    let mut tags_by_aih: HashMap<String, Vec<String>> = HashMap::new();
108    for row in tag_rows {
109        let aih: String = row.try_get("agent_instance_hierarchy")?;
110        let name: String = row.try_get("name")?;
111        tags_by_aih.entry(aih).or_default().push(name);
112    }
113
114    let mut out = Vec::with_capacity(rows.len());
115    for row in rows {
116        let aih: String = row.try_get("aih")?;
117        let spawned: Option<i64> = row.try_get("spawned")?;
118        let active: Option<i64> = row.try_get("active")?;
119        let logged: i64 = row.try_get("logged")?;
120        let queued: i64 = row.try_get("queued")?;
121        let tags = tags_by_aih.remove(&aih).unwrap_or_default();
122        out.push(ResponseItem {
123            agent_instance_hierarchy: aih,
124            tags,
125            queued: queued as u64,
126            timestamp_spawned: spawned,
127            timestamp_active: active,
128            logged: logged as u64,
129        });
130    }
131    Ok(out)
132}
133
134/// List the DIRECT children of `parent` (AIH `LIKE '{parent}/%'` with
135/// no further `/` — `parent` itself and deeper descendants excluded),
136/// with per-agent aggregates. Sorted by AIH ascending.
137pub async fn list_under_parent(
138    pool: &Pool,
139    parent: &str,
140) -> Result<Vec<ResponseItem>, Error> {
141    let prefix = format!("{parent}/%");
142    let prefix_deep = format!("{parent}/%/%");
143    aggregate(pool, None, Some(&prefix), Some(&prefix_deep)).await
144}
145
146/// Aggregate stats for one EXACT agent AIH. Always returns an item:
147/// when the agent has no logs/queue activity it is zero-filled (its
148/// AIH + bound tags + zero counts + null timestamps), so an
149/// explicitly-named target always yields a row.
150pub async fn get_exact(pool: &Pool, aih: &str) -> Result<ResponseItem, Error> {
151    let mut items = aggregate(pool, Some(aih), None, None).await?;
152    if let Some(item) = items.pop() {
153        return Ok(item);
154    }
155    // No logs/queue rows for this agent; still surface it with its
156    // tags (which may exist independently) and zeroed activity.
157    let tags = super::tags::tags_for_hierarchy(pool, aih).await?;
158    Ok(ResponseItem {
159        agent_instance_hierarchy: aih.to_string(),
160        tags,
161        queued: 0,
162        timestamp_spawned: None,
163        timestamp_active: None,
164        logged: 0,
165    })
166}