otherone_agent/multi_agent/
supervisor.rs1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::{Arc, Mutex, OnceLock};
4use std::time::Instant;
5
6use tokio::sync::{mpsc, RwLock, Semaphore};
7use tokio_util::sync::CancellationToken;
8
9use crate::error::AgentError;
10
11use super::event::EventBus;
12use super::registry::RuntimeSnapshot;
13use super::types::{
14 AgentCallId, AgentDefinition, AgentId, AgentRunCommand, EffectiveLimits, ModelProfile,
15 ResultContract, RunId, RunRecord, RunStatus, RunUsage, SessionTarget,
16};
17
18#[derive(Debug, Default)]
19struct BudgetState {
20 total_runs: usize,
21 model_calls: u32,
22 tool_calls: u32,
23 agent_calls: u32,
24 total_tokens: u64,
25 output_bytes: usize,
26}
27
28pub struct BudgetLedger {
30 limits: EffectiveLimits,
31 state: Mutex<BudgetState>,
32}
33
34impl BudgetLedger {
35 pub(crate) fn new(limits: EffectiveLimits) -> Self {
36 Self {
37 limits,
38 state: Mutex::new(BudgetState::default()),
39 }
40 }
41
42 pub(crate) fn limits(&self) -> &EffectiveLimits {
43 &self.limits
44 }
45
46 pub(crate) fn reserve_run(&self) -> Result<(), AgentError> {
47 let mut state = self.lock_state()?;
48 if state.total_runs >= self.limits.runtime.max_total_runs {
49 return Err(AgentError::BudgetExceeded("max_total_runs".to_string()));
50 }
51 state.total_runs += 1;
52 Ok(())
53 }
54
55 pub(crate) fn reserve_model_call(&self) -> Result<(), AgentError> {
56 let mut state = self.lock_state()?;
57 if state.model_calls >= self.limits.runtime.max_model_calls {
58 return Err(AgentError::BudgetExceeded("max_model_calls".to_string()));
59 }
60 state.model_calls += 1;
61 Ok(())
62 }
63
64 pub(crate) fn reserve_tool_call(&self) -> Result<(), AgentError> {
65 let mut state = self.lock_state()?;
66 if state.tool_calls >= self.limits.runtime.max_tool_calls {
67 return Err(AgentError::BudgetExceeded("max_tool_calls".to_string()));
68 }
69 state.tool_calls += 1;
70 Ok(())
71 }
72
73 pub(crate) fn reserve_agent_call(&self) -> Result<(), AgentError> {
74 let mut state = self.lock_state()?;
75 if state.agent_calls >= self.limits.runtime.max_agent_calls {
76 return Err(AgentError::BudgetExceeded("max_agent_calls".to_string()));
77 }
78 state.agent_calls += 1;
79 Ok(())
80 }
81
82 pub(crate) fn add_tokens(&self, tokens: u64) -> Result<(), AgentError> {
83 let mut state = self.lock_state()?;
84 state.total_tokens = state.total_tokens.saturating_add(tokens);
85 if self
86 .limits
87 .runtime
88 .token_budget
89 .is_some_and(|limit| state.total_tokens > limit)
90 {
91 return Err(AgentError::BudgetExceeded("token_budget".to_string()));
92 }
93 Ok(())
94 }
95
96 pub(crate) fn add_output_bytes(&self, bytes: usize) -> Result<(), AgentError> {
97 let mut state = self.lock_state()?;
98 state.output_bytes = state.output_bytes.saturating_add(bytes);
99 if state.output_bytes > self.limits.runtime.max_result_bytes {
100 return Err(AgentError::BudgetExceeded("max_result_bytes".to_string()));
101 }
102 Ok(())
103 }
104
105 fn lock_state(&self) -> Result<std::sync::MutexGuard<'_, BudgetState>, AgentError> {
106 self.state
107 .lock()
108 .map_err(|_| AgentError::Internal("budget ledger lock poisoned".to_string()))
109 }
110}
111
112#[derive(Clone, Default)]
113pub struct InMemoryRunStore {
114 records: Arc<RwLock<HashMap<RunId, RunRecord>>>,
115}
116
117impl InMemoryRunStore {
118 pub async fn create(&self, record: RunRecord) -> Result<(), AgentError> {
119 let mut records = self.records.write().await;
120 if records.contains_key(&record.run_id) {
121 return Err(AgentError::Internal(format!(
122 "run '{}' already exists",
123 record.run_id
124 )));
125 }
126 records.insert(record.run_id.clone(), record);
127 Ok(())
128 }
129
130 pub async fn update_status(&self, run_id: &RunId, status: RunStatus) -> Result<(), AgentError> {
131 let mut records = self.records.write().await;
132 let record = records
133 .get_mut(run_id)
134 .ok_or_else(|| AgentError::Internal(format!("run '{run_id}' not found")))?;
135 record.status = status;
136 Ok(())
137 }
138
139 pub async fn finish(
140 &self,
141 run_id: &RunId,
142 status: RunStatus,
143 usage: RunUsage,
144 failure: Option<super::types::AgentFailure>,
145 ) -> Result<(), AgentError> {
146 let mut records = self.records.write().await;
147 let record = records
148 .get_mut(run_id)
149 .ok_or_else(|| AgentError::Internal(format!("run '{run_id}' not found")))?;
150 record.status = status;
151 record.usage = usage;
152 record.failure = failure;
153 record.finished_at = Some(chrono::Utc::now().to_rfc3339());
154 Ok(())
155 }
156
157 pub async fn get(&self, run_id: &RunId) -> Option<RunRecord> {
158 self.records.read().await.get(run_id).cloned()
159 }
160
161 pub async fn list_root(&self, root_run_id: &RunId) -> Vec<RunRecord> {
162 let mut records = self
163 .records
164 .read()
165 .await
166 .values()
167 .filter(|record| &record.root_run_id == root_run_id)
168 .cloned()
169 .collect::<Vec<_>>();
170 records.sort_by(|left, right| left.started_at.cmp(&right.started_at));
171 records
172 }
173}
174
175#[derive(Clone)]
176pub(crate) struct RunContext {
177 pub run_id: RunId,
178 pub root_run_id: RunId,
179 pub parent_run_id: Option<RunId>,
180 pub agent_call_id: Option<AgentCallId>,
181 pub agent: Arc<AgentDefinition>,
182 pub model: Arc<ModelProfile>,
183 pub session_id: String,
184 pub parent_session_id: Option<String>,
185 pub session: SessionTarget,
186 pub runtime_context: Option<otherone_storage::types::RuntimeContext>,
187 pub depth: usize,
188 pub ancestry: Vec<AgentId>,
189 pub deadline: Instant,
190 pub max_iterations_override: Option<u32>,
191 pub result_contract: Option<ResultContract>,
192 pub cancellation: CancellationToken,
193 pub budget: Arc<BudgetLedger>,
194 pub snapshot: Arc<RuntimeSnapshot>,
195 pub events: EventBus,
196 pub child_count: Arc<AtomicUsize>,
197 pub active_children: Arc<AtomicUsize>,
198 pub usage: Arc<Mutex<RunUsage>>,
199 pub metadata: otherone_storage::types::AttributeBag,
200}
201
202impl RunContext {
203 pub fn reserve_child(&self) -> Result<(), AgentError> {
204 let previous = self.child_count.fetch_add(1, Ordering::SeqCst);
205 if previous >= self.budget.limits().runtime.max_children_per_run {
206 self.child_count.fetch_sub(1, Ordering::SeqCst);
207 return Err(AgentError::BudgetExceeded(
208 "max_children_per_run".to_string(),
209 ));
210 }
211 Ok(())
212 }
213
214 pub fn view(&self) -> super::types::AgentRunContextView {
215 super::types::AgentRunContextView {
216 run_id: self.run_id.clone(),
217 root_run_id: self.root_run_id.clone(),
218 parent_run_id: self.parent_run_id.clone(),
219 agent_id: self.agent.id.clone(),
220 session_id: self.session_id.clone(),
221 depth: self.depth,
222 ancestry: self.ancestry.clone(),
223 runtime_context: self.runtime_context.clone(),
224 }
225 }
226
227 pub fn record_usage(&self, usage: &RunUsage) {
228 if let Ok(mut current) = self.usage.lock() {
229 *current = usage.clone();
230 }
231 }
232
233 pub fn usage_snapshot(&self) -> RunUsage {
234 self.usage
235 .lock()
236 .map(|usage| usage.clone())
237 .unwrap_or_default()
238 }
239}
240
241#[derive(Clone)]
242pub(crate) struct ActiveRun {
243 pub context: RunContext,
244 pub commands: mpsc::Sender<AgentRunCommand>,
245}
246
247pub(crate) struct RunSupervisor {
248 pub run_store: InMemoryRunStore,
249 pub model_semaphore: Arc<Semaphore>,
250 pub tool_semaphore: Arc<Semaphore>,
251 active_runs: RwLock<HashMap<RunId, ActiveRun>>,
252 session_writers: Arc<Mutex<HashSet<String>>>,
253}
254
255impl RunSupervisor {
256 pub fn new(limits: &super::types::RuntimeLimits) -> Self {
257 Self {
258 run_store: InMemoryRunStore::default(),
259 model_semaphore: Arc::new(Semaphore::new(limits.max_concurrent_model_calls)),
260 tool_semaphore: Arc::new(Semaphore::new(limits.max_concurrent_tool_calls)),
261 active_runs: RwLock::new(HashMap::new()),
262 session_writers: global_session_writers(),
263 }
264 }
265
266 pub async fn register(
267 &self,
268 context: RunContext,
269 commands: mpsc::Sender<AgentRunCommand>,
270 ) -> Result<SessionLease, AgentError> {
271 let lease = SessionLease::acquire(
272 Arc::clone(&self.session_writers),
273 session_key(&context),
274 context.session_id.clone(),
275 )?;
276 self.active_runs
277 .write()
278 .await
279 .insert(context.run_id.clone(), ActiveRun { context, commands });
280 Ok(lease)
281 }
282
283 pub async fn unregister(&self, run_id: &RunId) {
284 self.active_runs.write().await.remove(run_id);
285 }
286
287 pub async fn active(&self, run_id: &RunId) -> Option<ActiveRun> {
288 self.active_runs.read().await.get(run_id).cloned()
289 }
290
291 pub async fn cancel(&self, run_id: &RunId) -> Result<(), AgentError> {
292 let active = self.active(run_id).await.ok_or_else(|| {
293 AgentError::InvalidConfiguration(format!("run '{run_id}' is not active"))
294 })?;
295 active.context.cancellation.cancel();
296 Ok(())
297 }
298
299 pub async fn send_message(
300 &self,
301 run_id: &RunId,
302 messages: Vec<String>,
303 ) -> Result<(), AgentError> {
304 let active = self.active(run_id).await.ok_or_else(|| {
305 AgentError::InvalidConfiguration(format!("run '{run_id}' is not active"))
306 })?;
307 active
308 .commands
309 .send(AgentRunCommand::EnqueueUserMessages(messages))
310 .await
311 .map_err(|_| AgentError::InvalidConfiguration(format!("run '{run_id}' is closed")))
312 }
313}
314
315fn session_key(context: &RunContext) -> String {
316 let partition = context
317 .runtime_context
318 .as_ref()
319 .map(|context| context.partition_key.as_str())
320 .unwrap_or("local");
321 match context.session.storage_type {
322 crate::types::StorageType::LocalFile => format!(
323 "local:{}:{partition}:{}",
324 otherone_storage::localfile::get_storage_path().display(),
325 context.session_id
326 ),
327 crate::types::StorageType::Database => {
328 let database = context
329 .session
330 .database_config
331 .as_ref()
332 .map(|config| format!("{}:{}/{}", config.host, config.port, config.database))
333 .unwrap_or_else(|| "database".to_string());
334 format!("database:{database}:{partition}:{}", context.session_id)
335 }
336 }
337}
338
339fn global_session_writers() -> Arc<Mutex<HashSet<String>>> {
340 static WRITERS: OnceLock<Arc<Mutex<HashSet<String>>>> = OnceLock::new();
341 Arc::clone(WRITERS.get_or_init(|| Arc::new(Mutex::new(HashSet::new()))))
342}
343
344pub(crate) struct SessionLease {
345 writers: Arc<Mutex<HashSet<String>>>,
346 key: String,
347}
348
349impl SessionLease {
350 fn acquire(
351 writers: Arc<Mutex<HashSet<String>>>,
352 key: String,
353 session_id: String,
354 ) -> Result<Self, AgentError> {
355 {
356 let mut active = writers
357 .lock()
358 .map_err(|_| AgentError::Internal("session lock poisoned".to_string()))?;
359 if !active.insert(key.clone()) {
360 return Err(AgentError::SessionBusy(session_id));
361 }
362 }
363 Ok(Self { writers, key })
364 }
365}
366
367impl Drop for SessionLease {
368 fn drop(&mut self) {
369 if let Ok(mut writers) = self.writers.lock() {
370 writers.remove(&self.key);
371 }
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn budget_enforces_total_runs() {
381 let limits = super::super::types::RuntimeLimits {
382 max_total_runs: 1,
383 ..Default::default()
384 };
385 let ledger = BudgetLedger::new(EffectiveLimits { runtime: limits });
386 assert!(ledger.reserve_run().is_ok());
387 assert!(matches!(
388 ledger.reserve_run(),
389 Err(AgentError::BudgetExceeded(_))
390 ));
391 }
392}