1use std::borrow::Cow;
2use std::sync::Arc;
3
4use mobius::backend::model::provider::MediaModelPreset;
5pub use mobius::backend::model::provider::{
6 ModelPreset as ProviderModel, ReasoningPreset as ReasoningChoice,
7};
8
9use super::*;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum GatewayNotification {
16 Sessions,
18 Bots,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ComputerView {
26 Unavailable,
28 EmbeddedBrowser,
30 RemoteDesktop,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct ReadyPayload {
37 pub gateway_version: String,
39 pub machine_name: String,
41 pub computer_view: ComputerView,
43 pub bots: Vec<BotRecord>,
45 pub sessions: Vec<SessionRecord>,
47 pub background_approvals: Vec<BackgroundApproval>,
49 pub providers: Vec<ProviderStatus>,
51 pub provider_instances: Vec<ProviderInstance>,
53 pub bot_defaults: Option<VersionedAgentConfig>,
55 pub models: Vec<ModelChoice>,
57 pub model_providers: BTreeMap<String, String>,
59 #[serde(default)]
61 pub image_models: Vec<ModelChoice>,
62 #[serde(default)]
64 pub voice_models: Vec<ModelChoice>,
65 pub middleware_features: Vec<MiddlewareFeature>,
67 pub extensions: Vec<ExtensionRecord>,
69 pub contributions: Vec<FrontendContribution>,
71 pub max_active_sessions: usize,
73 pub session_file_limits: SessionFileLimits,
75 pub revisions: BTreeMap<ReadySection, String>,
77 pub omitted: BTreeSet<ReadySection>,
79}
80
81impl ReadyPayload {
82 pub fn swap_section(&mut self, other: &mut Self, section: ReadySection) {
84 use std::mem::swap;
85 match section {
86 ReadySection::Config => {
87 swap(&mut self.providers, &mut other.providers);
88 swap(&mut self.provider_instances, &mut other.provider_instances);
89 swap(&mut self.bot_defaults, &mut other.bot_defaults);
90 swap(&mut self.models, &mut other.models);
91 swap(&mut self.model_providers, &mut other.model_providers);
92 swap(&mut self.image_models, &mut other.image_models);
93 swap(&mut self.voice_models, &mut other.voice_models);
94 swap(
95 &mut self.middleware_features,
96 &mut other.middleware_features,
97 );
98 swap(&mut self.extensions, &mut other.extensions);
99 swap(&mut self.contributions, &mut other.contributions);
100 }
101 ReadySection::Bots => swap(&mut self.bots, &mut other.bots),
102 ReadySection::Sessions => swap(&mut self.sessions, &mut other.sessions),
103 }
104 }
105
106 pub fn restore_omitted(&mut self, held: &mut Self) {
108 for section in std::mem::take(&mut self.omitted) {
109 self.swap_section(held, section);
110 }
111 }
112
113 pub fn update(&mut self, mut next: Self) {
115 next.restore_omitted(self);
116 *self = next;
117 }
118
119 #[must_use]
121 pub fn revision(&self, section: ReadySection) -> String {
122 match section {
123 ReadySection::Config => content_revision(&(
124 &self.providers,
125 &self.provider_instances,
126 &self.bot_defaults,
127 &self.models,
128 &self.model_providers,
129 &self.image_models,
130 &self.voice_models,
131 &self.middleware_features,
132 &self.extensions,
133 &self.contributions,
134 )),
135 ReadySection::Bots => content_revision(&self.bots),
136 ReadySection::Sessions => content_revision(&self.sessions),
137 }
138 }
139
140 pub(crate) fn blank(&self) -> Self {
142 Self {
143 gateway_version: String::new(),
144 machine_name: String::new(),
145 computer_view: self.computer_view,
146 bots: Vec::new(),
147 sessions: Vec::new(),
148 background_approvals: Vec::new(),
149 providers: Vec::new(),
150 provider_instances: Vec::new(),
151 bot_defaults: None,
152 models: Vec::new(),
153 model_providers: BTreeMap::new(),
154 image_models: Vec::new(),
155 voice_models: Vec::new(),
156 middleware_features: Vec::new(),
157 extensions: Vec::new(),
158 contributions: Vec::new(),
159 max_active_sessions: self.max_active_sessions,
160 session_file_limits: self.session_file_limits,
161 revisions: BTreeMap::new(),
162 omitted: BTreeSet::new(),
163 }
164 }
165}
166
167#[must_use]
169pub fn content_revision(value: &impl Serialize) -> String {
170 use sha2::Digest as _;
171 let mut hasher = sha2::Sha256::new();
172 let _ = serde_json::to_writer(&mut hasher, value);
174 hasher.finalize()[..16]
175 .iter()
176 .map(|byte| format!("{byte:02x}"))
177 .collect()
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum ReadySection {
184 Config,
186 Bots,
188 Sessions,
190}
191
192pub const READY_SECTIONS: [ReadySection; 3] = [
194 ReadySection::Config,
195 ReadySection::Bots,
196 ReadySection::Sessions,
197];
198
199#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
201pub struct CatalogHint {
202 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
204 pub known: BTreeMap<ReadySection, String>,
205 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
207 pub skip: BTreeSet<ReadySection>,
208}
209
210impl CatalogHint {
211 #[must_use]
213 pub fn is_empty(&self) -> bool {
214 self.known.is_empty() && self.skip.is_empty()
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(untagged)]
221pub enum SessionSlot {
222 Unchanged(String),
224 Changed(Box<SessionRecord>),
226}
227
228pub fn apply_session_changes(sessions: &mut Vec<SessionRecord>, changes: Vec<SessionSlot>) {
230 let mut held: Vec<_> = std::mem::take(sessions).into_iter().map(Some).collect();
231 *sessions = changes
232 .into_iter()
233 .filter_map(|slot| match slot {
234 SessionSlot::Changed(session) => Some(*session),
235 SessionSlot::Unchanged(id) => held
236 .iter_mut()
237 .find(|held| held.as_ref().is_some_and(|held| held.session_id == id))
238 .and_then(Option::take),
239 })
240 .collect();
241}
242
243#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
245pub struct DiffTotals {
246 pub additions: u64,
248 pub deletions: u64,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254pub struct BackgroundApproval {
255 pub session_id: String,
257 pub bot_id: String,
259 pub turn_id: String,
261 pub request_id: String,
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct SessionReadyPayload {
268 pub active_turn_ids: Vec<String>,
270 pub pending_approvals: Vec<mobius::protocol::ExecApprovalRequestEvent>,
272 pub latest_sequence: u64,
274 pub next_before_sequence: Option<u64>,
276 pub workspace: Option<WorkspaceInfo>,
278 pub attached_folders: Vec<PathBuf>,
280 pub git: Option<GitStatus>,
282 pub session: SessionConfiguredEvent,
284 pub contributions: Vec<FrontendContribution>,
286 pub widgets: Vec<SessionWidget>,
288 pub tool_count: usize,
290 pub compaction_count: u64,
292 pub context_limit_tokens: Option<i64>,
294 pub active_message_delivery: mobius::protocol::ActiveMessageDelivery,
296 pub run_stats: RunStats,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302pub struct SessionWidget {
303 pub capability: String,
305 pub item: FrontendWidget,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct SessionRecord {
312 pub session_id: String,
314 pub session_context: mobius::protocol::SessionContext,
316 pub parent_session_id: Option<String>,
318 pub parent_sequence: Option<u64>,
320 pub sequence: u64,
322 pub first_user_message: Option<String>,
324 pub execution_stats: mobius::backend::checkpoint::ExecutionStats,
326 pub title: Option<String>,
328 pub pinned: bool,
330 pub activity: SessionActivity,
332 pub created_at: i64,
334 pub updated_at: i64,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(deny_unknown_fields)]
341pub struct SessionActivity {
342 pub attention: u32,
344 pub state: SessionActivityState,
346 pub turn_id: Option<String>,
348 pub approval_request_id: Option<String>,
350 pub started_at: Option<i64>,
352 pub last_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
354 pub message: Option<String>,
356}
357
358impl Default for SessionActivity {
359 fn default() -> Self {
360 Self {
361 attention: 0,
362 state: SessionActivityState::Idle,
363 turn_id: None,
364 approval_request_id: None,
365 started_at: None,
366 last_outcome: None,
367 message: None,
368 }
369 }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub enum SessionActivityState {
376 Idle,
378 Running,
380 AwaitingApproval,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386pub struct WorkspaceInfo {
387 pub id: String,
389 pub path: PathBuf,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct GitStatus {
396 pub current_branch: String,
398 pub branches: Vec<String>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(deny_unknown_fields)]
405pub struct SshIdentityRecord {
406 pub label: String,
408 pub algorithm: String,
410 pub fingerprint: String,
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum GitDiffScope {
418 Staged,
420 Unstaged,
422 Committed,
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "snake_case")]
429pub enum WorkspaceFileScope {
430 Modified,
432 All,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438pub struct WorkspaceFileRecord {
439 pub path: String,
441 pub size: u64,
443}
444
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct DirectoryListing {
448 pub path: PathBuf,
450 pub parent: Option<PathBuf>,
452 pub entries: Vec<DirectoryEntry>,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct DirectoryEntry {
459 pub name: String,
461 pub path: PathBuf,
463 pub is_directory: bool,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct VersionedAgentConfig {
470 pub revision: u64,
472 pub config: AgentComposition,
474}
475
476#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
478#[serde(deny_unknown_fields)]
479pub struct AgentComposition {
480 pub provider: ProviderConfig,
482 pub realtime_voice: Option<String>,
484 pub middleware: MiddlewareConfig,
486 pub extensions: BTreeSet<String>,
488 pub system_prompt: String,
490 pub max_model_steps: u64,
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum ExtensionKind {
498 Skill,
500 Plugin,
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(deny_unknown_fields)]
507pub struct ExtensionHookRecord {
508 pub event: String,
510 pub matcher: Option<String>,
512 pub command: String,
514 pub timeout_seconds: u64,
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(deny_unknown_fields)]
521pub struct ExtensionRecord {
522 pub id: String,
524 pub capability: String,
526 pub kind: ExtensionKind,
528 pub name: String,
530 pub description: String,
532 pub version: Option<String>,
534 pub source: String,
536 pub reference: Option<String>,
538 pub subdirectory: Option<String>,
540 pub resolved_revision: String,
542 pub digest: String,
544 pub skills: Vec<String>,
546 pub hooks: Vec<ExtensionHookRecord>,
548 pub hooks_trusted: bool,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
554#[serde(deny_unknown_fields)]
555pub struct ProviderConfig {
556 pub instance: String,
559 pub provider: String,
561 #[serde(default)]
563 pub model: String,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
565 pub base_url: Option<String>,
567 pub endpoint_auth: ProviderEndpointAuth,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub reasoning_effort: Option<String>,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub service_tier: Option<String>,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub tool_discovery: Option<ToolDiscoveryMode>,
578 pub web_search: HostedWebSearch,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
584#[serde(rename_all = "snake_case")]
585pub enum ProviderEndpointAuth {
586 ProviderDefault,
588 Credentialless,
590}
591
592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
594pub struct ProviderStatus {
595 pub provider: String,
597 pub label: String,
599 pub symbol: FrontendSymbol,
601 pub description: String,
603 pub model_ids_configurable: bool,
605 pub auth: ProviderAuthKind,
607 pub default_base_url: Option<String>,
609 pub native_custom_endpoints: bool,
611 pub default_api_key_env: Option<String>,
613 pub models: Vec<ProviderModel>,
615 #[serde(default)]
617 pub image_models: Cow<'static, [MediaModelPreset]>,
618 #[serde(default)]
620 pub image_model_ids_configurable: bool,
621 #[serde(default)]
623 pub voice_models: Cow<'static, [MediaModelPreset]>,
624 pub web_search: Vec<FrontendSettingOption>,
626 pub tool_discovery: ToolDiscoveryMode,
628 pub supported_tool_discovery: Vec<ToolDiscoveryMode>,
630 pub custom_endpoint_tool_discovery: Option<ToolDiscoveryMode>,
632 pub realtime_voices: Vec<String>,
634}
635
636#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "snake_case")]
639pub enum ProviderTint {
640 #[default]
641 Blue,
643 Teal,
645 Green,
647 Yellow,
649 Orange,
651 Red,
653 Purple,
655 White,
657}
658
659#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(rename_all = "snake_case")]
662pub enum BotShape {
663 Circle,
665 Squircle,
667 Triangle,
669 Diamond,
671 Hexagon,
673 Star,
675 Flower,
677}
678
679impl BotShape {
680 pub const ALL: [Self; 7] = [
682 Self::Circle,
683 Self::Squircle,
684 Self::Triangle,
685 Self::Diamond,
686 Self::Hexagon,
687 Self::Star,
688 Self::Flower,
689 ];
690}
691
692#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694pub struct ProviderInstance {
695 pub label: String,
697 pub tint: ProviderTint,
699 pub configured: bool,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub credential_hint: Option<String>,
704 pub selection: ProviderConfig,
706 pub models: Vec<ProviderModel>,
708 #[serde(default, skip_serializing_if = "Vec::is_empty")]
710 pub image_model_ids: Vec<String>,
711}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
715#[serde(rename_all = "snake_case")]
716pub enum ClientKind {
717 Cli,
719 Macos,
721 Ios,
723 Ipados,
725 GatewayDashboard,
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731pub struct ClientStatus {
732 pub client_id: String,
734 pub label: String,
736 pub kinds: Vec<ClientKind>,
738 pub connections: usize,
740}
741
742impl ProviderStatus {
743 #[must_use]
744 pub fn configurable_base_url(&self) -> bool {
746 self.default_base_url.is_some()
747 }
748
749 #[must_use]
750 pub fn default_model(&self) -> Option<&ProviderModel> {
752 self.models.first()
753 }
754}
755
756#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(deny_unknown_fields)]
759pub struct ConfiguredModel {
760 pub id: String,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
764 pub reasoning_efforts: Option<Vec<String>>,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub default_reasoning: Option<String>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub label: Option<String>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub description: Option<String>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
776 pub context_window: Option<i64>,
777}
778
779#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
781#[serde(rename_all = "snake_case")]
782pub enum ProviderAuthKind {
783 ApiKey,
785 DeviceCode,
787}
788
789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792pub struct MiddlewareConfig {
793 pub(crate) enabled: BTreeSet<String>,
794 pub settings: BTreeMap<String, BTreeMap<String, FrontendSettingValue>>,
796}
797
798impl MiddlewareConfig {
799 #[must_use]
801 pub fn disabled_by<'a>(
802 &self,
803 features: &'a [mobius::protocol::MiddlewareFeature],
804 id: &str,
805 selected_model: Option<&mobius::protocol::ModelChoice>,
806 ) -> Option<&'a str> {
807 if let Some(capability) = features
808 .iter()
809 .find(|feature| feature.id == id)
810 .and_then(|feature| feature.required_model_capability)
811 && selected_model.is_some_and(|model| !model.supports(capability))
812 {
813 return Some(match capability {
814 mobius::protocol::ModelCapability::ImageGeneration => {
815 "a model without image generation"
816 }
817 mobius::protocol::ModelCapability::RealtimeVoice => {
818 "a model without realtime voice"
819 }
820 });
821 }
822 features
823 .iter()
824 .filter(|feature| feature.required || self.enabled(&feature.id))
825 .find_map(|feature| {
826 feature.settings.iter().find_map(|setting| {
827 let mobius::protocol::FrontendSettingKind::Select { options, .. } =
828 &setting.kind
829 else {
830 return None;
831 };
832 let Some(FrontendSettingValue::String(value)) =
833 self.setting(&feature.id, &setting.id)
834 else {
835 return None;
836 };
837 options
838 .iter()
839 .find(|option| {
840 option.value == *value
841 && option.disables.iter().any(|disabled| disabled == id)
842 })
843 .map(|option| option.label.as_str())
844 })
845 })
846 }
847
848 pub fn reconcile(
850 &mut self,
851 features: &[mobius::protocol::MiddlewareFeature],
852 selected_model: Option<&mobius::protocol::ModelChoice>,
853 ) {
854 let excluded = self
855 .enabled
856 .iter()
857 .filter(|id| self.disabled_by(features, id, selected_model).is_some())
858 .cloned()
860 .collect::<Vec<_>>();
861 for id in excluded {
862 self.enabled.remove(&id);
863 }
864 }
865
866 #[must_use]
868 pub fn enabled(&self, id: &str) -> bool {
869 self.enabled.contains(id)
870 }
871
872 pub fn set_enabled(&mut self, id: impl Into<String>, enabled: bool) {
874 let id = id.into();
875 if enabled {
876 self.enabled.insert(id);
877 } else {
878 self.enabled.remove(&id);
879 }
880 }
881
882 #[must_use]
884 pub fn setting(&self, middleware: &str, setting: &str) -> Option<&FrontendSettingValue> {
885 self.settings.get(middleware)?.get(setting)
886 }
887
888 pub fn set_setting(
890 &mut self,
891 middleware: impl Into<String>,
892 setting: impl Into<String>,
893 value: Option<FrontendSettingValue>,
894 ) {
895 let middleware = middleware.into();
896 let setting = setting.into();
897 if let Some(value) = value {
898 self.settings
899 .entry(middleware)
900 .or_default()
901 .insert(setting, value);
902 } else if let Some(settings) = self.settings.get_mut(&middleware) {
903 settings.remove(&setting);
904 if settings.is_empty() {
905 self.settings.remove(&middleware);
906 }
907 }
908 }
909
910 pub(crate) fn entries(&self) -> impl Iterator<Item = &str> {
911 self.enabled.iter().map(String::as_str)
912 }
913}
914
915#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
917pub struct RenderedPreview {
918 pub symbol: Option<FrontendSymbol>,
920 pub duration_ms: Option<u64>,
922 pub started_at_ms: Option<i64>,
924 pub id: String,
926 pub title: String,
928 pub subtitle: String,
930 pub page_id: String,
932 pub update: FrontendPreviewUpdate,
934 pub events: Vec<RenderedEvent>,
936 pub next: Option<Op>,
938}
939
940#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
942pub struct RenderedEvent {
943 pub submission_id: Option<Arc<str>>,
945 pub recorded_at_ms: i64,
947 pub event: EventMsg,
949 pub blocks: Vec<RenderedBlock>,
951}
952
953#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955pub struct RecordedEvent {
956 pub sequence: u64,
958 pub recorded_at_ms: i64,
960 pub event: Event,
962 pub stream_metrics: Vec<StreamMetrics>,
964 pub blocks: Vec<RenderedBlock>,
966 pub preview: Option<RenderedPreview>,
968}
969
970#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
972pub struct ProfileSnapshot {
973 pub user_name: Option<String>,
975 pub daily_usage: Vec<DailyUsage>,
977 pub provider_usage: Vec<ProviderUsage>,
979 pub run_stats: RunStats,
981 pub recent_run_groups: Vec<SessionRunGroup>,
983}
984
985#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
987pub struct ProviderUsage {
988 pub provider: String,
990 pub limits: Option<Vec<UsageLimit>>,
992 pub error: Option<String>,
994}
995
996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
998pub struct SessionRunGroup {
999 pub session_id: String,
1001 pub title: String,
1003 pub runs: Vec<RunSummary>,
1005}
1006
1007#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1009pub struct RunStats {
1010 #[serde(flatten)]
1011 pub completed: mobius::backend::checkpoint::ExecutionStats,
1013 pub active: Option<RunSummary>,
1015}
1016
1017#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1019pub struct RunSummary {
1020 pub session_id: String,
1022 pub submission_id: String,
1024 pub turn_id: String,
1026 pub started_at_ms: i64,
1028 pub finished_at_ms: Option<i64>,
1030 pub elapsed_ms: u64,
1032 pub outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1034 pub model_calls: u64,
1036 pub tool_calls: u64,
1038 pub failed_tool_calls: u64,
1040 pub usage: TokenUsage,
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1046pub struct DailyUsage {
1047 pub unix_day: u64,
1049 pub provider: String,
1051 pub usage: TokenUsage,
1053}
1054
1055#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1057pub struct BotRecord {
1058 pub conversation_session_id: String,
1060 pub id: String,
1062 pub handle: String,
1064 pub name: String,
1066 pub description: String,
1068 pub tint: ProviderTint,
1070 pub shape: BotShape,
1072 pub config: VersionedAgentConfig,
1074 pub accepts_file_attachments: bool,
1076 pub routine_interaction_policy: RoutineInteractionPolicy,
1078}
1079
1080#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1082#[serde(rename_all = "snake_case")]
1083pub enum RoutineInteractionPolicy {
1084 Unattended,
1086 MayPauseForApproval,
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1092pub struct Routine {
1093 pub id: String,
1095 pub bot_id: String,
1097 pub workspace: PathBuf,
1099 pub instructions: String,
1101 pub bindings: Vec<RoutineBinding>,
1103 pub enabled: bool,
1105 pub finished: bool,
1107 pub next_run_at: Option<i64>,
1109}
1110
1111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(deny_unknown_fields)]
1114pub struct RoutineSchedule {
1115 pub kind: RoutineScheduleKind,
1117 pub at: Option<i64>,
1119 pub every_seconds: Option<u64>,
1121 pub expression: Option<String>,
1123 pub time_zone: Option<String>,
1125}
1126
1127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1129#[serde(rename_all = "snake_case")]
1130pub enum RoutineScheduleKind {
1131 Once,
1133 Interval,
1135 Cron,
1137}
1138
1139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1141pub struct RoutineRunPreview {
1142 pub routine: Routine,
1144 pub run: RoutineRun,
1146 pub records: Vec<RecordedEvent>,
1148 pub next_before_sequence: Option<u64>,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1154pub struct RoutineRun {
1155 pub id: String,
1157 pub routine_id: String,
1159 pub bot_id: String,
1161 pub started_at: i64,
1163 pub finished_at: Option<i64>,
1165 pub status: RoutineRunStatus,
1167 pub session_id: Option<String>,
1169 pub message: Option<String>,
1171}
1172
1173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1175#[serde(rename_all = "snake_case")]
1176pub enum RoutineRunStatus {
1177 Running,
1179 Succeeded,
1181 Failed,
1183 Skipped,
1185 Cancelled,
1187}
1188
1189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1191#[serde(deny_unknown_fields)]
1192pub struct RoutineDefinition {
1193 pub workspace: PathBuf,
1195 pub instructions: String,
1197 pub bindings: Vec<RoutineBinding>,
1199}
1200
1201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1203#[serde(deny_unknown_fields)]
1204pub struct HookBinding<A> {
1205 pub id: String,
1207 pub on: HookSelector,
1209 pub action: A,
1211}
1212
1213pub type RoutineBinding = HookBinding<RoutineAction>;
1215
1216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1218#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1219pub enum BotAction {
1220 Report {
1222 instruction: String,
1224 },
1225 Routine {
1227 command: RoutineCommand,
1229 },
1230 Session {
1232 session_id: String,
1234 op: Box<mobius::protocol::Op>,
1236 },
1237}
1238
1239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1241#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1242pub enum RoutineAction {
1243 Start,
1245 Stop {
1247 run_id: String,
1249 },
1250 Pause,
1252 Resume,
1254 Delete,
1256 Update {
1258 definition: RoutineDefinition,
1260 },
1261}
1262
1263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1265#[serde(deny_unknown_fields)]
1266pub struct RoutineCommand {
1267 pub routine_id: String,
1269 pub action: RoutineAction,
1271}
1272
1273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1275#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1276pub enum HookSource {
1277 Bot {
1279 bot_id: String,
1281 },
1282 Routine {
1284 routine_id: String,
1286 },
1287 Session {
1289 session_id: String,
1291 },
1292 Schedule {
1294 routine_id: String,
1296 binding_id: String,
1298 },
1299 Gateway,
1301 Client {
1303 client_id: String,
1305 },
1306}
1307
1308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1310#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1311pub enum HookSelector {
1312 Schedule {
1314 schedule: RoutineSchedule,
1316 ends_at: Option<i64>,
1318 },
1319 Event {
1321 source: HookSource,
1323 kind: HookKind,
1325 routine_outcome: Option<RoutineRunStatus>,
1327 session_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1329 custom_name: Option<String>,
1331 },
1332}
1333
1334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1336#[serde(rename_all = "snake_case")]
1337pub enum HookKind {
1338 RoutineCreated,
1340 RoutineUpdated,
1342 RoutinePaused,
1344 RoutineResumed,
1346 RoutineDeleted,
1348 RunStarted,
1350 RunFinished,
1352 RunSkipped,
1354 ScheduleDue,
1356 SessionCreated,
1358 SessionTurnStarted,
1360 SessionTurnFinished,
1362 SessionApproval,
1364 SessionAttention,
1366 SessionDeleted,
1368 SessionOwnerChanged,
1370 ClientConnected,
1372 ClientDisconnected,
1374 CustomReceived,
1376}
1377
1378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1380#[serde(deny_unknown_fields)]
1381pub struct HookEvent {
1382 pub id: String,
1384 pub source: HookSource,
1386 pub cause_id: Option<String>,
1388 pub ancestry: Vec<String>,
1390 pub bot_id: String,
1392 pub occurred_at: i64,
1394 pub data: HookData,
1396}
1397
1398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1400#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1401pub enum HookData {
1402 RoutineCreated {
1404 routine_id: String,
1406 },
1407 RoutineUpdated {
1409 routine_id: String,
1411 },
1412 RoutinePaused {
1414 routine_id: String,
1416 },
1417 RoutineResumed {
1419 routine_id: String,
1421 },
1422 RoutineDeleted {
1424 routine_id: String,
1426 },
1427 RunStarted {
1429 routine_id: String,
1431 run_id: String,
1433 session_id: Option<String>,
1435 },
1436 RunFinished {
1438 routine_id: String,
1440 run_id: String,
1442 status: RoutineRunStatus,
1444 session_id: Option<String>,
1446 reason: Option<String>,
1448 },
1449 RunSkipped {
1451 routine_id: String,
1453 run_id: String,
1455 reason: String,
1457 },
1458 ScheduleDue {
1460 binding_id: String,
1462 },
1463 SessionCreated {
1465 session_id: String,
1467 },
1468 SessionTurnStarted {
1470 session_id: String,
1472 turn_id: String,
1474 },
1475 SessionTurnFinished {
1477 session_id: String,
1479 turn_id: String,
1481 outcome: mobius::backend::checkpoint::ExecutionOutcome,
1483 },
1484 SessionApproval {
1486 session_id: String,
1488 turn_id: String,
1490 request_id: String,
1492 },
1493 SessionAttention {
1495 session_id: String,
1497 capability: String,
1499 item_id: String,
1501 text: String,
1503 },
1504 SessionDeleted {
1506 session_id: String,
1508 },
1509 SessionOwnerChanged {
1511 session_id: String,
1513 previous_bot_id: String,
1515 },
1516 ClientConnected {
1518 client_id: String,
1520 },
1521 ClientDisconnected {
1523 client_id: String,
1525 },
1526 CustomReceived {
1528 name: String,
1530 data: serde_json::Value,
1532 },
1533}
1534
1535impl HookData {
1536 #[must_use]
1538 pub fn kind(&self) -> HookKind {
1539 match self {
1540 Self::RoutineCreated { .. } => HookKind::RoutineCreated,
1541 Self::RoutineUpdated { .. } => HookKind::RoutineUpdated,
1542 Self::RoutinePaused { .. } => HookKind::RoutinePaused,
1543 Self::RoutineResumed { .. } => HookKind::RoutineResumed,
1544 Self::RoutineDeleted { .. } => HookKind::RoutineDeleted,
1545 Self::RunStarted { .. } => HookKind::RunStarted,
1546 Self::RunFinished { .. } => HookKind::RunFinished,
1547 Self::RunSkipped { .. } => HookKind::RunSkipped,
1548 Self::ScheduleDue { .. } => HookKind::ScheduleDue,
1549 Self::SessionCreated { .. } => HookKind::SessionCreated,
1550 Self::SessionTurnStarted { .. } => HookKind::SessionTurnStarted,
1551 Self::SessionTurnFinished { .. } => HookKind::SessionTurnFinished,
1552 Self::SessionApproval { .. } => HookKind::SessionApproval,
1553 Self::SessionAttention { .. } => HookKind::SessionAttention,
1554 Self::SessionDeleted { .. } => HookKind::SessionDeleted,
1555 Self::SessionOwnerChanged { .. } => HookKind::SessionOwnerChanged,
1556 Self::ClientConnected { .. } => HookKind::ClientConnected,
1557 Self::ClientDisconnected { .. } => HookKind::ClientDisconnected,
1558 Self::CustomReceived { .. } => HookKind::CustomReceived,
1559 }
1560 }
1561}
1562
1563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1565#[serde(deny_unknown_fields)]
1566pub struct BotSubscription {
1567 pub bot_id: String,
1569 pub binding: HookBinding<BotAction>,
1571 pub enabled: bool,
1573}