1use serde::{Deserialize, Deserializer, Serialize};
4
5fn null_default<'de, D, T>(d: D) -> std::result::Result<T, D::Error>
7where
8 D: Deserializer<'de>,
9 T: Default + Deserialize<'de>,
10{
11 Ok(Option::<T>::deserialize(d)?.unwrap_or_default())
12}
13
14pub const BOSS_MAN: &str = "boss-man";
16
17pub struct HostRef;
19
20impl HostRef {
21 pub const LOCAL: &'static str = "local-default";
23 pub const AUTO: &'static str = "auto";
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ContainerProfile {
31 pub image: String,
32 pub cpus: f64,
33 pub memory_mb: u32,
34 pub network: bool,
35}
36
37impl ContainerProfile {
38 pub fn new(image: impl Into<String>) -> Self {
40 Self {
41 image: image.into(),
42 cpus: 1.0,
43 memory_mb: 1024,
44 network: true,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum SkillVerdict {
52 CollectingEvidence,
53 Healthy,
54 Underperforming,
55 RollbackRecommended,
56 ReadyToPromote,
57 DiscardRecommended,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum KernelPack {
64 Files,
65 Search,
66 Shell,
67 Web,
68 Memory,
69 Todo,
70 Agents,
71 Management,
73 Desktop,
75 Subagents,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "kebab-case")]
82pub enum PermissionProfile {
83 ReadOnly,
84 WorkspaceWrite,
85 DeveloperSafe,
86 Autonomous,
87 Manager,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum BotStatus {
93 Ready,
94 Running,
95 Paused,
96 Archived,
97 Degraded,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102pub enum TaskState {
103 Queued,
104 Preparing,
105 Running,
106 WaitingForTool,
107 WaitingForAgent,
108 WaitingForHuman,
109 Completed,
110 Failed,
111 Cancelled,
112 TimedOut,
113}
114
115impl TaskState {
116 pub fn is_terminal(self) -> bool {
118 matches!(
119 self,
120 Self::Completed | Self::Failed | Self::Cancelled | Self::TimedOut
121 )
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127pub enum AutoLearnMode {
128 Off,
129 MemoryOnly,
130 SuggestSkills,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135pub enum ApprovalScope {
136 Once,
137 Session,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum ApprovalState {
143 Pending,
144 Approved,
145 Rejected,
146 Expired,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum MemoryKind {
152 Semantic,
153 Episodic,
154 Procedural,
155 Relational,
156 Artifact,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub enum EventType {
162 BotCreated,
163 BotUpdated,
164 BotDeleted,
165 BotStateChanged,
166 MessageAdded,
167 TaskCreated,
168 TaskStateChanged,
169 TaskDelegated,
170 TaskProgressed,
171 AgentThinkingStarted,
172 AgentThinkingCompleted,
173 ToolCallStarted,
174 ToolCallCompleted,
175 ApprovalRequested,
176 ApprovalResolved,
177 MemoryWritten,
178 SkillLoaded,
179 ContextCompacted,
180 AutoLearnCandidateCreated,
181 ScheduleTriggered,
182 HostConnected,
183 TodoUpdated,
184 SettingsChanged,
185 #[serde(other)]
186 Unknown,
187}
188
189pub struct ModelRef;
191
192impl ModelRef {
193 pub const DEFAULT: &'static str = "default";
195
196 pub fn of(provider: &str, model: &str) -> String {
201 assert!(
202 !provider.is_empty() && !model.is_empty() && !provider.contains('/'),
203 "provider and model are required; provider cannot contain '/'"
204 );
205 format!("{provider}/{model}")
206 }
207
208 pub fn profile(name: &str) -> String {
210 name.to_string()
211 }
212}
213
214#[derive(Debug, Clone, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct Bot {
218 pub id: String,
219 pub name: String,
220 #[serde(default, deserialize_with = "null_default")]
221 pub role: String,
222 #[serde(default, deserialize_with = "null_default")]
223 pub description: String,
224 #[serde(default, deserialize_with = "null_default")]
225 pub persona: String,
226 #[serde(default, deserialize_with = "null_default")]
227 pub color: String,
228 #[serde(rename = "modelProfile", default, deserialize_with = "null_default")]
230 pub model: String,
231 #[serde(default, deserialize_with = "null_default")]
232 pub kernel_functions: Vec<KernelPack>,
233 #[serde(default, deserialize_with = "null_default")]
234 pub skills: Vec<String>,
235 #[serde(default, deserialize_with = "null_default")]
236 pub mcp_servers: Vec<String>,
237 pub permission_profile: PermissionProfile,
238 pub auto_learn: AutoLearnMode,
239 #[serde(default)]
240 pub short_term_memory: bool,
241 #[serde(default)]
242 pub long_term_memory: bool,
243 #[serde(default)]
244 pub max_steps: u32,
245 pub status: BotStatus,
246 #[serde(default)]
247 pub is_system: bool,
248 pub template_id: Option<String>,
249 #[serde(default, deserialize_with = "null_default")]
251 pub host_ref: String,
252 pub container: Option<ContainerProfile>,
253}
254
255impl Bot {
256 pub fn uses_default_model(&self) -> bool {
258 self.model.is_empty() || self.model == ModelRef::DEFAULT
259 }
260}
261
262#[derive(Debug, Clone, Serialize)]
264#[serde(rename_all = "camelCase")]
265pub struct BotSpec {
266 #[serde(skip_serializing_if = "String::is_empty")]
267 id: String,
268 name: String,
269 role: String,
270 description: String,
271 persona: String,
272 color: String,
273 #[serde(rename = "modelProfile")]
274 model: String,
275 kernel_functions: Vec<KernelPack>,
276 skills: Vec<String>,
277 mcp_servers: Vec<String>,
278 permission_profile: PermissionProfile,
279 auto_learn: AutoLearnMode,
280 short_term_memory: bool,
281 long_term_memory: bool,
282 max_steps: u32,
283 host_ref: String,
284 container: Option<ContainerProfile>,
285}
286
287impl BotSpec {
288 pub fn new(name: impl Into<String>) -> Self {
290 Self {
291 id: String::new(),
292 name: name.into(),
293 role: String::new(),
294 description: String::new(),
295 persona: String::new(),
296 color: "#2C3BA3".into(),
297 model: ModelRef::DEFAULT.into(),
298 kernel_functions: vec![
299 KernelPack::Files,
300 KernelPack::Search,
301 KernelPack::Web,
302 KernelPack::Memory,
303 KernelPack::Todo,
304 ],
305 skills: Vec::new(),
306 mcp_servers: Vec::new(),
307 permission_profile: PermissionProfile::DeveloperSafe,
308 auto_learn: AutoLearnMode::Off,
309 short_term_memory: true,
310 long_term_memory: true,
311 max_steps: 24,
312 host_ref: HostRef::LOCAL.into(),
313 container: None,
314 }
315 }
316 pub fn host_ref(mut self, v: impl Into<String>) -> Self {
318 self.host_ref = v.into();
319 self
320 }
321 pub fn container(mut self, v: ContainerProfile) -> Self {
323 self.container = Some(v);
324 self
325 }
326 pub fn role(mut self, v: impl Into<String>) -> Self {
327 self.role = v.into();
328 self
329 }
330 pub fn description(mut self, v: impl Into<String>) -> Self {
331 self.description = v.into();
332 self
333 }
334 pub fn persona(mut self, v: impl Into<String>) -> Self {
335 self.persona = v.into();
336 self
337 }
338 pub fn color(mut self, v: impl Into<String>) -> Self {
339 self.color = v.into();
340 self
341 }
342 pub fn model(mut self, v: impl Into<String>) -> Self {
344 self.model = v.into();
345 self
346 }
347 pub fn kernel_functions(mut self, v: impl IntoIterator<Item = KernelPack>) -> Self {
348 self.kernel_functions = v.into_iter().collect();
349 self
350 }
351 pub fn skills(mut self, v: impl IntoIterator<Item = impl Into<String>>) -> Self {
352 self.skills = v.into_iter().map(Into::into).collect();
353 self
354 }
355 pub fn mcp_servers(mut self, v: impl IntoIterator<Item = impl Into<String>>) -> Self {
356 self.mcp_servers = v.into_iter().map(Into::into).collect();
357 self
358 }
359 pub fn permission_profile(mut self, v: PermissionProfile) -> Self {
360 self.permission_profile = v;
361 self
362 }
363 pub fn auto_learn(mut self, v: AutoLearnMode) -> Self {
364 self.auto_learn = v;
365 self
366 }
367 pub fn memory(mut self, short_term: bool, long_term: bool) -> Self {
368 self.short_term_memory = short_term;
369 self.long_term_memory = long_term;
370 self
371 }
372 pub fn max_steps(mut self, v: u32) -> Self {
373 self.max_steps = v;
374 self
375 }
376 pub(crate) fn with_id(mut self, id: &str) -> Self {
377 self.id = id.to_string();
378 self
379 }
380}
381
382#[derive(Debug, Clone, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct BotTemplate {
386 pub id: String,
387 pub name: String,
388 pub category: String,
389 #[serde(default, deserialize_with = "null_default")]
390 pub role: String,
391 #[serde(default, deserialize_with = "null_default")]
392 pub description: String,
393 #[serde(rename = "modelProfile", default, deserialize_with = "null_default")]
394 pub model: String,
395 #[serde(default, deserialize_with = "null_default")]
396 pub skills: Vec<String>,
397 #[serde(default, deserialize_with = "null_default")]
398 pub kernel_functions: Vec<KernelPack>,
399 #[serde(default, deserialize_with = "null_default")]
400 pub tags: Vec<String>,
401 pub permission_profile: PermissionProfile,
402 #[serde(default)]
403 pub is_built_in: bool,
404}
405
406#[derive(Debug, Clone, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct BotModelInfo {
410 pub bot_id: String,
411 pub setting: String,
412 pub effective: String,
414 pub uses_default: bool,
415 pub warning: Option<String>,
416}
417
418#[derive(Debug, Clone, Deserialize)]
420pub struct ModelProfileInfo {
421 pub name: String,
422 pub provider: String,
423 pub model: String,
424 #[serde(default, deserialize_with = "null_default")]
425 pub fallbacks: Vec<String>,
426}
427
428#[derive(Debug, Clone, Deserialize)]
430pub struct ModelCatalog {
431 pub default: String,
433 pub choices: Vec<String>,
434 pub profiles: Vec<ModelProfileInfo>,
435}
436
437#[derive(Debug, Clone, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct ChatThread {
441 pub id: String,
442 pub title: String,
443 pub bot_id: String,
444 #[serde(default)]
445 pub pinned: bool,
446 #[serde(default)]
447 pub archived: bool,
448}
449
450#[derive(Debug, Clone, Deserialize)]
452pub struct ToolCall {
453 pub id: String,
454 pub name: String,
455 pub arguments: String,
456}
457
458#[derive(Debug, Clone, Deserialize)]
460#[serde(rename_all = "camelCase")]
461pub struct ChatMessage {
462 pub id: String,
463 pub thread_id: String,
464 pub seq: i64,
465 pub role: String,
466 pub author: String,
467 pub content: String,
468 #[serde(default, deserialize_with = "null_default")]
469 pub tool_calls: Vec<ToolCall>,
470 pub tool_name: Option<String>,
471 pub task_id: Option<String>,
472}
473
474#[derive(Debug, Clone, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct TaskRecord {
478 pub id: String,
479 pub parent_task_id: Option<String>,
480 pub thread_id: String,
481 pub bot_id: String,
482 #[serde(default)]
483 pub depth: u32,
484 pub objective: String,
485 pub state: TaskState,
486 pub result: Option<String>,
487 pub error: Option<String>,
488 pub current_activity: Option<String>,
489 pub model: Option<String>,
491 #[serde(default)]
492 pub steps: u32,
493 #[serde(default)]
494 pub input_tokens: u64,
495 #[serde(default)]
496 pub output_tokens: u64,
497 #[serde(default)]
498 pub cost_usd: f64,
499}
500
501#[derive(Debug, Clone, Deserialize)]
503pub struct SendResult {
504 pub task: TaskRecord,
505 pub reply: Option<ChatMessage>,
506}
507
508impl SendResult {
509 pub fn text(&self) -> &str {
511 self.reply
512 .as_ref()
513 .map(|r| r.content.as_str())
514 .or(self.task.result.as_deref())
515 .or(self.task.error.as_deref())
516 .unwrap_or("")
517 }
518}
519
520#[derive(Debug, Clone, Deserialize)]
522pub struct WorkspaceFile {
523 pub path: String,
524 pub size: u64,
525 pub host: Option<String>,
527 pub host_name: Option<String>,
528}
529
530#[derive(Debug, Clone, Deserialize)]
532#[serde(rename_all = "camelCase")]
533pub struct ApprovalRequest {
534 pub id: String,
535 pub task_id: String,
536 pub thread_id: String,
537 pub bot_id: String,
538 pub tool_name: String,
539 pub arguments: String,
540 pub reason: String,
541 pub state: ApprovalState,
542 pub resolved_by: Option<String>,
543}
544
545#[derive(Debug, Clone, Deserialize)]
547#[serde(rename_all = "camelCase")]
548pub struct AgentEvent {
549 pub id: i64,
550 #[serde(rename = "type")]
551 pub event_type: EventType,
552 pub timestamp: String,
553 pub thread_id: Option<String>,
554 pub task_id: Option<String>,
555 pub bot_id: Option<String>,
556 pub message: Option<String>,
557 pub data: Option<String>,
558}
559
560impl AgentEvent {
561 pub fn is_task_finished(&self) -> bool {
563 self.event_type == EventType::TaskStateChanged
564 && self
565 .data
566 .as_deref()
567 .and_then(|d| {
568 serde_json::from_value::<TaskState>(serde_json::Value::String(d.to_string()))
569 .ok()
570 })
571 .is_some_and(TaskState::is_terminal)
572 }
573}
574
575#[derive(Debug, Clone, Deserialize)]
577pub struct MemoryRecord {
578 pub id: String,
579 pub owner: String,
580 pub kind: MemoryKind,
581 pub content: String,
582 pub source: String,
583 pub confidence: f64,
584}
585
586#[derive(Debug, Clone, Deserialize)]
588pub struct SkillInfo {
589 pub name: String,
590 pub version: String,
591 pub description: String,
592 pub trust: String,
593 pub source: String,
594 #[serde(default)]
595 pub pending: bool,
596}
597
598#[derive(Debug, Clone, Deserialize)]
600#[serde(rename_all = "camelCase")]
601pub struct McpServer {
602 pub id: String,
603 pub name: String,
604 pub description: String,
605 pub transport: String,
606 pub command: Option<String>,
607 #[serde(default, deserialize_with = "null_default")]
608 pub args: Vec<String>,
609 pub url: Option<String>,
610 pub trust: String,
611 #[serde(default)]
612 pub is_catalog_entry: bool,
613}
614
615#[derive(Debug, Clone, Serialize)]
617#[serde(rename_all = "camelCase")]
618pub struct ScheduleSpec {
619 pub name: String,
620 pub bot_id: String,
621 pub prompt: String,
622 pub cron: String,
623 #[serde(skip_serializing_if = "Option::is_none")]
624 pub run_at: Option<String>,
625 pub time_zone: String,
626}
627
628impl ScheduleSpec {
629 pub fn cron(
631 name: impl Into<String>,
632 bot_id: impl Into<String>,
633 prompt: impl Into<String>,
634 cron: impl Into<String>,
635 ) -> Self {
636 Self {
637 name: name.into(),
638 bot_id: bot_id.into(),
639 prompt: prompt.into(),
640 cron: cron.into(),
641 run_at: None,
642 time_zone: "UTC".into(),
643 }
644 }
645}
646
647#[derive(Debug, Clone, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct ScheduleJob {
651 pub id: String,
652 pub name: String,
653 pub bot_id: String,
654 pub prompt: String,
655 pub cron: String,
656 pub time_zone: String,
657 pub enabled: bool,
658 pub next_run_at: Option<String>,
659 pub last_run_at: Option<String>,
660}
661
662#[derive(Debug, Clone, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub struct HostInfo {
666 pub id: String,
667 pub name: String,
668 pub kind: String,
669 pub os: String,
670 pub status: String,
671 pub processor_count: u32,
672 #[serde(default, deserialize_with = "null_default")]
673 pub architecture: String,
674 #[serde(default, deserialize_with = "null_default")]
675 pub agent_version: String,
676 #[serde(default, deserialize_with = "null_default")]
678 pub capabilities: Vec<String>,
679 pub metrics: Option<HostMetrics>,
680 pub installed_via: Option<String>,
681}
682
683#[derive(Debug, Clone, Deserialize)]
685#[serde(rename_all = "camelCase")]
686pub struct HostMetrics {
687 pub cpu_percent: f64,
688 pub free_memory_mb: i64,
689 pub running_calls: u32,
690 pub free_disk_mb: i64,
691}
692
693#[derive(Debug, Clone, Deserialize)]
695#[serde(rename_all = "camelCase")]
696pub struct EnrollmentToken {
697 pub token: String,
698 pub expires_at: String,
699 pub enroll_command: String,
700}
701
702#[derive(Debug, Clone, Serialize)]
704#[serde(rename_all = "camelCase")]
705pub struct BootstrapOptions {
706 host: String,
707 port: u16,
708 user: String,
709 #[serde(skip_serializing_if = "Option::is_none")]
710 password: Option<String>,
711 #[serde(skip_serializing_if = "Option::is_none")]
712 private_key: Option<String>,
713 name: String,
714 server_url: String,
715 update_only: bool,
716}
717
718impl BootstrapOptions {
719 pub fn new(
721 host: impl Into<String>,
722 user: impl Into<String>,
723 server_url: impl Into<String>,
724 ) -> Self {
725 let host = host.into();
726 Self {
727 name: host.clone(),
728 host,
729 port: 22,
730 user: user.into(),
731 password: None,
732 private_key: None,
733 server_url: server_url.into(),
734 update_only: false,
735 }
736 }
737 pub fn password(mut self, v: impl Into<String>) -> Self {
738 self.password = Some(v.into());
739 self
740 }
741 pub fn private_key(mut self, v: impl Into<String>) -> Self {
742 self.private_key = Some(v.into());
743 self
744 }
745 pub fn name(mut self, v: impl Into<String>) -> Self {
746 self.name = v.into();
747 self
748 }
749 pub fn port(mut self, v: u16) -> Self {
750 self.port = v;
751 self
752 }
753 pub fn update_only(mut self, v: bool) -> Self {
755 self.update_only = v;
756 self
757 }
758}
759
760#[derive(Debug, Clone, Deserialize)]
762#[serde(rename_all = "camelCase")]
763pub struct BootstrapResult {
764 pub success: bool,
765 pub host_id: Option<String>,
766 #[serde(default, deserialize_with = "null_default")]
767 pub log: Vec<String>,
768 pub error: Option<String>,
769}
770
771#[derive(Debug, Clone, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct SkillStats {
775 #[serde(default, deserialize_with = "null_default")]
776 pub name: String,
777 #[serde(default, deserialize_with = "null_default")]
778 pub version: String,
779 pub loads: u64,
780 pub successes: u64,
781 pub failures: u64,
782}
783
784impl SkillStats {
785 pub fn runs(&self) -> u64 {
786 self.successes + self.failures
787 }
788 pub fn success_rate(&self) -> f64 {
789 if self.runs() == 0 {
790 0.0
791 } else {
792 self.successes as f64 / self.runs() as f64
793 }
794 }
795}
796
797#[derive(Debug, Clone, Deserialize)]
799#[serde(rename_all = "camelCase")]
800pub struct SkillEvaluation {
801 pub name: String,
802 pub version: String,
803 pub pending: bool,
804 pub current: SkillStats,
805 pub previous_version: Option<String>,
806 pub previous: Option<SkillStats>,
807 pub verdict: SkillVerdict,
808 pub reason: String,
809 pub can_rollback: bool,
810}
811
812#[derive(Debug, Clone, Deserialize)]
814#[serde(rename_all = "camelCase")]
815pub struct SystemInfo {
816 pub product: String,
817 pub version: String,
818 pub credits: String,
819 pub credits_en: String,
820 pub model_configured: bool,
821}
822
823#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
825pub enum TenantRole {
826 Viewer,
827 Operator,
828 Admin,
829 Owner,
830}
831
832#[derive(Debug, Clone, Deserialize)]
834#[serde(rename_all = "camelCase")]
835pub struct Tenant {
836 pub id: String,
837 pub name: String,
838 #[serde(default)]
839 pub disabled: bool,
840 #[serde(default, deserialize_with = "null_default")]
841 pub created_at: String,
842}
843
844#[derive(Debug, Clone, Deserialize)]
846#[serde(rename_all = "camelCase")]
847pub struct ApiKeyInfo {
848 pub id: String,
849 pub tenant: String,
850 pub name: String,
851 pub role: TenantRole,
852 pub prefix: String,
853 pub last_used_at: Option<String>,
854}
855
856#[derive(Debug, Clone, Deserialize)]
858#[serde(rename_all = "camelCase")]
859pub struct NewApiKey {
860 pub id: String,
861 pub key: String,
862 pub tenant: String,
863 pub role: TenantRole,
864}
865
866#[derive(Debug, Clone, Deserialize)]
868#[serde(rename_all = "camelCase")]
869pub struct TenantMember {
870 pub tenant: String,
871 pub subject: String,
872 pub role: TenantRole,
873}
874
875#[derive(Debug, Clone, Deserialize)]
877#[serde(rename_all = "camelCase")]
878pub struct WhoAmI {
879 pub tenant: String,
880 pub role: TenantRole,
881 pub user: Option<String>,
882 pub platform_admin: bool,
883 pub multi_tenant: bool,
884 #[serde(default, deserialize_with = "null_default")]
885 pub tenants: Vec<String>,
886}