1#[cfg(feature = "exochain")]
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13
14use crate::capability::AgentCapabilities;
15use crate::error::KernelError;
16
17#[cfg(feature = "native")]
20pub use tokio_util::sync::CancellationToken;
21
22#[cfg(not(feature = "native"))]
23mod cancel_shim {
24 #[derive(Debug, Clone)]
31 pub struct CancellationToken {
32 _priv: (),
33 }
34
35 impl CancellationToken {
36 pub fn new() -> Self {
37 Self { _priv: () }
38 }
39
40 pub fn cancel(&self) {}
42 }
43
44 impl Default for CancellationToken {
45 fn default() -> Self {
46 Self::new()
47 }
48 }
49}
50
51#[cfg(not(feature = "native"))]
52pub use cancel_shim::CancellationToken;
53
54pub type Pid = u64;
56
57#[non_exhaustive]
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum ProcessState {
61 Starting,
63 Running,
65 Suspended,
67 Stopping,
69 Exited(i32),
71}
72
73impl ProcessState {
74 pub fn can_transition_to(&self, next: &ProcessState) -> bool {
83 matches!(
84 (self, next),
85 (ProcessState::Starting, ProcessState::Running)
86 | (ProcessState::Starting, ProcessState::Exited(_))
87 | (ProcessState::Running, ProcessState::Suspended)
88 | (ProcessState::Running, ProcessState::Stopping)
89 | (ProcessState::Running, ProcessState::Exited(_))
90 | (ProcessState::Suspended, ProcessState::Running)
91 | (ProcessState::Suspended, ProcessState::Stopping)
92 | (ProcessState::Suspended, ProcessState::Exited(_))
93 | (ProcessState::Stopping, ProcessState::Exited(_))
94 )
95 }
96}
97
98impl std::fmt::Display for ProcessState {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 ProcessState::Starting => write!(f, "starting"),
102 ProcessState::Running => write!(f, "running"),
103 ProcessState::Suspended => write!(f, "suspended"),
104 ProcessState::Stopping => write!(f, "stopping"),
105 ProcessState::Exited(code) => write!(f, "exited({code})"),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct ResourceUsage {
113 pub memory_bytes: u64,
115 pub cpu_time_ms: u64,
117 pub tool_calls: u64,
119 pub messages_sent: u64,
121}
122
123#[derive(Debug, Clone)]
125pub struct ProcessEntry {
126 pub pid: Pid,
128 pub agent_id: String,
130 pub state: ProcessState,
132 pub capabilities: AgentCapabilities,
134 pub resource_usage: ResourceUsage,
136 pub cancel_token: CancellationToken,
138 pub parent_pid: Option<Pid>,
140}
141
142pub struct ProcessTable {
148 next_pid: AtomicU64,
149 entries: DashMap<Pid, ProcessEntry>,
150 max_processes: u32,
151 #[cfg(feature = "exochain")]
152 chain_manager: Option<Arc<crate::chain::ChainManager>>,
153}
154
155impl ProcessTable {
156 pub fn new(max_processes: u32) -> Self {
158 Self {
159 next_pid: AtomicU64::new(1), entries: DashMap::new(),
161 max_processes,
162 #[cfg(feature = "exochain")]
163 chain_manager: None,
164 }
165 }
166
167 #[cfg(feature = "exochain")]
169 pub fn set_chain_manager(&mut self, cm: Arc<crate::chain::ChainManager>) {
170 self.chain_manager = Some(cm);
171 }
172
173 pub fn allocate_pid(&self) -> Pid {
175 self.next_pid.fetch_add(1, Ordering::Relaxed)
176 }
177
178 pub fn insert(&self, mut entry: ProcessEntry) -> Result<Pid, KernelError> {
184 if self.entries.len() >= self.max_processes as usize {
185 return Err(KernelError::ProcessTableFull {
186 max: self.max_processes,
187 });
188 }
189 let pid = self.allocate_pid();
190 entry.pid = pid;
191
192 #[cfg(feature = "exochain")]
193 if let Some(ref cm) = self.chain_manager {
194 cm.append(
195 "process",
196 crate::chain::EVENT_KIND_PROCESS_REGISTER,
197 Some(serde_json::json!({
198 "pid": pid,
199 "agent_id": &entry.agent_id,
200 "parent_pid": entry.parent_pid,
201 })),
202 );
203 }
204
205 self.entries.insert(pid, entry);
206 Ok(pid)
207 }
208
209 pub fn insert_with_pid(&self, entry: ProcessEntry) -> Result<(), KernelError> {
211 if self.entries.len() >= self.max_processes as usize {
212 return Err(KernelError::ProcessTableFull {
213 max: self.max_processes,
214 });
215 }
216 self.entries.insert(entry.pid, entry);
217 Ok(())
218 }
219
220 pub fn get(&self, pid: Pid) -> Option<ProcessEntry> {
222 self.entries.get(&pid).map(|e| e.value().clone())
223 }
224
225 pub fn remove(&self, pid: Pid) -> Option<ProcessEntry> {
227 let removed = self.entries.remove(&pid).map(|(_, e)| e);
228
229 #[cfg(feature = "exochain")]
230 if let Some(ref entry) = removed
231 && let Some(ref cm) = self.chain_manager
232 {
233 cm.append(
234 "process",
235 crate::chain::EVENT_KIND_PROCESS_DEREGISTER,
236 Some(serde_json::json!({
237 "pid": pid,
238 "agent_id": &entry.agent_id,
239 })),
240 );
241 }
242
243 removed
244 }
245
246 pub fn list(&self) -> Vec<ProcessEntry> {
248 self.entries.iter().map(|e| e.value().clone()).collect()
249 }
250
251 pub fn update_state(&self, pid: Pid, new_state: ProcessState) -> Result<(), KernelError> {
255 let mut entry = self
256 .entries
257 .get_mut(&pid)
258 .ok_or(KernelError::ProcessNotFound { pid })?;
259
260 if !entry.state.can_transition_to(&new_state) {
261 return Err(KernelError::InvalidStateTransition {
262 pid,
263 from: entry.state.clone(),
264 to: new_state,
265 });
266 }
267
268 #[cfg(feature = "exochain")]
269 if let Some(ref cm) = self.chain_manager {
270 cm.append(
271 "process",
272 crate::chain::EVENT_KIND_PROCESS_STATE,
273 Some(serde_json::json!({
274 "pid": pid,
275 "from": entry.state.to_string(),
276 "to": new_state.to_string(),
277 })),
278 );
279 }
280
281 entry.state = new_state;
282 Ok(())
283 }
284
285 pub fn update_resources(&self, pid: Pid, usage: ResourceUsage) -> Result<(), KernelError> {
287 let mut entry = self
288 .entries
289 .get_mut(&pid)
290 .ok_or(KernelError::ProcessNotFound { pid })?;
291 entry.resource_usage = usage;
292 Ok(())
293 }
294
295 pub fn len(&self) -> usize {
297 self.entries.len()
298 }
299
300 pub fn is_empty(&self) -> bool {
302 self.entries.is_empty()
303 }
304
305 pub fn max_processes(&self) -> u32 {
307 self.max_processes
308 }
309
310 pub fn set_capabilities(
316 &self,
317 pid: Pid,
318 capabilities: AgentCapabilities,
319 ) -> Result<(), KernelError> {
320 let mut entry = self
321 .entries
322 .get_mut(&pid)
323 .ok_or(KernelError::ProcessNotFound { pid })?;
324 entry.capabilities = capabilities;
325 Ok(())
326 }
327
328 pub fn count_by_state(&self, state: &ProcessState) -> usize {
330 self.entries.iter().filter(|e| &e.state == state).count()
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 fn make_entry(agent_id: &str) -> ProcessEntry {
339 ProcessEntry {
340 pid: 0, agent_id: agent_id.to_owned(),
342 state: ProcessState::Starting,
343 capabilities: AgentCapabilities::default(),
344 resource_usage: ResourceUsage::default(),
345 cancel_token: CancellationToken::new(),
346 parent_pid: None,
347 }
348 }
349
350 #[test]
351 fn insert_and_get() {
352 let table = ProcessTable::new(64);
353 let pid = table.insert(make_entry("agent-1")).unwrap();
354 assert_eq!(pid, 1);
355
356 let entry = table.get(pid).unwrap();
357 assert_eq!(entry.agent_id, "agent-1");
358 assert_eq!(entry.pid, 1);
359 }
360
361 #[test]
362 fn insert_multiple() {
363 let table = ProcessTable::new(64);
364 let p1 = table.insert(make_entry("a1")).unwrap();
365 let p2 = table.insert(make_entry("a2")).unwrap();
366 let p3 = table.insert(make_entry("a3")).unwrap();
367
368 assert_eq!(p1, 1);
369 assert_eq!(p2, 2);
370 assert_eq!(p3, 3);
371 assert_eq!(table.len(), 3);
372 }
373
374 #[test]
375 fn remove() {
376 let table = ProcessTable::new(64);
377 let pid = table.insert(make_entry("agent-1")).unwrap();
378 let removed = table.remove(pid);
379 assert!(removed.is_some());
380 assert!(table.get(pid).is_none());
381 assert!(table.is_empty());
382 }
383
384 #[test]
385 fn remove_nonexistent() {
386 let table = ProcessTable::new(64);
387 assert!(table.remove(999).is_none());
388 }
389
390 #[test]
391 fn list() {
392 let table = ProcessTable::new(64);
393 table.insert(make_entry("a1")).unwrap();
394 table.insert(make_entry("a2")).unwrap();
395
396 let entries = table.list();
397 assert_eq!(entries.len(), 2);
398 }
399
400 #[test]
401 fn update_state_valid() {
402 let table = ProcessTable::new(64);
403 let pid = table.insert(make_entry("agent-1")).unwrap();
404
405 table.update_state(pid, ProcessState::Running).unwrap();
407 assert_eq!(table.get(pid).unwrap().state, ProcessState::Running);
408
409 table.update_state(pid, ProcessState::Stopping).unwrap();
411 assert_eq!(table.get(pid).unwrap().state, ProcessState::Stopping);
412
413 table.update_state(pid, ProcessState::Exited(0)).unwrap();
415 assert_eq!(table.get(pid).unwrap().state, ProcessState::Exited(0));
416 }
417
418 #[test]
419 fn update_state_invalid_transition() {
420 let table = ProcessTable::new(64);
421 let pid = table.insert(make_entry("agent-1")).unwrap();
422
423 let result = table.update_state(pid, ProcessState::Suspended);
425 assert!(result.is_err());
426 }
427
428 #[test]
429 fn update_state_nonexistent_pid() {
430 let table = ProcessTable::new(64);
431 let result = table.update_state(999, ProcessState::Running);
432 assert!(result.is_err());
433 }
434
435 #[test]
436 fn process_table_full() {
437 let table = ProcessTable::new(2);
438 table.insert(make_entry("a1")).unwrap();
439 table.insert(make_entry("a2")).unwrap();
440 let result = table.insert(make_entry("a3"));
441 assert!(result.is_err());
442 }
443
444 #[test]
445 fn update_resources() {
446 let table = ProcessTable::new(64);
447 let pid = table.insert(make_entry("agent-1")).unwrap();
448
449 let usage = ResourceUsage {
450 memory_bytes: 1024,
451 cpu_time_ms: 500,
452 tool_calls: 10,
453 messages_sent: 5,
454 };
455 table.update_resources(pid, usage).unwrap();
456
457 let entry = table.get(pid).unwrap();
458 assert_eq!(entry.resource_usage.memory_bytes, 1024);
459 assert_eq!(entry.resource_usage.cpu_time_ms, 500);
460 }
461
462 #[test]
463 fn state_display() {
464 assert_eq!(ProcessState::Starting.to_string(), "starting");
465 assert_eq!(ProcessState::Running.to_string(), "running");
466 assert_eq!(ProcessState::Suspended.to_string(), "suspended");
467 assert_eq!(ProcessState::Stopping.to_string(), "stopping");
468 assert_eq!(ProcessState::Exited(0).to_string(), "exited(0)");
469 assert_eq!(ProcessState::Exited(-1).to_string(), "exited(-1)");
470 }
471
472 #[test]
473 fn state_transitions() {
474 assert!(ProcessState::Starting.can_transition_to(&ProcessState::Running));
476 assert!(ProcessState::Starting.can_transition_to(&ProcessState::Exited(1)));
477 assert!(ProcessState::Running.can_transition_to(&ProcessState::Suspended));
478 assert!(ProcessState::Running.can_transition_to(&ProcessState::Stopping));
479 assert!(ProcessState::Running.can_transition_to(&ProcessState::Exited(0)));
480 assert!(ProcessState::Suspended.can_transition_to(&ProcessState::Running));
481 assert!(ProcessState::Suspended.can_transition_to(&ProcessState::Stopping));
482 assert!(ProcessState::Stopping.can_transition_to(&ProcessState::Exited(0)));
483
484 assert!(!ProcessState::Starting.can_transition_to(&ProcessState::Suspended));
486 assert!(!ProcessState::Starting.can_transition_to(&ProcessState::Stopping));
487 assert!(!ProcessState::Stopping.can_transition_to(&ProcessState::Running));
488 assert!(!ProcessState::Exited(0).can_transition_to(&ProcessState::Running));
489 assert!(!ProcessState::Exited(0).can_transition_to(&ProcessState::Starting));
490 }
491
492 #[test]
493 fn insert_with_pid_kernel() {
494 let table = ProcessTable::new(64);
495 let entry = ProcessEntry {
496 pid: 0,
497 agent_id: "kernel".to_owned(),
498 state: ProcessState::Running,
499 capabilities: AgentCapabilities::default(),
500 resource_usage: ResourceUsage::default(),
501 cancel_token: CancellationToken::new(),
502 parent_pid: None,
503 };
504 table.insert_with_pid(entry).unwrap();
505 assert_eq!(table.get(0).unwrap().agent_id, "kernel");
506 }
507
508 #[test]
509 fn set_capabilities() {
510 let table = ProcessTable::new(64);
511 let pid = table.insert(make_entry("agent-1")).unwrap();
512
513 let new_caps = AgentCapabilities {
514 can_spawn: false,
515 can_network: true,
516 ..Default::default()
517 };
518 table.set_capabilities(pid, new_caps).unwrap();
519
520 let entry = table.get(pid).unwrap();
521 assert!(!entry.capabilities.can_spawn);
522 assert!(entry.capabilities.can_network);
523 }
524
525 #[test]
526 fn set_capabilities_nonexistent_pid() {
527 let table = ProcessTable::new(64);
528 let result = table.set_capabilities(999, AgentCapabilities::default());
529 assert!(result.is_err());
530 }
531
532 #[test]
533 fn count_by_state() {
534 let table = ProcessTable::new(64);
535 let p1 = table.insert(make_entry("a1")).unwrap();
536 let p2 = table.insert(make_entry("a2")).unwrap();
537 table.insert(make_entry("a3")).unwrap();
538
539 assert_eq!(table.count_by_state(&ProcessState::Starting), 3);
541 assert_eq!(table.count_by_state(&ProcessState::Running), 0);
542
543 table.update_state(p1, ProcessState::Running).unwrap();
544 table.update_state(p2, ProcessState::Running).unwrap();
545
546 assert_eq!(table.count_by_state(&ProcessState::Starting), 1);
547 assert_eq!(table.count_by_state(&ProcessState::Running), 2);
548 }
549}