Skip to main content

oino_core/agents/
feed.rs

1//! The frontend view of nested agents (`_oino/agents`, GA-44): the agent tree of a root
2//! session, live child event forwarding and lookups by id.
3//!
4//! Every nested agent belongs to the tree of one root session. Tree changes (spawn, close,
5//! run state) publish [`OinoEvent::AgentsChanged`] with the whole tree on the root session's
6//! hub; while a frontend follows the tree ([`AgentSession::follow_agents`]) every child client
7//! event is also published there as [`OinoEvent::AgentEvent`].
8
9use std::sync::Arc;
10use std::sync::atomic::Ordering;
11
12use serde::{Deserialize, Serialize};
13
14use super::ChildAgent;
15use crate::events::{ClientEvent, OinoEvent};
16use crate::session::{AgentSession, Inner};
17
18/// Run state of a nested agent.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum AgentRunState {
22    /// A run is active.
23    Running,
24    /// Waiting for input.
25    Idle,
26    /// Hibernated (E-HIBERNATE): the live session was disposed; the next handle
27    /// operation rebuilds it from the session file.
28    Hibernated,
29    /// Closed (final).
30    Closed,
31}
32
33/// One nested agent as frontends see it (`_oino/agents`, `agents_changed`).
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct AgentInfo {
37    /// Agent id (the child session id).
38    pub id: String,
39    /// Parent agent id; `None` for children of the root session.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub parent_id: Option<String>,
42    /// Label from the spec (`Subagent …`, `Sidekick`, `btw`, …).
43    pub label: String,
44    /// Extension that spawned it.
45    pub owner: String,
46    /// Child role (`AgentSpec.childRole`, else the owner).
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub role: Option<String>,
49    /// Run state.
50    pub state: AgentRunState,
51    /// Nesting depth (1 = child of the root).
52    pub depth: u8,
53    /// Child session file (`None` in memory).
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub session_file: Option<String>,
56}
57
58/// The root session of the tree `inner` belongs to.
59pub(crate) fn root_of(inner: &Arc<Inner>) -> Arc<Inner> {
60    let mut current = Arc::clone(inner);
61    loop {
62        let parent = current
63            .agents
64            .link
65            .as_ref()
66            .and_then(|link| link.parent.upgrade());
67        match parent {
68            Some(parent) => current = parent,
69            None => return current,
70        }
71    }
72}
73
74impl ChildAgent {
75    /// Run state.
76    #[must_use]
77    pub fn run_state(&self) -> AgentRunState {
78        if self.is_closed() {
79            AgentRunState::Closed
80        } else if self.is_hibernated() {
81            AgentRunState::Hibernated
82        } else if self.session().is_some_and(|session| session.is_idle()) {
83            AgentRunState::Idle
84        } else {
85            AgentRunState::Running
86        }
87    }
88
89    fn info(&self, parent_id: Option<&str>) -> AgentInfo {
90        AgentInfo {
91            id: self.id().to_owned(),
92            parent_id: parent_id.map(str::to_owned),
93            label: self.label().to_owned(),
94            owner: self.owner().to_owned(),
95            role: self.role().map(str::to_owned),
96            state: self.run_state(),
97            depth: self.depth(),
98            session_file: self.session_file().map(|p| p.display().to_string()),
99        }
100    }
101}
102
103fn collect(session: &AgentSession, parent_id: Option<&str>, out: &mut Vec<AgentInfo>) {
104    for child in session.child_agents() {
105        out.push(child.info(parent_id));
106        // A hibernated child has no live session — and no open children (its session's
107        // `dispose` closed them at hibernation).
108        if let Some(live) = child.session() {
109            collect(&live, Some(child.id()), out);
110        }
111    }
112}
113
114impl AgentSession {
115    /// Every open nested agent of this session's subtree, parents before children.
116    #[must_use]
117    pub fn agent_tree(&self) -> Vec<AgentInfo> {
118        let mut out = Vec::new();
119        collect(self, None, &mut out);
120        out
121    }
122
123    /// Finds a nested agent of this session's subtree by id.
124    #[must_use]
125    pub fn find_agent(&self, id: &str) -> Option<Arc<ChildAgent>> {
126        for child in self.child_agents() {
127            if child.id() == id {
128                return Some(child);
129            }
130            if let Some(live) = child.session()
131                && let Some(found) = live.find_agent(id)
132            {
133                return Some(found);
134            }
135        }
136        None
137    }
138
139    /// Turns live child event forwarding ([`OinoEvent::AgentEvent`]) on or off for the
140    /// whole tree.
141    pub fn follow_agents(&self, on: bool) {
142        self.inner.agents.tree.follow.store(on, Ordering::SeqCst);
143    }
144
145    /// Whether child events are forwarded.
146    #[must_use]
147    pub fn follows_agents(&self) -> bool {
148        self.inner.agents.tree.follow.load(Ordering::SeqCst)
149    }
150
151    /// Publishes [`OinoEvent::AgentsChanged`] on the root session of this tree.
152    pub(crate) fn publish_agents_changed(&self) {
153        let root = AgentSession::from_inner(root_of(&self.inner));
154        let agents = root.agent_tree();
155        root.hub().publish(OinoEvent::AgentsChanged { agents });
156    }
157}
158
159/// Forwards one child event to the root session while the tree is followed.
160pub(crate) fn forward_to_root(parent: &Arc<Inner>, agent_id: &str, event: &ClientEvent) {
161    if !parent.agents.tree.follow.load(Ordering::SeqCst) {
162        return;
163    }
164    let root = root_of(parent);
165    root.hub.publish(OinoEvent::AgentEvent {
166        agent_id: agent_id.to_owned(),
167        event: Box::new(event.clone()),
168    });
169}
170
171/// Whether `event` changes the child's run state (tree snapshot refresh).
172pub(crate) fn changes_state(event: &ClientEvent) -> bool {
173    use crate::events::AgentSessionEvent;
174    matches!(
175        event,
176        ClientEvent::Session(
177            AgentSessionEvent::AgentStart | AgentSessionEvent::AgentSettled { .. }
178        )
179    )
180}