use serde::{Deserialize, Deserializer, Serialize};
fn null_default<'de, D, T>(d: D) -> std::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
Ok(Option::<T>::deserialize(d)?.unwrap_or_default())
}
pub const BOSS_MAN: &str = "boss-man";
pub struct HostRef;
impl HostRef {
pub const LOCAL: &'static str = "local-default";
pub const AUTO: &'static str = "auto";
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContainerProfile {
pub image: String,
pub cpus: f64,
pub memory_mb: u32,
pub network: bool,
}
impl ContainerProfile {
pub fn new(image: impl Into<String>) -> Self {
Self {
image: image.into(),
cpus: 1.0,
memory_mb: 1024,
network: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkillVerdict {
CollectingEvidence,
Healthy,
Underperforming,
RollbackRecommended,
ReadyToPromote,
DiscardRecommended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KernelPack {
Files,
Search,
Shell,
Web,
Memory,
Todo,
Agents,
Management,
Desktop,
Subagents,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PermissionProfile {
ReadOnly,
WorkspaceWrite,
DeveloperSafe,
Autonomous,
Manager,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BotStatus {
Ready,
Running,
Paused,
Archived,
Degraded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskState {
Queued,
Preparing,
Running,
WaitingForTool,
WaitingForAgent,
WaitingForHuman,
Completed,
Failed,
Cancelled,
TimedOut,
}
impl TaskState {
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Completed | Self::Failed | Self::Cancelled | Self::TimedOut
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AutoLearnMode {
Off,
MemoryOnly,
SuggestSkills,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApprovalScope {
Once,
Session,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApprovalState {
Pending,
Approved,
Rejected,
Expired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MemoryKind {
Semantic,
Episodic,
Procedural,
Relational,
Artifact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EventType {
BotCreated,
BotUpdated,
BotDeleted,
BotStateChanged,
MessageAdded,
TaskCreated,
TaskStateChanged,
TaskDelegated,
TaskProgressed,
AgentThinkingStarted,
AgentThinkingCompleted,
ToolCallStarted,
ToolCallCompleted,
ApprovalRequested,
ApprovalResolved,
MemoryWritten,
SkillLoaded,
ContextCompacted,
AutoLearnCandidateCreated,
ScheduleTriggered,
HostConnected,
TodoUpdated,
SettingsChanged,
#[serde(other)]
Unknown,
}
pub struct ModelRef;
impl ModelRef {
pub const DEFAULT: &'static str = "default";
pub fn of(provider: &str, model: &str) -> String {
assert!(
!provider.is_empty() && !model.is_empty() && !provider.contains('/'),
"provider and model are required; provider cannot contain '/'"
);
format!("{provider}/{model}")
}
pub fn profile(name: &str) -> String {
name.to_string()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bot {
pub id: String,
pub name: String,
#[serde(default, deserialize_with = "null_default")]
pub role: String,
#[serde(default, deserialize_with = "null_default")]
pub description: String,
#[serde(default, deserialize_with = "null_default")]
pub persona: String,
#[serde(default, deserialize_with = "null_default")]
pub color: String,
#[serde(rename = "modelProfile", default, deserialize_with = "null_default")]
pub model: String,
#[serde(default, deserialize_with = "null_default")]
pub kernel_functions: Vec<KernelPack>,
#[serde(default, deserialize_with = "null_default")]
pub skills: Vec<String>,
#[serde(default, deserialize_with = "null_default")]
pub mcp_servers: Vec<String>,
pub permission_profile: PermissionProfile,
pub auto_learn: AutoLearnMode,
#[serde(default)]
pub short_term_memory: bool,
#[serde(default)]
pub long_term_memory: bool,
#[serde(default)]
pub max_steps: u32,
pub status: BotStatus,
#[serde(default)]
pub is_system: bool,
pub template_id: Option<String>,
#[serde(default, deserialize_with = "null_default")]
pub host_ref: String,
pub container: Option<ContainerProfile>,
}
impl Bot {
pub fn uses_default_model(&self) -> bool {
self.model.is_empty() || self.model == ModelRef::DEFAULT
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BotSpec {
#[serde(skip_serializing_if = "String::is_empty")]
id: String,
name: String,
role: String,
description: String,
persona: String,
color: String,
#[serde(rename = "modelProfile")]
model: String,
kernel_functions: Vec<KernelPack>,
skills: Vec<String>,
mcp_servers: Vec<String>,
permission_profile: PermissionProfile,
auto_learn: AutoLearnMode,
short_term_memory: bool,
long_term_memory: bool,
max_steps: u32,
host_ref: String,
container: Option<ContainerProfile>,
}
impl BotSpec {
pub fn new(name: impl Into<String>) -> Self {
Self {
id: String::new(),
name: name.into(),
role: String::new(),
description: String::new(),
persona: String::new(),
color: "#2C3BA3".into(),
model: ModelRef::DEFAULT.into(),
kernel_functions: vec![
KernelPack::Files,
KernelPack::Search,
KernelPack::Web,
KernelPack::Memory,
KernelPack::Todo,
],
skills: Vec::new(),
mcp_servers: Vec::new(),
permission_profile: PermissionProfile::DeveloperSafe,
auto_learn: AutoLearnMode::Off,
short_term_memory: true,
long_term_memory: true,
max_steps: 24,
host_ref: HostRef::LOCAL.into(),
container: None,
}
}
pub fn host_ref(mut self, v: impl Into<String>) -> Self {
self.host_ref = v.into();
self
}
pub fn container(mut self, v: ContainerProfile) -> Self {
self.container = Some(v);
self
}
pub fn role(mut self, v: impl Into<String>) -> Self {
self.role = v.into();
self
}
pub fn description(mut self, v: impl Into<String>) -> Self {
self.description = v.into();
self
}
pub fn persona(mut self, v: impl Into<String>) -> Self {
self.persona = v.into();
self
}
pub fn color(mut self, v: impl Into<String>) -> Self {
self.color = v.into();
self
}
pub fn model(mut self, v: impl Into<String>) -> Self {
self.model = v.into();
self
}
pub fn kernel_functions(mut self, v: impl IntoIterator<Item = KernelPack>) -> Self {
self.kernel_functions = v.into_iter().collect();
self
}
pub fn skills(mut self, v: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.skills = v.into_iter().map(Into::into).collect();
self
}
pub fn mcp_servers(mut self, v: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.mcp_servers = v.into_iter().map(Into::into).collect();
self
}
pub fn permission_profile(mut self, v: PermissionProfile) -> Self {
self.permission_profile = v;
self
}
pub fn auto_learn(mut self, v: AutoLearnMode) -> Self {
self.auto_learn = v;
self
}
pub fn memory(mut self, short_term: bool, long_term: bool) -> Self {
self.short_term_memory = short_term;
self.long_term_memory = long_term;
self
}
pub fn max_steps(mut self, v: u32) -> Self {
self.max_steps = v;
self
}
pub(crate) fn with_id(mut self, id: &str) -> Self {
self.id = id.to_string();
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BotTemplate {
pub id: String,
pub name: String,
pub category: String,
#[serde(default, deserialize_with = "null_default")]
pub role: String,
#[serde(default, deserialize_with = "null_default")]
pub description: String,
#[serde(rename = "modelProfile", default, deserialize_with = "null_default")]
pub model: String,
#[serde(default, deserialize_with = "null_default")]
pub skills: Vec<String>,
#[serde(default, deserialize_with = "null_default")]
pub kernel_functions: Vec<KernelPack>,
#[serde(default, deserialize_with = "null_default")]
pub tags: Vec<String>,
pub permission_profile: PermissionProfile,
#[serde(default)]
pub is_built_in: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BotModelInfo {
pub bot_id: String,
pub setting: String,
pub effective: String,
pub uses_default: bool,
pub warning: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModelProfileInfo {
pub name: String,
pub provider: String,
pub model: String,
#[serde(default, deserialize_with = "null_default")]
pub fallbacks: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModelCatalog {
pub default: String,
pub choices: Vec<String>,
pub profiles: Vec<ModelProfileInfo>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatThread {
pub id: String,
pub title: String,
pub bot_id: String,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub archived: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatMessage {
pub id: String,
pub thread_id: String,
pub seq: i64,
pub role: String,
pub author: String,
pub content: String,
#[serde(default, deserialize_with = "null_default")]
pub tool_calls: Vec<ToolCall>,
pub tool_name: Option<String>,
pub task_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskRecord {
pub id: String,
pub parent_task_id: Option<String>,
pub thread_id: String,
pub bot_id: String,
#[serde(default)]
pub depth: u32,
pub objective: String,
pub state: TaskState,
pub result: Option<String>,
pub error: Option<String>,
pub current_activity: Option<String>,
pub model: Option<String>,
#[serde(default)]
pub steps: u32,
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
#[serde(default)]
pub cost_usd: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SendResult {
pub task: TaskRecord,
pub reply: Option<ChatMessage>,
}
impl SendResult {
pub fn text(&self) -> &str {
self.reply
.as_ref()
.map(|r| r.content.as_str())
.or(self.task.result.as_deref())
.or(self.task.error.as_deref())
.unwrap_or("")
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct WorkspaceFile {
pub path: String,
pub size: u64,
pub host: Option<String>,
pub host_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalRequest {
pub id: String,
pub task_id: String,
pub thread_id: String,
pub bot_id: String,
pub tool_name: String,
pub arguments: String,
pub reason: String,
pub state: ApprovalState,
pub resolved_by: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentEvent {
pub id: i64,
#[serde(rename = "type")]
pub event_type: EventType,
pub timestamp: String,
pub thread_id: Option<String>,
pub task_id: Option<String>,
pub bot_id: Option<String>,
pub message: Option<String>,
pub data: Option<String>,
}
impl AgentEvent {
pub fn is_task_finished(&self) -> bool {
self.event_type == EventType::TaskStateChanged
&& self
.data
.as_deref()
.and_then(|d| {
serde_json::from_value::<TaskState>(serde_json::Value::String(d.to_string()))
.ok()
})
.is_some_and(TaskState::is_terminal)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct MemoryRecord {
pub id: String,
pub owner: String,
pub kind: MemoryKind,
pub content: String,
pub source: String,
pub confidence: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SkillInfo {
pub name: String,
pub version: String,
pub description: String,
pub trust: String,
pub source: String,
#[serde(default)]
pub pending: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServer {
pub id: String,
pub name: String,
pub description: String,
pub transport: String,
pub command: Option<String>,
#[serde(default, deserialize_with = "null_default")]
pub args: Vec<String>,
pub url: Option<String>,
pub trust: String,
#[serde(default)]
pub is_catalog_entry: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleSpec {
pub name: String,
pub bot_id: String,
pub prompt: String,
pub cron: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_at: Option<String>,
pub time_zone: String,
}
impl ScheduleSpec {
pub fn cron(
name: impl Into<String>,
bot_id: impl Into<String>,
prompt: impl Into<String>,
cron: impl Into<String>,
) -> Self {
Self {
name: name.into(),
bot_id: bot_id.into(),
prompt: prompt.into(),
cron: cron.into(),
run_at: None,
time_zone: "UTC".into(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleJob {
pub id: String,
pub name: String,
pub bot_id: String,
pub prompt: String,
pub cron: String,
pub time_zone: String,
pub enabled: bool,
pub next_run_at: Option<String>,
pub last_run_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostInfo {
pub id: String,
pub name: String,
pub kind: String,
pub os: String,
pub status: String,
pub processor_count: u32,
#[serde(default, deserialize_with = "null_default")]
pub architecture: String,
#[serde(default, deserialize_with = "null_default")]
pub agent_version: String,
#[serde(default, deserialize_with = "null_default")]
pub capabilities: Vec<String>,
pub metrics: Option<HostMetrics>,
pub installed_via: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostMetrics {
pub cpu_percent: f64,
pub free_memory_mb: i64,
pub running_calls: u32,
pub free_disk_mb: i64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnrollmentToken {
pub token: String,
pub expires_at: String,
pub enroll_command: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BootstrapOptions {
host: String,
port: u16,
user: String,
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
private_key: Option<String>,
name: String,
server_url: String,
update_only: bool,
}
impl BootstrapOptions {
pub fn new(
host: impl Into<String>,
user: impl Into<String>,
server_url: impl Into<String>,
) -> Self {
let host = host.into();
Self {
name: host.clone(),
host,
port: 22,
user: user.into(),
password: None,
private_key: None,
server_url: server_url.into(),
update_only: false,
}
}
pub fn password(mut self, v: impl Into<String>) -> Self {
self.password = Some(v.into());
self
}
pub fn private_key(mut self, v: impl Into<String>) -> Self {
self.private_key = Some(v.into());
self
}
pub fn name(mut self, v: impl Into<String>) -> Self {
self.name = v.into();
self
}
pub fn port(mut self, v: u16) -> Self {
self.port = v;
self
}
pub fn update_only(mut self, v: bool) -> Self {
self.update_only = v;
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BootstrapResult {
pub success: bool,
pub host_id: Option<String>,
#[serde(default, deserialize_with = "null_default")]
pub log: Vec<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillStats {
#[serde(default, deserialize_with = "null_default")]
pub name: String,
#[serde(default, deserialize_with = "null_default")]
pub version: String,
pub loads: u64,
pub successes: u64,
pub failures: u64,
}
impl SkillStats {
pub fn runs(&self) -> u64 {
self.successes + self.failures
}
pub fn success_rate(&self) -> f64 {
if self.runs() == 0 {
0.0
} else {
self.successes as f64 / self.runs() as f64
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillEvaluation {
pub name: String,
pub version: String,
pub pending: bool,
pub current: SkillStats,
pub previous_version: Option<String>,
pub previous: Option<SkillStats>,
pub verdict: SkillVerdict,
pub reason: String,
pub can_rollback: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemInfo {
pub product: String,
pub version: String,
pub credits: String,
pub credits_en: String,
pub model_configured: bool,
}