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/// Built-in tool packs a bot can enable.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum KernelPack {
21    Files,
22    Search,
23    Shell,
24    Web,
25    Memory,
26    Todo,
27    Agents,
28    /// Boss Man only.
29    Management,
30}
31
32/// What a bot may do without asking.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "kebab-case")]
35pub enum PermissionProfile {
36    ReadOnly,
37    WorkspaceWrite,
38    DeveloperSafe,
39    Autonomous,
40    Manager,
41}
42
43/// A bot's lifecycle state.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum BotStatus {
46    Ready,
47    Running,
48    Paused,
49    Archived,
50    Degraded,
51}
52
53/// A task's lifecycle state.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum TaskState {
56    Queued,
57    Preparing,
58    Running,
59    WaitingForTool,
60    WaitingForAgent,
61    WaitingForHuman,
62    Completed,
63    Failed,
64    Cancelled,
65    TimedOut,
66}
67
68impl TaskState {
69    /// True for Completed, Failed, Cancelled and TimedOut.
70    pub fn is_terminal(self) -> bool {
71        matches!(
72            self,
73            Self::Completed | Self::Failed | Self::Cancelled | Self::TimedOut
74        )
75    }
76}
77
78/// Optional learning after tasks.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80pub enum AutoLearnMode {
81    Off,
82    MemoryOnly,
83    SuggestSkills,
84}
85
86/// How far an approval reaches.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum ApprovalScope {
89    Once,
90    Session,
91}
92
93/// State of an approval request.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95pub enum ApprovalState {
96    Pending,
97    Approved,
98    Rejected,
99    Expired,
100}
101
102/// Kind of long-term memory.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104pub enum MemoryKind {
105    Semantic,
106    Episodic,
107    Procedural,
108    Relational,
109    Artifact,
110}
111
112/// Types of events on the live stream. Types newer than this SDK deserialize as [`EventType::Unknown`].
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114pub enum EventType {
115    BotCreated,
116    BotUpdated,
117    BotDeleted,
118    BotStateChanged,
119    MessageAdded,
120    TaskCreated,
121    TaskStateChanged,
122    TaskDelegated,
123    TaskProgressed,
124    AgentThinkingStarted,
125    AgentThinkingCompleted,
126    ToolCallStarted,
127    ToolCallCompleted,
128    ApprovalRequested,
129    ApprovalResolved,
130    MemoryWritten,
131    SkillLoaded,
132    ContextCompacted,
133    AutoLearnCandidateCreated,
134    ScheduleTriggered,
135    HostConnected,
136    TodoUpdated,
137    SettingsChanged,
138    #[serde(other)]
139    Unknown,
140}
141
142/// Builds a bot's model setting.
143pub struct ModelRef;
144
145impl ModelRef {
146    /// Follow the workspace default model.
147    pub const DEFAULT: &'static str = "default";
148
149    /// A direct provider/model pair, e.g. `ModelRef::of("azure", "gpt-5.6-luna")`.
150    ///
151    /// # Panics
152    /// When `provider` or `model` is empty or `provider` contains `/`.
153    pub fn of(provider: &str, model: &str) -> String {
154        assert!(
155            !provider.is_empty() && !model.is_empty() && !provider.contains('/'),
156            "provider and model are required; provider cannot contain '/'"
157        );
158        format!("{provider}/{model}")
159    }
160
161    /// A named model profile configured in Settings.
162    pub fn profile(name: &str) -> String {
163        name.to_string()
164    }
165}
166
167/// A durable AI teammate.
168#[derive(Debug, Clone, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct Bot {
171    pub id: String,
172    pub name: String,
173    #[serde(default, deserialize_with = "null_default")]
174    pub role: String,
175    #[serde(default, deserialize_with = "null_default")]
176    pub description: String,
177    #[serde(default, deserialize_with = "null_default")]
178    pub persona: String,
179    #[serde(default, deserialize_with = "null_default")]
180    pub color: String,
181    /// `"default"` (workspace default), a profile name, or `"provider/model"`.
182    #[serde(rename = "modelProfile", default, deserialize_with = "null_default")]
183    pub model: String,
184    #[serde(default, deserialize_with = "null_default")]
185    pub kernel_functions: Vec<KernelPack>,
186    #[serde(default, deserialize_with = "null_default")]
187    pub skills: Vec<String>,
188    #[serde(default, deserialize_with = "null_default")]
189    pub mcp_servers: Vec<String>,
190    pub permission_profile: PermissionProfile,
191    pub auto_learn: AutoLearnMode,
192    #[serde(default)]
193    pub short_term_memory: bool,
194    #[serde(default)]
195    pub long_term_memory: bool,
196    #[serde(default)]
197    pub max_steps: u32,
198    pub status: BotStatus,
199    #[serde(default)]
200    pub is_system: bool,
201    pub template_id: Option<String>,
202}
203
204impl Bot {
205    /// True when the bot follows the workspace default model.
206    pub fn uses_default_model(&self) -> bool {
207        self.model.is_empty() || self.model == ModelRef::DEFAULT
208    }
209}
210
211/// Options for creating or updating a bot.
212#[derive(Debug, Clone, Serialize)]
213#[serde(rename_all = "camelCase")]
214pub struct BotSpec {
215    #[serde(skip_serializing_if = "String::is_empty")]
216    id: String,
217    name: String,
218    role: String,
219    description: String,
220    persona: String,
221    color: String,
222    #[serde(rename = "modelProfile")]
223    model: String,
224    kernel_functions: Vec<KernelPack>,
225    skills: Vec<String>,
226    mcp_servers: Vec<String>,
227    permission_profile: PermissionProfile,
228    auto_learn: AutoLearnMode,
229    short_term_memory: bool,
230    long_term_memory: bool,
231    max_steps: u32,
232}
233
234impl BotSpec {
235    /// A spec for a bot named `name` with the server defaults.
236    pub fn new(name: impl Into<String>) -> Self {
237        Self {
238            id: String::new(),
239            name: name.into(),
240            role: String::new(),
241            description: String::new(),
242            persona: String::new(),
243            color: "#2C3BA3".into(),
244            model: ModelRef::DEFAULT.into(),
245            kernel_functions: vec![
246                KernelPack::Files,
247                KernelPack::Search,
248                KernelPack::Web,
249                KernelPack::Memory,
250                KernelPack::Todo,
251            ],
252            skills: Vec::new(),
253            mcp_servers: Vec::new(),
254            permission_profile: PermissionProfile::DeveloperSafe,
255            auto_learn: AutoLearnMode::Off,
256            short_term_memory: true,
257            long_term_memory: true,
258            max_steps: 24,
259        }
260    }
261    pub fn role(mut self, v: impl Into<String>) -> Self {
262        self.role = v.into();
263        self
264    }
265    pub fn description(mut self, v: impl Into<String>) -> Self {
266        self.description = v.into();
267        self
268    }
269    pub fn persona(mut self, v: impl Into<String>) -> Self {
270        self.persona = v.into();
271        self
272    }
273    pub fn color(mut self, v: impl Into<String>) -> Self {
274        self.color = v.into();
275        self
276    }
277    /// [`ModelRef::DEFAULT`], [`ModelRef::of`] or a profile name.
278    pub fn model(mut self, v: impl Into<String>) -> Self {
279        self.model = v.into();
280        self
281    }
282    pub fn kernel_functions(mut self, v: impl IntoIterator<Item = KernelPack>) -> Self {
283        self.kernel_functions = v.into_iter().collect();
284        self
285    }
286    pub fn skills(mut self, v: impl IntoIterator<Item = impl Into<String>>) -> Self {
287        self.skills = v.into_iter().map(Into::into).collect();
288        self
289    }
290    pub fn mcp_servers(mut self, v: impl IntoIterator<Item = impl Into<String>>) -> Self {
291        self.mcp_servers = v.into_iter().map(Into::into).collect();
292        self
293    }
294    pub fn permission_profile(mut self, v: PermissionProfile) -> Self {
295        self.permission_profile = v;
296        self
297    }
298    pub fn auto_learn(mut self, v: AutoLearnMode) -> Self {
299        self.auto_learn = v;
300        self
301    }
302    pub fn memory(mut self, short_term: bool, long_term: bool) -> Self {
303        self.short_term_memory = short_term;
304        self.long_term_memory = long_term;
305        self
306    }
307    pub fn max_steps(mut self, v: u32) -> Self {
308        self.max_steps = v;
309        self
310    }
311    pub(crate) fn with_id(mut self, id: &str) -> Self {
312        self.id = id.to_string();
313        self
314    }
315}
316
317/// A ready-made bot role from the template gallery.
318#[derive(Debug, Clone, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct BotTemplate {
321    pub id: String,
322    pub name: String,
323    pub category: String,
324    #[serde(default, deserialize_with = "null_default")]
325    pub role: String,
326    #[serde(default, deserialize_with = "null_default")]
327    pub description: String,
328    #[serde(rename = "modelProfile", default, deserialize_with = "null_default")]
329    pub model: String,
330    #[serde(default, deserialize_with = "null_default")]
331    pub skills: Vec<String>,
332    #[serde(default, deserialize_with = "null_default")]
333    pub kernel_functions: Vec<KernelPack>,
334    #[serde(default, deserialize_with = "null_default")]
335    pub tags: Vec<String>,
336    pub permission_profile: PermissionProfile,
337    #[serde(default)]
338    pub is_built_in: bool,
339}
340
341/// A bot's model setting and the model it actually runs on.
342#[derive(Debug, Clone, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct BotModelInfo {
345    pub bot_id: String,
346    pub setting: String,
347    /// The provider/model the bot runs on.
348    pub effective: String,
349    pub uses_default: bool,
350    pub warning: Option<String>,
351}
352
353/// A named model profile.
354#[derive(Debug, Clone, Deserialize)]
355pub struct ModelProfileInfo {
356    pub name: String,
357    pub provider: String,
358    pub model: String,
359    #[serde(default, deserialize_with = "null_default")]
360    pub fallbacks: Vec<String>,
361}
362
363/// The workspace default model, the choices and the profiles.
364#[derive(Debug, Clone, Deserialize)]
365pub struct ModelCatalog {
366    /// The workspace default provider/model.
367    pub default: String,
368    pub choices: Vec<String>,
369    pub profiles: Vec<ModelProfileInfo>,
370}
371
372/// A conversation with one bot.
373#[derive(Debug, Clone, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct ChatThread {
376    pub id: String,
377    pub title: String,
378    pub bot_id: String,
379    #[serde(default)]
380    pub pinned: bool,
381    #[serde(default)]
382    pub archived: bool,
383}
384
385/// A tool invocation requested by a model.
386#[derive(Debug, Clone, Deserialize)]
387pub struct ToolCall {
388    pub id: String,
389    pub name: String,
390    pub arguments: String,
391}
392
393/// One chat message.
394#[derive(Debug, Clone, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct ChatMessage {
397    pub id: String,
398    pub thread_id: String,
399    pub seq: i64,
400    pub role: String,
401    pub author: String,
402    pub content: String,
403    #[serde(default, deserialize_with = "null_default")]
404    pub tool_calls: Vec<ToolCall>,
405    pub tool_name: Option<String>,
406    pub task_id: Option<String>,
407}
408
409/// A durable unit of work.
410#[derive(Debug, Clone, Deserialize)]
411#[serde(rename_all = "camelCase")]
412pub struct TaskRecord {
413    pub id: String,
414    pub parent_task_id: Option<String>,
415    pub thread_id: String,
416    pub bot_id: String,
417    #[serde(default)]
418    pub depth: u32,
419    pub objective: String,
420    pub state: TaskState,
421    pub result: Option<String>,
422    pub error: Option<String>,
423    pub current_activity: Option<String>,
424    /// The provider/model that served the latest step.
425    pub model: Option<String>,
426    #[serde(default)]
427    pub steps: u32,
428    #[serde(default)]
429    pub input_tokens: u64,
430    #[serde(default)]
431    pub output_tokens: u64,
432    #[serde(default)]
433    pub cost_usd: f64,
434}
435
436/// Returned by `threads().send(...)`; `reply` is set when the call waited for the bot.
437#[derive(Debug, Clone, Deserialize)]
438pub struct SendResult {
439    pub task: TaskRecord,
440    pub reply: Option<ChatMessage>,
441}
442
443impl SendResult {
444    /// The reply text, or the task result/error.
445    pub fn text(&self) -> &str {
446        self.reply
447            .as_ref()
448            .map(|r| r.content.as_str())
449            .or(self.task.result.as_deref())
450            .or(self.task.error.as_deref())
451            .unwrap_or("")
452    }
453}
454
455/// A file in a thread's project workspace.
456#[derive(Debug, Clone, Deserialize)]
457pub struct WorkspaceFile {
458    pub path: String,
459    pub size: u64,
460}
461
462/// A risky action waiting for (or resolved by) a human.
463#[derive(Debug, Clone, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct ApprovalRequest {
466    pub id: String,
467    pub task_id: String,
468    pub thread_id: String,
469    pub bot_id: String,
470    pub tool_name: String,
471    pub arguments: String,
472    pub reason: String,
473    pub state: ApprovalState,
474    pub resolved_by: Option<String>,
475}
476
477/// One item on the live event stream.
478#[derive(Debug, Clone, Deserialize)]
479#[serde(rename_all = "camelCase")]
480pub struct AgentEvent {
481    pub id: i64,
482    #[serde(rename = "type")]
483    pub event_type: EventType,
484    pub timestamp: String,
485    pub thread_id: Option<String>,
486    pub task_id: Option<String>,
487    pub bot_id: Option<String>,
488    pub message: Option<String>,
489    pub data: Option<String>,
490}
491
492impl AgentEvent {
493    /// True when this event says a task finished.
494    pub fn is_task_finished(&self) -> bool {
495        self.event_type == EventType::TaskStateChanged
496            && self
497                .data
498                .as_deref()
499                .and_then(|d| {
500                    serde_json::from_value::<TaskState>(serde_json::Value::String(d.to_string()))
501                        .ok()
502                })
503                .is_some_and(TaskState::is_terminal)
504    }
505}
506
507/// A long-term memory.
508#[derive(Debug, Clone, Deserialize)]
509pub struct MemoryRecord {
510    pub id: String,
511    pub owner: String,
512    pub kind: MemoryKind,
513    pub content: String,
514    pub source: String,
515    pub confidence: f64,
516}
517
518/// An installed SKILL.md package.
519#[derive(Debug, Clone, Deserialize)]
520pub struct SkillInfo {
521    pub name: String,
522    pub version: String,
523    pub description: String,
524    pub trust: String,
525    pub source: String,
526    #[serde(default)]
527    pub pending: bool,
528}
529
530/// An MCP server from the gallery.
531#[derive(Debug, Clone, Deserialize)]
532#[serde(rename_all = "camelCase")]
533pub struct McpServer {
534    pub id: String,
535    pub name: String,
536    pub description: String,
537    pub transport: String,
538    pub command: Option<String>,
539    #[serde(default, deserialize_with = "null_default")]
540    pub args: Vec<String>,
541    pub url: Option<String>,
542    pub trust: String,
543    #[serde(default)]
544    pub is_catalog_entry: bool,
545}
546
547/// A recurring (`cron`) or one-off (`run_at`, ISO-8601) job.
548#[derive(Debug, Clone, Serialize)]
549#[serde(rename_all = "camelCase")]
550pub struct ScheduleSpec {
551    pub name: String,
552    pub bot_id: String,
553    pub prompt: String,
554    pub cron: String,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub run_at: Option<String>,
557    pub time_zone: String,
558}
559
560impl ScheduleSpec {
561    /// A recurring job with a five-field cron expression, in UTC.
562    pub fn cron(
563        name: impl Into<String>,
564        bot_id: impl Into<String>,
565        prompt: impl Into<String>,
566        cron: impl Into<String>,
567    ) -> Self {
568        Self {
569            name: name.into(),
570            bot_id: bot_id.into(),
571            prompt: prompt.into(),
572            cron: cron.into(),
573            run_at: None,
574            time_zone: "UTC".into(),
575        }
576    }
577}
578
579/// A saved schedule.
580#[derive(Debug, Clone, Deserialize)]
581#[serde(rename_all = "camelCase")]
582pub struct ScheduleJob {
583    pub id: String,
584    pub name: String,
585    pub bot_id: String,
586    pub prompt: String,
587    pub cron: String,
588    pub time_zone: String,
589    pub enabled: bool,
590    pub next_run_at: Option<String>,
591    pub last_run_at: Option<String>,
592}
593
594/// A machine that runs bots.
595#[derive(Debug, Clone, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub struct HostInfo {
598    pub id: String,
599    pub name: String,
600    pub kind: String,
601    pub os: String,
602    pub status: String,
603    pub processor_count: u32,
604}
605
606/// Server information.
607#[derive(Debug, Clone, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct SystemInfo {
610    pub product: String,
611    pub version: String,
612    pub credits: String,
613    pub credits_en: String,
614    pub model_configured: bool,
615}