oxios_kernel/kernel_handle/
agent_api.rs1use crate::agent_log_db::{AgentListFilter, AgentStats, QueryResult};
4use crate::budget::{BudgetExceeded, BudgetInfo, BudgetLimit, BudgetManager};
5use crate::event_bus::{EventBus, KernelEvent};
6use crate::memory::{HnswMemoryIndex, SemanticHit};
7use crate::memory::{MemoryEntry, MemoryManager, MemoryType};
8use crate::state_store::StateStore;
9use crate::supervisor::Supervisor;
10use crate::types::{AgentId, AgentInfo};
11use std::sync::Arc;
12
13pub struct AgentApi {
15 pub(crate) supervisor: Arc<dyn Supervisor>,
16 pub(crate) budget_manager: Arc<BudgetManager>,
17 pub(crate) memory_manager: Arc<MemoryManager>,
18 pub(crate) hnsw_index: Option<Arc<HnswMemoryIndex>>,
20 pub(crate) event_bus: Option<EventBus>,
22 pub(crate) state_store: Option<Arc<StateStore>>,
24 #[cfg(feature = "sqlite-memory")]
26 pub(crate) agent_log_db: Option<Arc<crate::agent_log_db::AgentLogDb>>,
27}
28
29impl AgentApi {
30 pub fn new(
32 supervisor: Arc<dyn Supervisor>,
33 budget_manager: Arc<BudgetManager>,
34 memory_manager: Arc<MemoryManager>,
35 event_bus: Option<EventBus>,
36 ) -> Self {
37 Self {
38 supervisor,
39 budget_manager,
40 memory_manager,
41 hnsw_index: None,
42 event_bus,
43 state_store: None,
44 #[cfg(feature = "sqlite-memory")]
45 agent_log_db: None,
46 }
47 }
48
49 pub fn set_state_store(&mut self, store: Arc<StateStore>) {
51 self.state_store = Some(store);
52 }
53
54 #[cfg(feature = "sqlite-memory")]
56 pub fn set_agent_log_db(&mut self, db: Arc<crate::agent_log_db::AgentLogDb>) {
57 self.agent_log_db = Some(db);
58 }
59
60 pub fn set_hnsw_index(&mut self, index: Arc<HnswMemoryIndex>) {
62 self.hnsw_index = Some(index);
63 }
64 pub fn has_hnsw_index(&self) -> bool {
66 self.hnsw_index.is_some()
67 }
68
69 fn publish(&self, event: KernelEvent) {
71 if let Some(ref eb) = self.event_bus {
72 let _ = eb.publish(event);
73 }
74 }
75 pub async fn list(&self) -> anyhow::Result<Vec<AgentInfo>> {
77 self.supervisor
78 .list()
79 .await
80 .map_err(|e| anyhow::anyhow!("supervisor: {e}"))
81 }
82
83 pub async fn kill(&self, agent_id: &str) -> anyhow::Result<()> {
85 let id = uuid::Uuid::parse_str(agent_id)
86 .map_err(|e| anyhow::anyhow!("invalid agent id: {e}"))?;
87 self.supervisor
88 .kill(id)
89 .await
90 .map_err(|e| anyhow::anyhow!("supervisor: {e}"))
91 }
92
93 pub fn check_budget(&self, agent_id: &AgentId) -> BudgetInfo {
95 self.budget_manager.remaining(agent_id)
96 }
97
98 pub fn set_budget(&self, limit: BudgetLimit) {
100 self.budget_manager.set_budget(limit);
101 }
102
103 pub fn remove_budget(&self, agent_id: &AgentId) {
105 self.budget_manager.remove_budget(agent_id);
106 }
107
108 pub fn reserve_budget(&self, agent_id: &AgentId, tokens: u64) -> Result<(), BudgetExceeded> {
110 self.budget_manager.reserve(agent_id, tokens)
111 }
112
113 pub fn reset_budget(&self, agent_id: &AgentId) {
115 self.budget_manager.reset_window(agent_id);
116 }
117
118 pub fn full_budget_info(&self, agent_id: &AgentId) -> Option<crate::budget::FullBudgetInfo> {
120 self.budget_manager.full_info(agent_id)
121 }
122
123 pub fn all_budget_info(&self) -> Vec<crate::budget::FullBudgetInfo> {
125 self.budget_manager.all_full_info()
126 }
127
128 pub async fn memory_stats(&self) -> (usize, usize) {
130 (
131 self.memory_manager.vector_index_size(),
132 self.memory_manager.total_entries().await,
133 )
134 }
135
136 pub async fn remember(&self, entry: MemoryEntry) -> anyhow::Result<String> {
138 let id = self.memory_manager.remember(entry.clone()).await?;
139
140 self.publish(KernelEvent::MemoryStored {
142 id: id.clone(),
143 memory_type: entry.memory_type.label().to_string(),
144 source: entry.source.clone(),
145 });
146
147 Ok(id)
148 }
149
150 pub async fn list_all_memories(&self, limit: usize) -> Vec<MemoryEntry> {
152 let mut all = Vec::new();
153 for mt in MemoryType::all() {
154 if let Ok(entries) = self.memory_manager.list(*mt, limit).await {
155 all.extend(entries);
156 }
157 }
158 all.sort_by_key(|e| std::cmp::Reverse(e.created_at));
159 all.into_iter().take(limit).collect()
160 }
161
162 pub async fn get_memory(&self, id: &str) -> Option<MemoryEntry> {
164 self.memory_manager.get_by_id(id).await.ok().flatten()
165 }
166
167 pub async fn set_memory_pinned(&self, id: &str, pinned: bool) -> bool {
169 if self
170 .memory_manager
171 .get_by_id(id)
172 .await
173 .ok()
174 .flatten()
175 .is_none()
176 {
177 return false;
178 }
179 let res = if pinned {
180 self.memory_manager.pin(id).await
181 } else {
182 self.memory_manager.unpin(id).await
183 };
184 res.is_ok()
185 }
186
187 pub async fn forget_memory(&self, id: &str) -> bool {
189 let memory_type = match self.memory_manager.get_by_id(id).await {
191 Ok(Some(entry)) => entry.memory_type,
192 _ => return false,
193 };
194 self.memory_manager
195 .forget(id, memory_type)
196 .await
197 .unwrap_or(false)
198 }
199
200 pub async fn search_memory(
202 &self,
203 query: &str,
204 memory_type: Option<MemoryType>,
205 limit: usize,
206 ) -> anyhow::Result<Vec<MemoryEntry>> {
207 self.memory_manager.search(query, memory_type, limit).await
208 }
209
210 pub async fn semantic_search_memory(
214 &self,
215 query: &str,
216 memory_type: Option<MemoryType>,
217 limit: usize,
218 ) -> anyhow::Result<Vec<SemanticHit>> {
219 if let Some(ref hnsw) = self.hnsw_index {
220 self.memory_manager
221 .semantic_search(query, memory_type, limit, hnsw)
222 .await
223 } else {
224 let entries = self.search_memory(query, memory_type, limit).await?;
226 Ok(entries
227 .into_iter()
228 .map(|entry| SemanticHit {
229 entry,
230 distance: 0.0,
231 similarity: 0.0,
232 })
233 .collect())
234 }
235 }
236
237 pub fn memory_manager(&self) -> &Arc<MemoryManager> {
239 &self.memory_manager
240 }
241
242 pub async fn query(&self, filter: &AgentListFilter) -> anyhow::Result<QueryResult> {
249 let running = self.supervisor.list().await.unwrap_or_default();
251
252 #[cfg(feature = "sqlite-memory")]
254 if let Some(ref db) = self.agent_log_db {
255 let mut result = db.query(filter).map_err(|e| anyhow::anyhow!("{e}"))?;
256
257 if filter.page == 1 {
260 let existing_ids: std::collections::HashSet<_> =
263 result.items.iter().map(|a| a.id).collect();
264 let mut prepended = 0u64;
265 for agent in &running {
266 if existing_ids.contains(&agent.id) {
267 continue;
268 }
269 if filter_matches(agent, filter) {
270 result.items.insert(0, agent.clone());
271 prepended += 1;
272 }
273 }
274 result.total = result.total.saturating_add(prepended);
275 result.total_pages = if result.total == 0 {
277 1
278 } else {
279 ((result.total as f64) / filter.per_page.max(1) as f64).ceil() as u32
280 };
281 }
282
283 return Ok(result);
284 }
285
286 #[allow(unused_mut)]
288 let mut persisted: Vec<AgentInfo> = Vec::new();
289 if let Some(ref store) = self.state_store {
290 let names = store.list_category("agents").await.unwrap_or_default();
291 for name in &names {
292 if let Ok(Some(agent)) = store.load_json::<AgentInfo>("agents", name).await {
293 persisted.push(agent);
294 }
295 }
296 }
297
298 let running_ids: std::collections::HashSet<_> = running.iter().map(|a| a.id).collect();
300 persisted.retain(|a| !running_ids.contains(&a.id));
301
302 let mut all = running;
303 all.extend(persisted);
304
305 let filtered = fallback_filter(all, filter);
307 let total = filtered.len() as u64;
308 let offset = ((filter.page.max(1) - 1) * filter.per_page) as usize;
309 let limit = filter.per_page.min(200) as usize;
310 let items: Vec<AgentInfo> = filtered.into_iter().skip(offset).take(limit).collect();
311 let total_pages = if total == 0 {
312 1
313 } else {
314 ((total as f64) / filter.per_page as f64).ceil() as u32
315 };
316
317 Ok(QueryResult {
318 items,
319 total,
320 page: filter.page,
321 per_page: filter.per_page,
322 total_pages,
323 stats: crate::agent_log_db::FilteredStats::default(),
324 })
325 }
326
327 pub async fn get(&self, id: &str) -> anyhow::Result<Option<AgentInfo>> {
329 #[cfg(feature = "sqlite-memory")]
331 if let Some(ref db) = self.agent_log_db
332 && let Ok(Some(agent)) = db.get(id)
333 {
334 return Ok(Some(agent));
335 }
336
337 if let Some(ref store) = self.state_store
339 && let Ok(Some(agent)) = store.load_json::<AgentInfo>("agents", id).await
340 {
341 return Ok(Some(agent));
342 }
343
344 if let Ok(agents) = self.supervisor.list().await
346 && let Some(agent) = agents.into_iter().find(|a| a.id.to_string() == id)
347 {
348 return Ok(Some(agent));
349 }
350
351 Ok(None)
352 }
353
354 pub async fn stats(&self) -> anyhow::Result<AgentStats> {
356 #[cfg(feature = "sqlite-memory")]
358 if let Some(ref db) = self.agent_log_db {
359 return db.stats().map_err(|e| anyhow::anyhow!("{e}"));
360 }
361
362 let mut s = AgentStats::default();
364 let running = self.supervisor.list().await.unwrap_or_default();
365 for a in &running {
366 s.total_agents += 1;
367 match a.status {
368 crate::types::AgentStatus::Running | crate::types::AgentStatus::Starting => {
369 s.running += 1
370 }
371 crate::types::AgentStatus::Idle
372 | crate::types::AgentStatus::Stopped
373 | crate::types::AgentStatus::Completed => s.completed += 1,
374 crate::types::AgentStatus::Failed => s.failed += 1,
375 }
376 s.total_tokens += a.tokens_input + a.tokens_output;
377 s.total_cost_usd += a.cost_usd;
378 }
379 Ok(s)
380 }
381
382 #[cfg(feature = "sqlite-memory")]
384 pub async fn reindex(&self) -> anyhow::Result<crate::agent_log_db::RebuildReport> {
385 match (self.agent_log_db.as_ref(), self.state_store.as_ref()) {
386 (Some(db), Some(store)) => db
387 .reindex_all(store)
388 .await
389 .map_err(|e| anyhow::anyhow!("{e}")),
390 _ => anyhow::bail!("Agent log DB not initialized"),
391 }
392 }
393
394 pub async fn rebuild_hnsw_index(&self) -> anyhow::Result<usize> {
396 if let Some(ref hnsw) = self.hnsw_index {
397 self.memory_manager.rebuild_hnsw_index(hnsw).await
398 } else {
399 Err(anyhow::anyhow!("HNSW index not initialized"))
400 }
401 }
402}
403
404fn filter_matches(agent: &AgentInfo, filter: &AgentListFilter) -> bool {
406 if let Some(status) = filter.status {
408 let status_str = agent.status.to_string();
409 if status_str != status.as_sql()
410 && !(status_str == "idle" && status.as_sql() == "completed")
411 && !(status_str == "idle" && status.as_sql() == "running")
412 {
413 return false;
414 }
415 }
416
417 if let Some(from) = filter.date_from
419 && agent.created_at < from
420 {
421 return false;
422 }
423 if let Some(to) = filter.date_to
424 && agent.created_at > to
425 {
426 return false;
427 }
428
429 if let Some(ref sid) = filter.session_id
431 && agent.session_id.as_deref() != Some(sid.as_str())
432 {
433 return false;
434 }
435 if let Some(ref pid) = filter.project_id
436 && agent.project_id.map(|p| p.to_string()).as_deref() != Some(pid.as_str())
437 {
438 return false;
439 }
440 if let Some(ref sid) = filter.seed_id
441 && agent.seed_id.map(|s| s.to_string()).as_deref() != Some(sid.as_str())
442 {
443 return false;
444 }
445
446 if let Some(ref model) = filter.model_id
448 && !agent.model_id.contains(model)
449 {
450 return false;
451 }
452
453 if let Some(ref q) = filter.q {
455 let q_lower = q.to_lowercase();
456 let name_match = agent.name.to_lowercase().contains(&q_lower);
457 let error_match = agent
458 .error
459 .as_deref()
460 .is_some_and(|e| e.to_lowercase().contains(&q_lower));
461 if !name_match && !error_match {
462 return false;
463 }
464 }
465
466 if let Some(has_err) = filter.has_error {
468 let agent_has_err = agent.error.as_deref().is_some_and(|e| !e.is_empty());
469 if has_err != agent_has_err {
470 return false;
471 }
472 }
473
474 if let Some(min) = filter.cost_min
476 && agent.cost_usd < min
477 {
478 return false;
479 }
480 if let Some(max) = filter.cost_max
481 && agent.cost_usd > max
482 {
483 return false;
484 }
485
486 true
487}
488
489fn fallback_filter(mut agents: Vec<AgentInfo>, filter: &AgentListFilter) -> Vec<AgentInfo> {
491 match filter.sort_by {
493 crate::agent_log_db::SortBy::CreatedAt => {
494 agents.sort_by_key(|a| std::cmp::Reverse(a.created_at));
495 }
496 crate::agent_log_db::SortBy::Cost => {
497 agents.sort_by(|a, b| {
498 b.cost_usd
499 .partial_cmp(&a.cost_usd)
500 .unwrap_or(std::cmp::Ordering::Equal)
501 });
502 }
503 crate::agent_log_db::SortBy::Duration => {
504 let dur = |a: &AgentInfo| -> i64 {
505 match (a.started_at, a.completed_at) {
506 (Some(s), Some(e)) => (e - s).num_seconds(),
507 _ => 0,
508 }
509 };
510 agents.sort_by_key(|a| std::cmp::Reverse(dur(a)));
511 }
512 crate::agent_log_db::SortBy::Tokens => {
513 agents.sort_by_key(|a| std::cmp::Reverse(a.tokens_input + a.tokens_output));
514 }
515 crate::agent_log_db::SortBy::Name => {
516 agents.sort_by(|a, b| a.name.cmp(&b.name));
517 }
518 }
519
520 agents
521}