Skip to main content

beyonder_core/
agent.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use ulid::Ulid;
4
5use crate::capability::CapabilitySet;
6
7/// Unique agent identifier.
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct AgentId(pub String);
10
11impl AgentId {
12    pub fn new() -> Self {
13        Self(Ulid::new().to_string())
14    }
15
16    pub fn named(name: &str) -> Self {
17        Self(format!("{}-{}", name, Ulid::new()))
18    }
19}
20
21impl Default for AgentId {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl std::fmt::Display for AgentId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33/// Metadata and state for an agent process.
34/// Agents are first-class citizens in Beyonder — they have lifecycle,
35/// resource limits, and capability sets, analogous to OS processes.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AgentInfo {
38    pub id: AgentId,
39    pub name: String,
40    pub kind: AgentKind,
41    pub state: AgentState,
42    pub capabilities: CapabilitySet,
43    pub resource_limits: ResourceLimits,
44    pub metrics: AgentMetrics,
45    pub spawned_at: DateTime<Utc>,
46}
47
48impl AgentInfo {
49    pub fn new(name: impl Into<String>, kind: AgentKind) -> Self {
50        Self {
51            id: AgentId::new(),
52            name: name.into(),
53            kind,
54            state: AgentState::Spawning,
55            capabilities: CapabilitySet::default(),
56            resource_limits: ResourceLimits::default(),
57            metrics: AgentMetrics::default(),
58            spawned_at: Utc::now(),
59        }
60    }
61}
62
63/// How the agent runs.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub enum AgentKind {
66    /// An ACP-compatible process (Claude Code, Gemini CLI, etc.)
67    /// Communicates via JSON-RPC over stdio.
68    AcpProcess { binary: String, args: Vec<String> },
69    /// LLM backend driven by Ollama — same API for local (localhost:11434)
70    /// and cloud (ollama.com Turbo/Pro). Only differs in base_url + auth.
71    Ollama {
72        base_url: String,
73        model: String,
74        /// Env var that holds the bearer token (e.g. "OLLAMA_API_KEY"). None for local.
75        api_key_env: Option<String>,
76    },
77    /// llama.cpp llama-server with OpenAI-compatible /v1/chat/completions endpoint.
78    /// Requires the server to be started with `--jinja` for reliable tool calling.
79    LlamaCpp {
80        base_url: String,
81        model: String,
82        /// Optional env var for auth (e.g. when fronted by a reverse proxy).
83        api_key_env: Option<String>,
84    },
85    /// Apple MLX mlx_lm.server with OpenAI-compatible /v1/chat/completions endpoint.
86    /// Requires mlx-lm >= 0.19 for tool calling support.
87    Mlx {
88        base_url: String,
89        model: String,
90        api_key_env: Option<String>,
91    },
92    /// A WASM component plugin (post-MVP).
93    WasmPlugin { module_path: String },
94    /// Built-in agent (system-level operations).
95    BuiltIn,
96}
97
98/// Agent lifecycle state — mirrors OS process states.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub enum AgentState {
101    Spawning,
102    Ready,
103    Busy { current_action: String },
104    Suspended,
105    Dead { reason: DeathReason },
106}
107
108impl AgentState {
109    pub fn is_alive(&self) -> bool {
110        !matches!(self, AgentState::Dead { .. })
111    }
112
113    pub fn is_available(&self) -> bool {
114        matches!(self, AgentState::Ready)
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub enum DeathReason {
120    Completed,
121    Killed,
122    Crashed { exit_code: Option<i32> },
123    ResourceLimitExceeded,
124    Timeout,
125}
126
127/// Resource limits enforced by the agent supervisor.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ResourceLimits {
130    pub max_memory_bytes: Option<u64>,
131    pub max_cpu_time_secs: Option<u64>,
132    pub max_file_writes: Option<u32>,
133    pub max_network_calls: Option<u32>,
134    pub max_tokens: Option<u64>,
135    pub timeout_secs: Option<u64>,
136}
137
138impl Default for ResourceLimits {
139    fn default() -> Self {
140        Self {
141            max_memory_bytes: Some(512 * 1024 * 1024), // 512 MB
142            max_cpu_time_secs: Some(300),              // 5 minutes
143            max_file_writes: None,
144            max_network_calls: None,
145            max_tokens: Some(100_000),
146            timeout_secs: Some(600), // 10 minutes
147        }
148    }
149}
150
151/// Runtime metrics tracked per agent.
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct AgentMetrics {
154    pub tokens_used: u64,
155    pub actions_taken: u32,
156    pub file_writes: u32,
157    pub network_calls: u32,
158    pub approvals_requested: u32,
159    pub approvals_granted: u32,
160    pub approvals_denied: u32,
161}