Skip to main content

vv_agent/runtime/
sub_agent_sessions.rs

1use std::collections::BTreeMap;
2use std::sync::{Arc, Mutex, OnceLock};
3
4use serde_json::Value;
5
6use crate::types::SubTaskOutcome;
7
8pub type SubAgentSessionListener =
9    Arc<dyn Fn(&str, &BTreeMap<String, Value>) + Send + Sync + 'static>;
10pub type SubAgentSessionUnsubscribe = Box<dyn FnOnce() + Send + 'static>;
11
12pub trait SubAgentSession: Send + Sync {
13    fn steer(&self, prompt: &str) -> Result<(), String>;
14
15    fn sanitize_for_resume(&self) -> usize {
16        0
17    }
18
19    fn continue_run(&self, _prompt: &str) -> Result<SubTaskOutcome, String> {
20        Err("Sub-agent session continuation is not supported.".to_string())
21    }
22
23    fn subscribe(&self, _listener: SubAgentSessionListener) -> Option<SubAgentSessionUnsubscribe> {
24        None
25    }
26}
27
28#[derive(Default)]
29pub struct SubAgentSessionRegistry {
30    sessions: Mutex<BTreeMap<String, Arc<dyn SubAgentSession>>>,
31}
32
33impl SubAgentSessionRegistry {
34    pub fn register(&self, session_id: impl Into<String>, session: Arc<dyn SubAgentSession>) {
35        let session_id = session_id.into();
36        let session_id = session_id.trim();
37        if session_id.is_empty() {
38            return;
39        }
40        self.sessions
41            .lock()
42            .expect("sub-agent session registry poisoned")
43            .insert(session_id.to_string(), session);
44    }
45
46    pub fn unregister(&self, session_id: &str) {
47        let session_id = session_id.trim();
48        if session_id.is_empty() {
49            return;
50        }
51        self.sessions
52            .lock()
53            .expect("sub-agent session registry poisoned")
54            .remove(session_id);
55    }
56
57    pub fn unregister_if_matches(
58        &self,
59        session_id: &str,
60        session: Option<&Arc<dyn SubAgentSession>>,
61    ) -> bool {
62        let session_id = session_id.trim();
63        if session_id.is_empty() {
64            return false;
65        }
66        let mut sessions = self
67            .sessions
68            .lock()
69            .expect("sub-agent session registry poisoned");
70        let Some(registered) = sessions.get(session_id) else {
71            return false;
72        };
73        if let Some(expected) = session {
74            if !Arc::ptr_eq(registered, expected) {
75                return false;
76            }
77        }
78        sessions.remove(session_id).is_some()
79    }
80
81    pub fn get(&self, session_id: &str) -> Option<Arc<dyn SubAgentSession>> {
82        let session_id = session_id.trim();
83        if session_id.is_empty() {
84            return None;
85        }
86        self.sessions
87            .lock()
88            .expect("sub-agent session registry poisoned")
89            .get(session_id)
90            .cloned()
91    }
92
93    pub fn clear(&self) {
94        self.sessions
95            .lock()
96            .expect("sub-agent session registry poisoned")
97            .clear();
98    }
99}
100
101static ACTIVE_SUB_AGENT_SESSIONS: OnceLock<SubAgentSessionRegistry> = OnceLock::new();
102
103pub fn sub_agent_session_registry() -> &'static SubAgentSessionRegistry {
104    ACTIVE_SUB_AGENT_SESSIONS.get_or_init(SubAgentSessionRegistry::default)
105}
106
107pub fn register_sub_agent_session(
108    session_id: impl Into<String>,
109    session: Arc<dyn SubAgentSession>,
110) {
111    sub_agent_session_registry().register(session_id, session);
112}
113
114pub fn unregister_sub_agent_session(session_id: &str) {
115    sub_agent_session_registry().unregister(session_id);
116}
117
118pub fn _register_sub_agent_session(
119    session_id: impl Into<String>,
120    session: Arc<dyn SubAgentSession>,
121) {
122    register_sub_agent_session(session_id, session);
123}
124
125pub fn _unregister_sub_agent_session(
126    session_id: &str,
127    session: Option<Arc<dyn SubAgentSession>>,
128) -> bool {
129    sub_agent_session_registry().unregister_if_matches(session_id, session.as_ref())
130}
131
132pub fn get_sub_agent_session(session_id: &str) -> Option<Arc<dyn SubAgentSession>> {
133    sub_agent_session_registry().get(session_id)
134}
135
136pub fn steer_sub_agent_session(session_id: &str, prompt: &str) -> bool {
137    let session_id = session_id.trim();
138    let prompt = prompt.trim();
139    if session_id.is_empty() || prompt.is_empty() {
140        return false;
141    }
142    let Some(session) = get_sub_agent_session(session_id) else {
143        return false;
144    };
145    session.steer(prompt).is_ok()
146}
147
148pub fn continue_sub_agent_session(
149    session_id: &str,
150    prompt: &str,
151) -> Result<SubTaskOutcome, String> {
152    let session_id = session_id.trim();
153    let prompt = prompt.trim();
154    if session_id.is_empty() {
155        return Err("Sub-agent session id cannot be empty.".to_string());
156    }
157    if prompt.is_empty() {
158        return Err("Follow-up prompt cannot be empty.".to_string());
159    }
160    let Some(session) = get_sub_agent_session(session_id) else {
161        return Err(format!("Sub-agent session {session_id} is not registered."));
162    };
163    session.continue_run(prompt)
164}
165
166pub fn subscribe_sub_agent_session(
167    session_id: &str,
168    listener: SubAgentSessionListener,
169) -> Option<SubAgentSessionUnsubscribe> {
170    get_sub_agent_session(session_id)?.subscribe(listener)
171}