Skip to main content

marbots_sdk/
types.rs

1//! Typed models and names. Wire shapes follow the server's `/api/v1` JSON (camelCase).
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5/// Treats JSON `null` like a missing value (empty list / empty string).
6fn 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
14/// Id of the protected manager bot.
15pub const BOSS_MAN: &str = "boss-man";
16
17/// `host_ref` values besides a registered host id.
18pub struct HostRef;
19
20impl HostRef {
21    /// Run the bot's tools on the server itself.
22    pub const LOCAL: &'static str = "local-default";
23    /// Let placement choose a computer per thread.
24    pub const AUTO: &'static str = "auto";
25}
26
27/// Runs the bot's shell commands in a throwaway Docker container with these quotas.
28#[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    /// `image` with 1 CPU, 1024 MB and network access.
39    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/// The learning evaluation's conclusion about a skill.
50#[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/// Built-in tool packs a bot can enable.
61#[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    /// Boss Man only.
72    Management,
73    /// Computer use: screenshots, mouse and keyboard on the bot's computer (Windows hosts).
74    Desktop,
75    /// `spawn_subagents`: parallel temporary copies of the bot.
76    Subagents,
77}
78
79/// What a bot may do without asking.
80#[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/// A bot's lifecycle state.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum BotStatus {
93    Ready,
94    Running,
95    Paused,
96    Archived,
97    Degraded,
98}
99
100/// A task's lifecycle state.
101#[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    /// True for Completed, Failed, Cancelled and TimedOut.
117    pub fn is_terminal(self) -> bool {
118        matches!(
119            self,
120            Self::Completed | Self::Failed | Self::Cancelled | Self::TimedOut
121        )
122    }
123}
124
125/// Optional learning after tasks.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127pub enum AutoLearnMode {
128    Off,
129    MemoryOnly,
130    SuggestSkills,
131}
132
133/// How far an approval reaches.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135pub enum ApprovalScope {
136    Once,
137    Session,
138}
139
140/// State of an approval request.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum ApprovalState {
143    Pending,
144    Approved,
145    Rejected,
146    Expired,
147}
148
149/// Kind of long-term memory.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum MemoryKind {
152    Semantic,
153    Episodic,
154    Procedural,
155    Relational,
156    Artifact,
157}
158
159/// Types of events on the live stream. Types newer than this SDK deserialize as [`EventType::Unknown`].
160#[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
189/// Builds a bot's model setting.
190pub struct ModelRef;
191
192impl ModelRef {
193    /// Follow the workspace default model.
194    pub const DEFAULT: &'static str = "default";
195
196    /// A direct provider/model pair, e.g. `ModelRef::of("azure", "gpt-5.6-luna")`.
197    ///
198    /// # Panics
199    /// When `provider` or `model` is empty or `provider` contains `/`.
200    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    /// A named model profile configured in Settings.
209    pub fn profile(name: &str) -> String {
210        name.to_string()
211    }
212}
213
214/// A durable AI teammate.
215#[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    /// `"default"` (workspace default), a profile name, or `"provider/model"`.
229    #[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    /// Where the bot's files/shell/desktop tools run: [`HostRef::LOCAL`], a host id or [`HostRef::AUTO`].
250    #[serde(default, deserialize_with = "null_default")]
251    pub host_ref: String,
252    pub container: Option<ContainerProfile>,
253}
254
255impl Bot {
256    /// True when the bot follows the workspace default model.
257    pub fn uses_default_model(&self) -> bool {
258        self.model.is_empty() || self.model == ModelRef::DEFAULT
259    }
260}
261
262/// Options for creating or updating a bot.
263#[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    /// A spec for a bot named `name` with the server defaults.
289    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    /// [`HostRef::LOCAL`] (default), a host id, or [`HostRef::AUTO`].
317    pub fn host_ref(mut self, v: impl Into<String>) -> Self {
318        self.host_ref = v.into();
319        self
320    }
321    /// Run the bot's shell commands in a Docker container.
322    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    /// [`ModelRef::DEFAULT`], [`ModelRef::of`] or a profile name.
343    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/// A ready-made bot role from the template gallery.
383#[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/// A bot's model setting and the model it actually runs on.
407#[derive(Debug, Clone, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct BotModelInfo {
410    pub bot_id: String,
411    pub setting: String,
412    /// The provider/model the bot runs on.
413    pub effective: String,
414    pub uses_default: bool,
415    pub warning: Option<String>,
416}
417
418/// A named model profile.
419#[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/// The workspace default model, the choices and the profiles.
429#[derive(Debug, Clone, Deserialize)]
430pub struct ModelCatalog {
431    /// The workspace default provider/model.
432    pub default: String,
433    pub choices: Vec<String>,
434    pub profiles: Vec<ModelProfileInfo>,
435}
436
437/// A conversation with one bot.
438#[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/// A tool invocation requested by a model.
451#[derive(Debug, Clone, Deserialize)]
452pub struct ToolCall {
453    pub id: String,
454    pub name: String,
455    pub arguments: String,
456}
457
458/// One chat message.
459#[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/// A durable unit of work.
475#[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    /// The provider/model that served the latest step.
490    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/// Returned by `threads().send(...)`; `reply` is set when the call waited for the bot.
502#[derive(Debug, Clone, Deserialize)]
503pub struct SendResult {
504    pub task: TaskRecord,
505    pub reply: Option<ChatMessage>,
506}
507
508impl SendResult {
509    /// The reply text, or the task result/error.
510    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/// A file in a thread's project workspace.
521#[derive(Debug, Clone, Deserialize)]
522pub struct WorkspaceFile {
523    pub path: String,
524    pub size: u64,
525    /// Set when the file lives on a remote agent host (download with `?host=<id>`).
526    pub host: Option<String>,
527    pub host_name: Option<String>,
528}
529
530/// A risky action waiting for (or resolved by) a human.
531#[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/// One item on the live event stream.
546#[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    /// True when this event says a task finished.
562    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/// A long-term memory.
576#[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/// An installed SKILL.md package.
587#[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/// An MCP server from the gallery.
599#[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/// A recurring (`cron`) or one-off (`run_at`, ISO-8601) job.
616#[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    /// A recurring job with a five-field cron expression, in UTC.
630    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/// A saved schedule.
648#[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/// A machine that runs bots.
663#[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    /// shell, files, desktop, docker, dotnet, node, python, `pkg:winget` …
677    #[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/// A host's latest load report.
684#[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/// One-time token for `marbots-host enroll`.
694#[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/// SSH bootstrap of an agent host. The password/key is used for that call only.
703#[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    /// `server_url` is the address the new host uses to reach this server (LAN address, not localhost).
720    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    /// Replace the binary and restart, keeping the enrollment.
754    pub fn update_only(mut self, v: bool) -> Self {
755        self.update_only = v;
756        self
757    }
758}
759
760/// What an SSH bootstrap did, step by step.
761#[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/// Outcomes of the tasks that loaded one skill version.
772#[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/// Learning evaluation of one skill: outcomes of its current version and a verdict.
798#[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/// Server information.
813#[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/// A role inside a tenant, weakest first.
824#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
825pub enum TenantRole {
826    Viewer,
827    Operator,
828    Admin,
829    Owner,
830}
831
832/// A tenant in multi-tenant mode; `default` always exists.
833#[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/// A tenant API key as listed; the key itself is only returned when created.
845#[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/// A freshly created key; `key` is shown only this once.
857#[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/// An OIDC user's role in a tenant (matched by e-mail or subject).
867#[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/// Who the caller is.
876#[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}