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 context_limit_tokens: Option<i64>,
290 pub active_message_delivery: mobius::protocol::ActiveMessageDelivery,
292 pub run_stats: RunStats,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct SessionWidget {
299 pub capability: String,
301 pub item: FrontendWidget,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307pub struct SessionRecord {
308 pub session_id: String,
310 pub session_context: mobius::protocol::SessionContext,
312 pub parent_session_id: Option<String>,
314 pub parent_sequence: Option<u64>,
316 pub sequence: u64,
318 pub first_user_message: Option<String>,
320 pub execution_stats: mobius::backend::checkpoint::ExecutionStats,
322 pub title: Option<String>,
324 pub pinned: bool,
326 pub activity: SessionActivity,
328 pub created_at: i64,
330 pub updated_at: i64,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(deny_unknown_fields)]
337pub struct SessionActivity {
338 pub attention: u32,
340 pub state: SessionActivityState,
342 pub turn_id: Option<String>,
344 pub approval_request_id: Option<String>,
346 pub started_at: Option<i64>,
348 pub last_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
350 pub message: Option<String>,
352}
353
354impl Default for SessionActivity {
355 fn default() -> Self {
356 Self {
357 attention: 0,
358 state: SessionActivityState::Idle,
359 turn_id: None,
360 approval_request_id: None,
361 started_at: None,
362 last_outcome: None,
363 message: None,
364 }
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
370#[serde(rename_all = "snake_case")]
371pub enum SessionActivityState {
372 Idle,
374 Running,
376 AwaitingApproval,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382pub struct WorkspaceInfo {
383 pub id: String,
385 pub path: PathBuf,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391pub struct GitStatus {
392 pub current_branch: String,
394 pub branches: Vec<String>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
400#[serde(deny_unknown_fields)]
401pub struct SshIdentityRecord {
402 pub label: String,
404 pub algorithm: String,
406 pub fingerprint: String,
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "snake_case")]
413pub enum GitDiffScope {
414 Staged,
416 Unstaged,
418 Committed,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum WorkspaceFileScope {
426 Modified,
428 All,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434pub struct WorkspaceFileRecord {
435 pub path: String,
437 pub size: u64,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
443pub struct DirectoryListing {
444 pub path: PathBuf,
446 pub parent: Option<PathBuf>,
448 pub entries: Vec<DirectoryEntry>,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454pub struct DirectoryEntry {
455 pub name: String,
457 pub path: PathBuf,
459 pub is_directory: bool,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct VersionedAgentConfig {
466 pub revision: u64,
468 pub config: AgentComposition,
470}
471
472#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474#[serde(deny_unknown_fields)]
475pub struct AgentComposition {
476 pub provider: ProviderConfig,
478 pub middleware: MiddlewareConfig,
480 pub extensions: BTreeSet<String>,
482 pub system_prompt: String,
484 pub max_model_steps: u64,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(rename_all = "snake_case")]
491pub enum ExtensionKind {
492 Skill,
494 Plugin,
496}
497
498impl ExtensionKind {
499 #[must_use]
501 pub const fn as_str(self) -> &'static str {
502 match self {
503 Self::Skill => "skill",
504 Self::Plugin => "plugin",
505 }
506 }
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
511#[serde(deny_unknown_fields)]
512pub struct ExtensionHookRecord {
513 pub event: String,
515 pub matcher: Option<String>,
517 pub command: String,
519 pub timeout_seconds: u64,
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct ExtensionRecord {
527 pub id: String,
529 pub capability: String,
531 pub kind: ExtensionKind,
533 pub name: String,
535 pub description: String,
537 pub version: Option<String>,
539 pub source: String,
541 pub reference: Option<String>,
543 pub subdirectory: Option<String>,
545 pub resolved_revision: String,
547 pub digest: String,
549 pub skills: Vec<String>,
551 pub hooks: Vec<ExtensionHookRecord>,
553 pub hooks_trusted: bool,
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(deny_unknown_fields)]
560pub struct ProviderConfig {
561 pub instance: String,
564 pub provider: String,
566 #[serde(default)]
568 pub model: String,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub base_url: Option<String>,
572 pub endpoint_auth: ProviderEndpointAuth,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub reasoning_effort: Option<String>,
577 #[serde(default, skip_serializing_if = "Option::is_none")]
578 pub service_tier: Option<String>,
580 #[serde(default, skip_serializing_if = "Option::is_none")]
582 pub tool_discovery: Option<ToolDiscoveryMode>,
583 pub web_search: HostedWebSearch,
585}
586
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(rename_all = "snake_case")]
590pub enum ProviderEndpointAuth {
591 ProviderDefault,
593 Credentialless,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
599pub struct ProviderStatus {
600 pub provider: String,
602 pub label: String,
604 pub symbol: FrontendSymbol,
606 pub description: String,
608 pub model_ids_configurable: bool,
610 pub auth: ProviderAuthKind,
612 pub default_base_url: Option<String>,
614 pub native_custom_endpoints: bool,
616 pub default_api_key_env: Option<String>,
618 pub models: Vec<ProviderModel>,
620 #[serde(default)]
622 pub image_models: Cow<'static, [MediaModelPreset]>,
623 #[serde(default)]
625 pub image_model_ids_configurable: bool,
626 #[serde(default)]
628 pub voice_models: Cow<'static, [MediaModelPreset]>,
629 pub web_search: Vec<FrontendSettingOption>,
631 pub tool_discovery: ToolDiscoveryMode,
633 pub supported_tool_discovery: Vec<ToolDiscoveryMode>,
635 pub custom_endpoint_tool_discovery: Option<ToolDiscoveryMode>,
637}
638
639#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
641#[serde(rename_all = "snake_case")]
642pub enum ProviderTint {
643 #[default]
644 Blue,
646 Teal,
648 Green,
650 Yellow,
652 Orange,
654 Red,
656 Purple,
658 White,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(rename_all = "snake_case")]
665pub enum BotShape {
666 Circle,
668 Squircle,
670 Triangle,
672 Diamond,
674 Hexagon,
676 Star,
678 Flower,
680}
681
682impl BotShape {
683 pub const ALL: [Self; 7] = [
685 Self::Circle,
686 Self::Squircle,
687 Self::Triangle,
688 Self::Diamond,
689 Self::Hexagon,
690 Self::Star,
691 Self::Flower,
692 ];
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
697pub struct ProviderInstance {
698 pub label: String,
700 pub tint: ProviderTint,
702 pub configured: bool,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub credential_hint: Option<String>,
707 pub selection: ProviderConfig,
709 pub models: Vec<ProviderModel>,
711 #[serde(default, skip_serializing_if = "Vec::is_empty")]
713 pub image_model_ids: Vec<String>,
714}
715
716#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
718#[serde(rename_all = "snake_case")]
719pub enum ClientKind {
720 Cli,
722 Macos,
724 Ios,
726 Ipados,
728 GatewayDashboard,
730}
731
732#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734pub struct ClientStatus {
735 pub client_id: String,
737 pub label: String,
739 pub kinds: Vec<ClientKind>,
741 pub connections: usize,
743}
744
745impl ProviderStatus {
746 #[must_use]
747 pub fn configurable_base_url(&self) -> bool {
749 self.default_base_url.is_some()
750 }
751
752 #[must_use]
753 pub fn default_model(&self) -> Option<&ProviderModel> {
755 self.models.first()
756 }
757}
758
759#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
761#[serde(deny_unknown_fields)]
762pub struct ConfiguredModel {
763 pub id: String,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub reasoning_efforts: Option<Vec<String>>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub default_reasoning: Option<String>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub label: Option<String>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
776 pub description: Option<String>,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
779 pub context_window: Option<i64>,
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
784#[serde(rename_all = "snake_case")]
785pub enum ProviderAuthKind {
786 ApiKey,
788 DeviceCode,
790}
791
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
794#[serde(deny_unknown_fields)]
795pub struct MiddlewareConfig {
796 pub(crate) enabled: BTreeSet<String>,
797 pub settings: BTreeMap<String, BTreeMap<String, FrontendSettingValue>>,
799}
800
801impl MiddlewareConfig {
802 #[must_use]
804 pub fn disabled_by<'a>(
805 &self,
806 features: &'a [mobius::protocol::MiddlewareFeature],
807 id: &str,
808 ) -> Option<&'a str> {
809 features
810 .iter()
811 .filter(|feature| feature.required || self.enabled(&feature.id))
812 .find_map(|feature| {
813 feature.settings.iter().find_map(|setting| {
814 let mobius::protocol::FrontendSettingKind::Select { options, .. } =
815 &setting.kind
816 else {
817 return None;
818 };
819 let Some(FrontendSettingValue::String(value)) =
820 self.setting(&feature.id, &setting.id)
821 else {
822 return None;
823 };
824 options
825 .iter()
826 .find(|option| {
827 option.value == *value
828 && option.disables.iter().any(|disabled| disabled == id)
829 })
830 .map(|option| option.label.as_str())
831 })
832 })
833 }
834
835 pub fn reconcile(&mut self, features: &[mobius::protocol::MiddlewareFeature]) {
837 let excluded = self
838 .enabled
839 .iter()
840 .filter(|id| self.disabled_by(features, id).is_some())
841 .cloned()
843 .collect::<Vec<_>>();
844 for id in excluded {
845 self.enabled.remove(&id);
846 }
847 }
848
849 #[must_use]
851 pub fn enabled(&self, id: &str) -> bool {
852 self.enabled.contains(id)
853 }
854
855 pub fn set_enabled(&mut self, id: impl Into<String>, enabled: bool) {
857 let id = id.into();
858 if enabled {
859 self.enabled.insert(id);
860 } else {
861 self.enabled.remove(&id);
862 }
863 }
864
865 #[must_use]
867 pub fn setting(&self, middleware: &str, setting: &str) -> Option<&FrontendSettingValue> {
868 self.settings.get(middleware)?.get(setting)
869 }
870
871 pub fn set_setting(
873 &mut self,
874 middleware: impl Into<String>,
875 setting: impl Into<String>,
876 value: Option<FrontendSettingValue>,
877 ) {
878 let middleware = middleware.into();
879 let setting = setting.into();
880 if let Some(value) = value {
881 self.settings
882 .entry(middleware)
883 .or_default()
884 .insert(setting, value);
885 } else if let Some(settings) = self.settings.get_mut(&middleware) {
886 settings.remove(&setting);
887 if settings.is_empty() {
888 self.settings.remove(&middleware);
889 }
890 }
891 }
892
893 pub(crate) fn entries(&self) -> impl Iterator<Item = &str> {
894 self.enabled.iter().map(String::as_str)
895 }
896}
897
898#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
900pub struct RenderedPreview {
901 pub symbol: Option<FrontendSymbol>,
903 pub duration_ms: Option<u64>,
905 pub started_at_ms: Option<i64>,
907 pub id: String,
909 pub title: String,
911 pub subtitle: String,
913 pub page_id: String,
915 pub update: FrontendPreviewUpdate,
917 pub events: Vec<RenderedEvent>,
919 pub next: Option<Op>,
921}
922
923#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
925pub struct RenderedEvent {
926 pub submission_id: Option<Arc<str>>,
928 pub recorded_at_ms: i64,
930 pub event: EventMsg,
932 pub blocks: Vec<RenderedBlock>,
934}
935
936#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
938pub struct RecordedEvent {
939 pub sequence: u64,
941 pub recorded_at_ms: i64,
943 pub event: Event,
945 pub stream_metrics: Vec<StreamMetrics>,
947 pub blocks: Vec<RenderedBlock>,
949 pub preview: Option<RenderedPreview>,
951}
952
953#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955pub struct ProfileSnapshot {
956 pub user_name: Option<String>,
958 pub daily_usage: Vec<DailyUsage>,
960 pub provider_usage: Vec<ProviderUsage>,
962 pub run_stats: RunStats,
964 pub recent_run_groups: Vec<SessionRunGroup>,
966}
967
968#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
970pub struct ProviderUsage {
971 pub provider: String,
973 pub limits: Option<Vec<UsageLimit>>,
975 pub error: Option<String>,
977}
978
979#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
981pub struct SessionRunGroup {
982 pub session_id: String,
984 pub title: String,
986 pub runs: Vec<RunSummary>,
988}
989
990#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
992pub struct RunStats {
993 #[serde(flatten)]
994 pub completed: mobius::backend::checkpoint::ExecutionStats,
996 pub active: Option<RunSummary>,
998}
999
1000#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1002pub struct RunSummary {
1003 pub session_id: String,
1005 pub submission_id: String,
1007 pub turn_id: String,
1009 pub started_at_ms: i64,
1011 pub finished_at_ms: Option<i64>,
1013 pub elapsed_ms: u64,
1015 pub outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1017 pub model_calls: u64,
1019 pub tool_calls: u64,
1021 pub failed_tool_calls: u64,
1023 pub usage: TokenUsage,
1025}
1026
1027#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1029pub struct DailyUsage {
1030 pub unix_day: u64,
1032 pub provider: String,
1034 pub usage: TokenUsage,
1036}
1037
1038#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1040pub struct BotRecord {
1041 pub conversation_session_id: String,
1043 pub id: String,
1045 pub handle: String,
1047 pub name: String,
1049 pub description: String,
1051 pub tint: ProviderTint,
1053 pub shape: BotShape,
1055 pub config: VersionedAgentConfig,
1057 pub accepts_file_attachments: bool,
1059 pub routine_interaction_policy: RoutineInteractionPolicy,
1061}
1062
1063#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1065#[serde(rename_all = "snake_case")]
1066pub enum RoutineInteractionPolicy {
1067 Unattended,
1069 MayPauseForApproval,
1071}
1072
1073#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1075pub struct Routine {
1076 pub id: String,
1078 pub bot_id: String,
1080 pub workspace: PathBuf,
1082 pub instructions: String,
1084 pub bindings: Vec<RoutineBinding>,
1086 pub enabled: bool,
1088 pub finished: bool,
1090 pub next_run_at: Option<i64>,
1092}
1093
1094#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1096#[serde(deny_unknown_fields)]
1097pub struct RoutineSchedule {
1098 pub kind: RoutineScheduleKind,
1100 pub at: Option<i64>,
1102 pub every_seconds: Option<u64>,
1104 pub expression: Option<String>,
1106 pub time_zone: Option<String>,
1108}
1109
1110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1112#[serde(rename_all = "snake_case")]
1113pub enum RoutineScheduleKind {
1114 Once,
1116 Interval,
1118 Cron,
1120}
1121
1122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1124pub struct RoutineRunPreview {
1125 pub routine: Routine,
1127 pub run: RoutineRun,
1129 pub records: Vec<RecordedEvent>,
1131 pub next_before_sequence: Option<u64>,
1133}
1134
1135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1137pub struct RoutineRun {
1138 pub id: String,
1140 pub routine_id: String,
1142 pub bot_id: String,
1144 pub started_at: i64,
1146 pub finished_at: Option<i64>,
1148 pub status: RoutineRunStatus,
1150 pub session_id: Option<String>,
1152 pub message: Option<String>,
1154}
1155
1156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1158#[serde(rename_all = "snake_case")]
1159pub enum RoutineRunStatus {
1160 Running,
1162 Succeeded,
1164 Failed,
1166 Skipped,
1168 Cancelled,
1170}
1171
1172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1174#[serde(deny_unknown_fields)]
1175pub struct RoutineDefinition {
1176 pub workspace: PathBuf,
1178 pub instructions: String,
1180 pub bindings: Vec<RoutineBinding>,
1182}
1183
1184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1186#[serde(deny_unknown_fields)]
1187pub struct HookBinding<A> {
1188 pub id: String,
1190 pub on: HookSelector,
1192 pub action: A,
1194}
1195
1196pub type RoutineBinding = HookBinding<RoutineAction>;
1198
1199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1201#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1202pub enum BotAction {
1203 Report {
1205 instruction: String,
1207 },
1208 Routine {
1210 command: RoutineCommand,
1212 },
1213 Session {
1215 session_id: String,
1217 op: Box<mobius::protocol::Op>,
1219 },
1220}
1221
1222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1224#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1225pub enum RoutineAction {
1226 Start,
1228 Stop {
1230 run_id: String,
1232 },
1233 Pause,
1235 Resume,
1237 Delete,
1239 Update {
1241 definition: RoutineDefinition,
1243 },
1244}
1245
1246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1248#[serde(deny_unknown_fields)]
1249pub struct RoutineCommand {
1250 pub routine_id: String,
1252 pub action: RoutineAction,
1254}
1255
1256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1258#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1259pub enum HookSource {
1260 Bot {
1262 bot_id: String,
1264 },
1265 Routine {
1267 routine_id: String,
1269 },
1270 Session {
1272 session_id: String,
1274 },
1275 Schedule {
1277 routine_id: String,
1279 binding_id: String,
1281 },
1282 Gateway,
1284 Client {
1286 client_id: String,
1288 },
1289}
1290
1291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1293#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1294pub enum HookSelector {
1295 Schedule {
1297 schedule: RoutineSchedule,
1299 ends_at: Option<i64>,
1301 },
1302 Event {
1304 source: HookSource,
1306 kind: HookKind,
1308 routine_outcome: Option<RoutineRunStatus>,
1310 session_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1312 custom_name: Option<String>,
1314 },
1315}
1316
1317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1319#[serde(rename_all = "snake_case")]
1320pub enum HookKind {
1321 RoutineCreated,
1323 RoutineUpdated,
1325 RoutinePaused,
1327 RoutineResumed,
1329 RoutineDeleted,
1331 RunStarted,
1333 RunFinished,
1335 RunSkipped,
1337 ScheduleDue,
1339 SessionCreated,
1341 SessionTurnStarted,
1343 SessionTurnFinished,
1345 SessionApproval,
1347 SessionAttention,
1349 SessionDeleted,
1351 SessionOwnerChanged,
1353 ClientConnected,
1355 ClientDisconnected,
1357 CustomReceived,
1359}
1360
1361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1363#[serde(deny_unknown_fields)]
1364pub struct HookEvent {
1365 pub id: String,
1367 pub source: HookSource,
1369 pub cause_id: Option<String>,
1371 pub ancestry: Vec<String>,
1373 pub bot_id: String,
1375 pub occurred_at: i64,
1377 pub data: HookData,
1379}
1380
1381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1383#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1384pub enum HookData {
1385 RoutineCreated {
1387 routine_id: String,
1389 },
1390 RoutineUpdated {
1392 routine_id: String,
1394 },
1395 RoutinePaused {
1397 routine_id: String,
1399 },
1400 RoutineResumed {
1402 routine_id: String,
1404 },
1405 RoutineDeleted {
1407 routine_id: String,
1409 },
1410 RunStarted {
1412 routine_id: String,
1414 run_id: String,
1416 session_id: Option<String>,
1418 },
1419 RunFinished {
1421 routine_id: String,
1423 run_id: String,
1425 status: RoutineRunStatus,
1427 session_id: Option<String>,
1429 reason: Option<String>,
1431 },
1432 RunSkipped {
1434 routine_id: String,
1436 run_id: String,
1438 reason: String,
1440 },
1441 ScheduleDue {
1443 binding_id: String,
1445 },
1446 SessionCreated {
1448 session_id: String,
1450 },
1451 SessionTurnStarted {
1453 session_id: String,
1455 turn_id: String,
1457 },
1458 SessionTurnFinished {
1460 session_id: String,
1462 turn_id: String,
1464 outcome: mobius::backend::checkpoint::ExecutionOutcome,
1466 },
1467 SessionApproval {
1469 session_id: String,
1471 turn_id: String,
1473 request_id: String,
1475 },
1476 SessionAttention {
1478 session_id: String,
1480 capability: String,
1482 item_id: String,
1484 text: String,
1486 },
1487 SessionDeleted {
1489 session_id: String,
1491 },
1492 SessionOwnerChanged {
1494 session_id: String,
1496 previous_bot_id: String,
1498 },
1499 ClientConnected {
1501 client_id: String,
1503 },
1504 ClientDisconnected {
1506 client_id: String,
1508 },
1509 CustomReceived {
1511 name: String,
1513 data: serde_json::Value,
1515 },
1516}
1517
1518impl HookData {
1519 #[must_use]
1521 pub fn kind(&self) -> HookKind {
1522 match self {
1523 Self::RoutineCreated { .. } => HookKind::RoutineCreated,
1524 Self::RoutineUpdated { .. } => HookKind::RoutineUpdated,
1525 Self::RoutinePaused { .. } => HookKind::RoutinePaused,
1526 Self::RoutineResumed { .. } => HookKind::RoutineResumed,
1527 Self::RoutineDeleted { .. } => HookKind::RoutineDeleted,
1528 Self::RunStarted { .. } => HookKind::RunStarted,
1529 Self::RunFinished { .. } => HookKind::RunFinished,
1530 Self::RunSkipped { .. } => HookKind::RunSkipped,
1531 Self::ScheduleDue { .. } => HookKind::ScheduleDue,
1532 Self::SessionCreated { .. } => HookKind::SessionCreated,
1533 Self::SessionTurnStarted { .. } => HookKind::SessionTurnStarted,
1534 Self::SessionTurnFinished { .. } => HookKind::SessionTurnFinished,
1535 Self::SessionApproval { .. } => HookKind::SessionApproval,
1536 Self::SessionAttention { .. } => HookKind::SessionAttention,
1537 Self::SessionDeleted { .. } => HookKind::SessionDeleted,
1538 Self::SessionOwnerChanged { .. } => HookKind::SessionOwnerChanged,
1539 Self::ClientConnected { .. } => HookKind::ClientConnected,
1540 Self::ClientDisconnected { .. } => HookKind::ClientDisconnected,
1541 Self::CustomReceived { .. } => HookKind::CustomReceived,
1542 }
1543 }
1544}
1545
1546#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1548#[serde(deny_unknown_fields)]
1549pub struct BotSubscription {
1550 pub bot_id: String,
1552 pub binding: HookBinding<BotAction>,
1554 pub enabled: bool,
1556}