Skip to main content

clawft_kernel/
agency.rs

1//! Agent-first architecture: roles, agency, and agent manifests.
2//!
3//! In WeftOS, agents ARE the OS. Every feature is delivered by an
4//! agent. The kernel boots a root agent which spawns service agents.
5//! Each agent has:
6//!
7//! - A **role** (root, supervisor, service, worker, user)
8//! - An **agency** (ability to spawn child agents)
9//! - A **manifest** (`.agent.toml`) describing its capabilities
10//!
11//! Agency is hierarchical: the root agent has unlimited agency,
12//! supervisors can spawn service/worker agents, services can spawn
13//! workers, and workers have no agency (leaf agents).
14
15use std::collections::HashMap;
16#[cfg(feature = "exochain")]
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21use crate::capability::AgentCapabilities;
22use crate::process::Pid;
23
24/// Agent role in the OS hierarchy.
25#[non_exhaustive]
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum AgentRole {
28    /// Superuser agent (user 1). Unlimited capabilities and agency.
29    Root,
30    /// Manages lifecycle of other agents. Can spawn/stop/restart.
31    Supervisor,
32    /// Provides a capability to other agents (memory, cron, API).
33    Service,
34    /// Performs specific tasks, spawned by others. No agency.
35    Worker,
36    /// Represents a human or external system.
37    User,
38    /// Custom role with a label.
39    Custom(String),
40}
41
42impl std::fmt::Display for AgentRole {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            AgentRole::Root => write!(f, "root"),
46            AgentRole::Supervisor => write!(f, "supervisor"),
47            AgentRole::Service => write!(f, "service"),
48            AgentRole::Worker => write!(f, "worker"),
49            AgentRole::User => write!(f, "user"),
50            AgentRole::Custom(name) => write!(f, "custom({name})"),
51        }
52    }
53}
54
55/// Agency: the ability to spawn child agents.
56///
57/// Hierarchical: root has unlimited agency, supervisors can spawn
58/// service/worker agents, services can spawn workers only, and
59/// workers have no agency.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Agency {
62    /// Maximum number of child agents this agent can spawn.
63    /// None = unlimited.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub max_children: Option<usize>,
66
67    /// Which roles this agent is allowed to spawn.
68    #[serde(default)]
69    pub allowed_roles: Vec<AgentRole>,
70
71    /// Capability ceiling: spawned agents cannot exceed these capabilities.
72    #[serde(default)]
73    pub capability_ceiling: AgentCapabilities,
74
75    /// Currently spawned child PIDs.
76    #[serde(default)]
77    pub children: Vec<Pid>,
78
79    /// Optional chain manager for exochain event logging.
80    #[cfg(feature = "exochain")]
81    #[serde(skip)]
82    pub chain_manager: Option<Arc<crate::chain::ChainManager>>,
83}
84
85impl Agency {
86    /// Root agent: unlimited agency.
87    pub fn root() -> Self {
88        Self {
89            max_children: None,
90            allowed_roles: vec![
91                AgentRole::Supervisor,
92                AgentRole::Service,
93                AgentRole::Worker,
94                AgentRole::User,
95            ],
96            capability_ceiling: AgentCapabilities::root(),
97            children: Vec::new(),
98            #[cfg(feature = "exochain")]
99            chain_manager: None,
100        }
101    }
102
103    /// Supervisor: can spawn services and workers.
104    pub fn supervisor(max_children: usize) -> Self {
105        Self {
106            max_children: Some(max_children),
107            allowed_roles: vec![AgentRole::Service, AgentRole::Worker],
108            capability_ceiling: AgentCapabilities::default(),
109            children: Vec::new(),
110            #[cfg(feature = "exochain")]
111            chain_manager: None,
112        }
113    }
114
115    /// Service: can spawn workers only.
116    pub fn service(max_workers: usize) -> Self {
117        Self {
118            max_children: Some(max_workers),
119            allowed_roles: vec![AgentRole::Worker],
120            capability_ceiling: AgentCapabilities::default(),
121            children: Vec::new(),
122            #[cfg(feature = "exochain")]
123            chain_manager: None,
124        }
125    }
126
127    /// Worker: no agency (leaf agent).
128    pub fn none() -> Self {
129        Self {
130            max_children: Some(0),
131            allowed_roles: Vec::new(),
132            capability_ceiling: AgentCapabilities::default(),
133            children: Vec::new(),
134            #[cfg(feature = "exochain")]
135            chain_manager: None,
136        }
137    }
138
139    /// Attach a chain manager for exochain event logging.
140    #[cfg(feature = "exochain")]
141    pub fn set_chain_manager(&mut self, cm: Arc<crate::chain::ChainManager>) {
142        self.chain_manager = Some(cm);
143    }
144
145    /// Check if this agency allows spawning a child with the given role.
146    pub fn can_spawn(&self, role: &AgentRole) -> bool {
147        if let Some(max) = self.max_children
148            && self.children.len() >= max
149        {
150            return false;
151        }
152        self.allowed_roles.contains(role)
153    }
154
155    /// Record a spawned child.
156    pub fn add_child(&mut self, pid: Pid) {
157        #[cfg(feature = "exochain")]
158        if let Some(ref cm) = self.chain_manager {
159            cm.append(
160                "agency",
161                crate::chain::EVENT_KIND_AGENT_HIERARCHY_ADD,
162                Some(serde_json::json!({
163                    "child_pid": pid,
164                    "current_children": self.children.len(),
165                })),
166            );
167        }
168
169        self.children.push(pid);
170    }
171
172    /// Remove a terminated child.
173    pub fn remove_child(&mut self, pid: Pid) {
174        #[cfg(feature = "exochain")]
175        if let Some(ref cm) = self.chain_manager {
176            cm.append(
177                "agency",
178                crate::chain::EVENT_KIND_AGENT_HIERARCHY_REMOVE,
179                Some(serde_json::json!({
180                    "child_pid": pid,
181                    "current_children": self.children.len(),
182                })),
183            );
184        }
185
186        self.children.retain(|&p| p != pid);
187    }
188
189    /// How many children remain before hitting the limit.
190    pub fn remaining_capacity(&self) -> Option<usize> {
191        self.max_children
192            .map(|max| max.saturating_sub(self.children.len()))
193    }
194}
195
196impl Default for Agency {
197    fn default() -> Self {
198        Self::none()
199    }
200}
201
202/// Agent manifest, parsed from `.agent.toml` files.
203///
204/// Describes what an agent does, what capabilities it needs,
205/// and how to communicate with it. Analogous to systemd unit
206/// files but for AI agents.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct AgentManifest {
209    /// Agent name (unique within the OS).
210    pub name: String,
211
212    /// Semantic version.
213    #[serde(default = "default_version")]
214    pub version: String,
215
216    /// Human-readable description.
217    #[serde(default)]
218    pub description: String,
219
220    /// Agent role.
221    pub role: AgentRole,
222
223    /// Capabilities this agent needs.
224    #[serde(default)]
225    pub capabilities: AgentCapabilities,
226
227    /// Agency configuration (spawn permissions).
228    #[serde(default)]
229    pub agency: Agency,
230
231    /// Tools this agent is allowed to use.
232    #[serde(default)]
233    pub tools: Vec<String>,
234
235    /// IPC topics this agent publishes to.
236    #[serde(default)]
237    pub topics_publish: Vec<String>,
238
239    /// IPC topics this agent subscribes to.
240    #[serde(default)]
241    pub topics_subscribe: Vec<String>,
242
243    /// Resource limits.
244    #[serde(default)]
245    pub resources: AgentResources,
246
247    /// Communication interface.
248    #[serde(default)]
249    pub interface: AgentInterface,
250
251    /// Health check configuration.
252    #[serde(default)]
253    pub health: AgentHealth,
254
255    /// Dependencies on other agents.
256    #[serde(default)]
257    pub dependencies: AgentDependencies,
258
259    /// Filesystem paths this agent can access.
260    #[serde(default)]
261    pub filesystem_access: Vec<String>,
262
263    /// Additional labels/metadata.
264    #[serde(default)]
265    pub labels: HashMap<String, String>,
266}
267
268fn default_version() -> String {
269    "0.1.0".into()
270}
271
272/// Agent resource configuration.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct AgentResources {
275    /// Maximum memory in MB.
276    #[serde(default = "default_max_memory_mb")]
277    pub max_memory_mb: u64,
278
279    /// Maximum concurrent requests this agent handles.
280    #[serde(default = "default_max_concurrent")]
281    pub max_concurrent_requests: u32,
282
283    /// Priority level.
284    #[serde(default)]
285    pub priority: AgentPriority,
286}
287
288fn default_max_memory_mb() -> u64 {
289    256
290}
291
292fn default_max_concurrent() -> u32 {
293    100
294}
295
296impl Default for AgentResources {
297    fn default() -> Self {
298        Self {
299            max_memory_mb: default_max_memory_mb(),
300            max_concurrent_requests: default_max_concurrent(),
301            priority: AgentPriority::default(),
302        }
303    }
304}
305
306/// Agent priority level for scheduling.
307#[non_exhaustive]
308#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
309pub enum AgentPriority {
310    /// Low priority.
311    Low,
312    /// Normal priority (default).
313    #[default]
314    Normal,
315    /// High priority.
316    High,
317    /// Critical -- must not be preempted.
318    Critical,
319}
320
321/// Agent communication interface.
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct AgentInterface {
324    /// Communication protocol.
325    #[serde(default)]
326    pub protocol: InterfaceProtocol,
327
328    /// Request topic (for IPC protocol).
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub request_topic: Option<String>,
331
332    /// Response mode.
333    #[serde(default)]
334    pub response_mode: ResponseMode,
335}
336
337impl Default for AgentInterface {
338    fn default() -> Self {
339        Self {
340            protocol: InterfaceProtocol::Ipc,
341            request_topic: None,
342            response_mode: ResponseMode::Direct,
343        }
344    }
345}
346
347/// Interface protocol.
348#[non_exhaustive]
349#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
350pub enum InterfaceProtocol {
351    /// Inter-process communication via kernel IPC.
352    #[default]
353    Ipc,
354    /// REST/HTTP API.
355    Rest,
356    /// gRPC.
357    Grpc,
358    /// Model Context Protocol.
359    Mcp,
360}
361
362/// Response delivery mode.
363#[non_exhaustive]
364#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
365pub enum ResponseMode {
366    /// Reply directly to the sender.
367    #[default]
368    Direct,
369    /// Broadcast response to all subscribers.
370    Broadcast,
371    /// Publish response to a specific topic.
372    Topic(String),
373}
374
375/// Agent health check configuration.
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct AgentHealth {
378    /// Health check interval.
379    #[serde(default = "default_check_interval")]
380    pub check_interval_secs: u64,
381
382    /// Health check timeout.
383    #[serde(default = "default_timeout")]
384    pub timeout_secs: u64,
385
386    /// Restart policy.
387    #[serde(default)]
388    pub restart_policy: AgentRestartPolicy,
389
390    /// Maximum number of restarts before giving up.
391    #[serde(default = "default_max_restarts")]
392    pub max_restarts: u32,
393}
394
395fn default_check_interval() -> u64 {
396    30
397}
398
399fn default_timeout() -> u64 {
400    5
401}
402
403fn default_max_restarts() -> u32 {
404    5
405}
406
407impl Default for AgentHealth {
408    fn default() -> Self {
409        Self {
410            check_interval_secs: default_check_interval(),
411            timeout_secs: default_timeout(),
412            restart_policy: AgentRestartPolicy::default(),
413            max_restarts: default_max_restarts(),
414        }
415    }
416}
417
418/// Agent restart policy.
419#[non_exhaustive]
420#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
421pub enum AgentRestartPolicy {
422    /// Never restart.
423    Never,
424    /// Restart on failure only.
425    #[default]
426    OnFailure,
427    /// Always restart.
428    Always,
429}
430
431/// Agent dependencies.
432#[derive(Debug, Clone, Default, Serialize, Deserialize)]
433pub struct AgentDependencies {
434    /// Agents that must be running before this one starts.
435    #[serde(default)]
436    pub requires: Vec<String>,
437
438    /// Agents that should start before this one (soft ordering).
439    #[serde(default)]
440    pub after: Vec<String>,
441}
442
443impl AgentCapabilities {
444    /// Root capabilities: everything allowed.
445    pub fn root() -> Self {
446        Self {
447            can_spawn: true,
448            can_ipc: true,
449            can_exec_tools: true,
450            can_network: true,
451            ipc_scope: crate::capability::IpcScope::All,
452            resource_limits: crate::capability::ResourceLimits {
453                max_memory_bytes: u64::MAX,
454                max_cpu_time_ms: u64::MAX,
455                max_tool_calls: u64::MAX,
456                max_messages: u64::MAX,
457                #[cfg(feature = "os-patterns")]
458                max_disk_bytes: u64::MAX,
459            },
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn agent_role_display() {
470        assert_eq!(AgentRole::Root.to_string(), "root");
471        assert_eq!(AgentRole::Service.to_string(), "service");
472        assert_eq!(AgentRole::Worker.to_string(), "worker");
473        assert_eq!(
474            AgentRole::Custom("analytics".into()).to_string(),
475            "custom(analytics)"
476        );
477    }
478
479    #[test]
480    fn root_agency_unlimited() {
481        let agency = Agency::root();
482        assert!(agency.max_children.is_none());
483        assert!(agency.can_spawn(&AgentRole::Service));
484        assert!(agency.can_spawn(&AgentRole::Worker));
485        assert!(agency.can_spawn(&AgentRole::User));
486        assert!(!agency.can_spawn(&AgentRole::Root)); // cannot spawn root
487    }
488
489    #[test]
490    fn supervisor_agency() {
491        let agency = Agency::supervisor(10);
492        assert_eq!(agency.max_children, Some(10));
493        assert!(agency.can_spawn(&AgentRole::Service));
494        assert!(agency.can_spawn(&AgentRole::Worker));
495        assert!(!agency.can_spawn(&AgentRole::Root));
496        assert!(!agency.can_spawn(&AgentRole::User));
497    }
498
499    #[test]
500    fn service_agency() {
501        let agency = Agency::service(5);
502        assert!(agency.can_spawn(&AgentRole::Worker));
503        assert!(!agency.can_spawn(&AgentRole::Service));
504    }
505
506    #[test]
507    fn worker_has_no_agency() {
508        let agency = Agency::none();
509        assert!(!agency.can_spawn(&AgentRole::Worker));
510        assert_eq!(agency.remaining_capacity(), Some(0));
511    }
512
513    #[test]
514    fn agency_child_tracking() {
515        let mut agency = Agency::service(2);
516        agency.add_child(100);
517        agency.add_child(101);
518        assert!(!agency.can_spawn(&AgentRole::Worker)); // at capacity
519        assert_eq!(agency.remaining_capacity(), Some(0));
520
521        agency.remove_child(100);
522        assert!(agency.can_spawn(&AgentRole::Worker));
523        assert_eq!(agency.remaining_capacity(), Some(1));
524    }
525
526    #[test]
527    fn agency_serde_roundtrip() {
528        let agency = Agency::supervisor(8);
529        let json = serde_json::to_string(&agency).unwrap();
530        let restored: Agency = serde_json::from_str(&json).unwrap();
531        assert_eq!(restored.max_children, Some(8));
532        assert!(restored.allowed_roles.contains(&AgentRole::Service));
533    }
534
535    #[test]
536    fn root_capabilities() {
537        let caps = AgentCapabilities::root();
538        assert!(caps.can_spawn);
539        assert!(caps.can_ipc);
540        assert!(caps.can_exec_tools);
541        assert!(caps.can_network);
542        assert_eq!(caps.resource_limits.max_memory_bytes, u64::MAX);
543    }
544
545    #[test]
546    fn agent_manifest_serde_roundtrip() {
547        let manifest = AgentManifest {
548            name: "memory-service".into(),
549            version: "0.1.0".into(),
550            description: "Persistent memory storage".into(),
551            role: AgentRole::Service,
552            capabilities: AgentCapabilities::default(),
553            agency: Agency::service(10),
554            tools: vec!["memory_store".into(), "memory_retrieve".into()],
555            topics_publish: vec!["memory.stored".into()],
556            topics_subscribe: vec!["memory.request".into()],
557            resources: AgentResources::default(),
558            interface: AgentInterface {
559                protocol: InterfaceProtocol::Ipc,
560                request_topic: Some("memory.request".into()),
561                response_mode: ResponseMode::Direct,
562            },
563            health: AgentHealth::default(),
564            dependencies: AgentDependencies {
565                requires: vec![],
566                after: vec!["kernel-init".into()],
567            },
568            filesystem_access: vec!["data/memory/".into()],
569            labels: HashMap::new(),
570        };
571        let json = serde_json::to_string_pretty(&manifest).unwrap();
572        let restored: AgentManifest = serde_json::from_str(&json).unwrap();
573        assert_eq!(restored.name, "memory-service");
574        assert_eq!(restored.role, AgentRole::Service);
575        assert_eq!(restored.tools.len(), 2);
576    }
577
578    #[test]
579    fn minimal_manifest_serde() {
580        let json = r#"{"name":"worker-1","role":"Worker"}"#;
581        let manifest: AgentManifest = serde_json::from_str(json).unwrap();
582        assert_eq!(manifest.name, "worker-1");
583        assert_eq!(manifest.role, AgentRole::Worker);
584        assert!(manifest.tools.is_empty());
585        assert_eq!(manifest.version, "0.1.0"); // default
586    }
587
588    #[test]
589    fn agent_priority_ordering() {
590        assert!(AgentPriority::Low < AgentPriority::Normal);
591        assert!(AgentPriority::Normal < AgentPriority::High);
592        assert!(AgentPriority::High < AgentPriority::Critical);
593    }
594
595    #[test]
596    fn agent_health_defaults() {
597        let health = AgentHealth::default();
598        assert_eq!(health.check_interval_secs, 30);
599        assert_eq!(health.timeout_secs, 5);
600        assert_eq!(health.max_restarts, 5);
601        assert_eq!(health.restart_policy, AgentRestartPolicy::OnFailure);
602    }
603
604    #[test]
605    fn interface_protocol_default() {
606        assert_eq!(InterfaceProtocol::default(), InterfaceProtocol::Ipc);
607    }
608
609    #[test]
610    fn response_mode_serde() {
611        let modes = vec![
612            ResponseMode::Direct,
613            ResponseMode::Broadcast,
614            ResponseMode::Topic("events".into()),
615        ];
616        for mode in &modes {
617            let json = serde_json::to_string(mode).unwrap();
618            let _restored: ResponseMode = serde_json::from_str(&json).unwrap();
619        }
620    }
621
622    #[test]
623    fn agent_dependencies_serde() {
624        let deps = AgentDependencies {
625            requires: vec!["message-bus".into()],
626            after: vec!["kernel-init".into(), "message-bus".into()],
627        };
628        let json = serde_json::to_string(&deps).unwrap();
629        let restored: AgentDependencies = serde_json::from_str(&json).unwrap();
630        assert_eq!(restored.requires, vec!["message-bus"]);
631        assert_eq!(restored.after.len(), 2);
632    }
633
634    #[test]
635    fn agent_resources_defaults() {
636        let res = AgentResources::default();
637        assert_eq!(res.max_memory_mb, 256);
638        assert_eq!(res.max_concurrent_requests, 100);
639        assert_eq!(res.priority, AgentPriority::Normal);
640    }
641}